From c0fe1063085fe797ef2d3b21ec735266981c7c43 Mon Sep 17 00:00:00 2001 From: Al-Hassan Abdel-Raouf Date: Sun, 11 Dec 2022 19:04:35 +0200 Subject: [PATCH] mpv configs --- .config/mpv/fonts/uosc_icons.otf | Bin 0 -> 400360 bytes .config/mpv/fonts/uosc_textures.ttf | Bin 0 -> 38228 bytes .config/mpv/mpv.conf | 6 + .config/mpv/script-opts/thumbfast.conf | 28 + .config/mpv/script-opts/uosc.conf | 203 ++++ .config/mpv/scripts/autosub.lua | 259 ++++ .config/mpv/scripts/thumbfast.lua | 712 +++++++++++ .config/mpv/scripts/uosc.lua | 1079 +++++++++++++++++ .../elements/BufferingIndicator.lua | 37 + .../scripts/uosc_shared/elements/Button.lua | 88 ++ .../scripts/uosc_shared/elements/Controls.lua | 330 +++++ .../scripts/uosc_shared/elements/Curtain.lua | 35 + .../uosc_shared/elements/CycleButton.lua | 64 + .../scripts/uosc_shared/elements/Element.lua | 148 +++ .../scripts/uosc_shared/elements/Elements.lua | 154 +++ .../mpv/scripts/uosc_shared/elements/Menu.lua | 772 ++++++++++++ .../uosc_shared/elements/PauseIndicator.lua | 80 ++ .../scripts/uosc_shared/elements/Speed.lua | 181 +++ .../scripts/uosc_shared/elements/Timeline.lua | 344 ++++++ .../scripts/uosc_shared/elements/TopBar.lua | 182 +++ .../scripts/uosc_shared/elements/Volume.lua | 240 ++++ .../uosc_shared/elements/WindowBorder.lua | 33 + .config/mpv/scripts/uosc_shared/lib/ass.lua | 170 +++ .config/mpv/scripts/uosc_shared/lib/menus.lua | 277 +++++ .config/mpv/scripts/uosc_shared/lib/std.lua | 181 +++ .config/mpv/scripts/uosc_shared/lib/text.lua | 414 +++++++ .config/mpv/scripts/uosc_shared/lib/utils.lua | 541 +++++++++ .config/mpv/scripts/uosc_shared/main.lua | 5 + 28 files changed, 6563 insertions(+) create mode 100644 .config/mpv/fonts/uosc_icons.otf create mode 100644 .config/mpv/fonts/uosc_textures.ttf create mode 100644 .config/mpv/mpv.conf create mode 100644 .config/mpv/script-opts/thumbfast.conf create mode 100644 .config/mpv/script-opts/uosc.conf create mode 100644 .config/mpv/scripts/autosub.lua create mode 100644 .config/mpv/scripts/thumbfast.lua create mode 100644 .config/mpv/scripts/uosc.lua create mode 100644 .config/mpv/scripts/uosc_shared/elements/BufferingIndicator.lua create mode 100644 .config/mpv/scripts/uosc_shared/elements/Button.lua create mode 100644 .config/mpv/scripts/uosc_shared/elements/Controls.lua create mode 100644 .config/mpv/scripts/uosc_shared/elements/Curtain.lua create mode 100644 .config/mpv/scripts/uosc_shared/elements/CycleButton.lua create mode 100644 .config/mpv/scripts/uosc_shared/elements/Element.lua create mode 100644 .config/mpv/scripts/uosc_shared/elements/Elements.lua create mode 100644 .config/mpv/scripts/uosc_shared/elements/Menu.lua create mode 100644 .config/mpv/scripts/uosc_shared/elements/PauseIndicator.lua create mode 100644 .config/mpv/scripts/uosc_shared/elements/Speed.lua create mode 100644 .config/mpv/scripts/uosc_shared/elements/Timeline.lua create mode 100644 .config/mpv/scripts/uosc_shared/elements/TopBar.lua create mode 100644 .config/mpv/scripts/uosc_shared/elements/Volume.lua create mode 100644 .config/mpv/scripts/uosc_shared/elements/WindowBorder.lua create mode 100644 .config/mpv/scripts/uosc_shared/lib/ass.lua create mode 100644 .config/mpv/scripts/uosc_shared/lib/menus.lua create mode 100644 .config/mpv/scripts/uosc_shared/lib/std.lua create mode 100644 .config/mpv/scripts/uosc_shared/lib/text.lua create mode 100644 .config/mpv/scripts/uosc_shared/lib/utils.lua create mode 100644 .config/mpv/scripts/uosc_shared/main.lua diff --git a/.config/mpv/fonts/uosc_icons.otf b/.config/mpv/fonts/uosc_icons.otf new file mode 100644 index 0000000000000000000000000000000000000000..4c4e0dcfe20684e18da5fd6f4dfbd489ee947c66 GIT binary patch literal 400360 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

Evhfm%Hl zoDENxyc5S`F;Df2;I3_y5365-BLHViOb<9!a@qsVHju01>fIfk-MiTlyQoou-G+KjFF8gQ0iZ|J4RR%s33ju)KV@uVNqdKX*(Z$H*tjBUw>r0MA3 z9AHu@O(~;Zm=+Amz$B@CdW3PKK)S~ovMDezUDM+0fM0b|>rSf5%s`8w-gJ>H$Dh`s*l^XZvnWnFl0L{u7QG)94)j(Q090MHdiN}MZ z=gyuf4tV-+hD}24mb-!;5iUi-Kn-f6(x4r1vRB3@bt2z#*b-fGi3OpyUX9w!n&LUpQfgsWIhIt(oFuPH$n?e*=9WO zp2a!69*-b-1?vvYD&S&!?6OhhJhq_Jyk&9DGH3Gt6Tnd`k|$Xc-^n#J7+-wFnx~gB zwvRc}?PI0g;7SV8H&q6lS6OVh>LJaeLfyDE5j=Tu8L0O<5B1xHH!6p z4ZqTqilr}+xHr2pu+A%VQc!F@1D0~cQF9(HEmpLu%d7x4&7+Qb<3?a1u#wIJ-1)PP z_o)-PdL(nqdHw}CzRcXZ2Q1s{Vz0S-#FD!xo!3~bqagLKvv$#nAj7rziEl{&7wcTw zq@v(7WP*|-n6OAuzlwu{fTzc}huwrIQyK`HNus?+Gzkd%&z5jB$j!Sm@R9vb;D5=$ z^Eq;D{QPE%Cr?uIMJELS8~HVcUoNB$i}ee8N8Qd=kgm&schc+h5Btj4Gq&$9?G+FaUaT2vf{!2cL5VmLh~+oUcQfCjM7oydh`U)gXPf_m5}CbZZknqb zou@u)4<+1K#QOJYTf%Xv;}L4FIA4yI;hWWb3%o$IoKoieDz-U_&e7ZYf6!ZwaB;9N z%ETUOinoF~6COoSV9U53VibVmn9{RM`0_yx$|SS~FqBosEc_Z|#GL`xsi&5i3fyRL zo}0^{WS5#>Gs>iJPh5}LZp)Hi<%c~8QQ!|sY%RTniuz4CNZaIZb6w$zKKsZ;Mjl<%=LRH!)ILf zosn~~xz?89Tn<}Kn?}!oINDzFYDsVu^3jwW>S9`Um*F9g(ff(*Sv{1Z{5XBjk&ogr zTqVcepj7I(I$Bc8rCO3ZP%5eK$v@PS*RwUKEo!T&K&ea`Fjh#L7c!TiR8r1eEaUPk z)baGx8FgsPD5bZ{`8u)ob9bd?Daxg4y~ww~JL3q-@zmn~u^jtSY;H!m%sx;v*jXL) zm1p!|#h&4}elp8(W+93y?TBGzt|_tQGRKqNHmO!>H8i#3#Hbv{DIR-wW`^^4nTeon z9fopA>ebPbc-r;@y0B3lU1&W{J7THvY)6!|G)I3ZOMITAN}Wdi&bd4CLxuSVO8zme zxsH*xun;BrWFkj~`ciZ~?xJ(R%utj}?T_;%+*wVuD+&E?F4$|ym`^Aj;Gdu^X88Cd zV+m?jN)q|S-8%~μ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%0Aaf{ouBq_<$=_pQ`g# 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�`Jl%i( zqqtXz^**>s=K!UD7!ritHGO_BAPHr(Ar#rD5BDO3QT=k)ncmhqfXy*nX(O-(RC$mTw=^RLS{y$!m*E>;Vn@&{i@ zRa@4yNtiTR-LZ};xhT^#2nuJCCD`+;-tBn=yg3^2s%urv_Vz$?LcHp?I)qF(Bm9)< z#N#&StoU(2l6&^B*v}zGyG$i_n;Ini>FSDjx$%L;k3aChk;SjPa`?zAy^bDphk!{6 zv_s~lhI0bU7qm>_3FH-`WuMyix1ygPJ^jjj|1&_qa`Ua6;|7RQR%(&$6ecVT8eh6s6q0i4OkCvoc@V zFf_wS06Tvds`sxWw?LV$FyG#)3Wpk_-ms-x=Y>Lv+i!B9W62SAgxtNlupcbC#xgZn zU_5@oU9NI?9UiBvBChi^x&Wszn{+<>6!Ny$=}=$yxEqmQtEpE(%M*u6n{+`>)E5r+ z#B?ztzVk*ZbnZs5;2HY>ZwW`h&(@=ghvJb~q}!>B_#(kjxT#wgLJH0kt^xIUq%j;Z z+?I)aqKo??!@HVh(zc%!aYC$#Bduu7r%{wd>aI~A(t&P zXQ{aqyshMSp_Cp3KOnz%W%n+ykty0_XtkWmH2~ZY5^nA~`S7oejq0+e7bcePt$$vh z@Y!Q_qiYPFHP#H-`)AQcv1ae~XZw#EKRYw?av@hu5aTCSmaQq}cfxqUCn1bGQ-wfx z%iDl#W)i9d&delTv!~E@xuJz^hf4?-R1gnJ25$bVs%neLRnhoFFn71}knO}vFTQ0N zacnC}u8OU97S~t2I!|FKk=qZn52O2F2MTVKDPPxL3=Aq&%noA`!iODqnSk^kkI-Ss z#8=PF;IeiCb!ZFNy|YSmwJu9twLyw%HVjntr}&q9RZyM`gneh9(j~mfKq7RyU)L6G zYV9yUxN2ZSXNi@6upF9G0Z&jLlMbOY{B%GJic_KV8DviQ{}~Y^7J_{C!F1{C6+-=e zk1W2=R^SE?c8fLy!a}DJa}G?BOB(Q|;my#uJQGU#lAeU#+T-f$GmvY~k0^0KVQhW6 zL|FfQm8Y(yzBT;WkZybJ9>-|(_f0?jdE!&8_?`5XXQh8MB9BAkD&$ht3(ucWfG7Zt zyRBfx83%6z@t5hRBmDXo^64vaq&PJj(+|@`iRoB?3szyd&^DgOhiVWlDVgA!&C*#& zyvd4J@cQ8S!J(dsLOrf;Qn8+&1w|t}4i7&$<95^3_*dA^=hVKWD`}5N-+xPo*6V=9 zCH?Rooz>;CSPjx`3hoT-Kf>GMx3X2gK&fU>DQM=%O~2xxlFv~-j8Fl}3Vzx^b?%PA72(IpT!t>2}#WP-&vXR;Guyf>f`_ubZk42SSZu-X98tL+l&{ zxL2&nEP(+S7M5IMIffPC#r0~x3rd#;f>!r-1K0r^LFq@M)vD|qLaR}nUn6qXi?*Df;9lk5I*vtuFxc_O_2&C^)@&f zosCYxEdGRpeGkMU;*USlMM4lpFtpUgElys#;wKWyX_i&V6 zr{E)GaLDRpxU~qTj)Zjzfj^qJ_P)CnG4e*oPi@=IO!vnPi7T;a(Dh_KIt3;T~c8d0JE&r zjp%L0XDl3)smbAiu|5c^kd+C=7{Eb6Xi`N`fiTiV-iU*hRG3PALTYu@=Hbq-R_VNu~n;e^sn>WQly7_YG-C=R6A*F%rd@(P51jGkx{G}o% zpRFm0mo=OC!q)ZMergbFb2-RrhFu|T)CNf(Xi&RdAZo<|?eJ>xMiVcR?$w5TA&iD9qS7U>-gsGcwfIkrZ-_Hwvxp^ zjaeX5x64mL!RfMxhmXaabN|5gX{c}uNDWCtuxK@}Or1NT63e88-2N4+ush%Y<*Xa9 z!w5i}-_f}vfjFFiNF3@JhnDC*&?K9}Q6~?D)-XIjk7wC(oqLe$?Je(BEz|T%SAjc< z41%&W-utH4qjI|efP_w5AQ1^`L*bh~P-SbLT)S$emtW#tHGI$jsO5o{7PqsB->H$V z6t9G);=~&){z>{1>zK+RgZ+ReW^Rbo8<*w7-`kacfCznoKOLdFBKT3cpdedBmvjZU z_=pPK;mAteq*-!EMOaEV{s{mxs(tyv2Om_qJpnte3-5L`a`2GuCZ=l~4l3>ULH8mTh3l|YN3_BrgqL>_1L4u{`__8SI0I+D!V+Jw7VUH z+jko~+wJu!VNds#-LV7O`D$x%adlyIW2CCf)2H3T_SoE8Q&xim#h4Bt2?jN5 zOePy)g8_1pESdKQLb~Upra|n=B6l=+jb!DWRnJ5=VRF_Oc@a6U3lKczU6^}?&L8AO zlw}cUZSe;~ECm6R8y7wSS1BGo2`&8?6jPL%VB4Ps$sx8A)#@Szb~}}u$v9CE>&Pot z?o1_r@&zxgR5;3=$;Fq>0y+j6>~SkF$^b8N>TVSAlu}>k^BCYoPVdnR@MlVNli6a; z5J+8-*@AULL0!lu=ArY_H~3UGib}F(5nwizm8_ADib#A{CCFH146V@BrKW;A%f?rB zfn-fCHjyzrP1(swCp!NED(Qm$+fOS#fV}wHlKkcQZR`7x_0%(rAsGT*)NA^h+6Q+T z_LuBpi^sV+XF=_OHgn3s&LVUH1;@-i7NoZ+8wS?~!8y8d2-twX509bgf${cIS`TSx z8b3g*&|^HwlVOfHjXLFeC__eRY?GD z%0vDQmqv%-P0YF@=gqE8E>U&ou}LXkYB!jSsXbs6+cNmE?h)*r5xVGBfZd~%)l=@k zG$K!Th&6)enS?#YE2e0dA$aEb>HO2UVY!J;8G(=KCglq_E6Z&v`A=qu@$OK%w33F2 zavkzlm68?vDl2J#CGh_WB^t0U=s71OpM8Qi0Y!%3*3@Wd*c1219Dci>=*b$_g`e^k z+qF>p^%)yFcHB6QJ9%FmSvjtU9XH^kdKepL z22)KjegX)~Ode-yn#MSzN{T)}d~(4!pO0{5^YENXx}AIY#TQ?MLLCv6%6N*|vlUeK zd$Thj4)!U7CO=HoRxr;e5=TLA%A89mq$f~+B_b(Mza^^Mb<*EyObJtK9sjEv&}$lK zz()?W3CFcp{PBt_KutwI%ZuBuex++|O`zx^{+*_}MqVvh+sC(l3&A+LCha57Z`km{ zqe?F;Uauf`Q2K`YM_0;BEe@Gf;@|#&iy;k5@f!y<$#7HDCCojcP;%cpSLbqtElE~X zoUAIMIO7?o4=ymd>VFGWkpvRKL9(;;h$@rsCt;m2+2?b__-J3Yp*>LWeX@1=fiO-=tnLy84+2zc;*f_tS2SdQ|`U&*%O*gUP9s zPI4e8uk!Gv?((iph7{$3qK!gI{QBX?Rr6VBz65LZzo=~~XG<&7p4-u1vKB=-m^N|x zXWY}i9eFDa(whtqKz?WQ({8gZVg$%ksP z@fWaFR*PS#p*!#+F=w=@H}+O;j_{q~<__?B$214`3?F3RKjd=1`wjfcz$n#!Cw3Zn;wOw?Oh&QN=-E%DA>6NCV1dtx%sik7kOBBHY;gKbSQZ8!lzKVw_k;X>j*7WRgXK?YF3 zi|B{SqRn4tacbRm$oUy-&GD#Rh&VkVkJ0D#`@DkPZ3{Z1+HF}}IPCSr1Xm*MXfvX< zA(9Z9VvTKfNI+VH4k&DRu|EaY)7akm1?C zV=xx=G}#;V%fgk1K0KmACOH-}%*^L1&6R1DxMW0H!rqSQXJv;;sq-_3Nhx;YLeAYd zjVV%|hh~()k#a}W1!%~YhBo?!jE1C4IWP6#VTz^d?rfqV#m^%h}05bjs1sN7&tRtX>-L_2@N7YM) zr3Cb3l@8firPz@&r7WC3iq^k@XHbs<&?U6K7m4CPBx1lHz-|uqs_@9s#kb+G4y}BR z9{*GII!DwIv4>s5I!Nz&`2}i^*XI#jt5tTd!|iZA)q;FB=8|W_6FMKh5%dm~>-=s6 zL&yaqOUxIQt1vgQec*9j(2KJ+?rhP;U2%896J4Y8gnVKCxH^PTRfz6JD_A5R)$gni z`!VyvdP)5}^4CCh0X;FU>M3rOZsICiRE;dn12GtXi0^Dw1%r(cR+G| zAyCPrD!K^-W2u1o&$2TO_nGN? z6d((Lj1-d<>qM7wTac9gNa5|+aFuSs0 zn8``Q;;=%uCL3j7PNaQGmaHZax!wFD4;bA3vNoqLK<1apGzX6kb-Vq&48U zhgP?gtr^KxiG+=#HrzsWz1d-L2q0jCEEqc_-6_RQFl%sTLIpjDpJt<}u8m#(m{&<^zswd^Mk35sHW<`7)V`ijR?V4=brvT&hJ z5n{YvP1mH;NC)3hdpQH1;48I=6Tf6EcXe;0qrEv->uF$}tc;r3)}OH#XKwBhZA#`$a3 z=Q{+kN~%%~*>`r17(U7#eyog_p8sFUV83?1t|;+TRi5GMu{F>2@dw*gSa*F9pQpxF zS8vl6=QiJc%=qf?;I6pv`n(D6jK=Kz5f|g@Qyb_358JG(E;2!gR4Z zox;8MqL>C0PI&H{x|l1HN*E8Q-*hd4U540-CIMMI;8J=?bJ*|V)+6~72>T%L6T>5@WEp(t=093^!XyxY-eg{xs}ZHzT( zI|owRhYg3S_Z4m9Nr4H`4VJu^UE8;@W$hZnT=70`!7-H#mde*$K6B>fYf|evHwZCH zt3L_6XJg8p*zmOb<()5W8adE?(4);~8Mi2Z{J0DwlQGf6u5j7E{EKv%blC-hmID$f z<}@(U^}HWuA>AsZH6@X9t*Z z4Lb*Vcf|O#hOB)HtUYhm3^?1NzpS-H>=v`p4FQix+$XfU&8bpjQFVP)O|xaxDvVn9 zRWCC5*A!PiC3mX6Ez<=No`pbDgwdsvBKbtXKcNN%xyPwo_e|3nwM0^hRDApj`_3AH zUodeH=7VL9KWmh^c#~V2sCqFOdV^$Xf>4lDu)0`VKE=&!GIXB%J8q!GQWF*I!K5pJ z!fQ)YOS4O`_f>Wj#kDPrf`L&EG-&KTr`O>fD$(T)*_u8-6{dE zv|Ze$(Av|~|Np@?qR;`q`{VNycw=g;vd(%#ZQ6{!`zO#%n{I2d#Vqjlu-;Bae+_^t zNi>Te!*8VFb{H`gV;PmvG6vSTvzTNKXBeOU2fByY z1z;u2!c2+B(4{N%EPGAVt76T)fmp-NyL6)Ztjyz%vwSs3>vZHgsfd)ZAY@Sl__d73 z5smVUy$J-r{MfV$HWgjCiOx%MEBC3(wjeQL0ipU&l#euz&ps}{v8G)zif zLKPf^_tRV`lB=kxUg#-A57>5IqM-od3kx2?CLH#qj zt7m(s5DU8#4x{v-+Tph2ajfgyRWZtZjKAT@7jwD!P{qNDf^qPW}EJf-@$*P{zUwH)ms`V0Rdu$ zK^}#5ffX4=-Qx>D6b0zOpb3*X2vur9%#$f3ju#PWRus?+^frhroN(~6q?syIp6n6l zS!t%u8c##!K|G}Q#l$$4}ZpIyjiL63hbBEL-cNv0bGQ*GJ> z+oY)%ZleR#pjn1en!=S7`$j$dPnW4;oOW=Fr>ERHJ3PQy)T zZn|mys~@0|=mTm$O7nX)A#;7O!I(8`=H*$ZXPy1*%;~c+KmG-z<(EX#2BZQaZAxeI zKsJo0H3`{}L7hPah+qS>pFFd1Wtm&b+>Vfkp03ZSZvioJ!8W=x(#}AoA{zDkqe3v& z7>gVF8#Yz6@YlU~-NUMi`i6}q29(5uPQEc3ZHyYlKX-FIxqAYEyj2x-abn21E*F(J zR`yql8hN?D(WGN>u;KLNVaHtTm}o#Fs?;LpoYwS0TE3?fD8N}z=x)(8#S+*NS8^H^ zgQzR$^f?^Gfudwl$3xF9AG3*<)&Aj+W#VNGujL$>*R(NQ=C5#Jm!K=6wKPRLTO&j9 zZH?O~&TYZ1v4KcyXEbHej-8Z=Q^9tU>2&19 zuieS{)MfWR6DWUE<%jt@@%-~APCWYj+_{fFI(P2#kDd_jd5b#?N(8`Gz=Z-L6h~M% zEb##FIBftbAp_?VEZgo2Sk|fB&=qqTZO&vOteBjsTvd(R0gjc~(ri_{Ez;g;pkwpW z+9Qg8yR}ePy2`vXx;QoCXSe^}dc<`wy=OBvjzd2w&p64G71v7t!4VvOD6yqwlQ2}I za@4wOstk0go$Zl$8$Vd2sxK?H7a7Hmrx0Gg$X;AuCJe4ub#G1Y-fNIS^($AKO$9tQ zwrtvx7$Q6Iw;ue_Y3eY$BYsLSj>Xisz!nusSwJp0m$@E0w)A5O0-`RX6*q9Mfs^h4kKZpJcxx8u zP;vlzqiz%i8apk0JPYJ{;vsI9?SXr41l^x_2RT(^;ttN6GrZy%du^K{TP915p{-WV z*FBb#Nzx(0j>kJXbd8$Xi3M*&f1&+Z^o`>`Gwx)SKYG|gzWUK8RzB**dg0>WQc=2r zWIT|hfP+Sj;gIW~%*xqOl;F>TData1K9F}q>myHJl6Rv_Ew1mYxnq_obJR|!$K?^K ztW}nBV|i6mrxg$pEI`INu$|9HIqK`raBI*pgms*djeItxB`mI(z;FW5KqP$ZxGoxV zBQ3#(rH1n!9p3j$qP7mQZlr@EOIJPj9O_9_etPUEsiZ)rqR6%{d*$?|jG;(U$7PvCdjmp`sMnEUX+ea6L0YSyn3Y)(I^M>nEm zlky{_-M4K|>t5pv&y?nE7aqR%-V;irtn^*!yOidLJMyVlg6MrouDpI=WxT0szL1$(zk7L$`W&bJ_pU$R3zr)k1Yf%1R>`a zF3>q$R%*_pRykJYK%lrwEzQWsk|VOv$K?gkK)5HzEMG*!);yi(A5G zf4RT1+Ed|tYGD122!kRZRSk)EG&}6dhfw>j{rmHGEf>Vcvj68u8pIzW`up|BPt<*m z^7fMP&a3|3_P)5V0BC4LB?|1B)d|n>O-LOC;;zVDM|FUC+5T|*&#y{p&5c&O*6+EJp%EKKcs&4^W-12XL8l=93{gg4v4pNZzY~T7J4W2 z{{3&=64c(*c>lbc40k>I@CgflQ`Q6s9A(aNWJuO2*%^3=%fPfqHj@N1`EystxDW1s z?Iu5etMC4ML$@^E^X$SmEZUo-Ib$itFDRf2BuB@zunKp|Q$B@GAp~MPptIv=$vvfh zC-K6|@3PG83CKyxx%%lOW=fUn&k2q)Wy*>Cy14AG6QHEQ*mCX`PW-O)cC5ChrWT4i z>O-ifK_pHusg~3@YEahJqj3FptbJ1;R>KVR56<1mNv}%Zt#LG#wK-wQR-%Q)6LjhK zO6pi`pFiGC-*`3FR?}YVRPwRkVcTGiv9wj^qG6M0yNk;IcxBe(NWP~ zG<@`EYwPB~_?ZJ6ap)>(zJfp3&VMB*r>+hsY}HG1#5r|sfz7S0`lCl5c=bG3tCn!B zGE^4T;6HL327EpR1(ZD|Lxl9f;D&+a%f)M?YxeI~4dJ%O$H3?N=;~l_($x9QIWu>I zU3H&p@xuEI_a1rV4JW_HW2sgJ9n=}Q5Sj}DV`EoLm3Hu5gQ{L%7s$N8 zGuT(zS;jyiAB}S2StVnARw-;sXQi_$vpG@GVHD4daM_UX8ykZ@CNs>xg<^4j*^dr))7>)Ecoq zq(e~|73H+OHJD6lrAQ-}w9?T#n|tCt;rdj|+qPHgu`vxHm%>Cj8J zi?>gd3t4x~wKo?$nl0Gy!ohtJO;V-uu81bF63P=Kf%Sa$&D*Xu&Ybe_V^xJKg!O}} zk>_6PIbl5UT1DP-!us{9tt%eud|1gy&f%`SZq@t;5Yzx1f`3)7K5*ThD~%_ldakLm zJi5`ivD{^;6r^v9|8U-6kX`u)W|5KG<-YPY{kycBYcF1x$~We(v(^*~*ZxK|I_cWv z+W230o6=%TsL40@@I$K2`72tM8ker9FU=Poeu`IhU(sjY*IWknSqa{=hGk zaV7yrS(WnL%aiqiDqFo?Dqg^FGT_de2(4JBkmpbmVwr z3dOUaeqryP#xEl+xyfCbPG9zK>%hGI#7Qtvvx`{`IbK>nq)Li(o6Fkc)+iFXDh0(pLEPCZY6NtuGfWJK{M~nx@W5jA?xN@`v z5Uw1t<`=@>3rDy!dH8M$yw0v@AQ6pfk35U!0r}@BAQw@W_Vsyk!-M|*c`lbd;F4Lo zCxbY^WqCflK0``Q%D1d2N7)VJY{6-&D1H%14NIqIBQ4F;HOqx%8lZ^Gbq2~LP&&8O zo;Jt%17jqT3nN55F6{A&(^_)TiMXVEIiMlOW-C!3kmu^ta zqP7C+u%jkX+!yR9=B&_lTK#?T234~y*#WWGYi4l+qtf)iK&}diibqd#LqOZ3=mHl` zC_?IE4Ru}c)OO)U+l6y*`>-({^ZP*Id9~b1MzfbZc|+*Jj^s zxO+$T(R@DU_S?{tV=mp`EHX;m52)yY zuHoI?uO8dQCt6&+wSIrVZ~T7awR7rzpq2EM|4Y({95b&rF~;Sd>UpPYPZ)qIyFaVp zmaD_k6m4T55HLp505lqR4Aqo$3fpa)3)dKms!K}id1qITr^oo(vB3kw0#Y_fd*kBZ ztpjb8b-L<) z@zF1?tE0%abVV(g9?Q1W?;0@-H}CA;!oT>iDpIg%<5ti9zm$xNHTSeAqp!jt2lz{`?_U?O70bbFRu%@>IhbBW1P)u_Tu( z8XX!f3e=hEx{Crsxkb4{f!6L;lL|ql{tWl|%|ij&T~(wOXV2tlKY7FC@8g>^8>XMT zjce4TH5s;Zp7aMY^&_AHcG&t0R~w|qH7iF|M9R=;7?t%ymaC=P#M@Njp8x!o;_`Vz zI*70MK=4THkaCk*JQxecc!-$EuAI`0IN9f0hA<{?RJEK%qD)`(Lv~>cfo8t_XR6j% zOH(^2zWpj{3K!sJVQ2Af=^l+3lzyw6*%Q{)IqNMoMuvr=+}1YDLoa>tIaQ}P=^pWJ zm2|gw53`y7C!v&^z@ja~B=4Mq_=uqK8wSsBGi|9m{-T_%WueRblS;tBmMb|@(26sq znOh1!PyZjKembGQ?JoLMf0be|eT{IyhYcMH-#NLiPE_Wv>W z9^i46XWDRz&&-ha?Iu3n(Mwg8Zo2Y}=)ZIP%2@ zv|XW~-W%~oykYuimA!P+3cWP_fAjK<(v4i3DwTBEVq#MR_mrxw35UEzeL`BnH>+Y{ zo5Lf{y_P#?#d-li`#H9v2+!gZJ3c9Yltr7!O&CyC?j@KDQB$$1_99t9ZZ2I5v)xp{ z3TD9VD2gfMAw=l6W056De|WxhjeO19)4HV{9QDtTL9w>l0%<|f5R>s&LZRH)`snIn z^U@9G$KmQp9;s3pK>+0xj*ChcbLZ?;MG00a4i_qYx=$UaFFd04`h7l~d?jt5XfzOv zh#(e@HvJfUEr=c=3Zx=X3~2LRbBQ?6S=zKt5e1TpCy+=Gv}7^+hc(!sRVsFUAR zhi=TxLh@>9wbtu!_<(p|7SdSFiL)?WO==wCSv49+`Du{q%$kvJUi@Z0^?gGc0O@%Ysy%3a9k{u1ck7ICpL4&LW9WU@Qm*ygK>w(&st&(4vceX7Q27><*vL zE=aTG7kJzc`DrvK6pRb@5|0X7dM>#~bc?7zHO8csVP?md^>ZHFXa_28Jx^kV2}DI~ zvY{?ht0PmzwzhDhiQ=r6VU(gwL7Kr5uZPPIJGA#P$nb7;R&WZg7Zn&rb~rDc&lOmt z-$KPmvgR83Q&h`g5GGsLyElCp*a@cB-7(s`hr#uK@R=CBlgandUnugeYSs;m~ZZJ$eDZ)j?P z>iIN_n#7SADWF?zO4(X~_nxm_wSV-HF{y#^tGieFCQsO)JYI{*5jsDR^vuC!7;rh5CZR*|v7=6$cbi}r0Yr8z%-i}NvtC{xe%WIz1(sIyx zK347m_UW>1@&qiC(FAHMF_ zDBx=3lV^EP$`Unu%pQj&Xa-@+kqRV((Udo-k%oW92hG?JOa-G*w)G_OMK2tg%>j!> zc2K`Pj2zkG#2-l)?nfw`3@6-aOIRaaCq2(6T}XwK5&U5ZTf-)N(`5~rk}i$>`~-{3 zZNf%N+MU3U!f9v1l{AMe_>BEB97 z@u)`rg}jnSv&k5RqD{t&n-mMA9SM8fgm1Drg$gKLJDk?67<0!PBT-GDF_G}Jm?8z~ zEgPd{`m&7<(-zU>^wcM;=vP6u(HjjFB-(dGy0EZ9MRu!mo3&bS+Z%0Br>4=Hu(kvv z>4J23w69;+@8~mi3(jn9jORyhrizKQ=d&ovEEMA}08mvz{ z3ryX8j(&ZAUo_nM)i(_M%t_%V$TZD+I#e=e3BQQgOggaVbZFlbI(n+^sPQSw7?egr%q}nz4H-uW> zf9(xvHP^*I;SEX3;^i*@t#AcdOp&0hOF!-5_2pdQ#R5qqhL-!Y>jYtpl5+G4Vaz5A|Kew!5|>6I0vx7F|7erZWv{<8Gt+=MQFS^n}^ixx5v z#6YIj#aC2}WuPn+%1TG%1Fig(aYgc+1$~ZN%M!hb3M&v0UWZDCE z{c+(V5y1f?jnkD74icq-%DnpnN)R1l3F;&SI_+uQ(HR#Hsux+;mCR=t5`vc`gyjr6 zQm&UOt(PYPwA^c#DDaEJs{i`dr7_e(se}h&tf{`WybqEVL_mNBSitIo&%4Xv(7bV9 zw!XKa7mv-GO0dUf8MC$cR4{rDEy}W5QF6!*u4*$Suaf@dD`&}9RAD~>xGQvoQDIKU zZ6?IJCh;lhUwC7U*=aZXz<%<%G9At6Rh;ce!++}v{ow0$)SVia^KBvd92s%ROZjdI}# zzhEV2v{WZrb-z-#W)m5UaAIY`=5MgtSQxxi8sfkGzcDtb6ert9(HLXekrY?nNG<|+ zb>x5jH7{Kye~G&R88Jfo57{a?G_n*Up82)hh~%2Wt1<5#HfSWf8tyFE0S*%xdPkA^pWK$VV;6-nnv(YuoNO-pCLcPZ-UcpI3o6`K|>1$1PN92!^;3o z{AZAR&_dTscT0EhRaMIlwYP`E(ED-~^ll=7co!3hM^&?z@ZF>MPo^4xOF}e^TLGZ0 zA}x_qAYj@Sxi}V0X62X2i+nlW+IgneAT}zc40;huhxv2t#!}@%IES|G++9nK*9i;9 z$$hZeC5}lwkF0dP>dgM*r!xXL0(r#?gSyF+aindUnkviYGM!YMMe4w$QCY-ZLXJ(j zjOhTwuOe%s$Agjz{$$s}CWS1MJQQyulMz%VANV6*-VMzt_3I0t`$e}Q2)fZ zaMt}g48!s*oKa~?y{&4CcVtYWeDg6yP~xKrqtF7X%+o+1G%Xuga2jL72PI-=XfhGY z(MRf+*PniruB^q_gwV}UFBsI18rK)k&|R%AFV8o>22^c{jQoJsZU8)+kvVlI|F$F46x&|#teX1EInDbp<WArWf5|)q%j&|bFS%#zkzIBhP5dX^M3)bcSg&-ye4qB9T5gwa=QnM9?lI0% z<*e9p$-V7X?Oxm4D}Nw;!19fcKc$=h@ONCwxABG}+;_W#c{90vswCzUs~sYNkEt5W z>85A^Fc$aML8oXctmrpB0OSKV-fe3g9Fhd012^r{a(r2oI+L-{Ikm@ILbL<-geD!?f6 zC=9nEfAbHFMT*2S7FWu5ny#(C`yI44kp7ck9Qqp?8LWM!>PL$OY53C|0mYddHx*(t zXao@Of}~~MP=uDH-gD^v=T;Ij-lb^*I^h&Okk>ApB}l*=k!dO7;!>4-)gSo-7L_#x z3nZbZCrAG*?;){r2zp!Qp`(JvLzxQ4xkAF&a#|V-?$W zc_N9|GwWZqq>(L*eLo zf&6c0*0==egP9FREhIePlHMqNgEx3gCXc~Y6R&HwX)J0$BZERe_|3tfm;}n6!1LV| z*>VW^LDd>uu||$zQi&kT=)TnH!IDdKUxcxO{NEFlhA-;Ya}Zb}g9|4#k#q>66ix0X zXGU-(OaVxbxqN6wYeKew)uOX_t>`9rAms=VE%};B;c#Qr6VbR69<&?w!|hTqs$F4k zC@Ade1>YO`m4WqrppfJJ*BR7gbk`6vL`)*GGcOqNbhC@G)?UoJAwt7|>ZBKNU!fIG zzDfj(65FAQOOs_I+SANK{ImEF%U_bdz!xs!jP|-ji|#+w&8^9dT^L@JvHPoyb{cll zuVGVsDJc?uodX+2Ns-jV zjYiOSbVW2<`WyBi(n%*KZLw`_s1aOjhfweLM`D_`&Q$*{-7a%~ZKvRjnZd!vqoGr1 zu8D8mqLbHtpU2~%y)N8h=OS@@f&5EVE@4tNyz}0X4$Kjd-oB9NK{YL)%-@JcIYhCAB`}_hq zqH>(<+}+yBc0kg}2otukHY=VXIbxH20p4JGX^QYZo{&ow*d1$j5EYKXKSp5OCdfC- z-{fIf0Isz;uHSq0SX2F@z3O;46>*5Oo`YB~tg2aTSuFjE|D6)WOi|TZD@_@3pcLvB z8&F#_Wh$+;x+zM?^EU1%ILC^ z?(&$6NIu}3$PSB!r3DiCtW;SDx(a~+gwrZ{KIqgVuC%BJKxDk4c&Psq=x9x?U|=yWcHG3e%>CH zVnlK0on%CTY3!Xl$%ulWsEQ*J5Ht}Npv%(fbv$;JBM#4F-C|&E*p5!EQ7K*%@4N$! zFd4GQ^M8z1Idzo9c`%}2xa4>41V@2D?(e*#jYJ6KzUGvQ?Hi{)Acc9Hf!aF zm@;Kr1`hD=Mu)*vsDdPkKl4 zTs<>%mW^ho#V(>tX|2{**Cm?FV$u#lTYXWEw?Jd-FXSiH^CW?B%D2^3*hMHR9(aj^ z*Qr0E$IRFm6|);4qZ?003e0T>vrp`g9WP)CP{r~!{$jn!Vlf59*up1P9IUxb=Cw|1 zzzFqRXF3o~WS`JVdeyf0j=COr@(@ak-rdpGk$fvyw@x2qAVGpAxoa`d>PuF4+A)s? zS~bvra)N2@g7~-St%=k(*N5B*ySGMjY7+e@qW{}|f~8!*VVF4h*l`+hoJQzSBB6Ie zd~@_baXJv)pDWTvi*mMtgh$KceH2u+FpMMv&yxsBZR+->wroh~4s}#l=#++sFobwx zG)KDR=X6wRuQc>=QI9>0*3LI+%3=Xp9)FJ2`GUG>BS%DJFizD9Dh_=g66zDKRt=*v zS*WTSM6!c#skh2j}yS0=WRh?DZq?tKg9tE*=)Xl#C!z)ht;2P_t+6|e@rJhcXKAw>^@=A z5^b9;Di@#)KABb+CHK6bT43TD+{R3(IekJa{a_luAk7h*TcL|joUBeS2sIdu4WWX! z(JzxHPF9^rbIqZQ(cNHLpuK=7X%yw-#WX#>4+})$CQ+J+wT@`&go0wrGvekvA10?E zaGSiJUvh-2HEnZk4Xuim4Y>B%hfY5=WZma@VnbwAXq&6jTniwG(v`B=(6CoJVpklT zZSrq;yV`2ATdlTOLWrwPq4Ib`zYCRlQm<-^hvM$Ay}%Sr*LUi-w7LhoYr`q-cV_;s%f?FF^MPY z(_1&`=FYQNysT>a@$;j0O4^mSCq#mc26Y5mf_VH-pLzDRkG^+K{*s6?l<9J#-=bl* zEe{x>&W`*P+xf8qWeqH+HiQBZVj*^ z27~SgYD3(RaR?E2)D!h+?jLw#(Xf8_jbpD5h|wUFqC=WYG~U#t`*(Gdt;rZgVE!Ba z#G%UdePUuwNqDJ#X^G3aM%>kRsN;lQK0jf>vW{IFMeCkH*J1tP!Ej=axPIfZ$_4*7 zp3dFR^OY}}Uw&DKr_k}D&8AtnOL+GYwA*l0nrl*FI;qZgy-=^ISnTuyqLe z2mfyCOR3#LCTz_Z^ayASR--W_HkC9#cDwHOhQ|ygg3+BYLWkglswt7lxJAQY!*jpN zRY)fCCy%FouozxFb zm_5&Bc}%SJIBPlca_6$;y5*5&spUeXw#~y?&YW^#;GPO6&xmcoNE??r7&&xMchGsr zd{A(<)dsobWAnnZM}gagj~eYuMe_>V${D(u@siX^!G8ET<2n8C^O@LTF*O)JFr<6Q zHfY{2#Foy=OjUk|HJ>0IOetVHqydPxDt!Y+8#cSeC%Tu{LeW%XsyEt7^d-i4x?Z%! zYSZq6nxxMbvyd>SIUt6Xw{7aIN!ZfPv|EEZ;oGQAr6Y-W=k~Upp+lNfz>+lU)WhNaRJ2&{8ux0?E!zkq~n4+Ivcz6U7bG94zXX>9qf_h!mnR9};5qoRpK;9*xb=prZq`aold9h5w!2$h&CasvOor^g@hk&CmbPWA z2p^@JI9saDZ#T=IlW)=*VPV&T`0kPKamrt%&nXJQBpVJXi_fcD+3y}cPmwidmFfIL zs;xaAlPcL-4&c}o*HEhQ?CP!Gx*b(rQosQD0xj{0Jlbq}c;osHyl0HOxXG8C6F7G17Mff<${zpRmAInP5;F&K(z5-rJkE@V%5 z;4l*kqPJ>k)ozFWHo6axpAc(QiB^s@DePfK$m(%9G}bz6E9Z_{gK)WVd8}baqE1^4 zYAxCp$U0yWX{m=Tpf&0St7a0^|AJvUNTQw#;~>#+EJO`lt8$QNeC!}0621tdgFJc= zSJaj}NQe$n!ZMy(TrL^{8I1;` z&E$ZfdD?D@H|QHoHj`akrF!{g&KH7;qMr5+_M|T){_JPtHsuX)A|al*OfmWw9YpN#frQRpZ>V{*N}05Qsrn+^Bn3Sx zm4e?ek$OVxA28`5iw*WA@&RCE^eg8XCGrU{QqcnKi0i1kYtE6w@t1Z3%Zv(fg(fcU&!-p}9+Ens*_jE1=Mhu7D{@ zjDDna%9M}Ttz?rEi{&ZtzFabi$*+ueK!d~CWJRi?9Y>YVE46H;{(%xy6)Sj~K`8|; ziiRLOEye*WKL96&@s&`^5H4PiyoI1=bc1O2BK81zjzA8Hr2FyPKGhIjpz+E-G!L@K zX+4VSfn(liuQ(7pvLp3#jlcVMvQ96nA>?nj`ox9u5Y&pp&ZzzkRjZ`p0G)%-iuCv& z_`B+hS4`EHsjB439NezL!hWFG!m&oNF_w5CkkmwhEs5$As^^aE_-RIr^muj{TUc3A ziv?)iAGl^mL#j?+N`-r+IXa-~FZ{OOp5+2#WUPmA)$HSx*sR(!nKP02u5HU>5ZwsiaYHM_k# zdw1)5I>_B&HRf;3<;0HOtCDmF&=zhO^3F)<5 z$O1@7C>e?Xn`GLp-QBoz^E#bx?dI~ejeor)Fj@9qo~(>%Bwa~1iPpb~sK`HbN%tG; z9#AhzcZPZ{lT_xEHDhnqTr0gXSgnYYom2D`vV-#H)J@)uE92BiU(LM}6HA!hEpa;t zsLLt|7e}u?^3pndU-)VY2mdM3(+E?QUi^5D&va%>f_gTE`lq-BqzZ!ePDA4 zopmkSHtNdM&?Ex5Q2K&W4q@M1$XW(>{7L2lj&wva%GmgaaG)ix*&tZa}YEu|y}OV^RM0goW4PVjC2i zV~#*AHmi?c*SS!*?g2jCI+)p`-!o`5;meCuQs69)tFA25t)=1-?n|?J@ZQdOVWDd8 z=mE`EugfZ?Yh#rax(fR?Q?1|zxGlvNt6LFhtt>5FxpJTsZ5QZKmJX~GCr_X>Iu9;j zdA3ws_?f=6u^7z1lV+1gw9a3#a=u-& z4El6R^@hqT)NPxHxib%U5@4)}Wt;Spg+ev~6X2veI|f;=mZwq8iWy$O{=HhM8zI?L zN=Er+M842E1##z2*q>sqB?J}C&6uM}3^JZo4Gwn<4h~nA(K^E* zod?}L+5p6uJ&-K%FcoKaIr)rvoUW}&h%r;1R=pO$T2gWDsH4BQJvz4^k&jSW4=@b= z=FP6fHz60T?@DqoKq7a=%}{_2c!T-_Xmlb;7ehO_2wL`$P?tp;#8(@|>~5TsFX#!gUZB-+1A3-ZB>O~BVLZi)-xw1W z#wa4X?C2}HMDZj-$z|dMqP>krO2Z0q!WCarY2sw|teQeE_aFm&x!e$fkuD8XsG$Q7 zfq5t2^r1zcaY;THTf7EQ^@C{(tQBK6kW2NqAWxw&Ool(C8wsli3KK!{V-=es;A_!G zw>uNjh`4iaI0APkXF=Ecqc1{W8WkDmiJxeZQ$yEGc~$W3>uL2w`@2{T);IOZ@V4ig zS@m)g|9F%$HJaQu>+03oNBb5X+xYyxz1ny@l=h{aPifmdbh^}#syyJXn4dCz!}5or&*CwnMGowEqNS$a*ODzj(;*Q{crw;# z!NG^8KQTp5(vY9bbgc&HWAFfwN1ytXO6Y-<&%s@0_)12|#FU&>W8Umx^XAAmrKNn^ zx}{Bz>mOfgs9h&kSxr^jboH0E(_7R#Ozl|UY7&)59IOo{ zahtWFYKzVuutT5EW^saaL~9z&|9|6keDgFL(!KseevvTj7OT_l5>RE0Sl~e51g3dO-j1%%tE`PJ=TUVS*X~<8ZYBFmE zVHPB$5C93oD#-OK_~~QEw8J?B0$@X|b2Q}vzQ1d0*~YC~%gVNP_4Rjk_0tksi)p!S zYzeJZCKT*_Jcc(-K#D*A_7p_BFWhBpCcfWoz4$qCCK%f z8Hrl4Li-=v>BRi2Fy3|fKkzEd4#;2GJe$TGJKveAd1ty`X$)wjd4+tUCA8h$Vc!K8 zpwQgt;w7=Uq4hnRb~)Nz*>Fo@-|9=0E?4i<-aF|sKViC{=_uu}k+6rNunF@y|f9CR|e;sHB;^4l_B%<~E)D?c-{9+>=b{$gmq8 z7YncEdxwAoq%IA~Rf`E$CcY%?|3@N}XrvY+?x5Kb@OE#^7xTt;XSIn)5Udqv(iF5h zHDz^9&uWT#no)f2-)qNBLAXr*{Z9w>)JM%jgIPett}1|K~9e+TcfoZT@Cd) zqYo(XNBYJS0C?s#(5zUBIgg|)?X5ktZx1_2g98{Fk+bcZ#jX2jcz4QdgFzQ)H7e5D1o@(Fr0TcI*^BJjYWX5!;~Q&$jiVfHUc`Z68j({I^kA#n-kQT5H>_DRa^g)--yk z9o6VWdJ7?0zt1Vcjs-0%g9~=nmf9*qU2Ro)dFjSTNu;W|s>iAEG#yW!(uY*1j+uka zVo#*0J=z`_EbS}rQP5b327Br{w#9|BV@0ut)D^b6%5q(KYh|ZRfCFdQs%36rfvaTS zaa}eK^myQ>Iem{Q>Y0aUzhoDFZhhtXpXtu3cf?yedUQqAtOA&?{xQTRjp6T0kN)E# zzz;@X?lDcZuAJ&h=hft>P4;_~tpio5CN*hc-o@z*14PvUuo(%Lgl9~Upts1G7IWza zmt#>2JQCbd~Me&d={E4;O~Ba zLKfO9_{%@^GUS5yf%qSVxs|GZ``d9diQRtFP1=F6L4%i-3~z7-(u;Um=PrM+e7Z|h z;lqMWQG0W1dq8+9!leUgSKPLDwYD|hoM=v=zG)^V|ELYK0z;$39GGE#bDo1@v(2SLD=cq^ssf5IRef2`;GjvecJ ze)V4Mr|iNk&x8o{cjTGokDV~x&zceaILH|^-DrLQP>f{U>r^0?@TDy8J^1RaNpqvc?lb$$F>A`Ji7n$nHq22Clb#M|OMRqL zV}+brLQje$NT(Qq(z<(d?^;wjz9HZHk2}?0)2ldLg;{TtDF3&bxc!8;#w^z>O8>3y zB@5)K7mF_kOMEyCQ*XS5#kseT#Q%XnGPG60!!A^rLm;8b4Mi=g)YFJWLWwF8%I-qd zq!p-A=W2yQmHK{YM$LPid%SP?!A+-5J*5qY11M26{6d>xUV*o;$}LI*kF?I|T`(~F z=;CKfPi%gs_64^lU&wChE5}FwhR5IALVz>F1T+h=C8g$)}9;E+uPfl;%?EiAuaGmF6BRst$$H^~0zo(A92-qb3NYey3A& z?>+at1+^upD&aqVE7X?Wy!YOFI6_!QoDnDVmm-?C-hA_34%Aotov_jX{@nA{TW{j8 z2}m(XFr5GYQ0MN@hJEgkOTUicGht9y>3}S?%U#f@u8MK=ZI8Qdqt@%f6?s|>P!NZm9!%;` z;jA-qZVxWn)w)p|bca3RKzkgY16K-oPq0esYy@yw|6~pm8tUNgc?%6(c&xMGfRZ;f+_%K@&=vhtonEu2UxGD#NYSe2}%$kf14C_K-` zweXlclV97uwu*DOZE&lX{^cY2U(|M+%KHI0OjRSBf#YBDd;h!%Y&GqgAgm|=}B{uAm-*^d723#Q+IyU#wSjM3Wh&b zdKegLWb-3ezsc`fa{5(H!2-UBo6IWD^7`E98GI&ZfG97KzQoIYgk?ZUyN`to6fd9+ z&N|lAY9GBm8Vb3*y}GKpit#vd?fPtcww>Db?N!-oL4HRb_!~SHi$Q%KECIW9c)}nW z3aUYLbwD|0yTsi1f%2h*XPo<1$)Iwai}_8``C%S~=|PqJ!35<`D*Ui>1VQ3E zB=*@n|t|=KCHDXw{M!t)(DubLhv!;v~kvS z+Boz9l{{_2=vhz4;n`<&*6c4O!{G1cPJ1Fq`O2#qPn%gn< z!a6LttlXdDE=!)S99JUE2=>dVli0UtY7pVHSyDZiRZ06Nu|MVOOZXLQRin!AAF7E? z9g9KSeanIFF=3+{#`NNMt1qmDN|FQQYq0o6_2CJaiwZx%afOmCD*27~DZohzPa%A{ z?+MTAkUz5XXB^7O{R|Dv}L6My!+I;A5fFrnQaA5((Q#*uW-k&V2HiqC+sYk zjOC}iprbGFO!-!xG#7>Htqjk_ZuP%YnLG&|B6X92Cb_VJIk^ZWSSpd3UtleKVNm(F zNIm>Z_LPK%s%9Y76u!LJpzMzc|Fiw2gf4t)2M@%%hR?QFSE=NQZ=pCt4XD5V{$p8@ zui~>q)j5j?M70!P5qkmoED88BS|Oepwf;a=7#bcUAbtv}r&>O*u<42HN{Aj@GoyGC zB_d8O4F#Puk@)&_FyuCLuu%@9axlPq2>Z{ts?<(ov`1EU;d{Vmp z#OT2!^T=cT#jmK1CaWnZ+>m|X+@ExRdhOhY*_=q%;3WAwERhhT@2E0~RK_FL{qo@# zf7Gfm1x+O4)i42HFs7~PpzzDq7hi0B2d?VYv{5gATV?hklK<;G;Ouh#ZQBcv#HVTG zujl6AudCtAayS3p%dh_$O|kdYw|uub|p>!}FR*HyM@k32HX#;R3>w)|`4 z1i3}7gz6}OAYK)SW)VC&n*#<1_C^m1EdjtEyIQrVvor?vv53|!Ue5B>1(aL$7?lg)%&;l+D)P-3}2`K7!Bb-*c)^gxPoC~x1wr~*1U6-+|~-|E0Ch&rU(<#RV`ybyc##=_x(7`$hj^*iE5Z$Jc3)abN( zT=ZEmhY-z$0}O-#m|x%JaK(=%y!MOv*A^}N$;}Ul zc9+#}4{L6IZP_bBxiQUme5du2V_hfcV%tHu)AKVpZ5n#girTkm}+@EER0tACkHdlm{4@# zLXm<2kzT%upEZk^vKBC90b?f2UI~ngEn$ZvXZr!>OTnz%M?^ZGj=C%kB`1 zijdKRw4=A%gAr8QUP4!pv|-9rixzSf75-J76~`87`(Yv#hN=zr8-ZIuOdpGRoG9@Y zfu0PxFMYS_=+VwD|GuqV?3og~XM8Iz<&Ry`ii5B}q)lK=&aCE`;1Ej_ZjmnJ)M-Lc zrx_abP&c$%ni7I^FIMby%)i|flkr!rAyH>( z7@LJZ4az!C?%*Yrx&_&TP58GtT-xrhG1-4OM{BSV^PPtN$4OAqm(HJ;(AoXoc@UmT zK>dQRRxA;TI)y)=%7Sd(>ALn_IMY}oG4}g<>0bU1Z*q!>$F*-F5dsoGhh)T_a7H6S zmvroJL)zk;4voyni{vvSZN0j_l{f-#9FZo;lX7J=rGj>e@!hbzm{;zRhHet*G=7(_ zK)OS|v#W&>W6fw0PFbY3x-E`~h;RrsU4J`@EL{%DzxM*W1Lx*><}%12M< zb6D$}RP{ToJNtF;e0*1sH$xaUmqK3{O`%z4Ep48UL>37w^cz_+yBILYE%Md;S@u9& z!c(R2m-J)Fc)V2W0P;e4Aicz`C}mL%)eqslMfsoND*%ag_KAh!kK89j#5cn@b~;Z* z)&!AYA&d&1DAsu@HsS>%@B`|LsQ(Qqa6abX0~fP5R$yIF1*O2s&JGoP%-+ zusE*oBiyT8zZ_BnM2{_+@F^b*6)}b^I0||4H+T}ju$o2b0WW{gbWS<5O}*&5(wQlz zrD-xe_dNP4#KBiYj@hF9_nzGlPMmnUpKul{Acvb{rHhwbmQ-5ASJPS3h2omC)d(Ch zKydwH7MGAm@K*z8h|6Kzbv(@r7*{lw@|$QR<$pFjLHR_%?<$|L0RwHf33!wQ`zP!> z8zI2HD|u0I9e=YLbn0kGc8YqLh7@0K7DDb{gra|v1y|1H0YdDPnEhip|qi|H`wj2X8(I-}jOzRn%J zQO(ooV=Erl)jI2~CIOZSW;{Y0jO3G==4`yHNB4{SU%b6u2)xH%Z@K$!^YxC0x6j^P zf-Kh;qy5FAWNWcuZ^P;IGfhLMPP~-TNT17oc>6WF6%A`DHVIH7HG@!OYXS*C)3=+n zR-~`XpZ}xG8`#477bhcRsOa#w*$tTjqw=?S}pD*#igm??3B5u=2^Z zp}AXDKoYy_K-2N#x<8V;^w%!04%6X2OCd%!$tP(NOo<;UuK{sYAe%`!ITPr>8aQoC z8$q6?AW)c42VwB>>M*SiL4nh;(X(OX3;gV#aI+W`2wh4-13}RSh+}kIrSDDWP*hSj z5l|hCOOlL(fp&w*Z17*6l!S*Er5j%&$8hY^3)~02d$>pv zN|5i8JLdlWLsa4XH^|dJl&25-rMsk#r?11mhW*%ngEXDRH#$i0vh>WrEhNOu1Kv378n{~ z?xFLlO$i7x3DVEy-8@j;0e3(Xv6^Dr^p!?vnTalt@XUIPD;TniW81Bw%R$@Cu0R;u zZS?(ZCcD!rk`t82ry)L|4Ldhxtm%*t19dqZ)P!T<0G+4T6&0O{w0#H8GZsmRL@%W6 z9(PQ1#xr(0|Br7EBioO8G--Dx-lPkFlnt7paIxSi9teEu^T309+&=!+eTS8OgXT;7 zc1G<%0WBPMFUDP)%kE~^%20p;RD~W**fhqu0&c4PaT)2wdozB_il9J{>gX<*!L=dGY|+>Zr6Ew=6S1r*b;Ah?364d^2>7JUY(`*` zPl!}l8v%kGpVXlM*^LVqqiE{TSl^?_#|kRqb!v0S8nX*8KFpzsN_9xY1%y!;RKNHF zBGV91kH1$m-m%X?M14xJ@-M7sk4OSh++QfYu!z~ovH{ z_sWTnUXKr`&`z0#h+pIp-#FCBL}@Gkd!@TD{=9EjOSj3laluFc72RmmlN<1Fj6|Os(_~noCqn-gXaxr;vZiK+R zApZzS#D*%v(ia{@j@oF7+Bz#TrHyL>cl_jmJA9hkeGk3zu5NGBlbziH?6;KQ)MvME z+)%UJx7c&d@DKhBQSi@ue)FF6Io*kxLuI=Jyj+a{fzS=OTY8V5wrvrVLzIuLACaYBEGS%QJ_BvSEJq!u1Pl#D&|B zuLC3J_>;%Ci^ppg?wYNa#RwSF&u9fZkY|N8Ze7a1y8FMXZgwF~zJ2u1g< z_1Z!P?*Jl?N4LX@21MZ?!Nqf>g0eANyE68gQrCidSi>bilILfw{;^b}pMHxu_bwNI z_0`J`=U+l#`(pOz9@XYl#K*u-%A&%?{S>4znAg{p!)B^biO9jYP#Rp*lraDcJ!aB9 ztR`ZnPzWI&R&AhOjWi9XM)UeG=HbgU)>fdyiEy96r%G~Nj}5p@_$6a|u=dO$cz`J< zq-gZLf3tS|C!b9UF3nN#`B|7RP_NEqWf!N8Ow~#~^2pdpy{P`Z&F>!qiwLJ`47?wJ!V&%S?6GWSgb~W%^+XzBH3J6qo$ILm2 z?l4ItkaPku=tX%B;5I*0HD_X(sQ9Y%FI)uX6Y!LXkky3dK^eA&{+p#@ZZTdGRzO2h zMfJ8^xOqOexgop0Uk`*F9h_DXR2Nh`23hF-|DZJ4kR`RQO56rI6S2BM{lFuSKj0Pb z_dYx8b^SQR)1Wkw{flZg5HnD}{_JzFd&Sqhk397NT69_dT=mo}4V7UG{UY^}y=#sT zPMWDc4FNbUogYN4Z*Tb%>k`6Y1p&Xtd&92vI8_5DT4OPpnNE~SKmyZ;OI z$<8NFd0+6nJ@ch$0nHgOyV(leYUCPy#0Rb4q=f>y->a{VvsVZxs!D7@4?N1?S(T~5j?9>8o-5Bt-+qBTw z!}=F&*Wx79qLaXcve5)0&I z@A6E-G^_s3pEww>z@hTim+l|73Jm^E^3W`XWg$M6F7{SY_>bV|1Y(f^lyM2Dt{~gR zj|SAxXwMkMDZk4tP*bL86@D|#8UHOE;pMN&U(Jc2KKh*2jw}eJTL@I6o_KiKe|-dP zLj0Z8ZtZrB+`EQ9-hObPPk5q?bsFQk-zuHPn^~t3B-+i~iq%_-AJKjHwZijNLdKIu z7MXCwtU+OV%bc_C=td?^`r`w`-)k3)toLq>LQ)4ZY|lJ&r*2`@^3sjMhW?xY>P?F9 z>HDfgD1|oJ=AGPuy5Kdxb~UW%RYG-%pjUtmS;&PL0%Da!W~k0GMAsC_ z$}3>%EwP%;ow^I{V;cT!`DK$K1e+(THrG^au?z6GwOLUEVA;h-szy{k$D-^h7uXC9 zadSMD3+Sl;c1t|vVP!=F*obCG7Auf?&4l5?!ePQq(boVS1B)IiJpz3Vo$10HRwnl~ zX&Q)C_$evU<+y&>Ey1UtxKCmh(!@f(aCo>77%nf50w6Vxt5FVA>VMGo6tarkEs~Kb z;NUSi+T5l~zy4Mk|A{}$ee0fid=-A}R(425J{$~2*z_CJ1)sVH)HSB7d+A`^eA}c3imq3mR?Fe3Ji*nXPUr+?Z!4f15aUnbPihHNm;f*1rwMbw~aG5Kv zKJbxBnm)*vP?%UUn8S3%0mv#jFsMSeLBV}Z`H06)N>uW#N!XGP6{+94foJP_61IJ8 zU8h$OEBRDPyDh>!zz%(u?@2SH|N2Z{tU~u_sIX+PXc#UZD{$shOU@0drVb)BD;dP) z;AGpg0O1`dDLki^Z~7%)R@M*G(D(%QC&GrfvW0HI@{y}xJ&z)Ke?NXsYNbfi%j9L3 zZv@U0LOHjRBT&$r zMU4~j%qP_z=!=N2E>5Xtc{*Yt>RybmRG@7@u%~KFqsm`dEH}PRL0LTxMt9?iO{z~) zO>zNl;#0AO?pn1y_8Binsh&*61@ zmWDO6ql=c!(ajqyIn~w@i8l!+<}>6vHPfg#HUEV6PA^1FiEgJ*?udpb+NBxjuDG2h z(cC&?vfW(ovn2|aotp72&S^1v;Jy{{q$6?7I}#sFwFcsrf8rY}3tepr8ept8JGY^sPYtt_or zQ(-Yxcp#APvuX@ples}(*U;P=6dRKP7@W2A_O-NiX?of$CyMnI4JLzCbeJ1paHc_* zyp6H}DEJ|0Rqzel=mrnryjkJqGhyZa0;*2Fcg^IaFw^Fsxq0MY30Bx5R^B1qmP3fdo_hDx!cpr|v`7O#;HXoQzgyDpBA!iX?m?s05U)vH zj|En;r%&VXj$h3Blq`iP8kfj-DcWLmawvl+>lAF%rjW(~q7sTogcV3Y^EEQetD|i? z_QK_&7ib)8kssw}&V;tH(Qk{Rdz^{KHAjyEFr)lE9?!roL-W#1MR8YwOi?w4$;nV# z3WE*2l;L3vHU<6-Od?HPPGmWoRK2fp(V#14*Z)Xa9mSTGs(0#gB{oFsCu1e9s1f^|yL@}?S-(_vyEuv=&I z0t*DeYpWxcfRGm~tb*Zj7FOkY6984NyoFWBorD2GbD}NTq3?!;Ra}gjLY0bz)yQ1H zy0!ta=rGml8_da8v-o6p=ipPi^LGv1apzq#zjxy=ir&d&0>O;1r8!%v^Hk63$%TeSubGIopLW=1liGzl*Q1_{L;cujZf7; zz3c{!A6}(meZ7B6O9N1bNlUXQ6Y4M1+D%}M>ld%ta}?My{BO;Y2A9DBr<8t5V_zNl zXWrnnG}P$;eO1u-j7yP^IRWPjs-zzdadCJwx)bh%C1iEW;qPdFDIWuJ5UN`{dv-n9 z0|a6D=5?afN0@LR67r~&YB^5*YlooN=N4N347G$6XZXx;b3rQv*_gS)v5{S)F>t%IyUF*7ENB_OM&%3 zx9e>8v1bxzHR(n&PnIi&rMfY+qFlFIB{g(HkuO5E`F9ucP_8?a^i*GNi8dkkk z$X5?%3surp6RP`E*#NhJ&P0k2QB4{1K71+aV2eS_Uu1ykc>MHfa3cvEbGld?N9_=D z1vVf$;MLyDkS|fN@rsvi z@#!3lzPMP+T4`Wl;!yE6w(m#gF5e_r9NvZmz?w;KJn}?GPghpiH+4zq4l*?Sr4uX! zl!&a7UlHY#m|{;MvzD&SRa*C~(T2d`cDv*C7CD5qH1uE`;f)KRQO^=;+f#) zJDiX*7ESXlM>#;sB39VGI;|ejyx4q_i+Y`5RQ-`Of!SMfazW~NdLd^E#GG;1M3E6x z#|K;@XpdX<@?T&`qf|2$!oYjCY85+q@g}uB>WB#eRV*5fi9H?a=28?=fFaf=qsfF@ zwCv!}N`yaUM%}y7wi+&%QCm_UjfA6(Vr0AY6Zs+BQNrmeXcR_G7$T-i7Gi&N_eEUA z5c)}}IzsogwjL8PQ zwTKUAlm2iuSFMsBJIVj`JGkSAM({wUv#ce9L2;&297Bg7P=_ zXrH{QzUF)lRqutfI2ggm@O z*+kfu6_}`K;!iW| zBZcZhFmPD_oC~8_XpGRA-l%g*XpG^5z*7mDF{%hx2Vf@!%?KeW^j3buWUN?NLL*nn z!P&KLIIz|Msw5DMDzl1<7GpzxA4Zl{OWpo_7EE8J#I}%xeo+o2xSp~Dh6Z5WANoFt3dj>@1A73>K~?qE zE!DsvWZS#N59LL;lx9``P;-C(5CIUBg^RFbpH^zc;*F8&CkoZFYg`tK!`xUPUxk_y z)`ySOXyn3c?sp$G8R1A2aJqCZwfTPTceDtY#d@XA^~U82MmZs|&2vGaqNzp+eejUm z)lV}WPf38tK(yXp=cspE1Lmj$bd;1i8$it5=YO)k-`-puG?^R@FWUZ^U;G*5aI&C2 z@R+*)J93;EFu3#~oc782?nJ+)edBTo>%TrvYb$Q@G>pJkD zwnLRpg%fcRkp4tm7xPBlK#+Y6sLOs;3(!0J7;$VOmXk8YP1n= zI1~tP|CKfv2advJu+i)+)zNUq0H-B^V3-)EY2oz+yvPeZ%k2c25_T#sA`Qw<9e4i(sRyQ-xwv@EgAW$( zIVwPuGpGaa1D3_qpkYi2A=})Wc%rfSP4qYLA97IuZs(*KOx|cXt|#ELiH=JxxFvDp zj3<0Mt?dm3u)PM?q9tRjH#sa8H=B~F0J20Cj5bE2x+kIARaIlDw+I%4rHN~?)Fi9) z!~$}Fz2p#=D9tR{L`B#fTZrN%lL9fBJa~DJQJy&Ivc98KD!)NJVkRb@mCh4p883hP zN5EkMnM*mrd#bV$&S8VWlOyiT1W@65?w#lE>T|Mr!BZ=o#-&e>Y7|>cRCVDX`bnMcJC|t1A7b=z2eH1oXMD|gqHA^m71FL zWl~5r83lL{(c}Ms^6zm}5_;){KTYFfbv5B?y%A6;lbABckQtdjN~v3{O$`|3X9wsv zJMuyi#Pk#QZ{b5GW5B4du5sDxM8yxBe$yIjPNjr{uU&o^mNXDayd_T(4--k;l_QDQ zgCt&!@Z@r^1|=Kvbo?&a)I{X+>}9`$z^LLzo9{N0^crdj`5%ryV3rQp z-eDEEn(!MS2CcBQ3bYF|=sJ5>=M35Q?1dqlJ7s0GiLZ`)m0v%waQow~yX{3qw!1ao zHvjU6f7G8nv+vn%F%z{k8^8^6J3QjH`Lj1XoLQV)v&VW=^KRXXZ~PKUdWOml5zCK} zJ)xiB0b>OJj49X}1(S(ApTB$t6$|<43C(3|<7@P5*4i4%L}0Hs$P3gc@_%s@g1Qcd zoTPKkvVnH*o z2@vJ;#{&I{oozdIJoWgPP$9H^a-G>*`N=oWxN)s`uoT+m;5|K$&Miyy4L`Q^!a zdHaORWGbZT6PQvn^jVoC#^#;t*{Ko^n1x;PlyO1Olw3r00u*E_pOSRJXsrYwDq4n6 ziX>1500)F;9<4(vT{jq2{LP<8YkoWSj5qgQa%VI`NdsFuG>5x0fVj4R!|k#UJtLTn^pKZ(hfk8sfC$IN0U+E z@X|}bH=2w0jNGFPy8=lE(S{v^$Nli8MCc_wevnTEp)`{GnIvd4F>7msUhYvzbL6?4 z{J8wN%OM{)B|=S%H|a&NGg^)=oWMR|uOo1f^a;!B5OYscKFU?Gl^-cwlIjZzgX9x_ zN>BI0|HIvzfJs%JX~S8lsxvWTGBX^ex+-%H#zcckj1iEy04|7#3Me45+0EV!4K%%X z?fbs(RlPS2G|((Uw`>9~fZ7pvTtmRbB;x1qj1xFq|M8mq_w%0W2BJwc-^_P?*H40U zpE`B+^S;k=FX+O;*g!#xldi5HzX5bPLxM9Xx%7Pq<-S&Sk4AcM#69>yEg$OR@q3?o z2Q1>1%i7SYm8|NHR%2a6tcuiCuh+wI;o`wyuIOP-JQUV|v-4Zf#}n?+|%r z)-2_=l{_;`xpRy|ISPPMsbXm{Aw6~+nNR@j&>6?Hu&YJhx)eInY`6me3pEmYm{4}Y zuvvi|=C3eMB?~KoqE9>YNGc_&Ss11VUBghpWB^Y~*KZ|x)!sZ3q#Vf2fr~rL&qjnf`C4?4Gi4)EQ;YZl?^FMa2 zv7F{FWxo2QMAopx^%v6 zg5kcy*4MZcvzM}1EwGo$j{fbv|K55cd*tctOPutrj$6+Dkm3hafdBet-^FH|*ku=L+O@=2W4dseiwdclupEj{whllysrfNK^*= z<9F-bo?yr;dO!;BMSzij8y@G6`lE4OK?4txU_=N-!LNYa0Haw1p(qv6M6Qv`3vj^I` zo`5Il12X}dp+XQw89_fsR(0fHr)MF5h$H8;unzKokq88&zsi;SJ8sCM@>D*hjzDuE zjLaqoj{wLK`hx-;5z-Dn&PE^f`;hC<3E>n1em{p33ixz>bRB%UMm5ep6c(@nX#0~f zSOljZzT~1oG!wF!Fck=SA(9$Ic^#Yc`vm;;`vvCfM;8b;C`Q-U!-a8g+&Yh+l5@Y8 z{Xr(?!G0mI>DGHa0XQzfWC>fuR`C^%?3tpv7B!BJt|tzR-3DOIpyj}>YDl@NluW@R zka{t=*7!unZW@l*&(vFS$9#HQPAy$&;GKcXA0Qk66vB!RQ1JlhiMZ?m3dBMYAs7u` z@&JXSfA#={JUByofCB6RYGe-(d>PmS1QF8W6GVqX*ZV6yLG<9!6U2~ndYmFV%fI^x zTCse-bwb1BBY341$GGAvef{qzf7ba@{@|gDuk>2jS4a1&4jEUdef6q-1^d9fU`%8D1-CrMGf)+KtiH&f+ zu!0(7uL|}=uSzgPuL}HTn6m{vzmRIBhnT&M^Z?^h;sM69fd>hH^!UN=7foT7bNtn| z6mqCW-aB>n?5W;qzS(;%$2rI-!$c&NbfUnYe7t|v!PK_c#*Oi9+%u`g2j=LP7LC8B zABWp!0M(7-$F#9{hUPlW>&K?w<)v5xcIt2Nz__v0bkr!|*hmTRlTq5Nxii_S+o3ss zvV77JvF`k|^YV4=EJhs#3_@FV%48C*zrMKs$Jc9frmj?*F6Kk|M7aIkci%-RIuwiP zq~WW-=H}lc!K5Fm-FWUj;^ z5C8HV-A5DN8m%}IZAxw|vtD*IVs{F%Q2y>@Y&mVtY)=FBH4GzUFvvE~)X?N9!H)*9G zJ;84??o0P>+p{NeY}J<4TV^^IFDWm#F3eXT|C_B<#z%!<&!Lo!|H<0R^HtjJmMv^R z-$7&9H^V<4BY#7;tcdSDJ{^M-X?`C+UaV&=Q=fYI#3{e!FC`OL@mGlGNm@f{i zHx)-`qfs>n4?o&5c*2xwRuPjl=kI#6elFMXD=V5e*Mn*tO2s4bSjrc5aP_%P%LY9e zE9|M*ws>uoalJ+CEmutf7ztR`lbVfdRXuInT6gM?9awOwdM7I2#n!ugM$^z~%jSfG z2UX-?bXfylkW2ZRM?EO$dlDtphac^7K&UbiE;-!QbvTlQex<#n^U)*KVAlmK4$quw zhUg>aiu4bo&&pg-m=L`8@g-C5k*|0`zT$it=T`XK=adXRs(GvD@QHVIWh2H)KT#j5 z+&=389dMz);N@S8m42a#08W(G=W{+!PJ~R=FZdU?KXa%@kbg2(3AWj&4@l-EfXyNV z^N)*QP(}r}T(-3>3%w93yHRlsDDwpimFKi1sFocghKNT%KrY-js0@h89?Ey-w)(7@ zyKsek0}n3uKgx>(^1sRd*k}l>VG`B_CSlzV3F|~GlhZw;8EAhs@XI>x5sf)!Aqng9 zX7dJ;uR`P#@6N+An_`=}C=!>>tGvhSn%}N9#XK$P)W9#b zsZ>6a^KNo$x5|}#%$f2hB3{^@=5SLSFaCzZF{ghcAJ0HsPgi4b1du0WH#=4 zd5TsVwS%8mdQYXx+df|#Gqre9>0b_Ddy%|1=ia21ADYESYMgaOlc8y@rNU+OnzP}2 zY+L3~b0L>+k9T@DxZCQZ>$x-16cptZ*ESN}VkWXQ6~S_q;Gj$(lj-Raw-KF-!B>nq z`4(TXI<Nf#DPeG-0wZuzT%RGWjuzavM70tvQV(Ak@f%@Mg6}A-U@+iv^^D1Pg*s{4p z>x+Py6I-9dN61A%Big#VDI z2HkMKYcw#0=&&bKOA67xXs{!Jt|yqe-e|Sf6NFT>ego-4M}lIsONBNahS*LAB&nmJ zP$b)-4MqGhZ@96b#j!I$4=^eomAPCgRph@YkIaEsq=`kLa5F`?VJud)G zLT=%frU&kJjpgt@xXwKA`Yp_(6*_pI@ORvyKoCpbxxHZ67-%k(yz>r5rj1R_C3n12 zRNblgDSzOA0vanhFu#b2oj>2<0IDoeGXH=BeTqb)M0y<xR#${W?{yBl$N@sJ+SC=-F>J3fdm_c-g>^`^4Y7IB!J)EPh!`rSu z_*BcTo#M%p0qGwnoup*ySJ=gKcb%<&yEhvRx7bqtpcdlwL5KH&sykZB2Fecw2YR34 zp6uD*zF)swV@rFWkaqky!F=ML$n>GAs%FNFs@Y4$c~e584^8EygHjVuZaxkN$Vv)u z4H=z+!}{bC@)E#IFzOT5h8~H8$ChgHxhyWL+v>4!W>4Cb(qh$Ny3f65u> z){W#%R+AYOC`-f|v&JpWz7%xcLP-zQGrS;xb1+ee+4axMJfC)@JsA(=HL@(|OFBXV zY`nvGz9TjSGysU&po*Dsa-V@6mn){9ixvUFj9JOP_i=5`+=29So921jzBv!*SoA3X z=xayxnYHD@#pv^c`(GcIVv)@&7e@1o_NvUe8e4^uFg{Gmyp34e*_bPK|$$$Qk$3f%JotdVP&E|CaL1@Pt=?L37dngIq975QvN0hIY zAK?`xl1w-qO~*2^P_qq}$QE>X=zn$}1WQ~Fo9^s$e9G1w!U|X|k|9yloJnuPEa(%x zcD&SBJ7k4P0gYuJ4KOaU2oL%8d~8=H3Wd0}AdmD{)`kz6-7wwFI zY0v=$C`E+~M^H#0y%XrM(Wea!+TW?4PaWC!qW-n#9=_*+g>$CE&;Fb2{bbi{;sAAl z@Sb9a#%B0wEQznC9l|ZFZ+qq9xKJN%F}CY}r*3E{=J`scAN#=-Lo1K)kx9HXWD$jz z0y4utQ$KH;v+n`@Jr84t`{o>1c1Ud{oFN%dg6RX}fnf^c=c6;sSy4S!`5nzHwj6@k zSWSI;1p7yt2&x(T2};)eWkbv5iOT2Wlz%WW_3+1_cqd#{dJYypV1{4Se~wLnqof(U zbhl>zvR%yQifxSN(;aa;kr#9&~Z?-jbi9mp1EajlnA3;16<+|3CXIARVS6Zsq zicpM&LOI|!JDY%kgN~{jB|uQI0?|lGDqD!apuOO+8 zr61&sVf?0V$roC@qVdDYuiljBq$`It1`Q@!(VX`S5^w&GA7|v@oFSB@Rda-T;lCb!DqPAs(BS;d^x2w~b?G~F` zFdr()m;Y%F50UXpq{Qh(Wm@BGZKgtgWSAmxUQtklu;&|~CP)}zGu}L2)#S|@n=!tnvLP|^#xsFZc_Y+T&A2~avB{UgXP*Lg zMV~f@EcqsVqqwq-${G< zg(Mw8#>|Q*GuKmj@+5@*Xdo;KCf;Y`Oc@Zka~kFd^pH$&yZ7u~Jg5PqC?Ao+VDVX%*8vTv#6(n}0o<2?Oqi@5 zjf8}U0w0WAb{!;KD}NiPCkl?KYC|@=gZR1fVpV4o;DuQGGS)McA&;vRrg8B^CrwhI z@g>sQgFL1QWwEl1YiMn3zywR+cqPK&T3Z`hfUmgZ+O;&WJ}=?v8JOPQ(8!{Mzgn5z zI*uxnOv;f8eN^WX_dra<7^%+~TRF#L8`i2&3Wr45WTu8knSt<2O-;9Lp~{aK*A<+R zfqMxCO87BH!!bX1(AM^!6&6)BGt*+LZ`HtFB@z-|>{Nlw46%URnpT8ET+|;%FUPL| z>e|I@f8l~ioHd%(Q1}@vIHEwr-icDwei42a#d#R0dfHKbsKuUf3)hjlwMZyDE5QBZAHc32789JL(3Zz$*iku9>i+=KOejK=QOy;GO&_!^< zxXz%qWeD@Vb0^#`C>UJ66D@Xl;pmLOcn^i(^oF}GEJclNzN*P-#Ta?bWk?k6_dO>* z{tS8^e~xO&%P)V?6s3QFSHRONcEC%U%x;ccq>RkdY+VK30{FwM24F!^u}?l!2I37e z(NPMR3W)V#oY;a!rPq5HmBOST#S%un{hpe9oNtpO*ILwup{X5ls7C1l(*vN zfXjR7&z!BP(cPe@?21N`4PEhUhq%3FUuaj$p4tO}J?)&Wt;5x!XSf_X{a=HEoXNEz zxl&AYxTGalZ_zacjYbUHCa4FVM!S{xuNy1(7+Xb8)B)E(&e%}7c%5zy`LfAl+W9)v ztC83#1#VOz6x0e(+9)R@wf|rjbQf9e(5w`pkyH8zSQFg zYKMy15Ey}v^7bin=1j43HsU`~=S~;2k{ap<=~rG(eGtk34jr_FtaiVZTV&_M8D|!HrBd0_*2>K)cn(ht@t#&E46uZYA5%iWalTn)l^s)Xfl*cnPLESuFzDn zvC3#%*G-(drlyk0%3?O#z0PQ?+E`rozA0>JQ_0Cm%}pBvh31l%Uv4f0HZ>JWsy62H z-C*Fc%(thfnE9^j&gVB)6`D2;txNf^{HgpY4xppstw}XCs=a&hb7$wtiTGJ6l}h=E zCp$YYeo|bEZp{@Jra&*>N@C}BMOI-@e@oGe09MzoNqf@vgn-r~B$wX@Q;+I9ie(ca zA_?q|F$r@X-f!wg&^QVP3KMByw;%7YLUDm_F-i+~a?Wdp9!LzQOps(~e~{HvD1QXb zkMj`vRbl}}_Ibhtl;dp}73zXZUlq zG)@4g8rS>|;;DU(kIr&DE7JhWKiVz20 zJ!BU!CyJdKfFD93;j_(>_ywdRv!tg?zB>a6zFWG|Az#xbKBPcqudhb{W65~)o_sWq89DP*M^7dhNrE3`sjPjBaQjBLAwpTg zg6-oz?~0#Df;;9lU~238UtJp z5A%d)0b)|(RHm50X3*WPfj&(VdaySxm2X^%49bbJ$n6?K!Iq2RSf5fs3p5~{I&tEZ zPzrb}iPtYUc5FX{WfnlZej_HzmD^Q-CIM6NGz@?g=Bhxk2J%iKNqXEcy^XS5?J(S* ztkcpO5-t>K`#YG5Av!0`#(WbB!A+O|Nq-l5fn1`_cXNA3 z(wl_G2yNyoO}3+E@Ps5Jt0iH+X4g?w1fp0`eI{2y)F+yC*m*cC{PLH@Lhh@RKmHgchc>%G2jqy?!wOFS z05z0V15g!PCHXL*pb-FG1FqCy8@Xf#fTyh8<%7UDOdMDEN8Em)` zx3bhi=x?7-#z43z@uYYZXM^l}m2x&^bqCnlh|;2{0xL(W6yN|w)P^#DmaC7|$R*N^ zLjrX}S9sE}!HYMv=+0q-^?)H6yGcWfhSs9{ISa8jfnpUPLj|;Yk#sOrEKRi)ipU5v zDTF@{jMpmEoxC$% zgpwCC^tkh;pUJmMw|@A$gkvx-SF~9jiDEc))6bd=@a8p?NVm$jqQ4nvZZ7%o!(xym zm&zjVLJDQqRcP*Oe)Ong5O${6dR&$H-FWGn`fMf`%7}?j1ZJ^QPH3fXNi!7}Y=-n8 zNwii}Q`_po!n$ZpT@_H4l#F5Ap|F{0EDd2%NP!TyOImYbEpN%x1uPc%2l5ZJO!MC& zrQ{UR^56v^{XqJGHj@psSu%1;N@-7h{v%$llj;OaRI!dHExsF9V_L3j| zkjy0PiIS7B8z^rn%g*<69-rGAhAiafO{!n6KJZ}X&X65k@L(boOZb!K_aA$C@=i{k zj%}}5tGa*7{1+^5LU1Q$_c{D7N6?@Ln26Wl3*2-ICj?bA=ovr#9 zbi1klCSQ$J%Go=^_&LOb#)U+eZxC(MtHExX3{ama&_0#E{CWO!;=HgHR7Si&To}X< zG%S8Tyq#eBO7p+SCQ|?c=&}~sW}iqa`Axf@iEORdus*W1Va>J*^v~d!;&Op3=!!#k zuDo_ZqGqRILvU-Mi)&jKG#OtK&hXE|`>=O`y>N}{-605B!fkR!g z3ztaQz#-+jll*?ojHQiBD#grm+nzg>=JsjczFQT=WaoBIxSLgD{&WFUpo$>;gt19N zy|a>01x^jI1$*5f3Mv2B#n(+9BR$UVM9~l4ht7muoWne(#MvPCJ4li=A?(t;0)y5g zOsPS98i&x6j)bUQG|p4s zaK2{91`9)e_Cz@jU-lyGk)K zceQD~8B5ISomj60)wMCw6ooHPbA*%nQ8F$XM$eg3Hco#0p(YR1_M1u!H_V^LD1k|5 z%9#ztliZYvy;%+;ZVtkFUH~K8cvpM{PmNEVlh9=quE6Km7M3U8_7lrobFf} z6wsVT+H>dieEV$tiuYryB6mhINLZ(KRl2n92xfIW^sCq)!k!v}aBa2rYtD`6A1E!_ z5)#LFv>IGXOq@UZ5qdS?66<8gP@i3)LY*y&OOf&bn_R)~Q7TQ)8KOzcDh!PHx*yQf zKKu*&_hA2i*`Z84qydHdG~@(462wde7MY#IgWN@5{IPfAp-RUnAsh zQoCZR4L!L%j(*#*c?YJ)Z%R*`n7%1K{lL6qwtmN+T+aqB<_a`@coWEP&{!h!Ti%|y z>33RCQooRC;jF%7tNW)XZc9y`?w-K)tD}2&?AROOo{2uSaF&j;GrH$BTs`^U$zlv{1NwZu}S{K%vXWVi7dQN&o9wl7~+{jBRc?O%~zV?u{ zK>9wv^yQ06vQvO_aa){m@%W75IUG9%>W({OXMB#Yy?yzI@A3*T>f~G0⪼z6bT&M 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 literal 0 HcmV?d00001 diff --git a/.config/mpv/fonts/uosc_textures.ttf b/.config/mpv/fonts/uosc_textures.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e89f1d8cb87262b02db089894e92a52e71f8bf8c GIT binary patch literal 38228 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 x7J+&k#Mt]{element}[:{paramN}][#{badge}[>{limit}]][?{tooltip}]` +# Common properties: +# `{icon}` - parameter used to specify an icon name (example: `face`) +# - you can pick one here: https://fonts.google.com/icons?selected=Material+Icons&icon.style=Rounded +# `{element}`s and their parameters: +# `{usoc_command}` - preconfigured shorthands for uosc commands that make sense to have as buttons: +# - `menu`, `subtitles`, `audio`, `video`, `playlist`, `chapters`, `editions`, `stream-quality`, +# `open-file`, `items`, `next`, `prev`, `first`, `last`, `audio-device` +# `fullscreen` - toggle fullscreen +# `loop-playlist` - button to toggle playlist looping +# `loop-file` - button to toggle current file looping +# `shuffle` - toggle for uosc's shuffle mode +# `speed[:{scale}]` - display speed slider, [{scale}] - factor of controls_size, default: 1.3 +# `command:{icon}:{command}` - button that executes a {command} when pressed +# `toggle:{icon}:{prop}[@{owner}]` - button that toggles mpv property +# `cycle:{default_icon}:{prop}[@{owner}]:{value1}[={icon1}][!]/{valueN}[={iconN}][!]` +# - button that cycles mpv property between values, each optionally having different icon and active flag +# - presence of `!` at the end will style the button as active +# - `{owner}` is the name of a scrip that manages this property if any +# `gap[:{scale}]` - display an empty gap, {scale} - factor of controls_size, default: 0.3 +# `space` - fills all available space between previous and next item, useful to align items to the right +# - multiple spaces divide the available space among themselves, which can be used for centering +# Item visibility control: +# `<[!]{disposition1}[,[!]{dispositionN}]>` - optional prefix to control element's visibility +# - `{disposition}` can be one of: +# - `idle` - true if mpv is in idle mode (no file loaded) +# - `image` - true if current file is a single image +# - `audio` - true for audio only files +# - `video` - true for files with a video track +# - `has_many_video` - true for files with more than one video track +# - `has_audio` - true for files with an audio track +# - `has_many_audio` - true for files with more than one audio track +# - `has_sub` - true for files with an subtitle track +# - `has_many_sub` - true for files with more than one subtitle track +# - `has_many_edition` - true for files with more than one edition +# - `has_chapter` - true for files with chapter list +# - `stream` - true if current file is read from a stream +# - `has_playlist` - true if current playlist has 2 or more items in it +# - prefix with `!` to negate the required disposition +# Examples: +# - `stream-quality` - show stream quality button only for streams +# - `audio` - show audio tracks button for all files that have +# an audio track, but are not exclusively audio only files +# Place `#{badge}[>{limit}]` after the element params to give it a badge. Available badges: +# `sub`, `audio`, `video` - track type counters +# `{mpv_prop}` - any mpv prop that makes sense to you: https://mpv.io/manual/master/#property-list +# - if prop value is an array it'll display its size +# `>{limit}` will display the badge only if it's numerical value is above this threshold. +# Example: `#audio>1` +# Place `?{tooltip}` after the element config to give it a tooltip. +# Example: `stream-quality?Stream quality` +# Example implementations of some of the available shorthands: +# menu = command:menu:script-binding uosc/menu-blurred?Menu +# subtitles = command:subtitles:script-binding uosc/subtitles#sub?Subtitles +# fullscreen = cycle:crop_free:fullscreen:no/yes=fullscreen_exit!?Fullscreen +# loop-playlist = cycle:repeat:loop-playlist:no/inf!?Loop playlist +# toggle:{icon}:{prop} = cycle:{icon}:{prop}:no/yes! +controls=menu,gap,subtitles,audio,video,editions,stream-quality,gap,space,speed,space,shuffle,loop-playlist,loop-file,gap,prev,items,next,gap,fullscreen +controls_size=32 +controls_size_fullscreen=40 +controls_margin=8 +controls_spacing=2 +controls_persistency= + +# Where to display volume controls: none, left, right +volume=right +volume_size=40 +volume_size_fullscreen=52 +volume_opacity=0.9 +volume_border=1 +volume_step=1 +volume_persistency= + +# Playback speed widget: mouse drag or wheel to change, click to reset +speed_opacity=0.6 +speed_step=0.1 +speed_step_is_factor=no +speed_persistency= + +# Controls all menus, such as context menu, subtitle loader/selector, etc +menu_item_height=36 +menu_item_height_fullscreen=50 +menu_min_width=260 +menu_min_width_fullscreen=360 +menu_opacity=1 +menu_parent_opacity=0.4 + +# Top bar with window controls and media title +# Can be: never, no-border, always +top_bar=no-border +top_bar_size=40 +top_bar_size_fullscreen=46 +top_bar_controls=yes +top_bar_title=yes +top_bar_title_opacity=0.8 +top_bar_persistency= + +# Window border drawn in no-border mode +window_border_size=1 +window_border_opacity=0.8 + +# If there's no playlist and file ends, load next file in the directory +# Requires `keep-open=yes` in `mpv.conf`. +autoload=no +# Enable uosc's playlist/directory shuffle mode +# This simply makes the next selected playlist or directory item be random, just +# like any other player in the world. It also has an easily togglable control button. +shuffle=no + +# Scale the interface by this factor +ui_scale=1 +# Adjust the text scaling to fit your font +font_scale=1 +# Border of text and icons when drawn directly on top of video +text_border=1.2 +# Use a faster estimation method instead of accurate measurement +# setting this to `no` might have a noticable impact on performance, especially in large menus. +text_width_estimation=yes +# Execute command for background clicks shorter than this number of milliseconds, 0 to disable +# Execution always waits for `input-doubleclick-time` to filter out double-clicks +click_threshold=0 +click_command=cycle pause; script-binding uosc/flash-pause-indicator +# Flash duration in milliseconds used by `flash-{element}` commands +flash_duration=1000 +# Distances in pixels below which elements are fully faded in/out +proximity_in=40 +proximity_out=120 +# RGB HEX color codes +foreground=ffffff +foreground_text=000000 +background=000000 +background_text=ffffff +# Use only bold font weight throughout the whole UI +font_bold=no +# Show total time instead of time remaining +total_time=no +# Display sub second fraction in timestamps up to this precision +time_precision=0 +# Display stream's buffered time in timeline if it's lower than this amount of seconds, 0 to disable +buffered_time_threshold=60 +# Hide UI when mpv autohides the cursor +autohide=no +# Can be: none, flash, static, manual (controlled by flash-pause-indicator and decide-pause-indicator commands) +pause_indicator=flash +# Screen dim when stuff like menu is open, 0 to disable +curtain_opacity=0.5 +# Sizes to list in stream quality menu +stream_quality_options=4320,2160,1440,1080,720,480,360,240,144 +# File types to look for when navigating media files +media_types=3g2,3gp,aac,aiff,ape,apng,asf,au,avi,avif,bmp,dsf,f4v,flac,flv,gif,h264,h265,j2k,jp2,jfif,jpeg,jpg,jxl,m2ts,m4a,m4v,mid,midi,mj2,mka,mkv,mov,mp3,mp4,mp4a,mp4v,mpeg,mpg,oga,ogg,ogm,ogv,opus,png,rm,rmvb,spx,svg,tak,tga,tta,tif,tiff,ts,vob,wav,weba,webm,webp,wma,wmv,wv,y4m +# File types to look for when loading external subtitles +subtitle_types=aqt,ass,gsub,idx,jss,lrc,mks,pgs,pjs,psb,rt,slt,smi,sub,sup,srt,ssa,ssf,ttxt,txt,usf,vt,vtt +# Default open-file menu directory +default_directory=~/ + +# Convers some common chapter types into chapter range indicators. +# Instead of displaying the start of the chapter as a diamond icon on top of the +# timeline, the portion of the timeline owned by that chapter is colored based +# on the config below. +# +# The syntax is a comma delimited list of `{type}:{color}` pairs, where. +# `{type}` - range type. Currently suported ones are: +# - `openings`, `endings` - anime openings/endings +# - `intros`, `outros` - video intros/outros +# - `ads` - sponsor segments created by script: https://github.com/po5/mpv_sponsorblock +# `{color}` - an RGB(A) HEX color code (`rrggbb`, or `rrggbbaa`) +# +# To not convert any of the range types, simply removed it from the list. +chapter_ranges=openings:30abf964,endings:30abf964,ads:c54e4e80 +# Add alternative lua patterns to identify begining's of simple chapter ranges (all but `ads`) +# Syntax: `{type}:{pattern}[,{patternN}][;{type}:{pattern}[,{patternN}]]` +chapter_range_patterns=openings:オープニング;endings:エンディング diff --git a/.config/mpv/scripts/autosub.lua b/.config/mpv/scripts/autosub.lua new file mode 100644 index 0000000..046971d --- /dev/null +++ b/.config/mpv/scripts/autosub.lua @@ -0,0 +1,259 @@ +--============================================================================= +-->> SUBLIMINAL PATH: +--============================================================================= +-- This script uses Subliminal to download subtitles, +-- so make sure to specify your system's Subliminal location below: +local subliminal = 'subliminal' +--============================================================================= +-->> SUBTITLE LANGUAGE: +--============================================================================= +-- Specify languages in this order: +-- { 'language name', 'ISO-639-1', 'ISO-639-2' } ! +-- (See: https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) +local languages = { +-- If subtitles are found for the first language, +-- other languages will NOT be downloaded, +-- so put your preferred language first: + { 'English', 'en', 'eng' }, + { 'Arabic', 'ar', 'ara' }, +-- { 'Spanish', 'es', 'spa' }, +-- { 'French', 'fr', 'fre' }, +-- { 'German', 'de', 'ger' }, +-- { 'Italian', 'it', 'ita' }, +-- { 'Portuguese', 'pt', 'por' }, +-- { 'Polish', 'pl', 'pol' }, +-- { 'Russian', 'ru', 'rus' }, +-- { 'Chinese', 'zh', 'chi' }, +-- { 'Arabic', 'ar', 'ara' }, +} +--============================================================================= +-->> PROVIDER LOGINS: +--============================================================================= +-- These are completely optional and not required +-- for the functioning of the script! +-- If you use any of these services, simply uncomment it +-- and replace 'USERNAME' and 'PASSWORD' with your own: +local logins = { +-- { '--addic7ed', 'USERNAME', 'PASSWORD' }, +-- { '--legendastv', 'USERNAME', 'PASSWORD' }, + { '--opensubtitles', 'alhassanaraouf', '123456As*' }, +-- { '--subscenter', 'USERNAME', 'PASSWORD' }, +} +--============================================================================= +-->> ADDITIONAL OPTIONS: +--============================================================================= +local bools = { + auto = true, -- Automatically download subtitles, no hotkeys required + debug = false, -- Use `--debug` in subliminal command for debug output + force = true, -- Force download; will overwrite existing subtitle files + utf8 = true, -- Save all subtitle files as UTF-8 +} +local excludes = { + -- Movies with a path containing any of these strings/paths + -- will be excluded from auto-downloading subtitles. + -- Full paths are also allowed, e.g.: + -- '/home/david/Videos', + 'no-subs-dl', +} +local includes = { + -- If anything is defined here, only the movies with a path + -- containing any of these strings/paths will auto-download subtitles. + -- Full paths are also allowed, e.g.: + -- '/home/david/Videos', +} +--============================================================================= +local utils = require 'mp.utils' + + +-- Download function: download the best subtitles in most preferred language +function download_subs(language) + language = language or languages[1] + if #language == 0 then + log('No Language found\n') + return false + end + + log('Searching ' .. language[1] .. ' subtitles ...', 30) + + -- Build the `subliminal` command, starting with the executable: + local table = { args = { subliminal } } + local a = table.args + + for _, login in ipairs(logins) do + a[#a + 1] = login[1] + a[#a + 1] = login[2] + a[#a + 1] = login[3] + end + if bools.debug then + -- To see `--debug` output start MPV from the terminal! + a[#a + 1] = '--debug' + end + + a[#a + 1] = 'download' + if bools.force then + a[#a + 1] = '-f' + end + if bools.utf8 then + a[#a + 1] = '-e' + a[#a + 1] = 'utf-8' + end + + a[#a + 1] = '-l' + a[#a + 1] = language[2] + a[#a + 1] = '-d' + a[#a + 1] = directory + a[#a + 1] = filename --> Subliminal command ends with the movie filename. + + local result = utils.subprocess(table) + + if string.find(result.stdout, 'Downloaded 1 subtitle') then + -- When multiple external files are present, + -- always activate the most recently downloaded: + mp.set_property('slang', language[2]) + -- Subtitles are downloaded successfully, so rescan to activate them: + mp.commandv('rescan_external_files') + log(language[1] .. ' subtitles ready!') + return true + else + log('No ' .. language[1] .. ' subtitles found\n') + return false + end +end + +-- Manually download second language subs by pressing 'n': +function download_subs2() + download_subs(languages[2]) +end + +-- Control function: only download if necessary +function control_downloads() + -- Make MPV accept external subtitle files with language specifier: + mp.set_property('sub-auto', 'fuzzy') + -- Set subtitle language preference: + mp.set_property('slang', languages[1][2]) + mp.msg.warn('Reactivate external subtitle files:') + mp.commandv('rescan_external_files') + directory, filename = utils.split_path(mp.get_property('path')) + + if not autosub_allowed() then + return + end + + sub_tracks = {} + for _, track in ipairs(mp.get_property_native('track-list')) do + if track['type'] == 'sub' then + sub_tracks[#sub_tracks + 1] = track + end + end + if bools.debug then -- Log subtitle properties to terminal: + for _, track in ipairs(sub_tracks) do + mp.msg.warn('Subtitle track', track['id'], ':\n{') + for k, v in pairs(track) do + if type(v) == 'string' then v = '"' .. v .. '"' end + mp.msg.warn(' "' .. k .. '":', v) + end + mp.msg.warn('}\n') + end + end + + for _, language in ipairs(languages) do + if should_download_subs_in(language) then + if download_subs(language) then return end -- Download successful! + else return end -- No need to download! + end + log('No subtitles were found') +end + +-- Check if subtitles should be auto-downloaded: +function autosub_allowed() + local duration = tonumber(mp.get_property('duration')) + local active_format = mp.get_property('file-format') + + if not bools.auto then + mp.msg.warn('Automatic downloading disabled!') + return false + elseif duration < 900 then + mp.msg.warn('Video is less than 15 minutes\n' .. + '=> NOT auto-downloading subtitles') + return false + elseif directory:find('^http') then + mp.msg.warn('Automatic subtitle downloading is disabled for web streaming') + return false + elseif active_format:find('^cue') then + mp.msg.warn('Automatic subtitle downloading is disabled for cue files') + return false + else + local not_allowed = {'aiff', 'ape', 'flac', 'mp3', 'ogg', 'wav', 'wv', 'tta'} + + for _, file_format in pairs(not_allowed) do + if file_format == active_format then + mp.msg.warn('Automatic subtitle downloading is disabled for audio files') + return false + end + end + + for _, exclude in pairs(excludes) do + local escaped_exclude = exclude:gsub('%W','%%%0') + local excluded = directory:find(escaped_exclude) + + if excluded then + mp.msg.warn('This path is excluded from auto-downloading subs') + return false + end + end + + for i, include in ipairs(includes) do + local escaped_include = include:gsub('%W','%%%0') + local included = directory:find(escaped_include) + + if included then break + elseif i == #includes then + mp.msg.warn('This path is not included for auto-downloading subs') + return false + end + end + end + + return true +end + +-- Check if subtitles should be downloaded in this language: +function should_download_subs_in(language) + for i, track in ipairs(sub_tracks) do + local subtitles = track['external'] and + 'subtitle file' or 'embedded subtitles' + + if not track['lang'] and (track['external'] or not track['title']) + and i == #sub_tracks then + local status = track['selected'] and ' active' or ' present' + log('Unknown ' .. subtitles .. status) + mp.msg.warn('=> NOT downloading new subtitles') + return false -- Don't download if 'lang' key is absent + elseif track['lang'] == language[3] or track['lang'] == language[2] or + (track['title'] and track['title']:lower():find(language[3])) then + if not track['selected'] then + mp.set_property('sid', track['id']) + log('Enabled ' .. language[1] .. ' ' .. subtitles .. '!') + else + log(language[1] .. ' ' .. subtitles .. ' active') + end + mp.msg.warn('=> NOT downloading new subtitles') + return false -- The right subtitles are already present + end + end + mp.msg.warn('No ' .. language[1] .. ' subtitles were detected\n' .. + '=> Proceeding to download:') + return true +end + +-- Log function: log to both terminal and MPV OSD (On-Screen Display) +function log(string, secs) + secs = secs or 2.5 -- secs defaults to 2.5 when secs parameter is absent + mp.msg.warn(string) -- This logs to the terminal + mp.osd_message(string, secs) -- This logs to MPV screen +end + + +mp.add_key_binding('b', 'download_subs', download_subs) +mp.add_key_binding('n', 'download_subs2', download_subs2) +mp.register_event('file-loaded', control_downloads) diff --git a/.config/mpv/scripts/thumbfast.lua b/.config/mpv/scripts/thumbfast.lua new file mode 100644 index 0000000..8712d79 --- /dev/null +++ b/.config/mpv/scripts/thumbfast.lua @@ -0,0 +1,712 @@ +-- thumbfast.lua +-- +-- High-performance on-the-fly thumbnailer +-- +-- Built for easy integration in third-party UIs. + +local options = { + -- Socket path (leave empty for auto) + socket = "", + + -- Thumbnail path (leave empty for auto) + thumbnail = "", + + -- Maximum thumbnail size in pixels (scaled down to fit) + -- Values are scaled when hidpi is enabled + max_height = 200, + max_width = 200, + + -- Overlay id + overlay_id = 42, + + -- Spawn thumbnailer on file load for faster initial thumbnails + spawn_first = false, + + -- Enable on network playback + network = false, + + -- Enable on audio playback + audio = false, + + -- Enable hardware decoding + hwdec = false, + + -- Windows only: use native Windows API to write to pipe (requires LuaJIT) + direct_io = false +} + +mp.utils = require "mp.utils" +mp.options = require "mp.options" +mp.options.read_options(options, "thumbfast") + +local pre_0_30_0 = mp.command_native_async == nil + +function subprocess(args, async, callback) + callback = callback or function() end + + if not pre_0_30_0 then + if async then + return mp.command_native_async({name = "subprocess", playback_only = true, args = args}, callback) + else + return mp.command_native({name = "subprocess", playback_only = false, capture_stdout = true, args = args}) + end + else + if async then + return mp.utils.subprocess_detached({args = args}, callback) + else + return mp.utils.subprocess({args = args}) + end + end +end + +local winapi = {} +if options.direct_io then + local ffi_loaded, ffi = pcall(require, "ffi") + if ffi_loaded then + winapi = { + ffi = ffi, + C = ffi.C, + bit = require("bit"), + socket_wc = "", + + -- WinAPI constants + CP_UTF8 = 65001, + GENERIC_WRITE = 0x40000000, + OPEN_EXISTING = 3, + FILE_FLAG_WRITE_THROUGH = 0x80000000, + FILE_FLAG_NO_BUFFERING = 0x20000000, + PIPE_NOWAIT = ffi.new("unsigned long[1]", 0x00000001), + + INVALID_HANDLE_VALUE = ffi.cast("void*", -1), + + -- don't care about how many bytes WriteFile wrote, so allocate something to store the result once + _lpNumberOfBytesWritten = ffi.new("unsigned long[1]"), + } + -- cache flags used in run() to avoid bor() call + winapi._createfile_pipe_flags = winapi.bit.bor(winapi.FILE_FLAG_WRITE_THROUGH, winapi.FILE_FLAG_NO_BUFFERING) + + ffi.cdef[[ + void* __stdcall CreateFileW(const wchar_t *lpFileName, unsigned long dwDesiredAccess, unsigned long dwShareMode, void *lpSecurityAttributes, unsigned long dwCreationDisposition, unsigned long dwFlagsAndAttributes, void *hTemplateFile); + bool __stdcall WriteFile(void *hFile, const void *lpBuffer, unsigned long nNumberOfBytesToWrite, unsigned long *lpNumberOfBytesWritten, void *lpOverlapped); + bool __stdcall CloseHandle(void *hObject); + bool __stdcall SetNamedPipeHandleState(void *hNamedPipe, unsigned long *lpMode, unsigned long *lpMaxCollectionCount, unsigned long *lpCollectDataTimeout); + int __stdcall MultiByteToWideChar(unsigned int CodePage, unsigned long dwFlags, const char *lpMultiByteStr, int cbMultiByte, wchar_t *lpWideCharStr, int cchWideChar); + ]] + + winapi.MultiByteToWideChar = function(MultiByteStr) + if MultiByteStr then + local utf16_len = winapi.C.MultiByteToWideChar(winapi.CP_UTF8, 0, MultiByteStr, -1, nil, 0) + if utf16_len > 0 then + local utf16_str = winapi.ffi.new("wchar_t[?]", utf16_len) + if winapi.C.MultiByteToWideChar(winapi.CP_UTF8, 0, MultiByteStr, -1, utf16_str, utf16_len) > 0 then + return utf16_str + end + end + end + return "" + end + + else + options.direct_io = false + end +end + +local spawned = false +local network = false +local disabled = false +local spawn_waiting = false + +local x = nil +local y = nil +local last_x = x +local last_y = y + +local last_seek_time = nil + +local effective_w = options.max_width +local effective_h = options.max_height +local real_w = nil +local real_h = nil +local last_real_w = nil +local last_real_h = nil + +local script_name = nil + +local show_thumbnail = false + +local filters_reset = {["lavfi-crop"]=true, crop=true} +local filters_runtime = {hflip=true, vflip=true} +local filters_all = filters_runtime +for k,v in pairs(filters_reset) do filters_all[k] = v end + +local last_vf_reset = "" +local last_vf_runtime = "" + +local last_rotate = 0 + +local par = "" +local last_par = "" + +local last_has_vid = 0 +local has_vid = 0 + +local file_timer = nil +local file_check_period = 1/60 +local first_file = false + +local function debounce(func, wait) + func = type(func) == "function" and func or function() end + wait = type(wait) == "number" and wait / 1000 or 0 + + local timer = nil + local timer_end = function () + timer:kill() + timer = nil + func() + end + + return function () + if timer then + timer:kill() + end + timer = mp.add_timeout(wait, timer_end) + end +end + +local client_script = [=[ +#!/bin/bash +MPV_IPC_FD=0; MPV_IPC_PATH="%s" +trap "kill 0" EXIT +while [[ $# -ne 0 ]]; do case $1 in --mpv-ipc-fd=*) MPV_IPC_FD=${1/--mpv-ipc-fd=/} ;; esac; shift; done +if echo "print-text thumbfast" >&"$MPV_IPC_FD"; then echo -n > "$MPV_IPC_PATH"; tail -f "$MPV_IPC_PATH" >&"$MPV_IPC_FD" & while read -r -u "$MPV_IPC_FD" 2>/dev/null; do :; done; fi +]=] + +local function get_os() + local raw_os_name = "" + + if jit and jit.os and jit.arch then + raw_os_name = jit.os + else + if package.config:sub(1,1) == "\\" then + -- Windows + local env_OS = os.getenv("OS") + if env_OS then + raw_os_name = env_OS + end + else + raw_os_name = subprocess({"uname", "-s"}).stdout + end + end + + raw_os_name = (raw_os_name):lower() + + local os_patterns = { + ["windows"] = "Windows", + ["linux"] = "Linux", + + ["osx"] = "Mac", + ["mac"] = "Mac", + ["darwin"] = "Mac", + + ["^mingw"] = "Windows", + ["^cygwin"] = "Windows", + + ["bsd$"] = "Mac", + ["sunos"] = "Mac" + } + + -- Default to linux + local str_os_name = "Linux" + + for pattern, name in pairs(os_patterns) do + if raw_os_name:match(pattern) then + str_os_name = name + break + end + end + + return str_os_name +end + +local os_name = get_os() + +if options.socket == "" then + if os_name == "Windows" then + options.socket = "thumbfast" + else + options.socket = "/tmp/thumbfast" + end +end + +if options.thumbnail == "" then + if os_name == "Windows" then + options.thumbnail = os.getenv("TEMP").."\\thumbfast.out" + else + options.thumbnail = "/tmp/thumbfast.out" + end +end + +local unique = mp.utils.getpid() + +options.socket = options.socket .. unique +options.thumbnail = options.thumbnail .. unique + +if options.direct_io then + if os_name == "Windows" then + winapi.socket_wc = winapi.MultiByteToWideChar("\\\\.\\pipe\\" .. options.socket) + end + + if winapi.socket_wc == "" then + options.direct_io = false + end +end + +local mpv_path = "mpv" + +if os_name == "Mac" and unique then + mpv_path = string.gsub(subprocess({"ps", "-o", "comm=", "-p", tostring(unique)}).stdout, "[\n\r]", "") + mpv_path = string.gsub(mpv_path, "/mpv%-bundle$", "/mpv") +end + +local function vf_string(filters, full) + local vf = "" + local vf_table = mp.get_property_native("vf") + + if #vf_table > 0 then + for i = #vf_table, 1, -1 do + if filters[vf_table[i].name] then + local args = "" + for key, value in pairs(vf_table[i].params) do + if args ~= "" then + args = args .. ":" + end + args = args .. key .. "=" .. value + end + vf = vf .. vf_table[i].name .. "=" .. args .. "," + end + end + end + + if full then + vf = vf.."scale=w="..effective_w..":h="..effective_h..par..",pad=w="..effective_w..":h="..effective_h..":x=-1:y=-1,format=bgra" + end + + return vf +end + +local function calc_dimensions() + local width = mp.get_property_number("video-out-params/dw") + local height = mp.get_property_number("video-out-params/dh") + if not width or not height then return end + + local scale = mp.get_property_number("display-hidpi-scale", 1) + + if width / height > options.max_width / options.max_height then + effective_w = math.floor(options.max_width * scale + 0.5) + effective_h = math.floor(height / width * effective_w + 0.5) + else + effective_h = math.floor(options.max_height * scale + 0.5) + effective_w = math.floor(width / height * effective_h + 0.5) + end + + local v_par = mp.get_property_number("video-out-params/par", 1) + if v_par == 1 then + par = ":force_original_aspect_ratio=decrease" + else + par = "" + end +end + +local info_timer = nil + +local function info(w, h) + local display_w, display_h = w, h + local rotate = mp.get_property_number("video-params/rotate") + + network = mp.get_property_bool("demuxer-via-network", false) + local image = mp.get_property_native("current-tracks/video/image", false) + local albumart = image and mp.get_property_native("current-tracks/video/albumart", false) + disabled = (w or 0) == 0 or (h or 0) == 0 or + has_vid == 0 or + (network and not options.network) or + (albumart and not options.audio) or + (image and not albumart) + + if info_timer then + info_timer:kill() + info_timer = nil + elseif has_vid == 0 or (rotate == nil and not disabled) then + info_timer = mp.add_timeout(0.05, function() info(w, h) end) + end + + if rotate ~= nil and rotate % 180 == 90 then + display_w, display_h = h, w + end + + local json, err = mp.utils.format_json({width=display_w, height=display_h, disabled=disabled, available=true, socket=options.socket, thumbnail=options.thumbnail, overlay_id=options.overlay_id}) + mp.commandv("script-message", "thumbfast-info", json) +end + +local function remove_thumbnail_files() + os.remove(options.thumbnail) + os.remove(options.thumbnail..".bgra") +end + +local function spawn(time) + if disabled then return end + + local path = mp.get_property("path") + if path == nil then return end + + local open_filename = mp.get_property("stream-open-filename") + local ytdl = open_filename and network and path ~= open_filename + if ytdl then + path = open_filename + end + + remove_thumbnail_files() + + local vid = mp.get_property_number("vid") + has_vid = vid or 0 + + local args = { + mpv_path, path, "--no-config", "--msg-level=all=no", "--idle", "--pause", "--keep-open=always", "--really-quiet", "--no-terminal", + "--edition="..(mp.get_property_number("edition") or "auto"), "--vid="..(vid or "auto"), "--no-sub", "--no-audio", + "--start="..time, "--hr-seek=no", + "--ytdl-format=worst", "--demuxer-readahead-secs=0", "--demuxer-max-bytes=128KiB", + "--vd-lavc-skiploopfilter=all", "--vd-lavc-software-fallback=1", "--vd-lavc-fast", "--vd-lavc-threads=2", "--hwdec="..(options.hwdec and "auto" or "no"), + "--vf="..vf_string(filters_all, true), + "--sws-scaler=fast-bilinear", + "--video-rotate="..last_rotate, + "--ovc=rawvideo", "--of=image2", "--ofopts=update=1", "--o="..options.thumbnail + } + + if not pre_0_30_0 then + table.insert(args, "--sws-allow-zimg=no") + end + + if os_name == "Windows" then + table.insert(args, "--input-ipc-server="..options.socket) + else + local client_script_path = options.socket..".run" + local file = io.open(client_script_path, "w+") + if file == nil then + mp.msg.error("client script write failed") + return + else + file:write(string.format(client_script, options.socket)) + file:close() + subprocess({"chmod", "+x", client_script_path}, true) + table.insert(args, "--script="..client_script_path) + end + end + + spawned = true + spawn_waiting = true + + subprocess(args, true, + function(success, result) + if spawn_waiting and (success == false or result.status ~= 0) then + mp.msg.error("mpv subprocess create failed") + end + spawned = false + end + ) +end + +local function run(command) + if not spawned then return end + + if options.direct_io then + local hPipe = winapi.C.CreateFileW(winapi.socket_wc, winapi.GENERIC_WRITE, 0, nil, winapi.OPEN_EXISTING, winapi._createfile_pipe_flags, nil) + if hPipe ~= winapi.INVALID_HANDLE_VALUE then + local buf = command .. "\n" + winapi.C.SetNamedPipeHandleState(hPipe, winapi.PIPE_NOWAIT, nil, nil) + winapi.C.WriteFile(hPipe, buf, #buf + 1, winapi._lpNumberOfBytesWritten, nil) + winapi.C.CloseHandle(hPipe) + end + + return + end + + local file = nil + if os_name == "Windows" then + file = io.open("\\\\.\\pipe\\"..options.socket, "r+") + else + file = io.open(options.socket, "r+") + end + if file ~= nil then + file:seek("end") + file:write(command.."\n") + file:close() + end +end + +local function draw(w, h, script) + if not w or not show_thumbnail then return end + local display_w, display_h = w, h + if mp.get_property_number("video-params/rotate", 0) % 180 == 90 then + display_w, display_h = h, w + end + + if x ~= nil then + mp.command_native({"overlay-add", options.overlay_id, x, y, options.thumbnail..".bgra", 0, "bgra", display_w, display_h, (4*display_w)}) + elseif script then + local json, err = mp.utils.format_json({width=display_w, height=display_h, x=x, y=y, socket=options.socket, thumbnail=options.thumbnail, overlay_id=options.overlay_id}) + mp.commandv("script-message-to", script, "thumbfast-render", json) + end +end + +local function real_res(req_w, req_h, filesize) + local count = filesize / 4 + local diff = (req_w * req_h) - count + + if diff == 0 then + return req_w, req_h + else + local threshold = 5 -- throw out results that change too much + local long_side, short_side = req_w, req_h + if req_h > req_w then + long_side, short_side = req_h, req_w + end + for a = short_side, short_side - threshold, -1 do + if count % a == 0 then + local b = count / a + if long_side - b < threshold then + if req_h < req_w then return b, a else return a, b end + end + end + end + return nil + end +end + +local function move_file(from, to) + if os_name == "Windows" then + os.remove(to) + end + -- move the file because it can get overwritten while overlay-add is reading it, and crash the player + os.rename(from, to) +end + +local function seek(fast) + if last_seek_time then + run("async seek " .. last_seek_time .. (fast and " absolute+keyframes" or " absolute+exact")) + end +end + +local seek_period = 3/60 +local seek_period_counter = 0 +local seek_timer +seek_timer = mp.add_periodic_timer(seek_period, function() + if seek_period_counter == 0 then + seek(true) + seek_period_counter = 1 + else + if seek_period_counter == 2 then + seek_timer:kill() + seek() + else seek_period_counter = seek_period_counter + 1 end + end +end) +seek_timer:kill() + +local function request_seek() + if seek_timer:is_enabled() then + seek_period_counter = 0 + else + seek_timer:resume() + seek(true) + seek_period_counter = 1 + end +end + +local function check_new_thumb() + -- the slave might start writing to the file after checking existance and + -- validity but before actually moving the file, so move to a temporary + -- location before validity check to make sure everything stays consistant + -- and valid thumbnails don't get overwritten by invalid ones + local tmp = options.thumbnail..".tmp" + move_file(options.thumbnail, tmp) + local finfo = mp.utils.file_info(tmp) + if not finfo then return false end + spawn_waiting = false + if first_file then + request_seek() + first_file = false + end + local w, h = real_res(effective_w, effective_h, finfo.size) + if w then -- only accept valid thumbnails + move_file(tmp, options.thumbnail..".bgra") + + real_w, real_h = w, h + if real_w and (real_w ~= last_real_w or real_h ~= last_real_h) then + last_real_w, last_real_h = real_w, real_h + info(real_w, real_h) + end + return true + end + return false +end + +file_timer = mp.add_periodic_timer(file_check_period, function() + if check_new_thumb() then + draw(real_w, real_h, script_name) + end +end) +file_timer:kill() + +local function thumb(time, r_x, r_y, script) + if disabled then return end + + time = tonumber(time) + if time == nil then return end + + if r_x == "" or r_y == "" then + x, y = nil, nil + else + x, y = math.floor(r_x + 0.5), math.floor(r_y + 0.5) + end + + script_name = script + if last_x ~= x or last_y ~= y or not show_thumbnail then + show_thumbnail = true + last_x = x + last_y = y + draw(real_w, real_h, script) + end + + if time == last_seek_time then return end + last_seek_time = time + if not spawned then spawn(time) end + request_seek() + if not file_timer:is_enabled() then file_timer:resume() end +end + +local function clear() + file_timer:kill() + seek_timer:kill() + last_seek = 0 + show_thumbnail = false + last_x = nil + last_y = nil + if script_name then return end + mp.command_native({"overlay-remove", options.overlay_id}) +end + +local function watch_changes() + local old_w = effective_w + local old_h = effective_h + + calc_dimensions() + + local vf_reset = vf_string(filters_reset) + local rotate = mp.get_property_number("video-rotate", 0) + + local resized = old_w ~= effective_w or + old_h ~= effective_h or + last_vf_reset ~= vf_reset or + (last_rotate % 180) ~= (rotate % 180) or + par ~= last_par + + if resized then + last_rotate = rotate + info(effective_w, effective_h) + elseif last_has_vid ~= has_vid and has_vid ~= 0 then + info(effective_w, effective_h) + end + + if spawned then + if resized then + -- mpv doesn't allow us to change output size + run("quit") + clear() + spawned = false + spawn(last_seek_time or mp.get_property_number("time-pos", 0)) + else + if rotate ~= last_rotate then + run("set video-rotate "..rotate) + end + local vf_runtime = vf_string(filters_runtime) + if vf_runtime ~= last_vf_runtime then + run("vf set "..vf_string(filters_all, true)) + last_vf_runtime = vf_runtime + end + end + else + last_vf_runtime = vf_string(filters_runtime) + end + + last_vf_reset = vf_reset + last_rotate = rotate + last_par = par + last_has_vid = has_vid +end + +local watch_changes_debounce = debounce(watch_changes, 500) + +local function sync_changes(prop, val) + if val == nil then return end + + if type(val) == "boolean" then + if prop == "vid" then + has_vid = 0 + last_has_vid = 0 + info(effective_w, effective_h) + clear() + return + end + val = val and "yes" or "no" + end + + if prop == "vid" then + has_vid = 1 + end + + if not spawned then return end + + run("set "..prop.." "..val) + watch_changes_debounce() +end + +local function file_load() + clear() + real_w, real_h = nil, nil + last_real_w, last_real_h = nil, nil + last_seek_time = nil + if info_timer then + info_timer:kill() + info_timer = nil + end + + calc_dimensions() + info(effective_w, effective_h) + if disabled then return end + + spawned = false + if options.spawn_first then + spawn(mp.get_property_number("time-pos", 0)) + first_file = true + end +end + +local function shutdown() + run("quit") + remove_thumbnail_files() + if os_name ~= "Windows" then + os.remove(options.socket) + os.remove(options.socket..".run") + end +end + +mp.observe_property("display-hidpi-scale", "native", watch_changes) +mp.observe_property("video-out-params", "native", watch_changes) +mp.observe_property("vf", "native", watch_changes_debounce) +mp.observe_property("vid", "native", sync_changes) +mp.observe_property("edition", "native", sync_changes) + +mp.register_script_message("thumb", thumb) +mp.register_script_message("clear", clear) + +mp.register_event("file-loaded", file_load) +mp.register_event("shutdown", shutdown) diff --git a/.config/mpv/scripts/uosc.lua b/.config/mpv/scripts/uosc.lua new file mode 100644 index 0000000..bd7351a --- /dev/null +++ b/.config/mpv/scripts/uosc.lua @@ -0,0 +1,1079 @@ +--[[ uosc 4.4.0 - 2022-Oct-28 | https://github.com/tomasklaen/uosc ]] +local uosc_version = '4.4.0' + +assdraw = require('mp.assdraw') +opt = require('mp.options') +utils = require('mp.utils') +msg = require('mp.msg') +osd = mp.create_osd_overlay('ass-events') +infinity = 1e309 +quarter_pi_sin = math.sin(math.pi / 4) + +-- Enables relative requires from `scripts` directory +package.path = package.path .. ';' .. mp.find_config_file('scripts') .. '/?.lua' + +require('uosc_shared/lib/std') + +--[[ OPTIONS ]] + +defaults = { + timeline_style = 'line', + timeline_line_width = 2, + timeline_line_width_fullscreen = 3, + timeline_line_width_minimized_scale = 10, + timeline_size_min = 2, + timeline_size_max = 40, + timeline_size_min_fullscreen = 0, + timeline_size_max_fullscreen = 60, + timeline_start_hidden = false, + timeline_persistency = 'paused', + timeline_opacity = 0.9, + timeline_border = 1, + timeline_step = 5, + timeline_chapters_opacity = 0.8, + timeline_cache = true, + + controls = 'menu,gap,subtitles,audio,video,editions,stream-quality,gap,space,speed,space,shuffle,loop-playlist,loop-file,gap,prev,items,next,gap,fullscreen', + controls_size = 32, + controls_size_fullscreen = 40, + controls_margin = 8, + controls_spacing = 2, + controls_persistency = '', + + volume = 'right', + volume_size = 40, + volume_size_fullscreen = 52, + volume_persistency = '', + volume_opacity = 0.9, + volume_border = 1, + volume_step = 1, + + speed_persistency = '', + speed_opacity = 0.6, + speed_step = 0.1, + speed_step_is_factor = false, + + menu_item_height = 36, + menu_item_height_fullscreen = 50, + menu_min_width = 260, + menu_min_width_fullscreen = 360, + menu_opacity = 1, + menu_parent_opacity = 0.4, + + top_bar = 'no-border', + top_bar_size = 40, + top_bar_size_fullscreen = 46, + top_bar_persistency = '', + top_bar_controls = true, + top_bar_title = true, + top_bar_title_opacity = 0.8, + + window_border_size = 1, + window_border_opacity = 0.8, + + autoload = false, + shuffle = false, + + ui_scale = 1, + font_scale = 1, + text_border = 1.2, + text_width_estimation = true, + pause_on_click_shorter_than = 0, -- deprecated by below + click_threshold = 0, + click_command = 'cycle pause; script-binding uosc/flash-pause-indicator', + flash_duration = 1000, + proximity_in = 40, + proximity_out = 120, + foreground = 'ffffff', + foreground_text = '000000', + background = '000000', + background_text = 'ffffff', + total_time = false, + time_precision = 0, + font_bold = false, + autohide = false, + buffered_time_threshold = 60, + pause_indicator = 'flash', + curtain_opacity = 0.5, + stream_quality_options = '4320,2160,1440,1080,720,480,360,240,144', + media_types = '3g2,3gp,aac,aiff,ape,apng,asf,au,avi,avif,bmp,dsf,f4v,flac,flv,gif,h264,h265,j2k,jp2,jfif,jpeg,jpg,jxl,m2ts,m4a,m4v,mid,midi,mj2,mka,mkv,mov,mp3,mp4,mp4a,mp4v,mpeg,mpg,oga,ogg,ogm,ogv,opus,png,rm,rmvb,spx,svg,tak,tga,tta,tif,tiff,ts,vob,wav,weba,webm,webp,wma,wmv,wv,y4m', + subtitle_types = 'aqt,ass,gsub,idx,jss,lrc,mks,pgs,pjs,psb,rt,slt,smi,sub,sup,srt,ssa,ssf,ttxt,txt,usf,vt,vtt', + default_directory = '~/', + chapter_ranges = 'openings:30abf964,endings:30abf964,ads:c54e4e80', + chapter_range_patterns = 'openings:オープニング;endings:エンディング', +} +options = table_shallow_copy(defaults) +opt.read_options(options, 'uosc') +-- Normalize values +options.proximity_out = math.max(options.proximity_out, options.proximity_in + 1) +if options.chapter_ranges:sub(1, 4) == '^op|' then options.chapter_ranges = defaults.chapter_ranges end +if options.pause_on_click_shorter_than > 0 and options.click_threshold == 0 then + msg.warn('`pause_on_click_shorter_than` is deprecated. Use `click_threshold` and `click_command` instead.') + options.click_threshold = options.pause_on_click_shorter_than +end +-- Ensure required environment configuration +if options.autoload then mp.commandv('set', 'keep-open-pause', 'no') end +-- Color shorthands +fg, bg = serialize_rgba(options.foreground).color, serialize_rgba(options.background).color +fgt, bgt = serialize_rgba(options.foreground_text).color, serialize_rgba(options.background_text).color + +--[[ CONFIG ]] + +function create_default_menu() + return { + {title = 'Subtitles', value = 'script-binding uosc/subtitles'}, + {title = 'Audio tracks', value = 'script-binding uosc/audio'}, + {title = 'Stream quality', value = 'script-binding uosc/stream-quality'}, + {title = 'Playlist', value = 'script-binding uosc/items'}, + {title = 'Chapters', value = 'script-binding uosc/chapters'}, + {title = 'Navigation', items = { + {title = 'Next', hint = 'playlist or file', value = 'script-binding uosc/next'}, + {title = 'Prev', hint = 'playlist or file', value = 'script-binding uosc/prev'}, + {title = 'Delete file & Next', value = 'script-binding uosc/delete-file-next'}, + {title = 'Delete file & Prev', value = 'script-binding uosc/delete-file-prev'}, + {title = 'Delete file & Quit', value = 'script-binding uosc/delete-file-quit'}, + {title = 'Open file', value = 'script-binding uosc/open-file'}, + },}, + {title = 'Utils', items = { + {title = 'Aspect ratio', items = { + {title = 'Default', value = 'set video-aspect-override "-1"'}, + {title = '16:9', value = 'set video-aspect-override "16:9"'}, + {title = '4:3', value = 'set video-aspect-override "4:3"'}, + {title = '2.35:1', value = 'set video-aspect-override "2.35:1"'}, + },}, + {title = 'Audio devices', value = 'script-binding uosc/audio-device'}, + {title = 'Editions', value = 'script-binding uosc/editions'}, + {title = 'Screenshot', value = 'async screenshot'}, + {title = 'Show in directory', value = 'script-binding uosc/show-in-directory'}, + {title = 'Open config folder', value = 'script-binding uosc/open-config-directory'}, + },}, + {title = 'Quit', value = 'quit'}, + } +end + +config = { + version = uosc_version, + -- sets max rendering frequency in case the + -- native rendering frequency could not be detected + render_delay = 1 / 60, + font = mp.get_property('options/osd-font'), + media_types = split(options.media_types, ' *, *'), + subtitle_types = split(options.subtitle_types, ' *, *'), + stream_quality_options = split(options.stream_quality_options, ' *, *'), + menu_items = (function() + local input_conf_property = mp.get_property_native('input-conf') + local input_conf_path = mp.command_native({ + 'expand-path', input_conf_property == '' and '~~/input.conf' or input_conf_property, + }) + local input_conf_meta, meta_error = utils.file_info(input_conf_path) + + -- File doesn't exist + if not input_conf_meta or not input_conf_meta.is_file then return create_default_menu() end + + local main_menu = {items = {}, items_by_command = {}} + local by_id = {} + + for line in io.lines(input_conf_path) do + local key, command, comment = string.match(line, '%s*([%S]+)%s+(.-)%s+#%s*(.-)%s*$') + local title = '' + if comment then + local comments = split(comment, '#') + local titles = itable_filter(comments, function(v, i) return v:match('^!') or v:match('^menu:') end) + if titles and #titles > 0 then + title = titles[1]:match('^!%s*(.*)%s*') or titles[1]:match('^menu:%s*(.*)%s*') + end + end + if title ~= '' then + local is_dummy = key:sub(1, 1) == '#' + local submenu_id = '' + local target_menu = main_menu + local title_parts = split(title or '', ' *> *') + + for index, title_part in ipairs(#title_parts > 0 and title_parts or {''}) do + if index < #title_parts then + submenu_id = submenu_id .. title_part + + if not by_id[submenu_id] then + local items = {} + by_id[submenu_id] = {items = items, items_by_command = {}} + target_menu.items[#target_menu.items + 1] = {title = title_part, items = items} + end + + target_menu = by_id[submenu_id] + else + if command == 'ignore' then break end + -- If command is already in menu, just append the key to it + if target_menu.items_by_command[command] then + local hint = target_menu.items_by_command[command].hint + target_menu.items_by_command[command].hint = hint and hint .. ', ' .. key or key + else + local item = { + title = title_part, + hint = not is_dummy and key or nil, + value = command, + } + target_menu.items_by_command[command] = item + target_menu.items[#target_menu.items + 1] = item + end + end + end + end + end + + if #main_menu.items > 0 then + return main_menu.items + else + -- Default context menu + return create_default_menu() + end + end)(), + chapter_ranges = (function() + ---@type table Alternative patterns. + local alt_patterns = {} + if options.chapter_range_patterns and options.chapter_range_patterns ~= '' then + for _, definition in ipairs(split(options.chapter_range_patterns, ';+ *')) do + local name_patterns = split(definition, ' *:') + local name, patterns = name_patterns[1], name_patterns[2] + if name and patterns then alt_patterns[name] = split(patterns, ',') end + end + end + + ---@type table + local ranges = {} + if options.chapter_ranges and options.chapter_ranges ~= '' then + for _, definition in ipairs(split(options.chapter_ranges, ' *,+ *')) do + local name_color = split(definition, ' *:+ *') + local name, color = name_color[1], name_color[2] + if name and color + and name:match('^[a-zA-Z0-9_]+$') and color:match('^[a-fA-F0-9]+$') + and (#color == 6 or #color == 8) then + local range = serialize_rgba(name_color[2]) + range.patterns = alt_patterns[name] + ranges[name_color[1]] = range + end + end + end + return ranges + end)(), +} +-- Adds `{element}_persistency` property with table of flags when the element should be visible (`{paused = true}`) +for _, name in ipairs({'timeline', 'controls', 'volume', 'top_bar', 'speed'}) do + local option_name = name .. '_persistency' + local value, flags = options[option_name], {} + if type(value) == 'string' then + for _, state in ipairs(split(value, ' *, *')) do flags[state] = true end + end + config[option_name] = flags +end + +--[[ STATE ]] + +display = {width = 1280, height = 720, scale_x = 1, scale_y = 1} +cursor = {hidden = true, x = 0, y = 0} +state = { + os = (function() + if os.getenv('windir') ~= nil then return 'windows' end + local homedir = os.getenv('HOME') + if homedir ~= nil and string.sub(homedir, 1, 6) == '/Users' then return 'macos' end + return 'linux' + end)(), + cwd = mp.get_property('working-directory'), + path = nil, -- current file path or URL + title = nil, + time = nil, -- current media playback time + speed = 1, + duration = nil, -- current media duration + time_human = nil, -- current playback time in human format + duration_or_remaining_time_human = nil, -- depends on options.total_time + pause = mp.get_property_native('pause'), + chapters = {}, + current_chapter = nil, + chapter_ranges = {}, + border = mp.get_property_native('border'), + fullscreen = mp.get_property_native('fullscreen'), + maximized = mp.get_property_native('window-maximized'), + fullormaxed = mp.get_property_native('fullscreen') or mp.get_property_native('window-maximized'), + render_timer = nil, + render_last_time = 0, + volume = nil, + volume_max = nil, + mute = nil, + is_idle = false, + is_video = false, + is_audio = false, -- true if file is audio only (mp3, etc) + is_image = false, + is_stream = false, + has_audio = false, + has_sub = false, + has_chapter = false, + has_playlist = false, + shuffle = options.shuffle, + cursor_autohide_timer = mp.add_timeout(mp.get_property_native('cursor-autohide') / 1000, function() + if not options.autohide then return end + handle_mouse_leave() + end), + mouse_bindings_enabled = false, + uncached_ranges = nil, + cache = nil, + cache_buffering = 100, + cache_underrun = false, + core_idle = false, + eof_reached = false, + render_delay = config.render_delay, + first_real_mouse_move_received = false, + playlist_count = 0, + playlist_pos = 0, + margin_top = 0, + margin_bottom = 0, + hidpi_scale = 1, +} +thumbnail = {width = 0, height = 0, disabled = false} +external = {} -- Properties set by external scripts +Elements = require('uosc_shared/elements/Elements') +Menu = require('uosc_shared/elements/Menu') + +-- State dependent utilities +require('uosc_shared/lib/utils') +require('uosc_shared/lib/text') +require('uosc_shared/lib/ass') +require('uosc_shared/lib/menus') + +--[[ STATE UPDATERS ]] + +function update_display_dimensions() + local scale = (state.hidpi_scale or 1) * options.ui_scale + local real_width, real_height = mp.get_osd_size() + local scaled_width, scaled_height = round(real_width / scale), round(real_height / scale) + display.width, display.height = scaled_width, scaled_height + display.scale_x, display.scale_y = real_width / scaled_width, real_height / scaled_height + + -- Tell elements about this + Elements:trigger('display') + + -- Some elements probably changed their rectangles as a reaction to `display` + Elements:update_proximities() + request_render() +end + +function update_fullormaxed() + state.fullormaxed = state.fullscreen or state.maximized + update_display_dimensions() + Elements:trigger('prop_fullormaxed', state.fullormaxed) +end + +function update_human_times() + if state.time then + state.time_human = format_time(state.time) + if state.duration then + local speed = state.speed or 1 + state.duration_or_remaining_time_human = format_time( + options.total_time and state.duration or ((state.time - state.duration) / speed) + ) + else + state.duration_or_remaining_time_human = nil + end + else + state.time_human = nil + end +end + +-- Notifies other scripts such as console about where the unoccupied parts of the screen are. +function update_margins() + -- margins are normalized to window size + local timeline, top_bar, controls = Elements.timeline, Elements.top_bar, Elements.controls + local bottom_y = controls and controls.enabled and controls.ay or timeline.ay + local top, bottom = 0, (display.height - bottom_y) / display.height + + if top_bar.enabled and top_bar:get_visibility() > 0 then + top = (top_bar.size or 0) / display.height + end + + if top == state.margin_top and bottom == state.margin_bottom then return end + + state.margin_top = top + state.margin_bottom = bottom + + utils.shared_script_property_set('osc-margins', string.format('%f,%f,%f,%f', 0, 0, top, bottom)) +end +function create_state_setter(name, callback) + return function(_, value) + set_state(name, value) + if callback then callback() end + request_render() + end +end + +function set_state(name, value) + state[name] = value + Elements:trigger('prop_' .. name, value) +end + +function update_cursor_position(x, y) + -- mpv reports initial mouse position on linux as (0, 0), which always + -- displays the top bar, so we hardcode cursor position as infinity until + -- we receive a first real mouse move event with coordinates other than 0,0. + if not state.first_real_mouse_move_received then + if x > 0 and y > 0 then state.first_real_mouse_move_received = true + else x, y = infinity, infinity end + end + + -- add 0.5 to be in the middle of the pixel + cursor.x, cursor.y = (x + 0.5) / display.scale_x, (y + 0.5) / display.scale_y + + Elements:update_proximities() + request_render() +end + +function handle_mouse_leave() + -- Slowly fadeout elements that are currently visible + for _, element_name in ipairs({'timeline', 'volume', 'top_bar'}) do + local element = Elements[element_name] + if element and element.proximity > 0 then + element:tween_property('forced_visibility', element:get_visibility(), 0, function() + element.forced_visibility = nil + end) + end + end + + cursor.hidden = true + Elements:update_proximities() + Elements:trigger('global_mouse_leave') +end + +function handle_mouse_enter(x, y) + cursor.hidden = false + update_cursor_position(x, y) + Elements:trigger('global_mouse_enter') +end + +function handle_mouse_move(x, y) + update_cursor_position(x, y) + Elements:proximity_trigger('mouse_move') + request_render() + + -- Restart timer that hides UI when mouse is autohidden + if options.autohide then + state.cursor_autohide_timer:kill() + state.cursor_autohide_timer:resume() + end +end + +function handle_file_end() + local resume = false + if not state.loop_file then + if state.has_playlist then resume = state.shuffle and navigate_playlist(1) + else resume = options.autoload and navigate_directory(1) end + end + -- Resume only when navigation happened + if resume then mp.command('set pause no') end +end +local file_end_timer = mp.add_timeout(1, handle_file_end) +file_end_timer:kill() + +function load_file_index_in_current_directory(index) + if not state.path or is_protocol(state.path) then return end + + local serialized = serialize_path(state.path) + if serialized and serialized.dirname then + local files = read_directory(serialized.dirname, config.media_types) + + if not files then return end + sort_filenames(files) + if index < 0 then index = #files + index + 1 end + + if files[index] then + mp.commandv('loadfile', utils.join_path(serialized.dirname, files[index])) + end + end +end + +function update_render_delay(name, fps) + if fps then state.render_delay = 1 / fps end +end + +function observe_display_fps(name, fps) + if fps then + mp.unobserve_property(update_render_delay) + mp.unobserve_property(observe_display_fps) + mp.observe_property('display-fps', 'native', update_render_delay) + end +end + +function select_current_chapter() + local current_chapter + if state.time and state.chapters then + _, current_chapter = itable_find(state.chapters, function(c) return state.time >= c.time end, true) + end + set_state('current_chapter', current_chapter) +end + +--[[ STATE HOOKS ]] + +-- Click detection +if options.click_threshold > 0 then + -- Executes custom command for clicks shorter than `options.click_threshold` + -- while filtering out double clicks. + local click_time = options.click_threshold / 1000 + local doubleclick_time = mp.get_property_native('input-doubleclick-time') / 1000 + local last_down, last_up = 0, 0 + local click_timer = mp.add_timeout(math.max(click_time, doubleclick_time), function() + local delta = last_up - last_down + if delta > 0 and delta < click_time and delta > 0.02 then mp.command(options.click_command) end + end) + click_timer:kill() + mp.set_key_bindings({{'mbtn_left', + function() last_up = mp.get_time() end, + function() + last_down = mp.get_time() + if click_timer:is_enabled() then click_timer:kill() else click_timer:resume() end + end, + },}, 'mouse_movement', 'force') + mp.enable_key_bindings('mouse_movement', 'allow-vo-dragging+allow-hide-cursor') +end + +function update_mouse_pos(_, mouse, ignore_hover) + if ignore_hover or mouse.hover then + if cursor.hidden then handle_mouse_enter(mouse.x, mouse.y) end + handle_mouse_move(mouse.x, mouse.y) + else handle_mouse_leave() end +end +mp.observe_property('mouse-pos', 'native', update_mouse_pos) +mp.observe_property('osc', 'bool', function(name, value) if value == true then mp.set_property('osc', 'no') end end) +mp.register_event('file-loaded', function() + set_state('path', normalize_path(mp.get_property_native('path'))) +end) +mp.register_event('end-file', function(event) + if event.reason == 'eof' then + file_end_timer:kill() + handle_file_end() + end +end) +do + local template = nil + function update_title() + if template:sub(-6) == ' - mpv' then template = template:sub(1, -7) end + -- escape ASS, and strip newlines and trailing slashes and trim whitespace + local t = mp.command_native({'expand-text', template}):gsub('\\n', ' '):gsub('[\\%s]+$', ''):gsub('^%s+', '') + set_state('title', ass_escape(t)) + end + mp.observe_property('title', 'string', function(_, title) + mp.unobserve_property(update_title) + template = title + local props = get_expansion_props(title) + for prop, _ in pairs(props) do + mp.observe_property(prop, 'native', update_title) + end + if not next(props) then update_title() end + end) +end +mp.observe_property('playback-time', 'number', create_state_setter('time', function() + -- Create a file-end event that triggers right before file ends + file_end_timer:kill() + if state.duration and state.time and not state.pause then + local remaining = (state.duration - state.time) / state.speed + if remaining < 5 then + local timeout = remaining - 0.02 + if timeout > 0 then + file_end_timer.timeout = timeout + file_end_timer:resume() + else handle_file_end() end + end + end + + update_human_times() + select_current_chapter() +end)) +mp.observe_property('duration', 'number', create_state_setter('duration', update_human_times)) +mp.observe_property('speed', 'number', create_state_setter('speed', update_human_times)) +mp.observe_property('track-list', 'native', function(name, value) + -- checks the file dispositions + local is_image = false + local types = {sub = 0, audio = 0, video = 0} + for _, track in ipairs(value) do + if track.type == 'video' then + is_image = track.image + if not is_image and not track.albumart then types.video = types.video + 1 end + elseif types[track.type] then types[track.type] = types[track.type] + 1 end + end + set_state('is_audio', types.video == 0 and types.audio > 0) + set_state('is_image', is_image) + set_state('has_audio', types.audio > 0) + set_state('has_many_audio', types.audio > 1) + set_state('has_sub', types.sub > 0) + set_state('has_many_sub', types.sub > 1) + set_state('is_video', types.video > 0) + set_state('has_many_video', types.video > 1) + Elements:trigger('dispositions') +end) +mp.observe_property('editions', 'number', function(_, editions) + if editions then set_state('has_many_edition', editions > 1) end + Elements:trigger('dispositions') +end) +mp.observe_property('chapter-list', 'native', function(_, chapters) + local chapters, chapter_ranges = serialize_chapters(chapters), {} + if chapters then chapters, chapter_ranges = serialize_chapter_ranges(chapters) end + set_state('chapters', chapters) + set_state('chapter_ranges', chapter_ranges) + set_state('has_chapter', #chapters > 0) + select_current_chapter() + Elements:trigger('dispositions') +end) +mp.observe_property('border', 'bool', create_state_setter('border')) +mp.observe_property('loop-file', 'native', create_state_setter('loop_file')) +mp.observe_property('ab-loop-a', 'number', create_state_setter('ab_loop_a')) +mp.observe_property('ab-loop-b', 'number', create_state_setter('ab_loop_b')) +mp.observe_property('playlist-pos-1', 'number', create_state_setter('playlist_pos')) +mp.observe_property('playlist-count', 'number', function(_, value) + set_state('playlist_count', value) + set_state('has_playlist', value > 1) + Elements:trigger('dispositions') +end) +mp.observe_property('fullscreen', 'bool', create_state_setter('fullscreen', update_fullormaxed)) +mp.observe_property('window-maximized', 'bool', create_state_setter('maximized', update_fullormaxed)) +mp.observe_property('idle-active', 'bool', function(_, idle) + set_state('is_idle', idle) + Elements:trigger('dispositions') +end) +mp.observe_property('pause', 'bool', create_state_setter('pause', function() file_end_timer:kill() end)) +mp.observe_property('volume', 'number', create_state_setter('volume')) +mp.observe_property('volume-max', 'number', create_state_setter('volume_max')) +mp.observe_property('mute', 'bool', create_state_setter('mute')) +mp.observe_property('osd-dimensions', 'native', function(name, val) + update_display_dimensions() + request_render() +end) +mp.observe_property('display-hidpi-scale', 'native', create_state_setter('hidpi_scale', update_display_dimensions)) +mp.observe_property('cache', 'native', create_state_setter('cache')) +mp.observe_property('cache-buffering-state', 'number', create_state_setter('cache_buffering')) +mp.observe_property('demuxer-via-network', 'native', create_state_setter('is_stream', function() + Elements:trigger('dispositions') +end)) +mp.observe_property('demuxer-cache-state', 'native', function(prop, cache_state) + local cached_ranges, bof, eof, uncached_ranges = nil, nil, nil, nil + if cache_state then + cached_ranges, bof, eof = cache_state['seekable-ranges'], cache_state['bof-cached'], cache_state['eof-cached'] + set_state('cache_underrun', cache_state['underrun']) + else cached_ranges = {} end + + if not (state.duration and (#cached_ranges > 0 or state.cache == 'yes' or + (state.cache == 'auto' and state.is_stream))) then + if state.uncached_ranges then set_state('uncached_ranges', nil) end + return + end + + -- Normalize + local ranges = {} + for _, range in ipairs(cached_ranges) do + ranges[#ranges + 1] = { + math.max(range['start'] or 0, 0), + math.min(range['end'] or state.duration, state.duration), + } + end + table.sort(ranges, function(a, b) return a[1] < b[1] end) + if bof then ranges[1][1] = 0 end + if eof then ranges[#ranges][2] = state.duration end + -- Invert cached ranges into uncached ranges, as that's what we're rendering + local inverted_ranges = {{0, state.duration}} + for _, cached in pairs(ranges) do + inverted_ranges[#inverted_ranges][2] = cached[1] + inverted_ranges[#inverted_ranges + 1] = {cached[2], state.duration} + end + uncached_ranges = {} + local last_range = nil + for _, range in ipairs(inverted_ranges) do + if last_range and last_range[2] + 0.5 > range[1] then -- fuse ranges + last_range[2] = range[2] + else + if range[2] - range[1] > 0.5 then -- skip short ranges + uncached_ranges[#uncached_ranges + 1] = range + last_range = range + end + end + end + + set_state('uncached_ranges', uncached_ranges) +end) +mp.observe_property('display-fps', 'native', observe_display_fps) +mp.observe_property('estimated-display-fps', 'native', update_render_delay) +mp.observe_property('eof-reached', 'native', create_state_setter('eof_reached')) +mp.observe_property('core-idle', 'native', create_state_setter('core_idle')) + +--[[ KEY BINDS ]] + +mp.add_key_binding(nil, 'toggle-ui', function() Elements:toggle({'timeline', 'controls', 'volume', 'top_bar'}) end) +mp.add_key_binding(nil, 'flash-ui', function() Elements:flash({'timeline', 'controls', 'volume', 'top_bar'}) end) +mp.add_key_binding(nil, 'flash-timeline', function() Elements:flash({'timeline'}) end) +mp.add_key_binding(nil, 'flash-top-bar', function() Elements:flash({'top_bar'}) end) +mp.add_key_binding(nil, 'flash-volume', function() Elements:flash({'volume'}) end) +mp.add_key_binding(nil, 'flash-speed', function() Elements:flash({'speed'}) end) +mp.add_key_binding(nil, 'flash-pause-indicator', function() Elements:flash({'pause_indicator'}) end) +mp.add_key_binding(nil, 'toggle-progress', function() + local timeline = Elements.timeline + if timeline.size_min_override then + timeline:tween_property('size_min_override', timeline.size_min_override, timeline.size_min, function() + timeline.size_min_override = nil + end) + else + timeline:tween_property('size_min_override', timeline.size_min, 0) + end +end) +mp.add_key_binding(nil, 'decide-pause-indicator', function() Elements.pause_indicator:decide() end) +mp.add_key_binding(nil, 'menu', function() toggle_menu_with_items() end) +mp.add_key_binding(nil, 'menu-blurred', function() toggle_menu_with_items({mouse_nav = true}) end) +local track_loaders = { + {name = 'subtitles', prop = 'sub', allowed_types = config.subtitle_types}, + {name = 'audio', prop = 'audio', allowed_types = config.media_types}, + {name = 'video', prop = 'video', allowed_types = config.media_types}, +} +for _, loader in ipairs(track_loaders) do + local menu_type = 'load-' .. loader.name + mp.add_key_binding(nil, menu_type, function() + if Menu:is_open(menu_type) then Menu:close() return end + + local path = state.path + if path then + if is_protocol(path) then + path = false + else + local serialized_path = serialize_path(path) + path = serialized_path ~= nil and serialized_path.dirname or false + end + end + if not path then + path = get_default_directory() + end + open_file_navigation_menu( + path, + function(path) mp.commandv(loader.prop .. '-add', path) end, + {type = menu_type, title = 'Load ' .. loader.name, allowed_types = loader.allowed_types} + ) + end) +end +mp.add_key_binding(nil, 'subtitles', create_select_tracklist_type_menu_opener( + 'Subtitles', 'sub', 'sid', 'script-binding uosc/load-subtitles' +)) +mp.add_key_binding(nil, 'audio', create_select_tracklist_type_menu_opener( + 'Audio', 'audio', 'aid', 'script-binding uosc/load-audio' +)) +mp.add_key_binding(nil, 'video', create_select_tracklist_type_menu_opener( + 'Video', 'video', 'vid', 'script-binding uosc/load-video' +)) +mp.add_key_binding(nil, 'playlist', create_self_updating_menu_opener({ + title = 'Playlist', + type = 'playlist', + list_prop = 'playlist', + serializer = function(playlist) + local items = {} + for index, item in ipairs(playlist) do + local is_url = item.filename:find('://') + local item_title = type(item.title) == 'string' and #item.title > 0 and item.title or false + items[index] = { + title = item_title or (is_url and item.filename or serialize_path(item.filename).basename), + hint = tostring(index), + active = item.current, + value = index, + } + end + return items + end, + on_select = function(index) mp.commandv('set', 'playlist-pos-1', tostring(index)) end, +})) +mp.add_key_binding(nil, 'chapters', create_self_updating_menu_opener({ + title = 'Chapters', + type = 'chapters', + list_prop = 'chapter-list', + active_prop = 'chapter', + serializer = function(chapters, current_chapter) + local items = {} + chapters = normalize_chapters(chapters) + for index, chapter in ipairs(chapters) do + items[index] = { + title = chapter.title or '', + hint = mp.format_time(chapter.time), + value = index, + active = index - 1 == current_chapter, + } + end + return items + end, + on_select = function(index) mp.commandv('set', 'chapter', tostring(index - 1)) end, +})) +mp.add_key_binding(nil, 'editions', create_self_updating_menu_opener({ + title = 'Editions', + type = 'editions', + list_prop = 'edition-list', + active_prop = 'current-edition', + serializer = function(editions, current_id) + local items = {} + for _, edition in ipairs(editions or {}) do + items[#items + 1] = { + title = edition.title or 'Edition', + hint = tostring(edition.id + 1), + value = edition.id, + active = edition.id == current_id, + } + end + return items + end, + on_select = function(id) mp.commandv('set', 'edition', id) end, +})) +mp.add_key_binding(nil, 'show-in-directory', function() + -- Ignore URLs + if not state.path or is_protocol(state.path) then return end + + if state.os == 'windows' then + utils.subprocess_detached({args = {'explorer', '/select,', state.path}, cancellable = false}) + elseif state.os == 'macos' then + utils.subprocess_detached({args = {'open', '-R', state.path}, cancellable = false}) + elseif state.os == 'linux' then + local result = utils.subprocess({args = {'nautilus', state.path}, cancellable = false}) + + -- Fallback opens the folder with xdg-open instead + if result.status ~= 0 then + utils.subprocess({args = {'xdg-open', serialize_path(state.path).dirname}, cancellable = false}) + end + end +end) +mp.add_key_binding(nil, 'stream-quality', function() + if Menu:is_open('stream-quality') then Menu:close() return end + + local ytdl_format = mp.get_property_native('ytdl-format') + local items = {} + + for _, height in ipairs(config.stream_quality_options) do + local format = 'bestvideo[height<=?' .. height .. ']+bestaudio/best[height<=?' .. height .. ']' + items[#items + 1] = {title = height .. 'p', value = format, active = format == ytdl_format} + end + + Menu:open({type = 'stream-quality', title = 'Stream quality', items = items}, function(format) + mp.set_property('ytdl-format', format) + + -- Reload the video to apply new format + -- This is taken from https://github.com/jgreco/mpv-youtube-quality + -- which is in turn taken from https://github.com/4e6/mpv-reload/ + -- Dunno if playlist_pos shenanigans below are necessary. + local playlist_pos = mp.get_property_number('playlist-pos') + local duration = mp.get_property_native('duration') + local time_pos = mp.get_property('time-pos') + + mp.set_property_number('playlist-pos', playlist_pos) + + -- Tries to determine live stream vs. pre-recorded VOD. VOD has non-zero + -- duration property. When reloading VOD, to keep the current time position + -- we should provide offset from the start. Stream doesn't have fixed start. + -- Decent choice would be to reload stream from it's current 'live' position. + -- That's the reason we don't pass the offset when reloading streams. + if duration and duration > 0 then + local function seeker() + mp.commandv('seek', time_pos, 'absolute') + mp.unregister_event(seeker) + end + mp.register_event('file-loaded', seeker) + end + end) +end) +mp.add_key_binding(nil, 'open-file', function() + if Menu:is_open('open-file') then Menu:close() return end + + local directory + local active_file + + if state.path == nil or is_protocol(state.path) then + local serialized = serialize_path(get_default_directory()) + if serialized then + directory = serialized.path + active_file = nil + end + else + local serialized = serialize_path(state.path) + if serialized then + directory = serialized.dirname + active_file = serialized.path + end + end + + if not directory then + msg.error('Couldn\'t serialize path "' .. state.path .. '".') + return + end + + -- Update active file in directory navigation menu + local function handle_file_loaded() + if Menu:is_open('open-file') then + Elements.menu:activate_value(normalize_path(mp.get_property_native('path'))) + end + end + + open_file_navigation_menu( + directory, + function(path) mp.commandv('loadfile', path) end, + { + type = 'open-file', + allowed_types = config.media_types, + active_path = active_file, + on_open = function() mp.register_event('file-loaded', handle_file_loaded) end, + on_close = function() mp.unregister_event(handle_file_loaded) end, + } + ) +end) +mp.add_key_binding(nil, 'shuffle', function() set_state('shuffle', not state.shuffle) end) +mp.add_key_binding(nil, 'items', function() + if state.has_playlist then + mp.command('script-binding uosc/playlist') + else + mp.command('script-binding uosc/open-file') + end +end) +mp.add_key_binding(nil, 'next', function() navigate_item(1) end) +mp.add_key_binding(nil, 'prev', function() navigate_item(-1) end) +mp.add_key_binding(nil, 'next-file', function() navigate_directory(1) end) +mp.add_key_binding(nil, 'prev-file', function() navigate_directory(-1) end) +mp.add_key_binding(nil, 'first', function() + if state.has_playlist then + mp.commandv('set', 'playlist-pos-1', '1') + else + load_file_index_in_current_directory(1) + end +end) +mp.add_key_binding(nil, 'last', function() + if state.has_playlist then + mp.commandv('set', 'playlist-pos-1', tostring(state.playlist_count)) + else + load_file_index_in_current_directory(-1) + end +end) +mp.add_key_binding(nil, 'first-file', function() load_file_index_in_current_directory(1) end) +mp.add_key_binding(nil, 'last-file', function() load_file_index_in_current_directory(-1) end) +mp.add_key_binding(nil, 'delete-file-next', function() + local next_file = nil + local is_local_file = state.path and not is_protocol(state.path) + + if is_local_file then + if Menu:is_open('open-file') then Elements.menu:delete_value(state.path) end + end + + if state.has_playlist then + mp.commandv('playlist-remove', 'current') + else + if is_local_file then + local paths, current_index = get_adjacent_files(state.path, config.media_types) + if paths and current_index then + local index, path = decide_navigation_in_list(paths, current_index, 1) + if path then next_file = path end + end + end + + if next_file then mp.commandv('loadfile', next_file) + else mp.commandv('stop') end + end + + if is_local_file then delete_file(state.path) end +end) +mp.add_key_binding(nil, 'delete-file-quit', function() + mp.command('stop') + if state.path and not is_protocol(state.path) then delete_file(state.path) end + mp.command('quit') +end) +mp.add_key_binding(nil, 'audio-device', create_self_updating_menu_opener({ + title = 'Audio devices', + type = 'audio-device-list', + list_prop = 'audio-device-list', + active_prop = 'audio-device', + serializer = function(audio_device_list, current_device) + current_device = current_device or 'auto' + local ao = mp.get_property('current-ao') or '' + local items = {} + for _, device in ipairs(audio_device_list) do + if device.name == 'auto' or string.match(device.name, '^' .. ao) then + local hint = string.match(device.name, ao .. '/(.+)') + if not hint then hint = device.name end + items[#items + 1] = { + title = device.description, + hint = hint, + active = device.name == current_device, + value = device.name, + } + end + end + return items + end, + on_select = function(name) mp.commandv('set', 'audio-device', name) end, +})) +mp.add_key_binding(nil, 'open-config-directory', function() + local config_path = mp.command_native({'expand-path', '~~/mpv.conf'}) + local config = serialize_path(config_path) + + if config then + local args + + if state.os == 'windows' then + args = {'explorer', '/select,', config.path} + elseif state.os == 'macos' then + args = {'open', '-R', config.path} + elseif state.os == 'linux' then + args = {'xdg-open', config.dirname} + end + + utils.subprocess_detached({args = args, cancellable = false}) + else + msg.error('Couldn\'t serialize config path "' .. config_path .. '".') + end +end) + +--[[ MESSAGE HANDLERS ]] + +mp.register_script_message('show-submenu', function(id) toggle_menu_with_items({submenu = id}) end) +mp.register_script_message('get-version', function(script) + mp.commandv('script-message-to', script, 'uosc-version', config.version) +end) +mp.register_script_message('open-menu', function(json, submenu_id) + local data = utils.parse_json(json) + if type(data) ~= 'table' or type(data.items) ~= 'table' then + msg.error('open-menu: received json didn\'t produce a table with menu configuration') + else + if data.type and Menu:is_open(data.type) then Menu:close() + else open_command_menu(data, {submenu_id = submenu_id}) end + end +end) +mp.register_script_message('update-menu', function(json) + local data = utils.parse_json(json) + if type(data) ~= 'table' or type(data.items) ~= 'table' then + msg.error('update-menu: received json didn\'t produce a table with menu configuration') + else + local menu = data.type and Menu:is_open(data.type) + if menu then menu:update(data) + else open_command_menu(data) end + end +end) +mp.register_script_message('thumbfast-info', function(json) + local data = utils.parse_json(json) + if type(data) ~= 'table' or not data.width or not data.height then + thumbnail.disabled = true + msg.error('thumbfast-info: received json didn\'t produce a table with thumbnail information') + else + thumbnail = data + request_render() + end +end) +mp.register_script_message('set', function(name, value) + external[name] = value + Elements:trigger('external_prop_' .. name, value) +end) +mp.register_script_message('toggle-elements', function(elements) Elements:toggle(split(elements, ' *, *')) end) +mp.register_script_message('set-min-visibility', function(visibility, elements) + local fraction = tonumber(visibility) + local ids = split(elements and elements ~= '' and elements or 'timeline,controls,volume,top_bar', ' *, *') + if fraction then Elements:set_min_visibility(clamp(0, fraction, 1), ids) end +end) +mp.register_script_message('flash-elements', function(elements) Elements:flash(split(elements, ' *, *')) end) + +--[[ ELEMENTS ]] + +require('uosc_shared/elements/WindowBorder'):new() +require('uosc_shared/elements/BufferingIndicator'):new() +require('uosc_shared/elements/PauseIndicator'):new() +require('uosc_shared/elements/TopBar'):new() +require('uosc_shared/elements/Timeline'):new() +if options.controls and options.controls ~= 'never' then require('uosc_shared/elements/Controls'):new() end +if itable_index_of({'left', 'right'}, options.volume) then require('uosc_shared/elements/Volume'):new() end +require('uosc_shared/elements/Curtain'):new() diff --git a/.config/mpv/scripts/uosc_shared/elements/BufferingIndicator.lua b/.config/mpv/scripts/uosc_shared/elements/BufferingIndicator.lua new file mode 100644 index 0000000..e2aa071 --- /dev/null +++ b/.config/mpv/scripts/uosc_shared/elements/BufferingIndicator.lua @@ -0,0 +1,37 @@ +local Element = require('uosc_shared/elements/Element') + +---@class BufferingIndicator : Element +local BufferingIndicator = class(Element) + +function BufferingIndicator:new() return Class.new(self) --[[@as BufferingIndicator]] end +function BufferingIndicator:init() + Element.init(self, 'buffer_indicator') + self.ignores_menu = true + self.enabled = false +end + +function BufferingIndicator:decide_enabled() + local cache = state.cache_underrun or state.cache_buffering and state.cache_buffering < 100 + local player = state.core_idle and not state.eof_reached + if self.enabled then + if not player or (state.pause and not cache) then self.enabled = false end + elseif player and cache and state.uncached_ranges then self.enabled = true end +end + +function BufferingIndicator:on_prop_pause() self:decide_enabled() end +function BufferingIndicator:on_prop_core_idle() self:decide_enabled() end +function BufferingIndicator:on_prop_eof_reached() self:decide_enabled() end +function BufferingIndicator:on_prop_uncached_ranges() self:decide_enabled() end +function BufferingIndicator:on_prop_cache_buffering() self:decide_enabled() end +function BufferingIndicator:on_prop_cache_underrun() self:decide_enabled() end + +function BufferingIndicator:render() + local ass = assdraw.ass_new() + ass:rect(0, 0, display.width, display.height, {color = bg, opacity = 0.3}) + local size = round(30 + math.min(display.width, display.height) / 10) + local opacity = (Elements.menu and not Elements.menu.is_closing) and 0.3 or 0.8 + ass:spinner(display.width / 2, display.height / 2, size, {color = fg, opacity = opacity}) + return ass +end + +return BufferingIndicator diff --git a/.config/mpv/scripts/uosc_shared/elements/Button.lua b/.config/mpv/scripts/uosc_shared/elements/Button.lua new file mode 100644 index 0000000..cf802ef --- /dev/null +++ b/.config/mpv/scripts/uosc_shared/elements/Button.lua @@ -0,0 +1,88 @@ +local Element = require('uosc_shared/elements/Element') + +---@alias ButtonProps {icon: string; on_click: function; anchor_id?: string; active?: boolean; badge?: string|number; foreground?: string; background?: string; tooltip?: string} + +---@class Button : Element +local Button = class(Element) + +---@param id string +---@param props ButtonProps +function Button:new(id, props) return Class.new(self, id, props) --[[@as Button]] end +---@param id string +---@param props ButtonProps +function Button:init(id, props) + self.icon = props.icon + self.active = props.active + self.tooltip = props.tooltip + self.badge = props.badge + self.foreground = props.foreground or fg + self.background = props.background or bg + ---@type fun() + self.on_click = props.on_click + Element.init(self, id, props) +end + +function Button:on_coordinates() self.font_size = round((self.by - self.ay) * 0.7) end +function Button:on_mbtn_left_down() + -- We delay the callback to next tick, otherwise we are risking race + -- conditions as we are in the middle of event dispatching. + -- For example, handler might add a menu to the end of the element stack, and that + -- than picks up this click even we are in right now, and instantly closes itself. + mp.add_timeout(0.01, self.on_click) +end + +function Button:render() + local visibility = self:get_visibility() + if visibility <= 0 then return end + + local ass = assdraw.ass_new() + local is_hover = self.proximity_raw == 0 + local is_hover_or_active = is_hover or self.active + local foreground = self.active and self.background or self.foreground + local background = self.active and self.foreground or self.background + + -- Background + if is_hover_or_active then + ass:rect(self.ax, self.ay, self.bx, self.by, { + color = self.active and background or foreground, radius = 2, + opacity = visibility * (self.active and 1 or 0.3), + }) + end + + -- Tooltip on hover + if is_hover and self.tooltip then ass:tooltip(self, self.tooltip) end + + + -- Badge + local icon_clip + if self.badge then + local badge_font_size = self.font_size * 0.6 + local badge_opts = {size = badge_font_size, color = background, opacity = visibility} + local badge_width = text_width(self.badge, badge_opts) + local width, height = math.ceil(badge_width + (badge_font_size / 7) * 2), math.ceil(badge_font_size * 0.93) + local bx, by = self.bx - 1, self.by - 1 + ass:rect(bx - width, by - height, bx, by, { + color = foreground, radius = 2, opacity = visibility, + border = self.active and 0 or 1, border_color = background, + }) + ass:txt(bx - width / 2, by - height / 2, 5, self.badge, badge_opts) + + local clip_border = math.max(self.font_size / 20, 1) + local clip_path = assdraw.ass_new() + clip_path:round_rect_cw( + math.floor((bx - width) - clip_border), math.floor((by - height) - clip_border), bx, by, 3 + ) + icon_clip = '\\iclip(' .. clip_path.scale .. ', ' .. clip_path.text .. ')' + end + + -- Icon + local x, y = round(self.ax + (self.bx - self.ax) / 2), round(self.ay + (self.by - self.ay) / 2) + ass:icon(x, y, self.font_size, self.icon, { + color = foreground, border = self.active and 0 or options.text_border, border_color = background, + opacity = visibility, clip = icon_clip, + }) + + return ass +end + +return Button diff --git a/.config/mpv/scripts/uosc_shared/elements/Controls.lua b/.config/mpv/scripts/uosc_shared/elements/Controls.lua new file mode 100644 index 0000000..f9985d3 --- /dev/null +++ b/.config/mpv/scripts/uosc_shared/elements/Controls.lua @@ -0,0 +1,330 @@ +local Element = require('uosc_shared/elements/Element') +local Button = require('uosc_shared/elements/Button') +local CycleButton = require('uosc_shared/elements/CycleButton') +local Speed = require('uosc_shared/elements/Speed') + +-- `scale` - `options.controls_size` scale factor. +-- `ratio` - Width/height ratio of a static or dynamic element. +-- `ratio_min` Min ratio for 'dynamic' sized element. +---@alias ControlItem {element?: Element; kind: string; sizing: 'space' | 'static' | 'dynamic'; scale: number; ratio?: number; ratio_min?: number; hide: boolean; dispositions?: table} + +---@class Controls : Element +local Controls = class(Element) + +function Controls:new() return Class.new(self) --[[@as Controls]] end +function Controls:init() + Element.init(self, 'controls') + ---@type ControlItem[] All control elements serialized from `options.controls`. + self.controls = {} + ---@type ControlItem[] Only controls that match current dispositions. + self.layout = {} + + -- Serialize control elements + local shorthands = { + menu = 'command:menu:script-binding uosc/menu-blurred?Menu', + subtitles = 'command:subtitles:script-binding uosc/subtitles#sub>0?Subtitles', + audio = 'command:graphic_eq:script-binding uosc/audio#audio>1?Audio', + ['audio-device'] = 'command:speaker:script-binding uosc/audio-device?Audio device', + video = 'command:theaters:script-binding uosc/video#video>1?Video', + playlist = 'command:list_alt:script-binding uosc/playlist?Playlist', + chapters = 'command:bookmark:script-binding uosc/chapters#chapters>0?Chapters', + ['editions'] = 'command:bookmarks:script-binding uosc/editions#editions>1?Editions', + ['stream-quality'] = 'command:high_quality:script-binding uosc/stream-quality?Stream quality', + ['open-file'] = 'command:file_open:script-binding uosc/open-file?Open file', + ['items'] = 'command:list_alt:script-binding uosc/items?Playlist/Files', + prev = 'command:arrow_back_ios:script-binding uosc/prev?Previous', + next = 'command:arrow_forward_ios:script-binding uosc/next?Next', + first = 'command:first_page:script-binding uosc/first?First', + last = 'command:last_page:script-binding uosc/last?Last', + ['loop-playlist'] = 'cycle:repeat:loop-playlist:no/inf!?Loop playlist', + ['loop-file'] = 'cycle:repeat_one:loop-file:no/inf!?Loop file', + shuffle = 'toggle:shuffle:shuffle?Shuffle', + fullscreen = 'cycle:crop_free:fullscreen:no/yes=fullscreen_exit!?Fullscreen', + } + + -- Parse out disposition/config pairs + local items = {} + local in_disposition = false + local current_item = nil + for c in options.controls:gmatch('.') do + if not current_item then current_item = {disposition = '', config = ''} end + if c == '<' and #current_item.config == 0 then in_disposition = true + elseif c == '>' and #current_item.config == 0 then in_disposition = false + elseif c == ',' and not in_disposition then + items[#items + 1] = current_item + current_item = nil + else + local prop = in_disposition and 'disposition' or 'config' + current_item[prop] = current_item[prop] .. c + end + end + items[#items + 1] = current_item + + -- Create controls + self.controls = {} + for i, item in ipairs(items) do + local config = shorthands[item.config] and shorthands[item.config] or item.config + local config_tooltip = split(config, ' *%? *') + local tooltip = config_tooltip[2] + config = shorthands[config_tooltip[1]] + and split(shorthands[config_tooltip[1]], ' *%? *')[1] or config_tooltip[1] + local config_badge = split(config, ' *# *') + config = config_badge[1] + local badge = config_badge[2] + local parts = split(config, ' *: *') + local kind, params = parts[1], itable_slice(parts, 2) + + -- Serialize dispositions + local dispositions = {} + for _, definition in ipairs(split(item.disposition, ' *, *')) do + if #definition > 0 then + local value = definition:sub(1, 1) ~= '!' + local name = not value and definition:sub(2) or definition + local prop = name:sub(1, 4) == 'has_' and name or 'is_' .. name + dispositions[prop] = value + end + end + + -- Convert toggles into cycles + if kind == 'toggle' then + kind = 'cycle' + params[#params + 1] = 'no/yes!' + end + + -- Create a control element + local control = {dispositions = dispositions, kind = kind} + + if kind == 'space' then + control.sizing = 'space' + elseif kind == 'gap' then + table_assign(control, {sizing = 'dynamic', scale = 1, ratio = params[1] or 0.3, ratio_min = 0}) + elseif kind == 'command' then + if #params ~= 2 then + mp.error(string.format( + 'command button needs 2 parameters, %d received: %s', #params, table.concat(params, '/') + )) + else + local element = Button:new('control_' .. i, { + icon = params[1], + anchor_id = 'controls', + on_click = function() mp.command(params[2]) end, + tooltip = tooltip, + count_prop = 'sub', + }) + table_assign(control, {element = element, sizing = 'static', scale = 1, ratio = 1}) + if badge then self:register_badge_updater(badge, element) end + end + elseif kind == 'cycle' then + if #params ~= 3 then + mp.error(string.format( + 'cycle button needs 3 parameters, %d received: %s', + #params, table.concat(params, '/') + )) + else + local state_configs = split(params[3], ' */ *') + local states = {} + + for _, state_config in ipairs(state_configs) do + local active = false + if state_config:sub(-1) == '!' then + active = true + state_config = state_config:sub(1, -2) + end + local state_params = split(state_config, ' *= *') + local value, icon = state_params[1], state_params[2] or params[1] + states[#states + 1] = {value = value, icon = icon, active = active} + end + + local element = CycleButton:new('control_' .. i, { + prop = params[2], anchor_id = 'controls', states = states, tooltip = tooltip, + }) + table_assign(control, {element = element, sizing = 'static', scale = 1, ratio = 1}) + if badge then self:register_badge_updater(badge, element) end + end + elseif kind == 'speed' then + if not Elements.speed then + local element = Speed:new({anchor_id = 'controls'}) + table_assign(control, { + element = element, sizing = 'dynamic', scale = params[1] or 1.3, ratio = 3.5, ratio_min = 2, + }) + else + msg.error('there can only be 1 speed slider') + end + else + msg.error('unknown element kind "' .. kind .. '"') + break + end + + self.controls[#self.controls + 1] = control + end + + self:reflow() +end + +function Controls:reflow() + -- Populate the layout only with items that match current disposition + self.layout = {} + for _, control in ipairs(self.controls) do + local matches = true + for prop, value in pairs(control.dispositions) do + if state[prop] ~= value then + matches = false + break + end + end + if control.element then control.element.enabled = matches end + if matches then self.layout[#self.layout + 1] = control end + end + + self:update_dimensions() + Elements:trigger('controls_reflow') +end + +---@param badge string +---@param element Element An element that supports `badge` property. +function Controls:register_badge_updater(badge, element) + local prop_and_limit = split(badge, ' *> *') + local prop, limit = prop_and_limit[1], tonumber(prop_and_limit[2] or -1) + local observable_name, serializer, is_external_prop = prop, nil, false + + if itable_index_of({'sub', 'audio', 'video'}, prop) then + observable_name = 'track-list' + serializer = function(value) + local count = 0 + for _, track in ipairs(value) do if track.type == prop then count = count + 1 end end + return count + end + else + local parts = split(prop, '@') + -- Support both new `prop@owner` and old `@prop` syntaxes + if #parts > 1 then prop, is_external_prop = parts[1] ~= '' and parts[1] or parts[2], true end + serializer = function(value) return value and (type(value) == 'table' and #value or tostring(value)) or nil end + end + + local function handler(_, value) + local new_value = serializer(value) --[[@as nil|string|integer]] + local value_number = tonumber(new_value) + if value_number then new_value = value_number > limit and value_number or nil end + element.badge = new_value + request_render() + end + + if is_external_prop then element['on_external_prop_' .. prop] = function(_, value) handler(prop, value) end + else mp.observe_property(observable_name, 'native', handler) end +end + +function Controls:get_visibility() + local timeline_is_hovered = Elements.timeline.enabled and Elements.timeline.proximity_raw == 0 + return (Elements.speed and Elements.speed.dragging) and 1 or timeline_is_hovered + and -1 or Element.get_visibility(self) +end + +function Controls:update_dimensions() + local window_border = Elements.window_border.size + local size = state.fullormaxed and options.controls_size_fullscreen or options.controls_size + local spacing = options.controls_spacing + local margin = options.controls_margin + + -- Disable when not enough space + local available_space = display.height - Elements.window_border.size * 2 + if Elements.top_bar.enabled then available_space = available_space - Elements.top_bar.size end + if Elements.timeline.enabled then available_space = available_space - Elements.timeline.size_max end + self.enabled = available_space > size + 10 + + -- Reset hide/enabled flags + for c, control in ipairs(self.layout) do + control.hide = false + if control.element then control.element.enabled = self.enabled end + end + + if not self.enabled then return end + + -- Container + self.bx = display.width - window_border - margin + self.by = (Elements.timeline.enabled and Elements.timeline.ay or display.height - window_border) - margin + self.ax, self.ay = window_border + margin, self.by - size + + -- Controls + local available_width = self.bx - self.ax + local statics_width = (#self.layout - 1) * spacing + local min_content_width = statics_width + local max_dynamics_width, dynamic_units, spaces = 0, 0, 0 + + -- Calculate statics_width, min_content_width, and count spaces + for c, control in ipairs(self.layout) do + if control.sizing == 'space' then + spaces = spaces + 1 + elseif control.sizing == 'static' then + local width = size * control.scale * control.ratio + statics_width = statics_width + width + min_content_width = min_content_width + width + elseif control.sizing == 'dynamic' then + min_content_width = min_content_width + size * control.scale * control.ratio_min + max_dynamics_width = max_dynamics_width + size * control.scale * control.ratio + dynamic_units = dynamic_units + control.scale * control.ratio + end + end + + -- Hide & disable elements in the middle until we fit into available width + if min_content_width > available_width then + local i = math.ceil(#self.layout / 2 + 0.1) + for a = 0, #self.layout - 1, 1 do + i = i + (a * (a % 2 == 0 and 1 or -1)) + local control = self.layout[i] + + if control.kind ~= 'gap' and control.kind ~= 'space' then + control.hide = true + if control.element then control.element.enabled = false end + if control.sizing == 'static' then + local width = size * control.scale * control.ratio + min_content_width = min_content_width - width - spacing + statics_width = statics_width - width - spacing + elseif control.sizing == 'dynamic' then + min_content_width = min_content_width - size * control.scale * control.ratio_min - spacing + max_dynamics_width = max_dynamics_width - size * control.scale * control.ratio + dynamic_units = dynamic_units - control.scale * control.ratio + end + + if min_content_width < available_width then break end + end + end + end + + -- Lay out the elements + local current_x = self.ax + local width_for_dynamics = available_width - statics_width + local space_width = (width_for_dynamics - max_dynamics_width) / spaces + + for c, control in ipairs(self.layout) do + if not control.hide then + local sizing, element, scale, ratio = control.sizing, control.element, control.scale, control.ratio + local width, height = 0, 0 + + if sizing == 'space' then + if space_width > 0 then width = space_width end + elseif sizing == 'static' then + height = size * scale + width = height * ratio + elseif sizing == 'dynamic' then + height = size * scale + width = max_dynamics_width < width_for_dynamics + and height * ratio or width_for_dynamics * ((scale * ratio) / dynamic_units) + end + + local bx = current_x + width + if element then element:set_coordinates(round(current_x), round(self.by - height), bx, self.by) end + current_x = bx + spacing + end + end + + Elements:update_proximities() + request_render() +end + +function Controls:on_dispositions() self:reflow() end +function Controls:on_display() self:update_dimensions() end +function Controls:on_prop_border() self:update_dimensions() end +function Controls:on_prop_fullormaxed() self:update_dimensions() end +function Controls:on_timeline_enabled() self:update_dimensions() end + +return Controls diff --git a/.config/mpv/scripts/uosc_shared/elements/Curtain.lua b/.config/mpv/scripts/uosc_shared/elements/Curtain.lua new file mode 100644 index 0000000..99b9f14 --- /dev/null +++ b/.config/mpv/scripts/uosc_shared/elements/Curtain.lua @@ -0,0 +1,35 @@ +local Element = require('uosc_shared/elements/Element') + +---@class Curtain : Element +local Curtain = class(Element) + +function Curtain:new() return Class.new(self) --[[@as Curtain]] end +function Curtain:init() + Element.init(self, 'curtain', {ignores_menu = true}) + self.opacity = 0 + ---@type string[] + self.dependents = {} +end + +---@param id string +function Curtain:register(id) + self.dependents[#self.dependents + 1] = id + if #self.dependents == 1 then self:tween_property('opacity', self.opacity, 1) end +end + +---@param id string +function Curtain:unregister(id) + self.dependents = itable_filter(self.dependents, function(item) return item ~= id end) + if #self.dependents == 0 then self:tween_property('opacity', self.opacity, 0) end +end + +function Curtain:render() + if self.opacity == 0 or options.curtain_opacity == 0 then return end + local ass = assdraw.ass_new() + ass:rect(0, 0, display.width, display.height, { + color = '000000', opacity = options.curtain_opacity * self.opacity, + }) + return ass +end + +return Curtain diff --git a/.config/mpv/scripts/uosc_shared/elements/CycleButton.lua b/.config/mpv/scripts/uosc_shared/elements/CycleButton.lua new file mode 100644 index 0000000..7f1c02f --- /dev/null +++ b/.config/mpv/scripts/uosc_shared/elements/CycleButton.lua @@ -0,0 +1,64 @@ +local Button = require('uosc_shared/elements/Button') + +---@alias CycleState {value: any; icon: string; active?: boolean} +---@alias CycleButtonProps {prop: string; states: CycleState[]; anchor_id?: string; tooltip?: string} + +---@class CycleButton : Button +local CycleButton = class(Button) + +---@param id string +---@param props CycleButtonProps +function CycleButton:new(id, props) return Class.new(self, id, props) --[[@as CycleButton]] end +---@param id string +---@param props CycleButtonProps +function CycleButton:init(id, props) + local is_state_prop = itable_index_of({'shuffle'}, props.prop) + self.prop = props.prop + self.states = props.states + + Button.init(self, id, props) + + self.icon = self.states[1].icon + self.active = self.states[1].active + self.current_state_index = 1 + self.on_click = function() + local new_state = self.states[self.current_state_index + 1] or self.states[1] + local new_value = new_state.value + if self.owner then + mp.commandv('script-message-to', self.owner, 'set', self.prop, new_value) + elseif is_state_prop then + if itable_index_of({'yes', 'no'}, new_value) then new_value = new_value == 'yes' end + set_state(self.prop, new_value) + else + mp.set_property(self.prop, new_value) + end + end + + self.handle_change = function(name, value) + if is_state_prop and type(value) == 'boolean' then value = value and 'yes' or 'no' end + local index = itable_find(self.states, function(state) return state.value == value end) + self.current_state_index = index or 1 + self.icon = self.states[self.current_state_index].icon + self.active = self.states[self.current_state_index].active + request_render() + end + + local prop_parts = split(self.prop, '@') + if #prop_parts == 2 then -- External prop with a script owner + self.prop, self.owner = prop_parts[1], prop_parts[2] + self['on_external_prop_' .. self.prop] = function(_, value) self.handle_change(self.prop, value) end + self.handle_change(self.prop, external[self.prop]) + elseif is_state_prop then -- uosc's state props + self['on_prop_' .. self.prop] = function(self, value) self.handle_change(self.prop, value) end + self.handle_change(self.prop, state[self.prop]) + else + mp.observe_property(self.prop, 'string', self.handle_change) + end +end + +function CycleButton:destroy() + Button.destroy(self) + mp.unobserve_property(self.handle_change) +end + +return CycleButton diff --git a/.config/mpv/scripts/uosc_shared/elements/Element.lua b/.config/mpv/scripts/uosc_shared/elements/Element.lua new file mode 100644 index 0000000..399c4e6 --- /dev/null +++ b/.config/mpv/scripts/uosc_shared/elements/Element.lua @@ -0,0 +1,148 @@ +---@alias ElementProps {enabled?: boolean; ax?: number; ay?: number; bx?: number; by?: number; ignores_menu?: boolean; anchor_id?: string;} + +-- Base class all elements inherit from. +---@class Element : Class +local Element = class() + +---@param id string +---@param props? ElementProps +function Element:init(id, props) + self.id = id + -- `false` means element won't be rendered, or receive events + self.enabled = true + -- Element coordinates + self.ax, self.ay, self.bx, self.by = 0, 0, 0, 0 + -- Relative proximity from `0` - mouse outside `proximity_max` range, to `1` - mouse within `proximity_min` range. + self.proximity = 0 + -- Raw proximity in pixels. + self.proximity_raw = infinity + ---@type number `0-1` factor to force min visibility. Used for toggling element's permanent visibility. + self.min_visibility = 0 + ---@type number `0-1` factor to force a visibility value. Used for flashing, fading out, and other animations + self.forced_visibility = nil + ---@type boolean Render this element even when menu is open. + self.ignores_menu = false + ---@type nil|string ID of an element from which this one should inherit visibility. + self.anchor_id = nil + + if props then table_assign(self, props) end + + -- Flash timer + self._flash_out_timer = mp.add_timeout(options.flash_duration / 1000, function() + local getTo = function() return self.proximity end + self:tween_property('forced_visibility', 1, getTo, function() + self.forced_visibility = nil + end) + end) + self._flash_out_timer:kill() + + Elements:add(self) +end + +function Element:destroy() + self.destroyed = true + Elements:remove(self) +end + +---@param ax number +---@param ay number +---@param bx number +---@param by number +function Element:set_coordinates(ax, ay, bx, by) + self.ax, self.ay, self.bx, self.by = ax, ay, bx, by + Elements:update_proximities() + self:maybe('on_coordinates') +end + +function Element:update_proximity() + if cursor.hidden then + self.proximity_raw = infinity + self.proximity = 0 + else + local range = options.proximity_out - options.proximity_in + self.proximity_raw = get_point_to_rectangle_proximity(cursor, self) + self.proximity = 1 - (clamp(0, self.proximity_raw - options.proximity_in, range) / range) + end +end + +-- Decide elements visibility based on proximity and various other factors +function Element:get_visibility() + -- Hide when menu is open, unless this is a menu + ---@diagnostic disable-next-line: undefined-global + if not self.ignores_menu and Menu and Menu:is_open() then return 0 end + + -- Persistency + local persist = config[self.id .. '_persistency'] + if persist and ( + (persist.audio and state.is_audio) + or (persist.paused and state.pause) + or (persist.video and state.is_video) + or (persist.image and state.is_image) + or (persist.idle and state.is_idle) + ) then return 1 end + + -- Forced visibility + if self.forced_visibility then return math.max(self.forced_visibility, self.min_visibility) end + + -- Anchor inheritance + -- If anchor returns -1, it means all attached elements should force hide. + local anchor = self.anchor_id and Elements[self.anchor_id] + local anchor_visibility = anchor and anchor:get_visibility() or 0 + + return anchor_visibility == -1 and 0 or math.max(self.proximity, anchor_visibility, self.min_visibility) +end + +-- Call method if it exists +function Element:maybe(name, ...) + if self[name] then return self[name](self, ...) end +end + +-- Attach a tweening animation to this element +---@param from number +---@param to number|fun():number +---@param setter fun(value: number) +---@param factor_or_callback? number|fun() +---@param callback? fun() Called either on animation end, or when animation is killed. +function Element:tween(from, to, setter, factor_or_callback, callback) + self:tween_stop() + self._kill_tween = self.enabled and tween( + from, to, setter, factor_or_callback, + function() + self._kill_tween = nil + if callback then callback() end + end + ) +end + +function Element:is_tweening() return self and self._kill_tween end +function Element:tween_stop() self:maybe('_kill_tween') end + +-- Animate an element property between 2 values. +---@param prop string +---@param from number +---@param to number|fun():number +---@param factor_or_callback? number|fun() +---@param callback? fun() Called either on animation end, or when animation is killed. +function Element:tween_property(prop, from, to, factor_or_callback, callback) + self:tween(from, to, function(value) self[prop] = value end, factor_or_callback, callback) +end + +---@param name string +function Element:trigger(name, ...) + self:maybe('on_' .. name, ...) + request_render() +end + +-- Briefly flashes the element for `options.flash_duration` milliseconds. +-- Useful to visualize changes of volume and timeline when changed via hotkeys. +function Element:flash() + if options.flash_duration > 0 and (self.proximity < 1 or self._flash_out_timer:is_enabled()) then + self:tween_stop() + self.forced_visibility = 1 + request_render() + self._flash_out_timer:kill() + self._flash_out_timer:resume() + end +end + +return Element diff --git a/.config/mpv/scripts/uosc_shared/elements/Elements.lua b/.config/mpv/scripts/uosc_shared/elements/Elements.lua new file mode 100644 index 0000000..2288760 --- /dev/null +++ b/.config/mpv/scripts/uosc_shared/elements/Elements.lua @@ -0,0 +1,154 @@ +local Elements = {itable = {}} + +---@param element Element +function Elements:add(element) + if not element.id then + msg.error('attempt to add element without "id" property') + return + end + + if self:has(element.id) then Elements:remove(element.id) end + + self.itable[#self.itable + 1] = element + self[element.id] = element + + request_render() +end + +function Elements:remove(idOrElement) + if not idOrElement then return end + local id = type(idOrElement) == 'table' and idOrElement.id or idOrElement + local element = Elements[id] + if element then + if not element.destroyed then element:destroy() end + element.enabled = false + self.itable = itable_remove(self.itable, self[id]) + self[id] = nil + request_render() + end +end + +function Elements:update_proximities() + local capture_mbtn_left = false + local capture_wheel = false + local menu_only = Elements.menu ~= nil + local mouse_leave_elements = {} + local mouse_enter_elements = {} + + -- Calculates proximities and opacities for defined elements + for _, element in self:ipairs() do + if element.enabled then + local previous_proximity_raw = element.proximity_raw + + -- If menu is open, all other elements have to be disabled + if menu_only then + if element.ignores_menu then + capture_mbtn_left = true + capture_wheel = true + element:update_proximity() + else + element.proximity_raw = infinity + element.proximity = 0 + end + else + element:update_proximity() + end + + -- Element has global forced key listeners + if element.on_global_mbtn_left_down then capture_mbtn_left = true end + if element.on_global_wheel_up or element.on_global_wheel_down then capture_wheel = true end + + if element.proximity_raw == 0 then + -- Element has local forced key listeners + if element.on_mbtn_left_down then capture_mbtn_left = true end + if element.on_wheel_up or element.on_wheel_up then capture_wheel = true end + + -- Mouse entered element area + if previous_proximity_raw ~= 0 then + mouse_enter_elements[#mouse_enter_elements + 1] = element + end + else + -- Mouse left element area + if previous_proximity_raw == 0 then + mouse_leave_elements[#mouse_leave_elements + 1] = element + end + end + end + end + + -- Enable key group captures requested by elements + mp[capture_mbtn_left and 'enable_key_bindings' or 'disable_key_bindings']('mbtn_left') + mp[capture_wheel and 'enable_key_bindings' or 'disable_key_bindings']('wheel') + + -- Trigger `mouse_leave` and `mouse_enter` events + for _, element in ipairs(mouse_leave_elements) do element:trigger('mouse_leave') end + for _, element in ipairs(mouse_enter_elements) do element:trigger('mouse_enter') end +end + +-- Toggles passed elements' min visibilities between 0 and 1. +---@param ids string[] IDs of elements to peek. +function Elements:toggle(ids) + local has_invisible = itable_find(ids, function(id) return Elements[id] and Elements[id].min_visibility ~= 1 end) + self:set_min_visibility(has_invisible and 1 or 0, ids) +end + +-- Set (animate) elements' min visibilities to passed value. +---@param visibility number 0-1 floating point. +---@param ids string[] IDs of elements to peek. +function Elements:set_min_visibility(visibility, ids) + for _, id in ipairs(ids) do + local element = Elements[id] + if element then element:tween_property('min_visibility', element.min_visibility, visibility) end + end +end + +-- Flash passed elements. +---@param ids string[] IDs of elements to peek. +function Elements:flash(ids) + local elements = itable_filter(self.itable, function(element) return itable_index_of(ids, element.id) ~= nil end) + for _, element in ipairs(elements) do element:flash() end +end + +---@param name string Event name. +function Elements:trigger(name, ...) + for _, element in self:ipairs() do element:trigger(name, ...) end +end + +-- Trigger two events, `name` and `global_name`, depending on element-cursor proximity. +-- Disabled elements don't receive these events. +---@param name string Event name. +function Elements:proximity_trigger(name, ...) + for _, element in self:ipairs() do + if element.enabled then + if element.proximity_raw == 0 then element:trigger(name, ...) end + element:trigger('global_' .. name, ...) + end + end +end + +function Elements:has(id) return self[id] ~= nil end +function Elements:ipairs() return ipairs(self.itable) end + +---@param name string Event name. +function Elements:create_proximity_dispatcher(name) + return function(...) self:proximity_trigger(name, ...) end +end + +mp.set_key_bindings({ + { + 'mbtn_left', + Elements:create_proximity_dispatcher('mbtn_left_up'), + function(...) + update_mouse_pos(nil, mp.get_property_native('mouse-pos'), true) + Elements:proximity_trigger('mbtn_left_down', ...) + end, + }, + {'mbtn_left_dbl', 'ignore'}, +}, 'mbtn_left', 'force') + +mp.set_key_bindings({ + {'wheel_up', Elements:create_proximity_dispatcher('wheel_up')}, + {'wheel_down', Elements:create_proximity_dispatcher('wheel_down')}, +}, 'wheel', 'force') + +return Elements diff --git a/.config/mpv/scripts/uosc_shared/elements/Menu.lua b/.config/mpv/scripts/uosc_shared/elements/Menu.lua new file mode 100644 index 0000000..950c3c9 --- /dev/null +++ b/.config/mpv/scripts/uosc_shared/elements/Menu.lua @@ -0,0 +1,772 @@ +local Element = require('uosc_shared/elements/Element') + +-- Menu data structure accepted by `Menu:open(menu)`. +---@alias MenuData {type?: string; title?: string; hint?: string; keep_open?: boolean; separator?: boolean; items?: MenuDataItem[]; selected_index?: integer;} +---@alias MenuDataItem MenuDataValue|MenuData +---@alias MenuDataValue {title?: string; hint?: string; icon?: string; value: any; bold?: boolean; italic?: boolean; muted?: boolean; active?: boolean; keep_open?: boolean; separator?: boolean;} +---@alias MenuOptions {mouse_nav?: boolean; on_open?: fun(), on_close?: fun()} + +-- Internal data structure created from `Menu`. +---@alias MenuStack {id?: string; type?: string; title?: string; hint?: string; selected_index?: number; keep_open?: boolean; separator?: boolean; items: MenuStackItem[]; parent_menu?: MenuStack; active?: boolean; width: number; height: number; top: number; scroll_y: number; scroll_height: number; title_width: number; hint_width: number; max_width: number; is_root?: boolean; fling?: Fling} +---@alias MenuStackItem MenuStackValue|MenuStack +---@alias MenuStackValue {title?: string; hint?: string; icon?: string; value: any; active?: boolean; bold?: boolean; italic?: boolean; muted?: boolean; keep_open?: boolean; separator?: boolean; title_width: number; hint_width: number} +---@alias Fling {y: number, distance: number, time: number, easing: fun(x: number), duration: number, update_cursor?: boolean} + +---@class Menu : Element +local Menu = class(Element) + +---@param data MenuData +---@param callback fun(value: any) +---@param opts? MenuOptions +function Menu:open(data, callback, opts) + local open_menu = self:is_open() + if open_menu then + open_menu.is_being_replaced = true + open_menu:close(true) + end + return Menu:new(data, callback, opts) +end + +---@param menu_type? string +---@return Menu|nil +function Menu:is_open(menu_type) + return Elements.menu and (not menu_type or Elements.menu.type == menu_type) and Elements.menu or nil +end + +---@param immediate? boolean Close immediately without fadeout animation. +---@param callback? fun() Called after the animation (if any) ends and element is removed and destroyed. +---@overload fun(callback: fun()) +function Menu:close(immediate, callback) + if type(immediate) ~= 'boolean' then callback = immediate end + + local menu = self == Menu and Elements.menu or self + + if menu and not menu.destroyed then + if menu.is_closing then + menu:tween_stop() + return + end + + local function close() + Elements:remove('menu') + menu.is_closing, menu.stack, menu.current, menu.all, menu.by_id = false, nil, nil, {}, {} + menu:disable_key_bindings() + Elements:update_proximities() + if callback then callback() end + request_render() + end + + menu.is_closing = true + + if immediate then close() + else menu:fadeout(close) end + end +end + +---@param data MenuData +---@param callback fun(value: any) +---@param opts? MenuOptions +---@return Menu +function Menu:new(data, callback, opts) return Class.new(self, data, callback, opts) --[[@as Menu]] end +---@param data MenuData +---@param callback fun(value: any) +---@param opts? MenuOptions +function Menu:init(data, callback, opts) + Element.init(self, 'menu', {ignores_menu = true}) + + -----@type fun() + self.callback = callback + self.opts = opts or {} + self.offset_x = 0 -- Used for submenu transition animation. + self.mouse_nav = self.opts.mouse_nav -- Stops pre-selecting items + self.item_height = nil + self.item_spacing = 1 + self.item_padding = nil + self.font_size = nil + self.font_size_hint = nil + self.scroll_step = nil -- Item height + item spacing. + self.scroll_height = nil -- Items + spacings - container height. + self.opacity = 0 -- Used to fade in/out. + self.type = data.type + ---@type MenuStack Root MenuStack. + self.root = nil + ---@type MenuStack Current MenuStack. + self.current = nil + ---@type MenuStack[] All menus in a flat array. + self.all = nil + ---@type table Map of submenus by their ids, such as `'Tools > Aspect ratio'`. + self.by_id = {} + self.key_bindings = {} + self.is_being_replaced = false + self.is_closing = false + ---@type {y: integer, time: number}[] + self.drag_data = nil + self.is_dragging = false + + self:update(data) + + if self.mouse_nav then + if self.current then self.current.selected_index = nil end + else + for _, menu in ipairs(self.all) do self:scroll_to_index(menu.selected_index, menu) end + end + + self:tween_property('opacity', 0, 1) + self:enable_key_bindings() + Elements.curtain:register('menu') + if self.opts.on_open then self.opts.on_open() end +end + +function Menu:destroy() + Element.destroy(self) + self:disable_key_bindings() + if not self.is_being_replaced then Elements.curtain:unregister('menu') end + if self.opts.on_close then self.opts.on_close() end +end + +---@param data MenuData +function Menu:update(data) + self.type = data.type + + local new_root = {is_root = true} + local new_all = {} + local new_by_id = {} + local menus_to_serialize = {{new_root, data}} + local old_current_id = self.current and self.current.id + + table_assign(new_root, data, {'title', 'hint', 'keep_open'}) + + local i = 0 + while i < #menus_to_serialize do + i = i + 1 + local menu, menu_data = menus_to_serialize[i][1], menus_to_serialize[i][2] + local parent_id = menu.parent_menu and not menu.parent_menu.is_root and menu.parent_menu.id + if not menu.is_root then + menu.id = (parent_id and parent_id .. ' > ' or '') .. (menu_data.title or i) + end + menu.icon = 'chevron_right' + + -- Update items + local first_active_index = nil + menu.items = {} + + for i, item_data in ipairs(menu_data.items or {}) do + if item_data.active and not first_active_index then first_active_index = i end + + local item = {} + table_assign(item, item_data, { + 'title', 'icon', 'hint', 'active', 'bold', 'italic', 'muted', 'value', 'keep_open', 'separator', + }) + if item.keep_open == nil then item.keep_open = menu.keep_open end + + -- Submenu + if item_data.items then + item.parent_menu = menu + menus_to_serialize[#menus_to_serialize + 1] = {item, item_data} + end + + menu.items[i] = item + end + + if menu.is_root then menu.selected_index = menu_data.selected_index or first_active_index end + + -- Retain old state + local old_menu = self.by_id[menu.is_root and '__root__' or menu.id] + if old_menu then table_assign(menu, old_menu, {'selected_index', 'scroll_y', 'fling'}) end + + new_all[#new_all + 1] = menu + new_by_id[menu.is_root and '__root__' or menu.id] = menu + end + + self.root, self.all, self.by_id = new_root, new_all, new_by_id + self.current = self.by_id[old_current_id] or self.root + + self:update_content_dimensions() + self:reset_navigation() +end + +---@param items MenuDataItem[] +function Menu:update_items(items) + local data = table_shallow_copy(self.root) + data.items = items + self:update(data) +end + +function Menu:update_content_dimensions() + self.item_height = state.fullormaxed and options.menu_item_height_fullscreen or options.menu_item_height + self.font_size = round(self.item_height * 0.48 * options.font_scale) + self.font_size_hint = self.font_size - 1 + self.item_padding = round((self.item_height - self.font_size) * 0.6) + self.scroll_step = self.item_height + self.item_spacing + + local title_opts = {size = self.font_size, italic = false, bold = false} + local hint_opts = {size = self.font_size_hint} + + for _, menu in ipairs(self.all) do + -- Estimate width of a widest item + local max_width = 0 + for _, item in ipairs(menu.items) do + local icon_width = item.icon and self.font_size or 0 + item.title_width = text_width(item.title, title_opts) + item.hint_width = text_width(item.hint, hint_opts) + local spacings_in_item = 1 + (item.title_width > 0 and 1 or 0) + + (item.hint_width > 0 and 1 or 0) + (icon_width > 0 and 1 or 0) + local estimated_width = item.title_width + item.hint_width + icon_width + + (self.item_padding * spacings_in_item) + if estimated_width > max_width then max_width = estimated_width end + end + + -- Also check menu title + title_opts.bold, title_opts.italic = true, false + local menu_title_width = text_width(menu.title, title_opts) + if menu_title_width > max_width then max_width = menu_title_width end + + menu.max_width = max_width + end + + self:update_dimensions() +end + +function Menu:update_dimensions() + -- Coordinates and sizes are of the scrollable area to make + -- consuming values in rendering and collisions easier. Title drawn above this, so + -- we need to account for that in max_height and ay position. + local min_width = state.fullormaxed and options.menu_min_width_fullscreen or options.menu_min_width + + for _, menu in ipairs(self.all) do + menu.width = round(clamp(min_width, menu.max_width, display.width * 0.9)) + local title_height = (menu.is_root and menu.title) and self.scroll_step or 0 + local max_height = round((display.height - title_height) * 0.9) + local content_height = self.scroll_step * #menu.items + menu.height = math.min(content_height - self.item_spacing, max_height) + menu.top = round(math.max((display.height - menu.height) / 2, title_height * 1.5)) + menu.scroll_height = math.max(content_height - menu.height - self.item_spacing, 0) + menu.scroll_y = menu.scroll_y or 0 + self:scroll_to(menu.scroll_y, menu) -- clamps scroll_y to scroll limits + end + + local ax = round((display.width - self.current.width) / 2) + self.offset_x + self:set_coordinates(ax, self.current.top, ax + self.current.width, self.current.top + self.current.height) +end + +function Menu:reset_navigation() + local menu = self.current + + -- Reset indexes and scroll + self:scroll_to(menu.scroll_y) -- clamps scroll_y to scroll limits + if self.mouse_nav then + self:select_item_below_cursor() + else + self:select_index((menu.items and #menu.items > 0) and clamp(1, menu.selected_index or 1, #menu.items) or nil) + end + + -- Walk up the parent menu chain and activate items that lead to current menu + local parent = menu.parent_menu + while parent do + parent.selected_index = itable_index_of(parent.items, menu) + menu, parent = parent, parent.parent_menu + end + + request_render() +end + +function Menu:set_offset_x(offset) + local delta = offset - self.offset_x + self.offset_x = offset + self:set_coordinates(self.ax + delta, self.ay, self.bx + delta, self.by) +end + +function Menu:fadeout(callback) self:tween_property('opacity', 1, 0, callback) end + +function Menu:get_item_index_below_cursor() + local menu = self.current + if #menu.items < 1 or self.proximity_raw > 0 then return nil end + return math.max(1, math.min(math.ceil((cursor.y - self.ay + menu.scroll_y) / self.scroll_step), #menu.items)) +end + +function Menu:get_first_active_index(menu) + menu = menu or self.current + for index, item in ipairs(self.current.items) do + if item.active then return index end + end +end + +---@param pos? number +---@param menu? MenuStack +function Menu:set_scroll_to(pos, menu) + menu = menu or self.current + menu.scroll_y = clamp(0, pos or 0, menu.scroll_height) + request_render() +end + +---@param delta? number +---@param menu? MenuStack +function Menu:set_scroll_by(delta, menu) + menu = menu or self.current + self:set_scroll_to(menu.scroll_y + delta, menu) +end + +---@param pos? number +---@param menu? MenuStack +---@param fling_options? table +function Menu:scroll_to(pos, menu, fling_options) + menu = menu or self.current + menu.fling = { + y = menu.scroll_y, distance = clamp(-menu.scroll_y, pos - menu.scroll_y, menu.scroll_height - menu.scroll_y), + time = mp.get_time(), duration = 0.1, easing = ease_out_sext, + } + if fling_options then table_assign(menu.fling, fling_options) end + request_render() +end + +---@param delta? number +---@param menu? MenuStack +---@param fling_options? Fling +function Menu:scroll_by(delta, menu, fling_options) + menu = menu or self.current + self:scroll_to((menu.fling and (menu.fling.y + menu.fling.distance) or menu.scroll_y) + delta, menu, fling_options) +end + +---@param index? integer +---@param menu? MenuStack +function Menu:scroll_to_index(index, menu) + menu = menu or self.current + if (index and index >= 1 and index <= #menu.items) then + self:scroll_to(round((self.scroll_step * (index - 1)) - ((menu.height - self.scroll_step) / 2)), menu) + end +end + +---@param index? integer +---@param menu? MenuStack +function Menu:select_index(index, menu) + menu = menu or self.current + menu.selected_index = (index and index >= 1 and index <= #menu.items) and index or nil + request_render() +end + +---@param value? any +---@param menu? MenuStack +function Menu:select_value(value, menu) + menu = menu or self.current + local index = itable_find(menu.items, function(_, item) return item.value == value end) + self:select_index(index, 5) +end + +---@param menu? MenuStack +function Menu:deactivate_items(menu) + menu = menu or self.current + for _, item in ipairs(menu.items) do item.active = false end + request_render() +end + +---@param index? integer +---@param menu? MenuStack +function Menu:activate_index(index, menu) + menu = menu or self.current + if index and index >= 1 and index <= #menu.items then menu.items[index].active = true end + request_render() +end + +---@param index? integer +---@param menu? MenuStack +function Menu:activate_unique_index(index, menu) + self:deactivate_items(menu) + self:activate_index(index, menu) +end + +---@param value? any +---@param menu? MenuStack +function Menu:activate_value(value, menu) + menu = menu or self.current + local index = itable_find(menu.items, function(_, item) return item.value == value end) + self:activate_index(index, menu) +end + +---@param value? any +---@param menu? MenuStack +function Menu:activate_unique_value(value, menu) + menu = menu or self.current + local index = itable_find(menu.items, function(_, item) return item.value == value end) + self:activate_unique_index(index, menu) +end + +---@param id string +function Menu:activate_submenu(id) + local submenu = self.by_id[id] + if submenu then + self.current = submenu + request_render() + else + msg.error(string.format('Requested submenu id "%s" doesn\'t exist', id)) + end + self:reset_navigation() +end + +---@param index? integer +---@param menu? MenuStack +function Menu:delete_index(index, menu) + menu = menu or self.current + if (index and index >= 1 and index <= #menu.items) then + table.remove(menu.items, index) + self:update_content_dimensions() + self:scroll_to_index(menu.selected_index, menu) + end +end + +---@param value? any +---@param menu? MenuStack +function Menu:delete_value(value, menu) + menu = menu or self.current + local index = itable_find(menu.items, function(_, item) return item.value == value end) + self:delete_index(index) +end + +---@param menu? MenuStack +function Menu:prev(menu) + menu = menu or self.current + menu.selected_index = math.max(menu.selected_index and menu.selected_index - 1 or #menu.items, 1) + self:scroll_to_index(menu.selected_index, menu) +end + +---@param menu? MenuStack +function Menu:next(menu) + menu = menu or self.current + menu.selected_index = math.min(menu.selected_index and menu.selected_index + 1 or 1, #menu.items) + self:scroll_to_index(menu.selected_index, menu) +end + +function Menu:back() + local menu = self.current + local parent = menu.parent_menu + + if not parent then return self:close() end + + menu.selected_index = nil + self.current = parent + self:update_dimensions() + self:tween(self.offset_x - menu.width / 2, 0, function(offset) self:set_offset_x(offset) end) + self.opacity = 1 -- in case tween above canceled fade in animation +end + +---@param opts? {keep_open?: boolean, preselect_submenu_item?: boolean} +function Menu:open_selected_item(opts) + opts = opts or {} + local menu = self.current + if menu.selected_index then + local item = menu.items[menu.selected_index] + -- Is submenu + if item.items then + self.current = item + if opts.preselect_submenu_item then + item.selected_index = #item.items > 0 and 1 or nil + end + self:update_dimensions() + self:tween(self.offset_x + menu.width / 2, 0, function(offset) self:set_offset_x(offset) end) + self.opacity = 1 -- in case tween above canceled fade in animation + else + self.callback(item.value) + if not item.keep_open and not opts.keep_open then self:close() end + end + end +end + +function Menu:open_selected_item_soft() self:open_selected_item({keep_open = true}) end +function Menu:open_selected_item_preselect() self:open_selected_item({preselect_submenu_item = true}) end +function Menu:select_item_below_cursor() self.current.selected_index = self:get_item_index_below_cursor() end + +function Menu:on_display() self:update_dimensions() end +function Menu:on_prop_fullormaxed() self:update_content_dimensions() end + +function Menu:on_global_mbtn_left_down() + if self.proximity_raw == 0 then + self.drag_data = {{y = cursor.y, time = mp.get_time()}} + self.current.fling = nil + else + if cursor.x < self.ax then self:back() + else self:close() end + end +end + +function Menu:fling_distance() + local first, last = self.drag_data[1], self.drag_data[#self.drag_data] + if mp.get_time() - last.time > 0.05 then return 0 end + for i = #self.drag_data - 1, 1, -1 do + local drag = self.drag_data[i] + if last.time - drag.time > 0.03 then return ((drag.y - last.y) / ((last.time - drag.time) / 0.03)) * 10 end + end + return #self.drag_data < 2 and 0 or ((first.y - last.y) / ((first.time - last.time) / 0.03)) * 10 +end + +function Menu:on_global_mbtn_left_up() + if self.proximity_raw == 0 and self.drag_data and not self.is_dragging then + self:select_item_below_cursor() + self:open_selected_item({preselect_submenu_item = false}) + end + if self.is_dragging then + local distance = self:fling_distance() + if math.abs(distance) > 50 then + self.current.fling = { + y = self.current.scroll_y, distance = distance, time = self.drag_data[#self.drag_data].time, + easing = ease_out_quart, duration = 0.5, update_cursor = true, + } + end + end + self.is_dragging = false + self.drag_data = nil +end + + +function Menu:on_global_mouse_move() + self.mouse_nav = true + if self.drag_data then + self.is_dragging = self.is_dragging or math.abs(cursor.y - self.drag_data[1].y) >= 10 + local distance = self.drag_data[#self.drag_data].y - cursor.y + if distance ~= 0 then self:set_scroll_by(distance) end + self.drag_data[#self.drag_data + 1] = {y = cursor.y, time = mp.get_time()} + end + if self.proximity_raw == 0 or self.is_dragging then self:select_item_below_cursor() + else self.current.selected_index = nil end + request_render() +end + +function Menu:on_wheel_up() self:scroll_by(self.scroll_step * -3, nil, {update_cursor = true}) end +function Menu:on_wheel_down() self:scroll_by(self.scroll_step * 3, nil, {update_cursor = true}) end + +function Menu:on_pgup() + local menu = self.current + local items_per_page = round((menu.height / self.scroll_step) * 0.4) + local paged_index = (menu.selected_index and menu.selected_index or #menu.items) - items_per_page + menu.selected_index = clamp(1, paged_index, #menu.items) + if menu.selected_index > 0 then self:scroll_to_index(menu.selected_index) end +end + +function Menu:on_pgdwn() + local menu = self.current + local items_per_page = round((menu.height / self.scroll_step) * 0.4) + local paged_index = (menu.selected_index and menu.selected_index or 1) + items_per_page + menu.selected_index = clamp(1, paged_index, #menu.items) + if menu.selected_index > 0 then self:scroll_to_index(menu.selected_index) end +end + +function Menu:on_home() + self.current.selected_index = math.min(1, #self.current.items) + if self.current.selected_index > 0 then self:scroll_to_index(self.current.selected_index) end +end + +function Menu:on_end() + self.current.selected_index = #self.current.items + if self.current.selected_index > 0 then self:scroll_to_index(self.current.selected_index) end +end + +function Menu:add_key_binding(key, name, fn, flags) + self.key_bindings[#self.key_bindings + 1] = name + mp.add_forced_key_binding(key, name, fn, flags) +end + +function Menu:enable_key_bindings() + -- The `mp.set_key_bindings()` method would be easier here, but that + -- doesn't support 'repeatable' flag, so we are stuck with this monster. + self:add_key_binding('up', 'menu-prev1', self:create_key_action('prev'), 'repeatable') + self:add_key_binding('down', 'menu-next1', self:create_key_action('next'), 'repeatable') + self:add_key_binding('left', 'menu-back1', self:create_key_action('back')) + self:add_key_binding('right', 'menu-select1', self:create_key_action('open_selected_item_preselect')) + self:add_key_binding('shift+right', 'menu-select-soft1', self:create_key_action('open_selected_item_soft')) + self:add_key_binding('shift+mbtn_left', 'menu-select-soft', self:create_key_action('open_selected_item_soft')) + self:add_key_binding('mbtn_back', 'menu-back-alt3', self:create_key_action('back')) + self:add_key_binding('bs', 'menu-back-alt4', self:create_key_action('back')) + self:add_key_binding('enter', 'menu-select-alt3', self:create_key_action('open_selected_item_preselect')) + self:add_key_binding('kp_enter', 'menu-select-alt4', self:create_key_action('open_selected_item_preselect')) + self:add_key_binding('shift+enter', 'menu-select-alt5', self:create_key_action('open_selected_item_soft')) + self:add_key_binding('shift+kp_enter', 'menu-select-alt6', self:create_key_action('open_selected_item_soft')) + self:add_key_binding('esc', 'menu-close', self:create_key_action('close')) + self:add_key_binding('pgup', 'menu-page-up', self:create_key_action('on_pgup')) + self:add_key_binding('pgdwn', 'menu-page-down', self:create_key_action('on_pgdwn')) + self:add_key_binding('home', 'menu-home', self:create_key_action('on_home')) + self:add_key_binding('end', 'menu-end', self:create_key_action('on_end')) +end + +function Menu:disable_key_bindings() + for _, name in ipairs(self.key_bindings) do mp.remove_key_binding(name) end + self.key_bindings = {} +end + +function Menu:create_key_action(name) + return function(...) + self.mouse_nav = false + self:maybe(name, ...) + end +end + +function Menu:render() + local update_cursor = false + for _, menu in ipairs(self.all) do + if menu.fling then + update_cursor = update_cursor or menu.fling.update_cursor or false + local time_delta = state.render_last_time - menu.fling.time + local progress = menu.fling.easing(math.min(time_delta / menu.fling.duration, 1)) + self:set_scroll_to(round(menu.fling.y + menu.fling.distance * progress), menu) + if progress < 1 then request_render() else menu.fling = nil end + end + end + if update_cursor then self:select_item_below_cursor() end + + local ass = assdraw.ass_new() + local opacity = options.menu_opacity * self.opacity + local spacing = self.item_padding + local icon_size = self.font_size + + function draw_menu(menu, x, y, opacity) + local ax, ay, bx, by = x, y, x + menu.width, y + menu.height + local draw_title = menu.is_root and menu.title + local scroll_clip = '\\clip(0,' .. ay .. ',' .. display.width .. ',' .. by .. ')' + local start_index = math.floor(menu.scroll_y / self.scroll_step) + 1 + local end_index = math.ceil((menu.scroll_y + menu.height) / self.scroll_step) + local selected_index = menu.selected_index or -1 + -- remove menu_opacity to start off with full opacity, but still decay for parent menus + local text_opacity = opacity / options.menu_opacity + + -- Background + ass:rect(ax, ay - (draw_title and self.item_height or 0) - 2, bx, by + 2, { + color = bg, opacity = opacity, radius = 4, + }) + + for index = start_index, end_index, 1 do + local item = menu.items[index] + local next_item = menu.items[index + 1] + local is_highlighted = selected_index == index or item.active + local next_is_active = next_item and next_item.active + local next_is_highlighted = selected_index == index + 1 or next_is_active + + if not item then break end + + local item_ay = ay - menu.scroll_y + self.scroll_step * (index - 1) + local item_by = item_ay + self.item_height + local item_center_y = item_ay + (self.item_height / 2) + local item_clip = (item_ay < ay or item_by > by) and scroll_clip or nil + local content_ax, content_bx = ax + spacing, bx - spacing + local font_color = item.active and fgt or bgt + local shadow_color = item.active and fg or bg + + -- Separator + local separator_ay = item.separator and item_by - 1 or item_by + local separator_by = item_by + (item.separator and 2 or 1) + if is_highlighted then separator_ay = item_by + 1 end + if next_is_highlighted then separator_by = item_by end + if separator_by - separator_ay > 0 and item_by < by then + ass:rect(ax + spacing / 2, separator_ay, bx - spacing / 2, separator_by, { + color = fg, opacity = opacity * (item.separator and 0.08 or 0.06), + }) + end + + -- Highlight + local highlight_opacity = 0 + (item.active and 0.8 or 0) + (selected_index == index and 0.15 or 0) + if highlight_opacity > 0 then + ass:rect(ax + 2, item_ay, bx - 2, item_by, { + radius = 2, color = fg, opacity = highlight_opacity * text_opacity, + clip = item_clip, + }) + end + + -- Icon + if item.icon then + local x, y = content_bx - (icon_size / 2), item_center_y + if item.icon == 'spinner' then + ass:spinner(x, y, icon_size * 1.5, {color = font_color, opacity = text_opacity * 0.8}) + else + ass:icon(x, y, icon_size * 1.5, item.icon, { + color = font_color, opacity = text_opacity, clip = item_clip, + shadow = 1, shadow_color = shadow_color, + }) + end + content_bx = content_bx - icon_size - spacing + end + + local title_cut_x = content_bx + if item.hint_width > 0 then + -- controls title & hint clipping proportional to the ratio of their widths + local title_content_ratio = item.title_width / (item.title_width + item.hint_width) + title_cut_x = round(content_ax + (content_bx - content_ax - spacing) * title_content_ratio + + (item.title_width > 0 and spacing / 2 or 0)) + end + + -- Hint + if item.hint then + item.ass_safe_hint = item.ass_safe_hint or ass_escape(item.hint) + local clip = '\\clip(' .. title_cut_x .. ',' .. + math.max(item_ay, ay) .. ',' .. bx .. ',' .. math.min(item_by, by) .. ')' + ass:txt(content_bx, item_center_y, 6, item.ass_safe_hint, { + size = self.font_size_hint, color = font_color, wrap = 2, opacity = 0.5 * opacity, clip = clip, + shadow = 1, shadow_color = shadow_color, + }) + end + + -- Title + if item.title then + item.ass_safe_title = item.ass_safe_title or ass_escape(item.title) + local clip = '\\clip(' .. ax .. ',' .. math.max(item_ay, ay) .. ',' + .. title_cut_x .. ',' .. math.min(item_by, by) .. ')' + ass:txt(content_ax, item_center_y, 4, item.ass_safe_title, { + size = self.font_size, color = font_color, italic = item.italic, bold = item.bold, wrap = 2, + opacity = text_opacity * (item.muted and 0.5 or 1), clip = clip, + shadow = 1, shadow_color = shadow_color, + }) + end + end + + -- Menu title + if draw_title then + local title_ay = ay - self.item_height + local title_height = self.item_height - 3 + menu.ass_safe_title = menu.ass_safe_title or ass_escape(menu.title) + + -- Background + ass:rect(ax + 2, title_ay, bx - 2, title_ay + title_height, { + color = fg, opacity = opacity * 0.8, radius = 2, + }) + ass:texture(ax + 2, title_ay, bx - 2, title_ay + title_height, 'n', { + size = 80, color = bg, opacity = opacity * 0.1, + }) + + -- Title + ass:txt(ax + menu.width / 2, title_ay + (title_height / 2), 5, menu.ass_safe_title, { + size = self.font_size, bold = true, color = bg, wrap = 2, opacity = opacity, + clip = '\\clip(' .. ax .. ',' .. title_ay .. ',' .. bx .. ',' .. ay .. ')', + }) + end + + -- Scrollbar + if menu.scroll_height > 0 then + local groove_height = menu.height - 2 + local thumb_height = math.max((menu.height / (menu.scroll_height + menu.height)) * groove_height, 40) + local thumb_y = ay + 1 + ((menu.scroll_y / menu.scroll_height) * (groove_height - thumb_height)) + ass:rect(bx - 3, thumb_y, bx - 1, thumb_y + thumb_height, {color = fg, opacity = opacity * 0.8}) + end + end + + -- Main menu + draw_menu(self.current, self.ax, self.ay, opacity) + + -- Parent menus + local parent_menu = self.current.parent_menu + local parent_offset_x = self.ax + local parent_opacity_factor = options.menu_parent_opacity + local menu_gap = 2 + + while parent_menu do + parent_offset_x = parent_offset_x - parent_menu.width - menu_gap + draw_menu(parent_menu, parent_offset_x, parent_menu.top, parent_opacity_factor * opacity) + parent_opacity_factor = parent_opacity_factor * parent_opacity_factor + parent_menu = parent_menu.parent_menu + end + + -- Selected menu + local selected_menu = self.current.items[self.current.selected_index] + + if selected_menu and selected_menu.items then + draw_menu(selected_menu, self.bx + menu_gap, selected_menu.top, options.menu_parent_opacity * opacity) + end + + return ass +end + +return Menu diff --git a/.config/mpv/scripts/uosc_shared/elements/PauseIndicator.lua b/.config/mpv/scripts/uosc_shared/elements/PauseIndicator.lua new file mode 100644 index 0000000..82a7e43 --- /dev/null +++ b/.config/mpv/scripts/uosc_shared/elements/PauseIndicator.lua @@ -0,0 +1,80 @@ +local Element = require('uosc_shared/elements/Element') + +---@class PauseIndicator : Element +local PauseIndicator = class(Element) + +function PauseIndicator:new() return Class.new(self) --[[@as PauseIndicator]] end +function PauseIndicator:init() + Element.init(self, 'pause_indicator') + self.ignores_menu = true + self.base_icon_opacity = options.pause_indicator == 'flash' and 1 or 0.8 + self.paused = state.pause + self.type = options.pause_indicator + self.is_manual = options.pause_indicator == 'manual' + self.fadeout_requested = false + self.opacity = 0 + + mp.observe_property('pause', 'bool', function(_, paused) + if Elements.timeline.pressed then return end + if options.pause_indicator == 'flash' then + if self.paused == paused then return end + self:flash() + elseif options.pause_indicator == 'static' then + self:decide() + end + end) +end + +function PauseIndicator:flash() + if not self.is_manual and self.type ~= 'flash' then return end + -- can't wait for pause property event listener to set this, because when this is used inside a binding like: + -- cycle pause; script-binding uosc/flash-pause-indicator + -- the pause event is not fired fast enough, and indicator starts rendering with old icon + self.paused = mp.get_property_native('pause') + if self.is_manual then self.type = 'flash' end + self.opacity = 1 + self:tween_property('opacity', 1, 0, 0.15) +end + +-- decides whether static indicator should be visible or not +function PauseIndicator:decide() + if not self.is_manual and self.type ~= 'static' then return end + self.paused = mp.get_property_native('pause') -- see flash() for why this line is necessary + if self.is_manual then self.type = 'static' end + self.opacity = self.paused and 1 or 0 + request_render() + + -- Workaround for an mpv race condition bug during pause on windows builds, which causes osd updates to be ignored. + -- .03 was still loosing renders, .04 was fine, but to be safe I added 10ms more + mp.add_timeout(.05, function() osd:update() end) +end + +function PauseIndicator:render() + if self.opacity == 0 then return end + + local ass = assdraw.ass_new() + local is_static = self.type == 'static' + + -- Background fadeout + if is_static then + ass:rect(0, 0, display.width, display.height, {color = bg, opacity = self.opacity * 0.3}) + end + + -- Icon + local size = round(math.min(display.width, display.height) * (is_static and 0.20 or 0.15)) + size = size + size * (1 - self.opacity) + + if self.paused then + ass:icon(display.width / 2, display.height / 2, size, 'pause', + {border = 1, opacity = self.base_icon_opacity * self.opacity} + ) + else + ass:icon(display.width / 2, display.height / 2, size * 1.2, 'play_arrow', + {border = 1, opacity = self.base_icon_opacity * self.opacity} + ) + end + + return ass +end + +return PauseIndicator diff --git a/.config/mpv/scripts/uosc_shared/elements/Speed.lua b/.config/mpv/scripts/uosc_shared/elements/Speed.lua new file mode 100644 index 0000000..2cca26b --- /dev/null +++ b/.config/mpv/scripts/uosc_shared/elements/Speed.lua @@ -0,0 +1,181 @@ +local Element = require('uosc_shared/elements/Element') + +---@alias Dragging { start_time: number; start_x: number; distance: number; speed_distance: number; start_speed: number; } + +---@class Speed : Element +local Speed = class(Element) + +---@param props? ElementProps +function Speed:new(props) return Class.new(self, props) --[[@as Speed]] end +function Speed:init(props) + Element.init(self, 'speed', props) + + self.width = 0 + self.height = 0 + self.notches = 10 + self.notch_every = 0.1 + ---@type number + self.notch_spacing = nil + ---@type number + self.font_size = nil + ---@type Dragging|nil + self.dragging = nil +end + +function Speed:on_coordinates() + self.height, self.width = self.by - self.ay, self.bx - self.ax + self.notch_spacing = self.width / (self.notches + 1) + self.font_size = round(self.height * 0.48 * options.font_scale) +end + +function Speed:speed_step(speed, up) + if options.speed_step_is_factor then + if up then + return speed * options.speed_step + else + return speed * 1 / options.speed_step + end + else + if up then + return speed + options.speed_step + else + return speed - options.speed_step + end + end +end + +function Speed:on_mbtn_left_down() + self:tween_stop() -- Stop and cleanup possible ongoing animations + self.dragging = { + start_time = mp.get_time(), + start_x = cursor.x, + distance = 0, + speed_distance = 0, + start_speed = state.speed, + } +end + +function Speed:on_global_mouse_move() + if not self.dragging then return end + + self.dragging.distance = cursor.x - self.dragging.start_x + self.dragging.speed_distance = (-self.dragging.distance / self.notch_spacing * self.notch_every) + + local speed_current = state.speed + local speed_drag_current = self.dragging.start_speed + self.dragging.speed_distance + speed_drag_current = clamp(0.01, speed_drag_current, 100) + local drag_dir_up = speed_drag_current > speed_current + + local speed_step_next = speed_current + local speed_drag_diff = math.abs(speed_drag_current - speed_current) + while math.abs(speed_step_next - speed_current) < speed_drag_diff do + speed_step_next = self:speed_step(speed_step_next, drag_dir_up) + end + local speed_step_prev = self:speed_step(speed_step_next, not drag_dir_up) + + local speed_new = speed_step_prev + local speed_next_diff = math.abs(speed_drag_current - speed_step_next) + local speed_prev_diff = math.abs(speed_drag_current - speed_step_prev) + if speed_next_diff < speed_prev_diff then + speed_new = speed_step_next + end + + if speed_new ~= speed_current then + mp.set_property_native('speed', speed_new) + end +end + +function Speed:on_mbtn_left_up() + -- Reset speed on short clicks + if self.dragging and math.abs(self.dragging.distance) < 6 and mp.get_time() - self.dragging.start_time < 0.15 then + mp.set_property_native('speed', 1) + end +end + +function Speed:on_global_mbtn_left_up() + self.dragging = nil + request_render() +end + +function Speed:on_global_mouse_leave() + self.dragging = nil + request_render() +end + +function Speed:on_wheel_up() mp.set_property_native('speed', self:speed_step(state.speed, true)) end +function Speed:on_wheel_down() mp.set_property_native('speed', self:speed_step(state.speed, false)) end + +function Speed:render() + local visibility = self:get_visibility() + local opacity = self.dragging and 1 or visibility + + if opacity <= 0 then return end + + local ass = assdraw.ass_new() + + -- Background + ass:rect(self.ax, self.ay, self.bx, self.by, {color = bg, radius = 2, opacity = opacity * options.speed_opacity}) + + -- Coordinates + local ax, ay = self.ax, self.ay + local bx, by = self.bx, ay + self.height + local half_width = (self.width / 2) + local half_x = ax + half_width + + -- Notches + local speed_at_center = state.speed + if self.dragging then + speed_at_center = self.dragging.start_speed + self.dragging.speed_distance + speed_at_center = clamp(0.01, speed_at_center, 100) + end + local nearest_notch_speed = round(speed_at_center / self.notch_every) * self.notch_every + local nearest_notch_x = half_x + (((nearest_notch_speed - speed_at_center) / self.notch_every) * self.notch_spacing) + local guide_size = math.floor(self.height / 7.5) + local notch_by = by - guide_size + local notch_ay_big = ay + round(self.font_size * 1.1) + local notch_ay_medium = notch_ay_big + ((notch_by - notch_ay_big) * 0.2) + local notch_ay_small = notch_ay_big + ((notch_by - notch_ay_big) * 0.4) + local from_to_index = math.floor(self.notches / 2) + + for i = -from_to_index, from_to_index do + local notch_speed = nearest_notch_speed + (i * self.notch_every) + + if notch_speed >= 0 and notch_speed <= 100 then + local notch_x = nearest_notch_x + (i * self.notch_spacing) + local notch_thickness = 1 + local notch_ay = notch_ay_small + if (notch_speed % (self.notch_every * 10)) < 0.00000001 then + notch_ay = notch_ay_big + notch_thickness = 1.5 + elseif (notch_speed % (self.notch_every * 5)) < 0.00000001 then + notch_ay = notch_ay_medium + end + + ass:rect(notch_x - notch_thickness, notch_ay, notch_x + notch_thickness, notch_by, { + color = fg, border = 1, border_color = bg, + opacity = math.min(1.2 - (math.abs((notch_x - ax - half_width) / half_width)), 1) * opacity, + }) + end + end + + -- Center guide + ass:new_event() + ass:append('{\\rDefault\\an7\\blur0\\bord1\\shad0\\1c&H' .. fg .. '\\3c&H' .. bg .. '}') + ass:opacity(opacity) + ass:pos(0, 0) + ass:draw_start() + ass:move_to(half_x, by - 2 - guide_size) + ass:line_to(half_x + guide_size, by - 2) + ass:line_to(half_x - guide_size, by - 2) + ass:draw_stop() + + -- Speed value + local speed_text = (round(state.speed * 100) / 100) .. 'x' + ass:txt(half_x, ay + (notch_ay_big - ay) / 2, 5, speed_text, { + size = self.font_size, color = bgt, border = options.text_border, border_color = bg, opacity = opacity, + }) + + return ass +end + +return Speed diff --git a/.config/mpv/scripts/uosc_shared/elements/Timeline.lua b/.config/mpv/scripts/uosc_shared/elements/Timeline.lua new file mode 100644 index 0000000..9b7811b --- /dev/null +++ b/.config/mpv/scripts/uosc_shared/elements/Timeline.lua @@ -0,0 +1,344 @@ +local Element = require('uosc_shared/elements/Element') + +---@class Timeline : Element +local Timeline = class(Element) + +function Timeline:new() return Class.new(self) --[[@as Timeline]] end +function Timeline:init() + Element.init(self, 'timeline') + self.pressed = false + self.obstructed = false + self.size_max = 0 + self.size_min = 0 + self.size_min_override = options.timeline_start_hidden and 0 or nil + self.font_size = 0 + self.top_border = options.timeline_border + + -- Release any dragging when file gets unloaded + mp.register_event('end-file', function() self.pressed = false end) +end + +function Timeline:get_visibility() + return Elements.controls and math.max(Elements.controls.proximity, Element.get_visibility(self)) + or Element.get_visibility(self) +end + +function Timeline:decide_enabled() + local previous = self.enabled + self.enabled = not self.obstructed and state.duration ~= nil and state.duration > 0 and state.time ~= nil + if self.enabled ~= previous then Elements:trigger('timeline_enabled', self.enabled) end +end + +function Timeline:get_effective_size_min() + return self.size_min_override or self.size_min +end + +function Timeline:get_effective_size() + if Elements.speed and Elements.speed.dragging then return self.size_max end + local size_min = self:get_effective_size_min() + return size_min + math.ceil((self.size_max - size_min) * self:get_visibility()) +end + +function Timeline:get_effective_line_width() + return state.fullormaxed and options.timeline_line_width_fullscreen or options.timeline_line_width +end + +function Timeline:update_dimensions() + if state.fullormaxed then + self.size_min = options.timeline_size_min_fullscreen + self.size_max = options.timeline_size_max_fullscreen + else + self.size_min = options.timeline_size_min + self.size_max = options.timeline_size_max + end + self.font_size = math.floor(math.min((self.size_max + 60) * 0.2, self.size_max * 0.96) * options.font_scale) + self.ax = Elements.window_border.size + self.ay = display.height - Elements.window_border.size - self.size_max - self.top_border + self.bx = display.width - Elements.window_border.size + self.by = display.height - Elements.window_border.size + self.width = self.bx - self.ax + + -- Disable if not enough space + local available_space = display.height - Elements.window_border.size * 2 + if Elements.top_bar.enabled then available_space = available_space - Elements.top_bar.size end + self.obstructed = available_space < self.size_max + 10 + self:decide_enabled() +end + +function Timeline:get_time_at_x(x) + local line_width = (options.timeline_style == 'line' and self:get_effective_line_width() - 1 or 0) + local time_width = self.width - line_width - 1 + local fax = (time_width) * state.time / state.duration + local fbx = fax + line_width + -- time starts 0.5 pixels in + x = x - self.ax - 0.5 + if x > fbx then x = x - line_width + elseif x > fax then x = fax end + local progress = clamp(0, x / time_width, 1) + return state.duration * progress +end + +---@param fast? boolean +function Timeline:set_from_cursor(fast) + if state.time and state.duration then + mp.commandv('seek', self:get_time_at_x(cursor.x), fast and 'absolute+keyframes' or 'absolute+exact') + end +end +function Timeline:clear_thumbnail() mp.commandv('script-message-to', 'thumbfast', 'clear') end + +function Timeline:on_mbtn_left_down() + self.pressed = true + self.pressed_pause = state.pause + mp.set_property_native('pause', true) + self:set_from_cursor() +end +function Timeline:on_prop_duration() self:decide_enabled() end +function Timeline:on_prop_time() self:decide_enabled() end +function Timeline:on_prop_border() self:update_dimensions() end +function Timeline:on_prop_fullormaxed() self:update_dimensions() end +function Timeline:on_display() self:update_dimensions() end +function Timeline:on_mouse_leave() self:clear_thumbnail() end +function Timeline:on_global_mbtn_left_up() + if self.pressed then + mp.set_property_native('pause', self.pressed_pause) + self.pressed = false + end + self:clear_thumbnail() +end +function Timeline:on_global_mouse_leave() + self.pressed = false + self:clear_thumbnail() +end + +Timeline.seek_timer = mp.add_timeout(0.05, function() Elements.timeline:set_from_cursor() end) +Timeline.seek_timer:kill() +function Timeline:on_global_mouse_move() + if self.pressed then + if self.width / state.duration < 10 then + self:set_from_cursor(true) + self.seek_timer:kill() + self.seek_timer:resume() + else self:set_from_cursor() end + end +end +function Timeline:on_wheel_up() mp.commandv('seek', options.timeline_step) end +function Timeline:on_wheel_down() mp.commandv('seek', -options.timeline_step) end + +function Timeline:render() + if self.size_max == 0 then return end + + local size_min = self:get_effective_size_min() + local size = self:get_effective_size() + local visibility = self:get_visibility() + + if size < 1 then return end + + local ass = assdraw.ass_new() + + -- Text opacity rapidly drops to 0 just before it starts overflowing, or before it reaches timeline.size_min + local hide_text_below = math.max(self.font_size * 0.8, size_min * 2) + local hide_text_ramp = hide_text_below / 2 + local text_opacity = clamp(0, size - hide_text_below, hide_text_ramp) / hide_text_ramp + + local spacing = math.max(math.floor((self.size_max - self.font_size) / 2.5), 4) + local progress = state.time / state.duration + local is_line = options.timeline_style == 'line' + + -- Foreground & Background bar coordinates + local bax, bay, bbx, bby = self.ax, self.by - size - self.top_border, self.bx, self.by + local fax, fay, fbx, fby = 0, bay + self.top_border, 0, bby + local fcy = fay + (size / 2) + + local line_width = 0 + + if is_line then + local minimized_fraction = 1 - math.min((size - size_min) / ((self.size_max - size_min) / 8), 1) + local line_width_max = self:get_effective_line_width() + local max_min_width_delta = size_min > 0 + and line_width_max - line_width_max * options.timeline_line_width_minimized_scale + or 0 + line_width = line_width_max - (max_min_width_delta * minimized_fraction) + fax = bax + (self.width - line_width) * progress + fbx = fax + line_width + line_width = line_width - 1 + else + fax, fbx = bax, bax + self.width * progress + end + + local foreground_size = fby - fay + local foreground_coordinates = round(fax) .. ',' .. fay .. ',' .. round(fbx) .. ',' .. fby -- for clipping + + -- time starts 0.5 pixels in + local time_ax = bax + 0.5 + local time_width = self.width - line_width - 1 + + -- time to x: calculates x coordinate so that it never lies inside of the line + local function t2x(time) + local x = time_ax + time_width * time / state.duration + return time <= state.time and x or x + line_width + end + + -- Background + ass:new_event() + ass:pos(0, 0) + ass:append('{\\rDefault\\an7\\blur0\\bord0\\1c&H' .. bg .. '}') + ass:opacity(options.timeline_opacity) + ass:draw_start() + ass:rect_cw(bax, bay, fax, bby) --left of progress + ass:rect_cw(fbx, bay, bbx, bby) --right of progress + ass:rect_cw(fax, bay, fbx, fay) --above progress + ass:draw_stop() + + -- Progress + ass:rect(fax, fay, fbx, fby, {opacity = options.timeline_opacity}) + + -- Uncached ranges + local buffered_time = nil + if state.uncached_ranges then + local opts = {size = 80, anchor_y = fby} + local texture_char = visibility > 0 and 'b' or 'a' + local offset = opts.size / (visibility > 0 and 24 or 28) + for _, range in ipairs(state.uncached_ranges) do + if not buffered_time and (range[1] > state.time or range[2] > state.time) then + buffered_time = range[1] - state.time + end + if options.timeline_cache then + local ax = range[1] < 0.5 and bax or math.floor(t2x(range[1])) + local bx = range[2] > state.duration - 0.5 and bbx or math.ceil(t2x(range[2])) + opts.color, opts.opacity, opts.anchor_x = 'ffffff', 0.4 - (0.2 * visibility), bax + ass:texture(ax, fay, bx, fby, texture_char, opts) + opts.color, opts.opacity, opts.anchor_x = '000000', 0.6 - (0.2 * visibility), bax + offset + ass:texture(ax, fay, bx, fby, texture_char, opts) + end + end + end + + -- Custom ranges + for _, chapter_range in ipairs(state.chapter_ranges) do + local rax = chapter_range.start < 0.1 and bax or t2x(chapter_range.start) + local rbx = chapter_range['end'] > state.duration - 0.1 and bbx + or t2x(math.min(chapter_range['end'], state.duration)) + ass:rect(rax, fay, rbx, fby, {color = chapter_range.color, opacity = chapter_range.opacity}) + end + + -- Chapters + if (options.timeline_chapters_opacity > 0 + and (#state.chapters > 0 or state.ab_loop_a or state.ab_loop_b) + ) then + local diamond_radius = foreground_size < 3 and foreground_size or math.max(foreground_size / 10, 3) + local diamond_border = options.timeline_border and math.max(options.timeline_border, 1) or 1 + + if diamond_radius > 0 then + local function draw_chapter(time) + local chapter_x = t2x(time) + local chapter_y = fay - 1 + ass:new_event() + ass:append(string.format( + '{\\pos(0,0)\\rDefault\\an7\\blur0\\yshad0.01\\bord%f\\1c&H%s\\3c&H%s\\4c&H%s\\1a&H%X&\\3a&H00&\\4a&H00&}', + diamond_border, fg, bg, bg, opacity_to_alpha(options.timeline_opacity * options.timeline_chapters_opacity) + )) + ass:draw_start() + ass:move_to(chapter_x - diamond_radius, chapter_y) + ass:line_to(chapter_x, chapter_y - diamond_radius) + ass:line_to(chapter_x + diamond_radius, chapter_y) + ass:line_to(chapter_x, chapter_y + diamond_radius) + ass:draw_stop() + end + + if state.chapters ~= nil then + for i, chapter in ipairs(state.chapters) do + draw_chapter(chapter.time) + end + end + + if state.ab_loop_a and state.ab_loop_a > 0 then draw_chapter(state.ab_loop_a) end + if state.ab_loop_b and state.ab_loop_b > 0 then draw_chapter(state.ab_loop_b) end + end + end + + local function draw_timeline_text(x, y, align, text, opts) + opts.color, opts.border_color = fgt, fg + opts.clip = '\\clip(' .. foreground_coordinates .. ')' + ass:txt(x, y, align, text, opts) + opts.color, opts.border_color = bgt, bg + opts.clip = '\\iclip(' .. foreground_coordinates .. ')' + ass:txt(x, y, align, text, opts) + end + + -- Time values + if text_opacity > 0 then + local time_opts = {size = self.font_size, opacity = text_opacity, border = 2} + -- Upcoming cache time + if buffered_time and options.buffered_time_threshold > 0 and buffered_time < options.buffered_time_threshold then + local x, align = fbx + 5, 4 + local cache_opts = {size = self.font_size * 0.8, opacity = text_opacity * 0.6, border = 1} + local human = round(math.max(buffered_time, 0)) .. 's' + local width = text_width(human, cache_opts) + local time_width = text_width('00:00:00', time_opts) + local min_x, max_x = bax + spacing + 5 + time_width, bbx - spacing - 5 - time_width + if x < min_x then x = min_x elseif x + width > max_x then x, align = max_x, 6 end + draw_timeline_text(x, fcy, align, human, cache_opts) + end + + -- Elapsed time + if state.time_human then + draw_timeline_text(bax + spacing, fcy, 4, state.time_human, time_opts) + end + + -- End time + if state.duration_or_remaining_time_human then + draw_timeline_text(bbx - spacing, fcy, 6, state.duration_or_remaining_time_human, time_opts) + end + end + + -- Hovered time and chapter + if (self.proximity_raw == 0 or self.pressed) and not (Elements.speed and Elements.speed.dragging) then + local hovered_seconds = self:get_time_at_x(cursor.x) + + -- Cursor line + -- 0.5 to switch when the pixel is half filled in + local color = ((fax - 0.5) < cursor.x and cursor.x < (fbx + 0.5)) and bg or fg + local ax, ay, bx, by = cursor.x - 0.5, fay, cursor.x + 0.5, fby + ass:rect(ax, ay, bx, by, {color = color, opacity = 0.2}) + local tooltip_anchor = {ax = ax, ay = ay, bx = bx, by = by} + + -- Timestamp + local opts = {size = self.font_size, offset = 4} + opts.width_overwrite = text_width('00:00:00', opts) + ass:tooltip(tooltip_anchor, format_time(hovered_seconds), opts) + tooltip_anchor.ay = tooltip_anchor.ay - self.font_size - 4 + + -- Thumbnail + if not thumbnail.disabled and thumbnail.width ~= 0 and thumbnail.height ~= 0 then + local scale_x, scale_y = display.scale_x, display.scale_y + local border, margin_x, margin_y = math.ceil(2 * scale_x), round(10 * scale_x), round(5 * scale_y) + local thumb_x_margin, thumb_y_margin = border + margin_x, border + margin_y + local thumb_width, thumb_height = thumbnail.width, thumbnail.height + local thumb_x = round(clamp( + thumb_x_margin, cursor.x * scale_x - thumb_width / 2, + display.width * scale_x - thumb_width - thumb_x_margin + )) + local thumb_y = round(tooltip_anchor.ay * scale_y - thumb_y_margin - thumb_height) + local ax, ay = (thumb_x - border) / scale_x, (thumb_y - border) / scale_y + local bx, by = (thumb_x + thumb_width + border) / scale_x, (thumb_y + thumb_height + border) / scale_y + ass:rect(ax, ay, bx, by, {color = bg, border = 1, border_color = fg, border_opacity = 0.08, radius = 2}) + mp.commandv('script-message-to', 'thumbfast', 'thumb', hovered_seconds, thumb_x, thumb_y) + tooltip_anchor.ax, tooltip_anchor.bx, tooltip_anchor.ay = ax, bx, ay + end + + -- Chapter title + if #state.chapters > 0 then + local _, chapter = itable_find(state.chapters, function(c) return hovered_seconds >= c.time end, true) + if chapter and not chapter.is_end_only then + ass:tooltip(tooltip_anchor, chapter.title_wrapped, { + size = self.font_size, offset = 10, responsive = false, bold = true, + width_overwrite = chapter.title_wrapped_width * self.font_size, + }) + end + end + end + + return ass +end + +return Timeline diff --git a/.config/mpv/scripts/uosc_shared/elements/TopBar.lua b/.config/mpv/scripts/uosc_shared/elements/TopBar.lua new file mode 100644 index 0000000..977d5b8 --- /dev/null +++ b/.config/mpv/scripts/uosc_shared/elements/TopBar.lua @@ -0,0 +1,182 @@ +local Element = require('uosc_shared/elements/Element') + +---@alias TopBarButtonProps {icon: string; background: string; anchor_id?: string; command: string|fun()} + +---@class TopBarButton : Element +local TopBarButton = class(Element) + +---@param id string +---@param props TopBarButtonProps +function TopBarButton:new(id, props) return Class.new(self, id, props) --[[@as TopBarButton]] end +function TopBarButton:init(id, props) + Element.init(self, id, props) + self.anchor_id = 'top_bar' + self.icon = props.icon + self.background = props.background + self.command = props.command +end + +function TopBarButton:on_mbtn_left_down() + mp.command(type(self.command) == 'function' and self.command() or self.command) +end + +function TopBarButton:render() + local visibility = self:get_visibility() + if visibility <= 0 then return end + local ass = assdraw.ass_new() + + -- Background on hover + if self.proximity_raw == 0 then + ass:rect(self.ax, self.ay, self.bx, self.by, {color = self.background, opacity = visibility}) + end + + local width, height = self.bx - self.ax, self.by - self.ay + local icon_size = math.min(width, height) * 0.5 + ass:icon(self.ax + width / 2, self.ay + height / 2, icon_size, self.icon, { + opacity = visibility, border = options.text_border, + }) + + return ass +end + +--[[ TopBar ]] + +---@class TopBar : Element +local TopBar = class(Element) + +function TopBar:new() return Class.new(self) --[[@as TopBar]] end +function TopBar:init() + Element.init(self, 'top_bar') + self.pressed = false + self.size, self.size_max, self.size_min = 0, 0, 0 + self.icon_size, self.spacing, self.font_size, self.title_bx = 1, 1, 1, 1 + self.size_min_override = options.timeline_start_hidden and 0 or nil + self.top_border = options.timeline_border + + local function decide_maximized_command() + return state.border + and (state.fullscreen and 'set fullscreen no;cycle window-maximized' or 'cycle window-maximized') + or 'set window-maximized no;cycle fullscreen' + end + + -- Order aligns from right to left + self.buttons = { + TopBarButton:new('tb_close', {icon = 'close', background = '2311e8', command = 'quit'}), + TopBarButton:new('tb_max', {icon = 'crop_square', background = '222222', command = decide_maximized_command}), + TopBarButton:new('tb_min', {icon = 'minimize', background = '222222', command = 'cycle window-minimized'}), + } +end + +function TopBar:decide_enabled() + if options.top_bar == 'no-border' then + self.enabled = not state.border or state.fullscreen + else + self.enabled = options.top_bar == 'always' + end + self.enabled = self.enabled and (options.top_bar_controls or options.top_bar_title) + for _, element in ipairs(self.buttons) do + element.enabled = self.enabled and options.top_bar_controls + end +end + +function TopBar:update_dimensions() + self.size = state.fullormaxed and options.top_bar_size_fullscreen or options.top_bar_size + self.icon_size = round(self.size * 0.5) + self.spacing = math.ceil(self.size * 0.25) + self.font_size = math.floor((self.size - (self.spacing * 2)) * options.font_scale) + self.button_width = round(self.size * 1.15) + self.ay = Elements.window_border.size + self.bx = display.width - Elements.window_border.size + self.by = self.size + Elements.window_border.size + self.title_bx = self.bx - (options.top_bar_controls and (self.button_width * 3) or 0) + self.ax = options.top_bar_title and Elements.window_border.size or self.title_bx + + local button_bx = self.bx + for _, element in pairs(self.buttons) do + element.ax, element.bx = button_bx - self.button_width, button_bx + element.ay, element.by = self.ay, self.by + button_bx = button_bx - self.button_width + end +end + +function TopBar:on_prop_border() + self:decide_enabled() + self:update_dimensions() +end + +function TopBar:on_prop_fullscreen() + self:decide_enabled() + self:update_dimensions() +end + +function TopBar:on_prop_maximized() + self:decide_enabled() + self:update_dimensions() +end + +function TopBar:on_display() self:update_dimensions() end + +function TopBar:render() + local visibility = self:get_visibility() + if visibility <= 0 then return end + local ass = assdraw.ass_new() + + -- Window title + if options.top_bar_title and (state.title or state.has_playlist) then + local bg_margin = math.floor((self.size - self.font_size) / 4) + local padding = self.font_size / 2 + local title_ax = self.ax + bg_margin + local title_ay = self.ay + bg_margin + local max_bx = self.title_bx - self.spacing + + -- Playlist position + if state.has_playlist then + local text = state.playlist_pos .. '' .. state.playlist_count + local formatted_text = '{\\b1}' .. state.playlist_pos .. '{\\b0\\fs' .. self.font_size * 0.9 .. '}/' + .. state.playlist_count + local opts = {size = self.font_size, wrap = 2, color = fgt, opacity = visibility} + local bx = round(title_ax + text_width(text, opts) + padding * 2) + ass:rect(title_ax, title_ay, bx, self.by - bg_margin, {color = fg, opacity = visibility, radius = 2}) + ass:txt(title_ax + (bx - title_ax) / 2, self.ay + (self.size / 2), 5, formatted_text, opts) + title_ax = bx + bg_margin + end + + -- Title + local text = state.title + if max_bx - title_ax > self.font_size * 3 and text and text ~= '' then + local opts = { + size = self.font_size, wrap = 2, color = bgt, border = 1, border_color = bg, opacity = visibility, + clip = string.format('\\clip(%d, %d, %d, %d)', self.ax, self.ay, max_bx, self.by), + } + local bx = math.min(max_bx, title_ax + text_width(text, opts) + padding * 2) + local by = self.by - bg_margin + ass:rect(title_ax, title_ay, bx, by, { + color = bg, opacity = visibility * options.top_bar_title_opacity, radius = 2, + }) + ass:txt(title_ax + padding, self.ay + (self.size / 2), 4, text, opts) + title_ay = by + 1 + end + + -- Subtitle: current chapter + if state.current_chapter and max_bx - title_ax > self.font_size * 3 then + local font_size = self.font_size * 0.8 + local height = font_size * 1.5 + local text = '└ ' .. state.current_chapter.index .. ': ' .. state.current_chapter.title + local by = title_ay + height + local opts = { + size = font_size, italic = true, wrap = 2, color = bgt, + border = 1, border_color = bg, opacity = visibility * 0.8, + } + local bx = math.min(max_bx, title_ax + text_width(text, opts) + padding * 2) + opts.clip = string.format('\\clip(%d, %d, %d, %d)', title_ax, title_ay, bx, by) + ass:rect(title_ax, title_ay, bx, by, { + color = bg, opacity = visibility * options.top_bar_title_opacity, radius = 2, + }) + ass:txt(title_ax + padding, title_ay + height / 2, 4, text, opts) + end + end + + return ass +end + +return TopBar diff --git a/.config/mpv/scripts/uosc_shared/elements/Volume.lua b/.config/mpv/scripts/uosc_shared/elements/Volume.lua new file mode 100644 index 0000000..13bfb5f --- /dev/null +++ b/.config/mpv/scripts/uosc_shared/elements/Volume.lua @@ -0,0 +1,240 @@ +local Element = require('uosc_shared/elements/Element') + +--[[ MuteButton ]] + +---@class MuteButton : Element +local MuteButton = class(Element) +---@param props? ElementProps +function MuteButton:new(props) return Class.new(self, 'volume_mute', props) --[[@as MuteButton]] end +function MuteButton:on_mbtn_left_down() mp.commandv('cycle', 'mute') end +function MuteButton:render() + local visibility = self:get_visibility() + if visibility <= 0 then return end + local ass = assdraw.ass_new() + local icon_name = state.mute and 'volume_off' or 'volume_up' + local width = self.bx - self.ax + ass:icon(self.ax + (width / 2), self.by, width * 0.7, icon_name, + {border = options.text_border, opacity = options.volume_opacity * visibility, align = 2} + ) + return ass +end + +--[[ VolumeSlider ]] + +---@class VolumeSlider : Element +local VolumeSlider = class(Element) +---@param props? ElementProps +function VolumeSlider:new(props) return Class.new(self, props) --[[@as VolumeSlider]] end +function VolumeSlider:init(props) + Element.init(self, 'volume_slider', props) + self.pressed = false + self.nudge_y = 0 -- vertical position where volume overflows 100 + self.nudge_size = 0 + self.draw_nudge = false + self.spacing = 0 + self.radius = 1 +end + +function VolumeSlider:set_volume(volume) + volume = round(volume / options.volume_step) * options.volume_step + if state.volume == volume then return end + mp.commandv('set', 'volume', clamp(0, volume, state.volume_max)) +end + +function VolumeSlider:set_from_cursor() + local volume_fraction = (self.by - cursor.y - options.volume_border) / (self.by - self.ay - options.volume_border) + self:set_volume(volume_fraction * state.volume_max) +end + +function VolumeSlider:on_coordinates() + if type(state.volume_max) ~= 'number' or state.volume_max <= 0 then return end + local width = self.bx - self.ax + self.nudge_y = self.by - round((self.by - self.ay) * (100 / state.volume_max)) + self.nudge_size = round(width * 0.18) + self.draw_nudge = self.ay < self.nudge_y + self.spacing = round(width * 0.2) + self.radius = math.max(2, (self.bx - self.ax) / 10) +end +function VolumeSlider:on_mbtn_left_down() + self.pressed = true + self:set_from_cursor() +end +function VolumeSlider:on_global_mbtn_left_up() self.pressed = false end +function VolumeSlider:on_global_mouse_leave() self.pressed = false end +function VolumeSlider:on_global_mouse_move() + if self.pressed then self:set_from_cursor() end +end +function VolumeSlider:on_wheel_up() self:set_volume(state.volume + options.volume_step) end +function VolumeSlider:on_wheel_down() self:set_volume(state.volume - options.volume_step) end + +function VolumeSlider:render() + local visibility = self:get_visibility() + local ax, ay, bx, by = self.ax, self.ay, self.bx, self.by + local width, height = bx - ax, by - ay + + if width <= 0 or height <= 0 or visibility <= 0 then return end + + local ass = assdraw.ass_new() + local nudge_y, nudge_size = self.draw_nudge and self.nudge_y or -infinity, self.nudge_size + local volume_y = self.ay + options.volume_border + + ((height - (options.volume_border * 2)) * (1 - math.min(state.volume / state.volume_max, 1))) + + -- Draws a rectangle with nudge at requested position + ---@param p number Padding from slider edges. + ---@param cy? number A y coordinate where to clip the path from the bottom. + function create_nudged_path(p, cy) + cy = cy or ay + p + local ax, bx, by = ax + p, bx - p, by - p + local r = math.max(1, self.radius - p) + local d, rh = r * 2, r / 2 + local nudge_size = ((quarter_pi_sin * (nudge_size - p)) + p) / quarter_pi_sin + local path = assdraw.ass_new() + path:move_to(bx - r, by) + path:line_to(ax + r, by) + if cy > by - d then + local subtracted_radius = (d - (cy - (by - d))) / 2 + local xbd = (r - subtracted_radius * 1.35) -- x bezier delta + path:bezier_curve(ax + xbd, by, ax + xbd, cy, ax + r, cy) + path:line_to(bx - r, cy) + path:bezier_curve(bx - xbd, cy, bx - xbd, by, bx - r, by) + else + path:bezier_curve(ax + rh, by, ax, by - rh, ax, by - r) + local nudge_bottom_y = nudge_y + nudge_size + + if cy + rh <= nudge_bottom_y then + path:line_to(ax, nudge_bottom_y) + if cy <= nudge_y then + path:line_to((ax + nudge_size), nudge_y) + local nudge_top_y = nudge_y - nudge_size + if cy <= nudge_top_y then + local r, rh = r, rh + if cy > nudge_top_y - r then + r = nudge_top_y - cy + rh = r / 2 + end + path:line_to(ax, nudge_top_y) + path:line_to(ax, cy + r) + path:bezier_curve(ax, cy + rh, ax + rh, cy, ax + r, cy) + path:line_to(bx - r, cy) + path:bezier_curve(bx - rh, cy, bx, cy + rh, bx, cy + r) + path:line_to(bx, nudge_top_y) + else + local triangle_side = cy - nudge_top_y + path:line_to((ax + triangle_side), cy) + path:line_to((bx - triangle_side), cy) + end + path:line_to((bx - nudge_size), nudge_y) + else + local triangle_side = nudge_bottom_y - cy + path:line_to((ax + triangle_side), cy) + path:line_to((bx - triangle_side), cy) + end + path:line_to(bx, nudge_bottom_y) + else + path:line_to(ax, cy + r) + path:bezier_curve(ax, cy + rh, ax + rh, cy, ax + r, cy) + path:line_to(bx - r, cy) + path:bezier_curve(bx - rh, cy, bx, cy + rh, bx, cy + r) + end + path:line_to(bx, by - r) + path:bezier_curve(bx, by - rh, bx - rh, by, bx - r, by) + end + return path + end + + -- BG & FG paths + local bg_path = create_nudged_path(0) + local fg_path = create_nudged_path(options.volume_border, volume_y) + + -- Background + ass:new_event() + ass:append('{\\rDefault\\an7\\blur0\\bord0\\1c&H' .. bg .. + '\\iclip(' .. fg_path.scale .. ', ' .. fg_path.text .. ')}') + ass:opacity(options.volume_opacity, visibility) + ass:pos(0, 0) + ass:draw_start() + ass:append(bg_path.text) + ass:draw_stop() + + -- Foreground + ass:new_event() + ass:append('{\\rDefault\\an7\\blur0\\bord0\\1c&H' .. fg .. '}') + ass:opacity(options.volume_opacity, visibility) + ass:pos(0, 0) + ass:draw_start() + ass:append(fg_path.text) + ass:draw_stop() + + -- Current volume value + local volume_string = tostring(round(state.volume * 10) / 10) + local font_size = round(((width * 0.6) - (#volume_string * (width / 20))) * options.font_scale) + if volume_y < self.by - self.spacing then + ass:txt(self.ax + (width / 2), self.by - self.spacing, 2, volume_string, { + size = font_size, color = fgt, opacity = visibility, + clip = '\\clip(' .. fg_path.scale .. ', ' .. fg_path.text .. ')', + }) + end + if volume_y > self.by - self.spacing - font_size then + ass:txt(self.ax + (width / 2), self.by - self.spacing, 2, volume_string, { + size = font_size, color = bgt, opacity = visibility, + clip = '\\iclip(' .. fg_path.scale .. ', ' .. fg_path.text .. ')', + }) + end + + -- Disabled stripes for no audio + if not state.has_audio then + local fg_100_path = create_nudged_path(options.volume_border) + local texture_opts = { + size = 200, color = 'ffffff', opacity = visibility * 0.1, anchor_x = ax, + clip = '\\clip(' .. fg_100_path.scale .. ',' .. fg_100_path.text .. ')', + } + ass:texture(ax, ay, bx, by, 'a', texture_opts) + texture_opts.color = '000000' + texture_opts.anchor_x = ax + texture_opts.size / 28 + ass:texture(ax, ay, bx, by, 'a', texture_opts) + end + + return ass +end + +--[[ Volume ]] + +---@class Volume : Element +local Volume = class(Element) + +function Volume:new() return Class.new(self) --[[@as Volume]] end +function Volume:init() + Element.init(self, 'volume') + self.mute = MuteButton:new({anchor_id = 'volume'}) + self.slider = VolumeSlider:new({anchor_id = 'volume'}) +end + +function Volume:get_visibility() + return self.slider.pressed and 1 or Elements.timeline.proximity_raw == 0 and -1 or Element.get_visibility(self) +end + +function Volume:update_dimensions() + local width = state.fullormaxed and options.volume_size_fullscreen or options.volume_size + local controls, timeline, top_bar = Elements.controls, Elements.timeline, Elements.top_bar + local min_y = top_bar.enabled and top_bar.by or 0 + local max_y = (controls and controls.enabled and controls.ay) or (timeline.enabled and timeline.ay) + or display.height - top_bar.size + local available_height = max_y - min_y + local max_height = available_height * 0.8 + local height = round(math.min(width * 8, max_height)) + self.enabled = height > width * 2 -- don't render if too small + local margin = (width / 2) + Elements.window_border.size + self.ax = round(options.volume == 'left' and margin or display.width - margin - width) + self.ay = min_y + round((available_height - height) / 2) + self.bx = round(self.ax + width) + self.by = round(self.ay + height) + self.mute.enabled, self.slider.enabled = self.enabled, self.enabled + self.mute:set_coordinates(self.ax, self.by - round(width * 0.8), self.bx, self.by) + self.slider:set_coordinates(self.ax, self.ay, self.bx, self.mute.ay) +end + +function Volume:on_display() self:update_dimensions() end +function Volume:on_prop_border() self:update_dimensions() end +function Volume:on_controls_reflow() self:update_dimensions() end + +return Volume diff --git a/.config/mpv/scripts/uosc_shared/elements/WindowBorder.lua b/.config/mpv/scripts/uosc_shared/elements/WindowBorder.lua new file mode 100644 index 0000000..c5544f5 --- /dev/null +++ b/.config/mpv/scripts/uosc_shared/elements/WindowBorder.lua @@ -0,0 +1,33 @@ +local Element = require('uosc_shared/elements/Element') + +---@class WindowBorder : Element +local WindowBorder = class(Element) + +function WindowBorder:new() return Class.new(self) --[[@as WindowBorder]] end +function WindowBorder:init() + Element.init(self, 'window_border') + self.ignores_menu = true + self.size = 0 +end + +function WindowBorder:decide_enabled() + self.enabled = options.window_border_size > 0 and not state.fullormaxed and not state.border + self.size = self.enabled and options.window_border_size or 0 +end + +function WindowBorder:on_prop_border() self:decide_enabled() end +function WindowBorder:on_prop_fullormaxed() self:decide_enabled() end + +function WindowBorder:render() + if self.size > 0 then + local ass = assdraw.ass_new() + local clip = '\\iclip(' .. self.size .. ',' .. self.size .. ',' .. + (display.width - self.size) .. ',' .. (display.height - self.size) .. ')' + ass:rect(0, 0, display.width + 1, display.height + 1, { + color = bg, clip = clip, opacity = options.window_border_opacity, + }) + return ass + end +end + +return WindowBorder diff --git a/.config/mpv/scripts/uosc_shared/lib/ass.lua b/.config/mpv/scripts/uosc_shared/lib/ass.lua new file mode 100644 index 0000000..108953f --- /dev/null +++ b/.config/mpv/scripts/uosc_shared/lib/ass.lua @@ -0,0 +1,170 @@ +--[[ ASSDRAW EXTENSIONS ]] + +local ass_mt = getmetatable(assdraw.ass_new()) + +-- Opacity. +---@param opacity number|number[] Opacity of all elements, or an array of [primary, secondary, border, shadow] opacities. +---@param fraction? number Optionally adjust the above opacity by this fraction. +function ass_mt:opacity(opacity, fraction) + fraction = fraction ~= nil and fraction or 1 + if type(opacity) == 'number' then + self.text = self.text .. string.format('{\\alpha&H%X&}', opacity_to_alpha(opacity * fraction)) + else + self.text = self.text .. string.format( + '{\\1a&H%X&\\2a&H%X&\\3a&H%X&\\4a&H%X&}', + opacity_to_alpha((opacity[1] or 0) * fraction), + opacity_to_alpha((opacity[2] or 0) * fraction), + opacity_to_alpha((opacity[3] or 0) * fraction), + opacity_to_alpha((opacity[4] or 0) * fraction) + ) + end +end + +-- Icon. +---@param x number +---@param y number +---@param size number +---@param name string +---@param opts? {color?: string; border?: number; border_color?: string; opacity?: number; clip?: string; align?: number} +function ass_mt:icon(x, y, size, name, opts) + opts = opts or {} + opts.font, opts.size, opts.bold = 'MaterialIconsRound-Regular', size, false + self:txt(x, y, opts.align or 5, name, opts) +end + +-- Text. +-- Named `txt` because `ass.text` is a value. +---@param x number +---@param y number +---@param align number +---@param value string|number +---@param opts {size: number; font?: string; color?: string; bold?: boolean; italic?: boolean; border?: number; border_color?: string; shadow?: number; shadow_color?: string; rotate?: number; wrap?: number; opacity?: number; clip?: string} +function ass_mt:txt(x, y, align, value, opts) + local border_size = opts.border or 0 + local shadow_size = opts.shadow or 0 + local tags = '\\pos(' .. x .. ',' .. y .. ')\\rDefault\\an' .. align .. '\\blur0' + -- font + tags = tags .. '\\fn' .. (opts.font or config.font) + -- font size + tags = tags .. '\\fs' .. opts.size + -- bold + if opts.bold or (opts.bold == nil and options.font_bold) then tags = tags .. '\\b1' end + -- italic + if opts.italic then tags = tags .. '\\i1' end + -- rotate + if opts.rotate then tags = tags .. '\\frz' .. opts.rotate end + -- wrap + if opts.wrap then tags = tags .. '\\q' .. opts.wrap end + -- border + tags = tags .. '\\bord' .. border_size + -- shadow + tags = tags .. '\\shad' .. shadow_size + -- colors + tags = tags .. '\\1c&H' .. (opts.color or bgt) + if border_size > 0 then tags = tags .. '\\3c&H' .. (opts.border_color or bg) end + if shadow_size > 0 then tags = tags .. '\\4c&H' .. (opts.shadow_color or bg) end + -- opacity + if opts.opacity then tags = tags .. string.format('\\alpha&H%X&', opacity_to_alpha(opts.opacity)) end + -- clip + if opts.clip then tags = tags .. opts.clip end + -- render + self:new_event() + self.text = self.text .. '{' .. tags .. '}' .. value +end + +-- Tooltip. +---@param element {ax: number; ay: number; bx: number; by: number} +---@param value string|number +---@param opts? {size?: number; offset?: number; bold?: boolean; italic?: boolean; width_overwrite?: number, responsive?: boolean} +function ass_mt:tooltip(element, value, opts) + opts = opts or {} + opts.size = opts.size or 16 + opts.border = options.text_border + opts.border_color = bg + local offset = opts.offset or opts.size / 2 + local align_top = opts.responsive == false or element.ay - offset > opts.size * 2 + local x = element.ax + (element.bx - element.ax) / 2 + local y = align_top and element.ay - offset or element.by + offset + local margin = (opts.width_overwrite or text_width(value, opts)) / 2 + 10 + self:txt(clamp(margin, x, display.width - margin), y, align_top and 2 or 8, value, opts) +end + +-- Rectangle. +---@param ax number +---@param ay number +---@param bx number +---@param by number +---@param opts? {color?: string; border?: number; border_color?: string; opacity?: number; border_opacity?: number; clip?: string, radius?: number} +function ass_mt:rect(ax, ay, bx, by, opts) + opts = opts or {} + local border_size = opts.border or 0 + local tags = '\\pos(0,0)\\rDefault\\an7\\blur0' + -- border + tags = tags .. '\\bord' .. border_size + -- colors + tags = tags .. '\\1c&H' .. (opts.color or fg) + if border_size > 0 then tags = tags .. '\\3c&H' .. (opts.border_color or bg) end + -- opacity + if opts.opacity then tags = tags .. string.format('\\alpha&H%X&', opacity_to_alpha(opts.opacity)) end + if opts.border_opacity then tags = tags .. string.format('\\3a&H%X&', opacity_to_alpha(opts.border_opacity)) end + -- clip + if opts.clip then + tags = tags .. opts.clip + end + -- draw + self:new_event() + self.text = self.text .. '{' .. tags .. '}' + self:draw_start() + if opts.radius then + self:round_rect_cw(ax, ay, bx, by, opts.radius) + else + self:rect_cw(ax, ay, bx, by) + end + self:draw_stop() +end + +-- Circle. +---@param x number +---@param y number +---@param radius number +---@param opts? {color?: string; border?: number; border_color?: string; opacity?: number; clip?: string} +function ass_mt:circle(x, y, radius, opts) + opts = opts or {} + opts.radius = radius + self:rect(x - radius, y - radius, x + radius, y + radius, opts) +end + +-- Texture. +---@param ax number +---@param ay number +---@param bx number +---@param by number +---@param char string Texture font character. +---@param opts {size?: number; color: string; opacity?: number; clip?: string; anchor_x?: number, anchor_y?: number} +function ass_mt:texture(ax, ay, bx, by, char, opts) + opts = opts or {} + local anchor_x, anchor_y = opts.anchor_x or ax, opts.anchor_y or ay + local clip = opts.clip or ('\\clip(' .. ax .. ',' .. ay .. ',' .. bx .. ',' .. by .. ')') + local tile_size, opacity = opts.size or 100, opts.opacity or 0.2 + local x, y = ax - (ax - anchor_x) % tile_size, ay - (ay - anchor_y) % tile_size + local width, height = bx - x, by - y + local line = string.rep(char, math.ceil((width / tile_size))) + local lines = '' + for i = 1, math.ceil(height / tile_size), 1 do lines = lines .. (lines == '' and '' or '\\N') .. line end + self:txt( + x, y, 7, lines, + {font = 'uosc_textures', size = tile_size, color = opts.color, bold = false, opacity = opacity, clip = clip}) +end + +-- Rotating spinner icon. +---@param x number +---@param y number +---@param size number +---@param opts? {color?: string; opacity?: number; clip?: string; border?: number; border_color?: string;} +function ass_mt:spinner(x, y, size, opts) + opts = opts or {} + opts.rotate = (state.render_last_time * 1.75 % 1) * -360 + opts.color = opts.color or fg + self:icon(x, y, size, 'autorenew', opts) + request_render() +end diff --git a/.config/mpv/scripts/uosc_shared/lib/menus.lua b/.config/mpv/scripts/uosc_shared/lib/menus.lua new file mode 100644 index 0000000..d6303e4 --- /dev/null +++ b/.config/mpv/scripts/uosc_shared/lib/menus.lua @@ -0,0 +1,277 @@ +---@param data MenuData +---@param opts? {submenu?: string; mouse_nav?: boolean} +function open_command_menu(data, opts) + local menu = Menu:open(data, function(value) + if type(value) == 'string' then + mp.command(value) + else + ---@diagnostic disable-next-line: deprecated + mp.commandv(unpack(value)) + end + end, opts) + if opts and opts.submenu then menu:activate_submenu(opts.submenu) end + return menu +end + +---@param opts? {submenu?: string; mouse_nav?: boolean} +function toggle_menu_with_items(opts) + if Menu:is_open('menu') then Menu:close() + else open_command_menu({type = 'menu', items = config.menu_items}, opts) end +end + +---@param options {type: string; title: string; list_prop: string; active_prop?: string; serializer: fun(list: any, active: any): MenuDataItem[]; on_select: fun(value: any)} +function create_self_updating_menu_opener(options) + return function() + if Menu:is_open(options.type) then Menu:close() return end + local list = mp.get_property_native(options.list_prop) + local active = options.active_prop and mp.get_property_native(options.active_prop) or nil + local menu + + local function update() menu:update_items(options.serializer(list, active)) end + + local ignore_initial_list = true + local function handle_list_prop_change(name, value) + if ignore_initial_list then ignore_initial_list = false + else list = value update() end + end + + local ignore_initial_active = true + local function handle_active_prop_change(name, value) + if ignore_initial_active then ignore_initial_active = false + else active = value update() end + end + + local initial_items, selected_index = options.serializer(list, active) + + -- Items and active_index are set in the handle_prop_change callback, since adding + -- a property observer triggers its handler immediately, we just let that initialize the items. + menu = Menu:open( + {type = options.type, title = options.title, items = initial_items, selected_index = selected_index}, + options.on_select, { + on_open = function() + mp.observe_property(options.list_prop, 'native', handle_list_prop_change) + if options.active_prop then + mp.observe_property(options.active_prop, 'native', handle_active_prop_change) + end + end, + on_close = function() + mp.unobserve_property(handle_list_prop_change) + mp.unobserve_property(handle_active_prop_change) + end, + }) + end +end + +function create_select_tracklist_type_menu_opener(menu_title, track_type, track_prop, load_command) + local function serialize_tracklist(tracklist) + local items = {} + + if load_command then + items[#items + 1] = { + title = 'Load', bold = true, italic = true, hint = 'open file', value = '{load}', separator = true, + } + end + + local first_item_index = #items + 1 + local active_index = nil + local disabled_item = nil + + -- Add option to disable a subtitle track. This works for all tracks, + -- but why would anyone want to disable audio or video? Better to not + -- let people mistakenly select what is unwanted 99.999% of the time. + -- If I'm mistaken and there is an active need for this, feel free to + -- open an issue. + if track_type == 'sub' then + disabled_item = {title = 'Disabled', italic = true, muted = true, hint = '—', value = nil, active = true} + items[#items + 1] = disabled_item + end + + for _, track in ipairs(tracklist) do + if track.type == track_type then + local hint_values = {} + local function h(value) hint_values[#hint_values + 1] = value end + + if track.lang then h(track.lang:upper()) end + if track['demux-h'] then + h(track['demux-w'] and (track['demux-w'] .. 'x' .. track['demux-h']) or (track['demux-h'] .. 'p')) + end + if track['demux-fps'] then h(string.format('%.5gfps', track['demux-fps'])) end + h(track.codec) + if track['audio-channels'] then h(track['audio-channels'] .. ' channels') end + if track['demux-samplerate'] then h(string.format('%.3gkHz', track['demux-samplerate'] / 1000)) end + if track.forced then h('forced') end + if track.default then h('default') end + if track.external then h('external') end + + items[#items + 1] = { + title = (track.title and track.title or 'Track ' .. track.id), + hint = table.concat(hint_values, ', '), + value = track.id, + active = track.selected, + } + + if track.selected then + if disabled_item then disabled_item.active = false end + active_index = #items + end + end + end + + return items, active_index or first_item_index + end + + local function selection_handler(value) + if value == '{load}' then + mp.command(load_command) + else + mp.commandv('set', track_prop, value and value or 'no') + + -- If subtitle track was selected, assume user also wants to see it + if value and track_type == 'sub' then + mp.commandv('set', 'sub-visibility', 'yes') + end + end + end + + return create_self_updating_menu_opener({ + title = menu_title, + type = track_type, + list_prop = 'track-list', + serializer = serialize_tracklist, + on_select = selection_handler, + }) +end + +---@alias NavigationMenuOptions {type: string, title?: string, allowed_types?: string[], active_path?: string, selected_path?: string; on_open?: fun(); on_close?: fun()} + +-- Opens a file navigation menu with items inside `directory_path`. +---@param directory_path string +---@param handle_select fun(path: string): nil +---@param opts NavigationMenuOptions +function open_file_navigation_menu(directory_path, handle_select, opts) + directory = serialize_path(normalize_path(directory_path)) + opts = opts or {} + + if not directory then + msg.error('Couldn\'t serialize path "' .. directory_path .. '.') + return + end + + local files, directories = read_directory(directory.path, opts.allowed_types) + local is_root = not directory.dirname + + if not files or not directories then return end + + sort_filenames(directories) + sort_filenames(files) + + -- Pre-populate items with parent directory selector if not at root + -- Each item value is a serialized path table it points to. + local items = {} + + if is_root then + if state.os == 'windows' then + items[#items + 1] = { + title = '..', hint = 'Drives', value = {is_drives = true, is_to_parent = true}, separator = true, + } + end + else + local serialized = serialize_path(directory.dirname) + serialized.is_directory = true + serialized.is_to_parent = true + items[#items + 1] = {title = '..', hint = 'parent dir', value = serialized, separator = true} + end + + local items_start_index = #items + 1 + + local path_separator = path_separator(directory.path) + for _, dir in ipairs(directories) do + local serialized = serialize_path(utils.join_path(directory.path, dir)) + if serialized then + serialized.is_directory = true + items[#items + 1] = {title = serialized.basename, value = serialized, hint = path_separator} + end + end + + for _, file in ipairs(files) do + local serialized = serialize_path(utils.join_path(directory.path, file)) + if serialized then + serialized.is_file = true + items[#items + 1] = {title = serialized.basename, value = serialized} + end + end + + for index, item in ipairs(items) do + if not item.value.is_to_parent then + if index == items_start_index then item.selected = true end + + if opts.active_path == item.value.path then + item.active = true + if not opts.selected_path then item.selected = true end + end + + if opts.selected_path == item.value.path then item.selected = true end + end + end + + local menu_data = { + type = opts.type, title = opts.title or directory.basename .. path_separator, items = items, + on_open = opts.on_open, on_close = opts.on_close, + } + + return Menu:open(menu_data, function(path) + local inheritable_options = { + type = opts.type, title = opts.title, allowed_types = opts.allowed_types, active_path = opts.active_path, + } + + if path.is_drives then + open_drives_menu(function(drive_path) + open_file_navigation_menu(drive_path, handle_select, inheritable_options) + end, {type = inheritable_options.type, title = inheritable_options.title, selected_path = directory.path}) + return + end + + if path.is_directory then + -- Preselect directory we are coming from + if path.is_to_parent then + inheritable_options.selected_path = directory.path + end + + open_file_navigation_menu(path.path, handle_select, inheritable_options) + else + handle_select(path.path) + end + end) +end + +-- Opens a file navigation menu with Windows drives as items. +---@param handle_select fun(path: string): nil +---@param opts? NavigationMenuOptions +function open_drives_menu(handle_select, opts) + opts = opts or {} + local process = mp.command_native({ + name = 'subprocess', + capture_stdout = true, + playback_only = false, + args = {'wmic', 'logicaldisk', 'get', 'name', '/value'}, + }) + local items = {} + + if process.status == 0 then + for _, value in ipairs(split(process.stdout, '\n')) do + local drive = string.match(value, 'Name=([A-Z]:)') + if drive then + local drive_path = normalize_path(drive) + items[#items + 1] = { + title = drive, hint = 'Drive', value = drive_path, + selected = opts.selected_path == drive_path, + active = opts.active_path == drive_path, + } + end + end + else + msg.error(process.stderr) + end + + return Menu:open({type = opts.type, title = opts.title or 'Drives', items = items}, handle_select) +end diff --git a/.config/mpv/scripts/uosc_shared/lib/std.lua b/.config/mpv/scripts/uosc_shared/lib/std.lua new file mode 100644 index 0000000..1261666 --- /dev/null +++ b/.config/mpv/scripts/uosc_shared/lib/std.lua @@ -0,0 +1,181 @@ +--[[ Stateless utilities missing in lua standard library ]] + +---@param number number +function round(number) return math.floor(number + 0.5) end + +---@param min number +---@param value number +---@param max number +function clamp(min, value, max) return math.max(min, math.min(value, max)) end + +---@param rgba string `rrggbb` or `rrggbbaa` hex string. +function serialize_rgba(rgba) + local a = rgba:sub(7, 8) + return { + color = rgba:sub(5, 6) .. rgba:sub(3, 4) .. rgba:sub(1, 2), + opacity = clamp(0, tonumber(#a == 2 and a or 'ff', 16) / 255, 1), + } +end + +-- Trim any `char` from the end of the string. +---@param str string +---@param char string +---@return string +function trim_end(str, char) + local char, end_i = char:byte(), 0 + for i = #str, 1, -1 do + if str:byte(i) ~= char then + end_i = i + break + end + end + return str:sub(1, end_i) +end + +---@param str string +---@param pattern string +---@return string[] +function split(str, pattern) + local list = {} + local full_pattern = '(.-)' .. pattern + local last_end = 1 + local start_index, end_index, capture = str:find(full_pattern, 1) + while start_index do + list[#list + 1] = capture + last_end = end_index + 1 + start_index, end_index, capture = str:find(full_pattern, last_end) + end + if last_end <= (#str + 1) then + capture = str:sub(last_end) + list[#list + 1] = capture + end + return list +end + +-- Get index of the last appearance of `sub` in `str`. +---@param str string +---@param sub string +---@return integer|nil +function string_last_index_of(str, sub) + local sub_length = #sub + for i = #str, 1, -1 do + for j = 1, sub_length do + if str:byte(i + j - 1) ~= sub:byte(j) then break end + if j == sub_length then return i end + end + end +end + +---@param itable table +---@param value any +---@return integer|nil +function itable_index_of(itable, value) + for index, item in ipairs(itable) do + if item == value then return index end + end +end + +---@param itable table +---@param compare fun(value: any, index: number) +---@param from_end? boolean Search from the end of the table. +---@return number|nil index +---@return any|nil value +function itable_find(itable, compare, from_end) + local from, to, step = from_end and #itable or 1, from_end and 1 or #itable, from_end and -1 or 1 + for index = from, to, step do + if compare(itable[index], index) then return index, itable[index] end + end +end + +---@param itable table +---@param decider fun(value: any, index: number) +function itable_filter(itable, decider) + local filtered = {} + for index, value in ipairs(itable) do + if decider(value, index) then filtered[#filtered + 1] = value end + end + return filtered +end + +---@param itable table +---@param value any +function itable_remove(itable, value) + return itable_filter(itable, function(item) return item ~= value end) +end + +---@param itable table +---@param start_pos? integer +---@param end_pos? integer +function itable_slice(itable, start_pos, end_pos) + start_pos = start_pos and start_pos or 1 + end_pos = end_pos and end_pos or #itable + + if end_pos < 0 then end_pos = #itable + end_pos + 1 end + if start_pos < 0 then start_pos = #itable + start_pos + 1 end + + local new_table = {} + for index, value in ipairs(itable) do + if index >= start_pos and index <= end_pos then + new_table[#new_table + 1] = value + end + end + return new_table +end + +---@generic T +---@param a T[]|nil +---@param b T[]|nil +---@return T[] +function itable_join(a, b) + local result = {} + if a then for _, value in ipairs(a) do result[#result + 1] = value end end + if b then for _, value in ipairs(b) do result[#result + 1] = value end end + return result +end + +---@param target any[] +---@param source any[] +function itable_append(target, source) + for _, value in ipairs(source) do target[#target + 1] = value end + return target +end + +---@param target any[] +---@param source any[] +---@param props? string[] +function table_assign(target, source, props) + if props then + for _, name in ipairs(props) do target[name] = source[name] end + else + for prop, value in pairs(source) do target[prop] = value end + end + return target +end + +---@generic T +---@param table T +---@return T +function table_shallow_copy(table) + local result = {} + for key, value in pairs(table) do result[key] = value end + return result +end + +--[[ EASING FUNCTIONS ]] + +function ease_out_quart(x) return 1 - ((1 - x) ^ 4) end +function ease_out_sext(x) return 1 - ((1 - x) ^ 6) end + +--[[ CLASSES ]] + +---@class Class +Class = {} +function Class:new(...) + local object = setmetatable({}, {__index = self}) + object:init(...) + return object +end +function Class:init() end +function Class:destroy() end + +function class(parent) return setmetatable({}, {__index = parent or Class}) end diff --git a/.config/mpv/scripts/uosc_shared/lib/text.lua b/.config/mpv/scripts/uosc_shared/lib/text.lua new file mode 100644 index 0000000..307845d --- /dev/null +++ b/.config/mpv/scripts/uosc_shared/lib/text.lua @@ -0,0 +1,414 @@ +-- https://en.wikipedia.org/wiki/Unicode_block +---@alias CodePointRange {[1]: integer; [2]: integer} + +---@type CodePointRange[] +local zero_width_blocks = { + {0x0000, 0x001F}, -- C0 + {0x007F, 0x009F}, -- Delete + C1 + {0x034F, 0x034F}, -- combining grapheme joiner + {0x061C, 0x061C}, -- Arabic Letter Strong + {0x200B, 0x200F}, -- {zero-width space, zero-width non-joiner, zero-width joiner, left-to-right mark, right-to-left mark} + {0x2028, 0x202E}, -- {line separator, paragraph separator, Left-to-Right Embedding, Right-to-Left Embedding, Pop Directional Format, Left-to-Right Override, Right-to-Left Override} + {0x2060, 0x2060}, -- word joiner + {0x2066, 0x2069}, -- {Left-to-Right Isolate, Right-to-Left Isolate, First Strong Isolate, Pop Directional Isolate} + {0xFEFF, 0xFEFF}, -- zero-width non-breaking space + -- Some other characters can also be combined https://en.wikipedia.org/wiki/Combining_character + {0x0300, 0x036F}, -- Combining Diacritical Marks 0 BMP Inherited + {0x1AB0, 0x1AFF}, -- Combining Diacritical Marks Extended 0 BMP Inherited + {0x1DC0, 0x1DFF}, -- Combining Diacritical Marks Supplement 0 BMP Inherited + {0x20D0, 0x20FF}, -- Combining Diacritical Marks for Symbols 0 BMP Inherited + {0xFE20, 0xFE2F}, -- Combining Half Marks 0 BMP Cyrillic (2 characters), Inherited (14 characters) + -- Egyptian Hieroglyph Format Controls and Shorthand format Controls + {0x13430, 0x1345F}, -- Egyptian Hieroglyph Format Controls 1 SMP Egyptian Hieroglyphs + {0x1BCA0, 0x1BCAF}, -- Shorthand Format Controls 1 SMP Common + -- not sure how to deal with those https://en.wikipedia.org/wiki/Spacing_Modifier_Letters + {0x02B0, 0x02FF}, -- Spacing Modifier Letters 0 BMP Bopomofo (2 characters), Latin (14 characters), Common (64 characters) +} + +-- All characters have the same width as the first one +---@type CodePointRange[] +local same_width_blocks = { + {0x3400, 0x4DBF}, -- CJK Unified Ideographs Extension A 0 BMP Han + {0x4E00, 0x9FFF}, -- CJK Unified Ideographs 0 BMP Han + {0x20000, 0x2A6DF}, -- CJK Unified Ideographs Extension B 2 SIP Han + {0x2A700, 0x2B73F}, -- CJK Unified Ideographs Extension C 2 SIP Han + {0x2B740, 0x2B81F}, -- CJK Unified Ideographs Extension D 2 SIP Han + {0x2B820, 0x2CEAF}, -- CJK Unified Ideographs Extension E 2 SIP Han + {0x2CEB0, 0x2EBEF}, -- CJK Unified Ideographs Extension F 2 SIP Han + {0x2F800, 0x2FA1F}, -- CJK Compatibility Ideographs Supplement 2 SIP Han + {0x30000, 0x3134F}, -- CJK Unified Ideographs Extension G 3 TIP Han + {0x31350, 0x323AF}, -- CJK Unified Ideographs Extension H 3 TIP Han +} + +---Get byte count of utf-8 character at index i in str +---@param str string +---@param i integer? +---@return integer +local function utf8_char_bytes(str, i) + local char_byte = str:byte(i) + if char_byte < 0xC0 then return 1 + elseif char_byte < 0xE0 then return 2 + elseif char_byte < 0xF0 then return 3 + elseif char_byte < 0xF8 then return 4 + else return 1 end +end + +---Creates an iterator for an utf-8 encoded string +---Iterates over utf-8 characters instead of bytes +---@param str string +---@return fun(): string +local function utf8_iter(str) + local byte_start = 1 + return function() + local start = byte_start + if #str < start then return nil end + local byte_count = utf8_char_bytes(str, start) + byte_start = start + byte_count + return start, str:sub(start, start + byte_count - 1) + end +end + +---Extract Unicode code point from utf-8 character at index i in str +---@param str string +---@param i integer +---@return integer +local function utf8_to_unicode(str, i) + local byte_count = utf8_char_bytes(str, i) + local char_byte = str:byte(i) + local unicode = char_byte + if byte_count ~= 1 then + local shift = 2 ^ (8 - byte_count) + char_byte = char_byte - math.floor(0xFF / shift) * shift + unicode = char_byte * (2 ^ 6) ^ (byte_count - 1) + end + for j = 2, byte_count do + char_byte = str:byte(i + j - 1) - 0x80 + unicode = unicode + char_byte * (2 ^ 6) ^ (byte_count - j) + end + return round(unicode) +end + +---Convert Unicode code point to utf-8 string +---@param unicode integer +---@return string? +local function unicode_to_utf8(unicode) + if unicode < 0x80 then return string.char(unicode) + else + local byte_count + if unicode < 0x800 then byte_count = 2 + elseif unicode < 0x10000 then byte_count = 3 + elseif unicode < 0x110000 then byte_count = 4 + else return end -- too big + + local res = {} + local shift = 2 ^ 6 + local after_shift = unicode + for _ = byte_count, 2, -1 do + local before_shift = after_shift + after_shift = math.floor(before_shift / shift) + table.insert(res, 1, before_shift - after_shift * shift + 0x80) + end + shift = 2 ^ (8 - byte_count) + table.insert(res, 1, after_shift + math.floor(0xFF / shift) * shift) + ---@diagnostic disable-next-line: deprecated + return string.char(unpack(res)) + end +end + +local text_osd = mp.create_osd_overlay("ass-events") +text_osd.compute_bounds, text_osd.hidden = true, true +---@type integer, integer +local osd_width, osd_height = 100, 100 +mp.observe_property('osd-dimensions', 'native', function (_, dim) + if dim then osd_width, osd_height = dim.w, dim.h end +end) + +---@param ass_text string +---@return integer, integer, integer, integer +local function measure_bounds(ass_text) + osd_width, osd_height = mp.get_osd_size() + text_osd.res_x, text_osd.res_y = osd_width, osd_height + text_osd.data = ass_text + local res = text_osd:update() + return res.x0, res.y0, res.x1, res.y1 +end + +---@type {wrap: integer; bold: boolean; italic: boolean, rotate: number; size: number} +local bounds_opts = {wrap = 2, bold = false, italic = false, rotate = 0, size = 0} + +---Measure text width and normalize to a font size of 1 +---text has to be ass safe +---@param text string +---@param size number +---@param bold boolean +---@param italic boolean +---@param horizontal boolean +---@return number, integer +local function normalized_text_width(text, size, bold, italic, horizontal) + bounds_opts.bold, bounds_opts.italic, bounds_opts.rotate = bold, italic, horizontal and 0 or -90 + local x1, y1 = nil, nil + size = size / 0.8 + -- prevent endless loop + local repetitions_left = 5 + repeat + size = size * 0.8 + bounds_opts.size = size + local ass = assdraw.ass_new() + ass:txt(0, 0, horizontal and 7 or 1, text, bounds_opts) + _, _, x1, y1 = measure_bounds(ass.text) + repetitions_left = repetitions_left - 1 + -- make sure nothing got clipped + until (x1 and x1 < osd_width and y1 < osd_height) or repetitions_left == 0 + local width = (repetitions_left == 0 and not x1) and 0 or (horizontal and x1 or y1) + return width / size, horizontal and osd_width or osd_height +end + +---Estimates character length based on utf8 byte count +---1 character length is roughly the size of a latin character +---@param char string +---@return number +local function char_length(char) + return #char > 2 and 2 or 1 +end + +---Estimates string length based on utf8 byte count +---Note: Making a string in the iterator with the character is a waste here, +---but as this function is only used when measuring whole string widths it's fine +---@param text string +---@return number +local function text_length(text) + if not text or text == '' then return 0 end + local text_length = 0 + for _, char in utf8_iter(tostring(text)) do text_length = text_length + char_length(char) end + return text_length +end + +local width_length_ratio = 0.5 +---@type {[boolean]: {[string]: {[1]: number, [2]: integer}}} +local char_width_cache = {} + +---Finds the best orientation of text on screen and returns the estimated max size +---and if the text should be drawn horizontally +---@param text string +---@return number, boolean +local function fit_on_screen(text) + local estimated_width = text_length(text) * width_length_ratio + if osd_width >= osd_height then + -- Fill the screen as much as we can, bigger is more accurate. + return math.min(osd_width / estimated_width, osd_height), true + else + return math.min(osd_height / estimated_width, osd_width), false + end +end + +---Gets next stage from cache +---@param cache {[any]: table} +---@param value any +local function get_cache_stage(cache, value) + local stage = cache[value] + if not stage then + stage = {} + cache[value] = stage + end + return stage +end + +---Is measured resolution sufficient +---@param px integer +---@return boolean +local function no_remeasure_required(px) + return px >= 800 or (px * 1.1 >= osd_width and px * 1.1 >= osd_height) +end + +---Get measured width of character +---@param char string +---@param bold boolean +---@return number, integer +local function character_width(char, bold) + ---@type {[string]: {[1]: number, [2]: integer}} + local char_widths = get_cache_stage(char_width_cache, bold) + local width_px = char_widths[char] + if width_px and no_remeasure_required(width_px[2]) then return width_px[1], width_px[2] end + + local unicode = utf8_to_unicode(char, 1) + for _, block in ipairs(zero_width_blocks) do + if unicode >= block[1] and unicode <= block[2] then + char_widths[char] = {0, infinity} + return 0, infinity + end + end + + local measured_char = nil + for _, block in ipairs(same_width_blocks) do + if unicode >= block[1] and unicode <= block[2] then + measured_char = unicode_to_utf8(block[1]) + width_px = char_widths[measured_char] + if width_px and no_remeasure_required(width_px[2]) then + char_widths[char] = width_px + return width_px[1], width_px[2] + end + break + end + end + + if not measured_char then measured_char = char end + -- half as many repetitions for wide characters + local char_count = 10 / char_length(char) + local max_size, horizontal = fit_on_screen(measured_char:rep(char_count)) + local size = math.min(max_size * 0.9, 50) + char_count = math.min(math.floor(char_count * max_size / size * 0.8), 100) + local enclosing_char, enclosing_width, next_char_count = '|', 0, char_count + if measured_char == enclosing_char then enclosing_char = '' + else enclosing_width = 2 * character_width(enclosing_char, bold) end + local width_ratio, width, px = nil, nil, nil + repeat + char_count = next_char_count + local str = enclosing_char .. measured_char:rep(char_count) .. enclosing_char + width, px = normalized_text_width(str, size, bold, false, horizontal) + width = width - enclosing_width + width_ratio = width * size / (horizontal and osd_width or osd_height) + next_char_count = math.min(math.floor(char_count / width_ratio * 0.9), 100) + until width_ratio < 0.05 or width_ratio > 0.5 or char_count == next_char_count + width = width / char_count + + width_px = {width, px} + if char ~= measured_char then char_widths[measured_char] = width_px end + char_widths[char] = width_px + return width, px +end + +---Calculate text width from individual measured characters +---@param text string|number +---@param bold boolean +---@return number, integer +local function character_based_width(text, bold) + local max_width = 0 + local min_px = infinity + for line in tostring(text):gmatch("([^\n]*)\n?") do + local total_width = 0 + for _, char in utf8_iter(line) do + local width, px = character_width(char, bold) + total_width = total_width + width + if px < min_px then min_px = px end + end + if total_width > max_width then max_width = total_width end + end + return max_width, min_px +end + +---Measure width of whole text +---@param text string|number +---@param bold boolean +---@param italic boolean +---@return number, integer +local function whole_text_width(text, bold, italic) + text = tostring(text) + local size, horizontal = fit_on_screen(text) + return normalized_text_width(ass_escape(text), size * 0.9, bold, italic, horizontal) +end + +---Get scale factor calculated from font size, bold and italic +---@param opts {size: number; bold?: boolean; italic?: boolean} +local function opts_scale_factor(opts) + return (opts.italic and 1.01 or 1) * opts.size +end + +---@type {[boolean]: {[boolean]: {[string|number]: {[1]: number, [2]: integer}}}} | {[boolean]: {[string|number]: {[1]: number, [2]: integer}}} +local width_cache = {} + +---Calculate width of text with the given opts +---@param text string|number +---@return number +---@param opts {size: number; bold?: boolean; italic?: boolean} +function text_width(text, opts) + if not text or text == '' then return 0 end + + ---@type boolean, boolean + local bold, italic = opts.bold or false, opts.italic or false + + if options.text_width_estimation then + ---@type {[string|number]: {[1]: number, [2]: integer}} + local text_width = get_cache_stage(width_cache, bold) + local width_px = text_width[text] + if width_px and no_remeasure_required(width_px[2]) then return width_px[1] * opts_scale_factor(opts) end + + local width, px = character_based_width(text, bold) + width_cache[bold][text] = {width, px} + return width * opts_scale_factor(opts) + else + ---@type {[string|number]: {[1]: number, [2]: integer}} + local text_width = get_cache_stage(get_cache_stage(width_cache, bold), italic) + local width_px = text_width[text] + if width_px and no_remeasure_required(width_px[2]) then return width_px[1] * opts.size end + + local width, px = whole_text_width(text, bold, italic) + width_cache[bold][italic][text] = {width, px} + return width * opts.size + end +end + +---Wrap the text at the closest opportunity to target_line_length +---@param text string +---@param opts {size: number; bold?: boolean; italic?: boolean} +---@param target_line_length number +---@return string +function wrap_text(text, opts, target_line_length) + local target_line_width = target_line_length * width_length_ratio * opts.size + local bold, scale_factor = opts.bold or false, opts_scale_factor(opts) + local wrap_at_chars = {' ', ' ', '-', '–'} + local remove_when_wrap = {' ', ' '} + local lines = {} + for text_line in text:gmatch("([^\n]*)\n?") do + local line_width = 0 + local line_start = 1 + local before_end = nil + local before_width = 0 + local before_line_start = 0 + local before_removed_width = 0 + for char_start, char in utf8_iter(text_line) do + local char_end = char_start + #char - 1 + local can_wrap = false + for _, c in ipairs(wrap_at_chars) do + if char == c then + can_wrap = true + break + end + end + local char_width = character_width(char, bold) * scale_factor + line_width = line_width + char_width + if can_wrap or (char_end == #text_line) then + local remove = false + for _, c in ipairs(remove_when_wrap) do + if char == c then + remove = true + break + end + end + local line_width_after_remove = line_width - (remove and char_width or 0) + if line_width_after_remove < target_line_width then + before_end = remove and char_start - 1 or char_end + before_width = line_width_after_remove + before_line_start = char_end + 1 + before_removed_width = remove and char_width or 0 + else + if (target_line_width - before_width) < + (line_width_after_remove - target_line_width) then + lines[#lines + 1] = text_line:sub(line_start, before_end) + line_start = before_line_start + line_width = line_width - before_width - before_removed_width + else + lines[#lines + 1] = text_line:sub(line_start, remove and char_start - 1 or char_end) + line_start = char_end + 1 + line_width = remove and line_width - char_width or line_width + line_width = 0 + end + before_end = line_start + before_width = 0 + end + end + end + if #text_line >= line_start then lines[#lines + 1] = text_line:sub(line_start) + elseif text_line == '' then lines[#lines + 1] = '' end + end + return table.concat(lines, '\n') +end diff --git a/.config/mpv/scripts/uosc_shared/lib/utils.lua b/.config/mpv/scripts/uosc_shared/lib/utils.lua new file mode 100644 index 0000000..1156075 --- /dev/null +++ b/.config/mpv/scripts/uosc_shared/lib/utils.lua @@ -0,0 +1,541 @@ +--[[ UI specific utilities that might or might not depend on its state or options ]] + +-- Sorting comparator close to (but not exactly) how file explorers sort files. +sort_filenames = (function() + local symbol_order + local default_order + + if state.os == 'windows' then + symbol_order = { + ['!'] = 1, ['#'] = 2, ['$'] = 3, ['%'] = 4, ['&'] = 5, ['('] = 6, [')'] = 6, [','] = 7, + ['.'] = 8, ["'"] = 9, ['-'] = 10, [';'] = 11, ['@'] = 12, ['['] = 13, [']'] = 13, ['^'] = 14, + ['_'] = 15, ['`'] = 16, ['{'] = 17, ['}'] = 17, ['~'] = 18, ['+'] = 19, ['='] = 20, + } + default_order = 21 + else + symbol_order = { + ['`'] = 1, ['^'] = 2, ['~'] = 3, ['='] = 4, ['_'] = 5, ['-'] = 6, [','] = 7, [';'] = 8, + ['!'] = 9, ["'"] = 10, ['('] = 11, [')'] = 11, ['['] = 12, [']'] = 12, ['{'] = 13, ['}'] = 14, + ['@'] = 15, ['$'] = 16, ['*'] = 17, ['&'] = 18, ['%'] = 19, ['+'] = 20, ['.'] = 22, ['#'] = 23, + } + default_order = 21 + end + + -- Alphanumeric sorting for humans in Lua + -- http://notebook.kulchenko.com/algorithms/alphanumeric-natural-sorting-for-humans-in-lua + local function pad_number(d) + local dec, n = d:match('(%.?)0*(.+)') + return #dec > 0 and ('%.12f'):format(d) or ('%03d%s'):format(#n, n) + end + + --- In place sorting of filenames + ---@param filenames string[] + return function(filenames) + local tuples = {} + for i, filename in ipairs(filenames) do + local first_char = filename:sub(1, 1) + local order = symbol_order[first_char] or default_order + local formatted = filename:lower():gsub('%.?%d+', pad_number) + tuples[i] = {order, formatted, filename} + end + table.sort(tuples, function(a, b) + if a[1] ~= b[1] then return a[1] < b[1] end + return a[2] == b[2] and #b[3] < #a[3] or a[2] < b[2] + end) + for i, tuple in ipairs(tuples) do filenames[i] = tuple[3] end + end +end)() + +-- Creates in-between frames to animate value from `from` to `to` numbers. +---@param from number +---@param to number|fun():number +---@param setter fun(value: number) +---@param factor_or_callback? number|fun() +---@param callback? fun() Called either on animation end, or when animation is killed. +function tween(from, to, setter, factor_or_callback, callback) + local factor = factor_or_callback + if type(factor_or_callback) == 'function' then callback = factor_or_callback end + if type(factor) ~= 'number' then factor = 0.3 end + + local current, done, timeout = from, false, nil + local get_to = type(to) == 'function' and to or function() return to --[[@as number]] end + local cutoff = math.abs(get_to() - from) * 0.01 + + local function finish() + if not done then + done = true + timeout:kill() + if callback then callback() end + end + end + + local function tick() + local to = get_to() + current = current + ((to - current) * factor) + local is_end = math.abs(to - current) <= cutoff + setter(is_end and to or current) + request_render() + if is_end then finish() + else timeout:resume() end + end + + timeout = mp.add_timeout(state.render_delay, tick) + tick() + + return finish +end + +---@param point {x: number; y: number} +---@param rect {ax: number; ay: number; bx: number; by: number} +function get_point_to_rectangle_proximity(point, rect) + local dx = math.max(rect.ax - point.x, 0, point.x - rect.bx) + local dy = math.max(rect.ay - point.y, 0, point.y - rect.by) + return math.sqrt(dx * dx + dy * dy) +end + +-- Extracts the properties used by property expansion of that string. +---@param str string +---@param res { [string] : boolean } | nil +---@return { [string] : boolean } +function get_expansion_props(str, res) + res = res or {} + for str in str:gmatch('%$(%b{})') do + local name, str = str:match('^{[?!]?=?([^:]+):?(.*)}$') + if name then + local s = name:find('==') or nil + if s then name = name:sub(0, s - 1) end + res[name] = true + if str and str ~= '' then get_expansion_props(str, res) end + end + end + return res +end + +-- Escape a string for verbatim display on the OSD. +---@param str string +function ass_escape(str) + -- There is no escape for '\' in ASS (I think?) but '\' is used verbatim if + -- it isn't followed by a recognized character, so add a zero-width + -- non-breaking space + str = str:gsub('\\', '\\\239\187\191') + str = str:gsub('{', '\\{') + str = str:gsub('}', '\\}') + -- Precede newlines with a ZWNBSP to prevent ASS's weird collapsing of + -- consecutive newlines + str = str:gsub('\n', '\239\187\191\\N') + -- Turn leading spaces into hard spaces to prevent ASS from stripping them + str = str:gsub('\\N ', '\\N\\h') + str = str:gsub('^ ', '\\h') + return str +end + +---@param seconds number +---@return string +function format_time(seconds) + local human = mp.format_time(seconds) + if options.time_precision > 0 then + local formatted = string.format('%.' .. options.time_precision .. 'f', math.abs(seconds) % 1) + human = human .. '.' .. string.sub(formatted, 3) + end + return human +end + +---@param opacity number 0-1 +function opacity_to_alpha(opacity) + return 255 - math.ceil(255 * opacity) +end + +do + local os_separator = state.os == 'windows' and '\\' or '/' + + -- Get appropriate path separator for the given path. + ---@param path string + ---@return string + function path_separator(path) + return path:sub(1, 2) == '\\\\' and '\\' or os_separator + end + + -- Joins paths with the OS aware path separator or UNC separator. + ---@param p1 string + ---@param p2 string + ---@return string + function utils.join_path(p1, p2) + return p1 .. path_separator(p1) .. p2 + end +end + +-- Check if path is absolute. +---@param path string +---@return boolean +function is_absolute(path) + if path:sub(1, 2) == '\\\\' then return true + elseif state.os == 'windows' then return path:find('^%a+:') ~= nil + else return path:sub(1, 1) == '/' end +end + +-- Ensure path is absolute. +---@param path string +---@return string +function ensure_absolute(path) + if is_absolute(path) then return path end + return utils.join_path(state.cwd, path) +end + +-- Remove trailing slashes/backslashes. +---@param path string +---@return string +function trim_trailing_separator(path) + path = trim_end(path, path_separator(path)) + if state.os == 'windows' then + -- Drive letters on windows need trailing backslash + if path:sub(#path) == ':' then return path .. '\\' end + return path + else + if path == '' then return '/' end + return path + end +end + +-- Ensures path is absolute, remove trailing slashes/backslashes. +-- Lightweight version of normalize_path for performance critical parts. +---@param path string +---@return string +function normalize_path_lite(path) + if not path or is_protocol(path) then return path end + path = ensure_absolute(path) + return trim_trailing_separator(path) +end + +-- Ensures path is absolute, remove trailing slashes/backslashes, normalization of path separators and deduplication. +---@param path string +---@return string +function normalize_path(path) + if not path or is_protocol(path) then return path end + + path = ensure_absolute(path) + local is_unc = path:sub(1, 2) == '\\\\' + if state.os == 'windows' or is_unc then path = path:gsub('/', '\\') end + path = trim_trailing_separator(path) + + --Deduplication of path separators + if is_unc then path = path:gsub('(.\\)\\+', '%1') + elseif state.os == 'windows' then path = path:gsub('\\\\+', '\\') + else path = path:gsub('//+', '/') end + + return path +end + +-- Check if path is a protocol, such as `http://...`. +---@param path string +function is_protocol(path) + return type(path) == 'string' and (path:find('^%a[%a%d-_]+://') ~= nil or path:find('^%a[%a%d-_]+:\\?') ~= nil) +end + +---@param path string +---@param extensions string[] Lowercase extensions without the dot. +function has_any_extension(path, extensions) + local path_last_dot_index = string_last_index_of(path, '.') + if not path_last_dot_index then return false end + local path_extension = path:sub(path_last_dot_index + 1):lower() + for _, extension in ipairs(extensions) do + if path_extension == extension then return true end + end + return false +end + +---@return string +function get_default_directory() + return mp.command_native({'expand-path', options.default_directory}) +end + +-- Serializes path into its semantic parts. +---@param path string +---@return nil|{path: string; is_root: boolean; dirname?: string; basename: string; filename: string; extension?: string;} +function serialize_path(path) + if not path or is_protocol(path) then return end + + local normal_path = normalize_path_lite(path) + local dirname, basename = utils.split_path(normal_path) + if basename == '' then basename, dirname = dirname:sub(1, #dirname - 1), nil end + local dot_i = string_last_index_of(basename, '.') + + return { + path = normal_path, + is_root = dirname == nil, + dirname = dirname, + basename = basename, + filename = dot_i and basename:sub(1, dot_i - 1) or basename, + extension = dot_i and basename:sub(dot_i + 1) or nil, + } +end + +-- Reads items in directory and splits it into directories and files tables. +---@param path string +---@param allowed_types? string[] Filter `files` table to contain only files with these extensions. +---@return string[]|nil files +---@return string[]|nil directories +function read_directory(path, allowed_types) + local items, error = utils.readdir(path, 'all') + + if not items then + msg.error('Reading files from "' .. path .. '" failed: ' .. error) + return nil, nil + end + + local files, directories = {}, {} + + for _, item in ipairs(items) do + if item ~= '.' and item ~= '..' then + local info = utils.file_info(utils.join_path(path, item)) + if info then + if info.is_file then + if not allowed_types or has_any_extension(item, allowed_types) then + files[#files + 1] = item + end + else directories[#directories + 1] = item end + end + end + end + + return files, directories +end + +-- Returns full absolute paths of files in the same directory as file_path, +-- and index of the current file in the table. +---@param file_path string +---@param allowed_types? string[] +function get_adjacent_files(file_path, allowed_types) + local current_file = serialize_path(file_path) + if not current_file then return end + local files = read_directory(current_file.dirname, allowed_types) + if not files then return end + sort_filenames(files) + local current_file_index + local paths = {} + for index, file in ipairs(files) do + paths[#paths + 1] = utils.join_path(current_file.dirname, file) + if current_file.basename == file then current_file_index = index end + end + if not current_file_index then return end + return paths, current_file_index +end + +-- Navigates in a list, using delta or, when `state.shuffle` is enabled, +-- randomness to determine the next item. Loops around if `loop-playlist` is enabled. +---@param list table +---@param current_index number +---@param delta number +function decide_navigation_in_list(list, current_index, delta) + if #list < 2 then return #list, list[#list] end + + if state.shuffle then + local new_index = current_index + math.randomseed(os.time()) + while current_index == new_index do new_index = math.random(#list) end + return new_index, list[new_index] + end + + local new_index = current_index + delta + if mp.get_property_native('loop-playlist') then + if new_index > #list then new_index = new_index % #list + elseif new_index < 1 then new_index = #list - new_index end + elseif new_index < 1 or new_index > #list then + return + end + + return new_index, list[new_index] +end + +---@param delta number +function navigate_directory(delta) + if not state.path or is_protocol(state.path) then return false end + local paths, current_index = get_adjacent_files(state.path, config.media_types) + if paths and current_index then + local _, path = decide_navigation_in_list(paths, current_index, delta) + if path then mp.commandv('loadfile', path) return true end + end + return false +end + +---@param delta number +function navigate_playlist(delta) + local playlist, pos = mp.get_property_native('playlist'), mp.get_property_native('playlist-pos-1') + if playlist and #playlist > 1 and pos then + local index = decide_navigation_in_list(playlist, pos, delta) + if index then mp.commandv('playlist-play-index', index - 1) return true end + end + return false +end + +---@param delta number +function navigate_item(delta) + if state.has_playlist then return navigate_playlist(delta) else return navigate_directory(delta) end +end + +-- Can't use `os.remove()` as it fails on paths with unicode characters. +-- Returns `result, error`, result is table of: +-- `status:number(<0=error), stdout, stderr, error_string, killed_by_us:boolean` +---@param path string +function delete_file(path) + local args = state.os == 'windows' and {'cmd', '/C', 'del', path} or {'rm', path} + return mp.command_native({ + name = 'subprocess', + args = args, + playback_only = false, + capture_stdout = true, + capture_stderr = true, + }) +end + +function serialize_chapter_ranges(normalized_chapters) + local ranges = {} + local simple_ranges = { + {name = 'openings', patterns = {'^op ', '^op$', ' op$', 'opening$'}, requires_next_chapter = true}, + {name = 'intros', patterns = {'^intro$'}, requires_next_chapter = true}, + {name = 'endings', patterns = {'^ed ', '^ed$', ' ed$', 'ending$', 'closing$'}}, + {name = 'outros', patterns = {'^outro$'}}, + } + local sponsor_ranges = {} + + -- Extend with alt patterns + for _, meta in ipairs(simple_ranges) do + local alt_patterns = config.chapter_ranges[meta.name] and config.chapter_ranges[meta.name].patterns + if alt_patterns then meta.patterns = itable_join(meta.patterns, alt_patterns) end + end + + -- Clone chapters + local chapters = {} + for i, normalized in ipairs(normalized_chapters) do chapters[i] = table_shallow_copy(normalized) end + + for i, chapter in ipairs(chapters) do + -- Simple ranges + for _, meta in ipairs(simple_ranges) do + if config.chapter_ranges[meta.name] then + local match = itable_find(meta.patterns, function(p) return chapter.lowercase_title:find(p) end) + if match then + local next_chapter = chapters[i + 1] + if next_chapter or not meta.requires_next_chapter then + ranges[#ranges + 1] = table_assign({ + start = chapter.time, + ['end'] = next_chapter and next_chapter.time or infinity, + }, config.chapter_ranges[meta.name]) + end + end + end + end + + -- Sponsor blocks + if config.chapter_ranges.ads then + local id = chapter.lowercase_title:match('segment start *%(([%w]%w-)%)') + if id then -- ad range from sponsorblock + for j = i + 1, #chapters, 1 do + local end_chapter = chapters[j] + local end_match = end_chapter.lowercase_title:match('segment end *%(' .. id .. '%)') + if end_match then + local range = table_assign({ + start_chapter = chapter, end_chapter = end_chapter, + start = chapter.time, ['end'] = end_chapter.time, + }, config.chapter_ranges.ads) + ranges[#ranges + 1], sponsor_ranges[#sponsor_ranges + 1] = range, range + end_chapter.is_end_only = true + break + end + end -- single chapter for ad + elseif not chapter.is_end_only and + (chapter.lowercase_title:find('%[sponsorblock%]:') or chapter.lowercase_title:find('^sponsors?')) then + local next_chapter = chapters[i + 1] + ranges[#ranges + 1] = table_assign({ + start = chapter.time, + ['end'] = next_chapter and next_chapter.time or infinity, + }, config.chapter_ranges.ads) + end + end + end + + -- Fix overlapping sponsor block segments + for index, range in ipairs(sponsor_ranges) do + local next_range = sponsor_ranges[index + 1] + if next_range then + local delta = next_range.start - range['end'] + if delta < 0 then + local mid_point = range['end'] + delta / 2 + range['end'], range.end_chapter.time = mid_point - 0.01, mid_point - 0.01 + next_range.start, next_range.start_chapter.time = mid_point, mid_point + end + end + end + table.sort(chapters, function(a, b) return a.time < b.time end) + + return chapters, ranges +end + +-- Ensures chapters are in chronological order +function normalize_chapters(chapters) + if not chapters then return {} end + -- Ensure chronological order + table.sort(chapters, function(a, b) return a.time < b.time end) + -- Ensure titles + for index, chapter in ipairs(chapters) do + chapter.title = chapter.title or ('Chapter ' .. index) + chapter.lowercase_title = chapter.title:lower() + end + return chapters +end + +function serialize_chapters(chapters) + chapters = normalize_chapters(chapters) + if not chapters then return end + --- timeline font size isn't accessible here, so normalize to size 1 and then scale during rendering + local opts = {size = 1, bold = true} + for index, chapter in ipairs(chapters) do + chapter.index = index + chapter.title_wrapped = wrap_text(chapter.title, opts, 25) + chapter.title_wrapped_width = text_width(chapter.title_wrapped, opts) + chapter.title_wrapped = ass_escape(chapter.title_wrapped) + end + return chapters +end + +--[[ RENDERING ]] + +function render() + state.render_last_time = mp.get_time() + + -- Actual rendering + local ass = assdraw.ass_new() + + for _, element in Elements:ipairs() do + if element.enabled then + local result = element:maybe('render') + if result then + ass:new_event() + ass:merge(result) + end + end + end + + -- submit + if osd.res_x == display.width and osd.res_y == display.height and osd.data == ass.text then + return + end + + osd.res_x = display.width + osd.res_y = display.height + osd.data = ass.text + osd.z = 2000 + osd:update() + + update_margins() +end + +-- Request that render() is called. +-- The render is then either executed immediately, or rate-limited if it was +-- called a small time ago. +state.render_timer = mp.add_timeout(0, render) +state.render_timer:kill() +function request_render() + if state.render_timer:is_enabled() then return end + local timeout = math.max(0, state.render_delay - (mp.get_time() - state.render_last_time)) + state.render_timer.timeout = timeout + state.render_timer:resume() +end diff --git a/.config/mpv/scripts/uosc_shared/main.lua b/.config/mpv/scripts/uosc_shared/main.lua new file mode 100644 index 0000000..323027f --- /dev/null +++ b/.config/mpv/scripts/uosc_shared/main.lua @@ -0,0 +1,5 @@ +--[[ +File required for compatibility between mpv: +- 0.32 - doesn't support `dir/main.lua`, so we need `uosc.lua` in root +- 0.33 - requires `main.lua` in directories +]]