From f75566d62bc40a1ae1f21d2c4757fcf994e3d41c Mon Sep 17 00:00:00 2001 From: sameerasw Date: Wed, 27 May 2026 01:22:28 +0530 Subject: [PATCH 01/11] refactor: generalize ClickThroughHostingView by removing generic type constraint --- airsync-mac/Core/MenuBarManager.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/airsync-mac/Core/MenuBarManager.swift b/airsync-mac/Core/MenuBarManager.swift index 8b163d50..4658b5a9 100644 --- a/airsync-mac/Core/MenuBarManager.swift +++ b/airsync-mac/Core/MenuBarManager.swift @@ -18,7 +18,7 @@ class MenuBarManager: NSObject { private var cancellables = Set() private var appState = AppState.shared private var temporaryDragLabel: String? - private var hostingView: ClickThroughHostingView? + private var hostingView: ClickThroughHostingView? private let statusButton: MenuBarStatusButton = { let view = MenuBarStatusButton(frame: NSRect(x: 0, y: 0, width: 22, height: 22)) @@ -289,7 +289,7 @@ class MenuBarStatusButton: NSView { } // MARK: - Click-Through Hosting View Subclass -class ClickThroughHostingView: NSHostingView { +class ClickThroughHostingView: NSHostingView { override func hitTest(_ point: NSPoint) -> NSView? { return nil } From 22d4990b923cb89c066ca204d2110c3909189039 Mon Sep 17 00:00:00 2001 From: sameerasw Date: Wed, 27 May 2026 16:45:33 +0530 Subject: [PATCH 02/11] feat: QR pair for wireless adb --- .../adb-pair-prompt.imageset/Contents.json | 21 ++ .../adb-pair-prompt.jpeg | Bin 0 -> 35608 bytes .../Images/adb-pair.imageset/Contents.json | 21 ++ .../Images/adb-pair.imageset/adb-pair.jpeg | Bin 0 -> 47875 bytes .../Core/Util/CLI/ADBPairingManager.swift | 239 ++++++++++++++++++ airsync-mac/Localization/en.json | 7 +- .../Settings/ADBPairingSheetView.swift | 188 ++++++++++++++ .../Screens/Settings/SyncSettingsView.swift | 18 +- 8 files changed, 491 insertions(+), 3 deletions(-) create mode 100644 airsync-mac/Assets.xcassets/Images/adb-pair-prompt.imageset/Contents.json create mode 100644 airsync-mac/Assets.xcassets/Images/adb-pair-prompt.imageset/adb-pair-prompt.jpeg create mode 100644 airsync-mac/Assets.xcassets/Images/adb-pair.imageset/Contents.json create mode 100644 airsync-mac/Assets.xcassets/Images/adb-pair.imageset/adb-pair.jpeg create mode 100644 airsync-mac/Core/Util/CLI/ADBPairingManager.swift create mode 100644 airsync-mac/Screens/Settings/ADBPairingSheetView.swift diff --git a/airsync-mac/Assets.xcassets/Images/adb-pair-prompt.imageset/Contents.json b/airsync-mac/Assets.xcassets/Images/adb-pair-prompt.imageset/Contents.json new file mode 100644 index 00000000..c5292b28 --- /dev/null +++ b/airsync-mac/Assets.xcassets/Images/adb-pair-prompt.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "adb-pair-prompt.jpeg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/airsync-mac/Assets.xcassets/Images/adb-pair-prompt.imageset/adb-pair-prompt.jpeg b/airsync-mac/Assets.xcassets/Images/adb-pair-prompt.imageset/adb-pair-prompt.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..1821d48f45af09511a296d6d09afcdbb0e6be679 GIT binary patch literal 35608 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 literal 0 HcmV?d00001 diff --git a/airsync-mac/Assets.xcassets/Images/adb-pair.imageset/Contents.json b/airsync-mac/Assets.xcassets/Images/adb-pair.imageset/Contents.json new file mode 100644 index 00000000..4cf5bc1e --- /dev/null +++ b/airsync-mac/Assets.xcassets/Images/adb-pair.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "adb-pair.jpeg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/airsync-mac/Assets.xcassets/Images/adb-pair.imageset/adb-pair.jpeg b/airsync-mac/Assets.xcassets/Images/adb-pair.imageset/adb-pair.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..4e9f05bed449f84ed9ebb74552c5efd77719cd8e GIT binary patch literal 47875 zcmeFZ1yo$iwkX=TyE_SPA;G;v2yOvFa0tO6KyarM2yOuaB)CI@1!&xzKyVE%!L1u@ zpy_^{ea^mjpZo87=iWcwALEVpPeIYEYE5d*SzUA1v|7k{;GPWqx=T?UC_`mFtM<4aPja7 zPyltLC7hHk4>RtiF4nLQs`Y`9xmIH>MknX2?)FJ zEB6pQd}VI(O8%IVn)c~)dVWD+QE^G>m#;Oo;JW&T#-`@(p5DIxfx+)XlT*_( zvvc!57uGj6e{F5=?C$MDPtVRTF0WwMH^1pZ&C1`%LjCX$O)js4W9de5U+5h!<&(s`>MlGsVO zP-9?XV&b5FNeS=>NdMafxr|y|Y{&%wAqE<1F=3DafPkNfl{w%pn+FmA%7NSewc*4Y z{j2F5yXx|?2?>yA$hn0h0h`MI&@y_i^F|cZP&amiyMzR+?jQm5z<+2B<;T1$yKAci zjVtXSkN_ZT;;)TaeM(azi=!hCM2I->>KX~?Pxx!2rZj)e5eaa0$~h-Pq0ljg5CQ$A zA)2B{gJEzRV2T9f#*N(}K+uA}Hp;(pH_~@3F6`W4x`0m3kbpRrzcy@i6gy}BO74H8 z{lUK||JQf^uhsqs*Z-IJ966p8&0Q(HiyqCL`+q_uK~DUM5Z{m-4_tq$o%-kAoWZ5A z1;}1);vEInLMhrSbuS4SV)!XHy}|f}KW|akMSY2tbLr1|*n3MAumse$^i zj>16x_%2ueU(*k~(EIMkg*eAOYKmgLd5bNXOKCE}+@@%?OTQcuCE~;c(s(u704bc< z39ajYqg=WhbTHP=8)!V)PHk+c9pu0PUw9w;g;JyRiE_xHy1z9*I!6sV|5DaA8j=&k zLbtd+8<)55EoIa$wQwD^-4tc@`0M;DwCmCJf$Az#$aYQpeF!3>ie12^Ij+?sa`lAk zQ{K1Wj~Fp(+cT6p^7j-epP?!Gkwmr&?jjgr2}N+JrkZr4i+j{Al$C?&ubx^tY}F`p zHMIHf?mb@k2G&tT$LG#}`|q`&9mg2JU3Cvq(s#$!mn}O3$8)j~5q2=Q<$P6Sb85&2 zFG%TIjIj_Sq{QP}13+ur6X~N8)V9%PTbZ~f4rc~{2LV=j z8|@L7xh2%7r{#XgkLCvaH!22l^6$%_72`4)a&@`dLr^w2=eh8-dReganYStu5MK|% zzq2idKXf?NanCPVA@z>}A*2e$$e%E$>xb4ijfWTNvuA#tSu7b`VQsYfx$BL7eVdcX`0tbq(TmXE0^lk?qe(#Bes7R7206h*5^H z{TvA`6Z}huZ_XD>m_bUQEz#y`f)q{2tLt^}tqeKZxL?HRC)EB+3okQSw?s{)94xs@ zkLAIbE;(3oFYFHf6x2z8{{_Oy3GL{2C5PR(wewjC_iIZyMes1oC4R{jb5U*&w16T3 zVKQh%du1DDtWKmW8ZRWgmG(ne@jxr+Lba5}lh_H{xuqqKot9y6bTs_T1VIakDpCn3*8o(8ksl?HMZIV^ZC zD864vIf4^NceN@B^w@1Be*5Vng*KJEXC9XiH-{YU^fb1!IoaPRW~3=F=16}gi#|%A zU2S1`0Jz$9inz;^q08ILAt{F|U)pa?dwH;Z7cRBKt<=#`-38D|>}&eY9Vu7f1-jZ$DOQt?*Uc;p=~#&|3_owjBVw9nTz?N%Me@^mV1;yed$)x^l8 z^l22P{F8Xe3$7-a?2BJ3cu=CctAS-{Gl*E4Td|>|9%lN_kCwXny-Qp}6hH|kwEQh8 z$3>=+K6KD}F>D49i0T zxWw02ufVH!gtK->3Y8gEk;d8)C$0TcBCz}EDw`5^!jdZnGA{GEBx-2=m(pK49Cw+O zbF?5uGkx#vhbUAN_wiIX}?HRATY#bplaXq2Kvap#mrGV zivwO3*M#5jfv91&XUYwqpknH4{vWNMJU(dv#<>HjM1WMjI2$)`Bk-hFT~RH23BI9H z_dN=}HS+gX?qZLoCR;uQOTn37DYhac^b4ZtYD&5n7*82J?>}d+l+{|m%&(|E@t=!r zm0m{zh{R*+82Zfkm1@e%&irDymT;w#ey$meoFZnJJ=#VS*9nFzq=Xc-tl0oQ(mE-@ zVP)g(qQ3YieI0~(6M+om;+GyhvTdzTv65&%mOho0JFxNFq55S((qS(k-nqb(_73ix zyR=v1P|7Mu`O*agmd&-$l>jBn%(ll>G_l$pi(sJ)6o-myr=L7MyO%v(n!WPcFY$QS zU5HML7#IGyN~P)%>9hOvbEWl56ui%9HqS%I` zBJmdzK#q9iYqFj+fL+vnrjz9};DE<)3G$nVLGs5OD1#*rZdRkl&3ay*X?^%U^hovTvoAOyrIG_0@6vBQ zj3JjX8v?w}B!;!|+e6m#6E(4D^Hon-^l|sduPk8luiU&a+rk&48wvCG3Kv)iPOLA`ee1$ zP~H#Vs>4{Co73GGq}RXjw1dG*n-VtEZT4z(Xy1OlPO}hHkn@1Gt8u<@N!bH7;l9OC zyR>#!c2tlq>E()VwrDu?n1Vkqp5zT3l?lZ0#-|&>IU!?Nuy}xAX`~VJVPYuYBmUZX zS}q-GA?UaY?s!)>A{4}+k6PR#DczL5i$i>1=s+G*G6ku{i9zq2G ztz`rzX?MwQ!Noq;yLw67hy;ui1r9@iMokbTpfiWw>4kOqrH84EwKu4?pAvz&{h1f9 zf2Y*<_A(xl7AmUhDJ!>N=5n(fJ#^norNC{5;DgV@V}QiPhz-p3vH;@!^#R-5>yn9` zaIO@}Zz&w}MfH4uMvqv465gihQ1CD_;lwdGOQF9x7SF3RsBp1+^6fTJ?mI>o&)p{xyE|GTCVQrL>KTA-j(FmC>ks zPa|NnX~YjjuI3A8g0+a~x-!>`S)mn;?9tR+gNyRjpH5v1T4mG~o6hDOZ5`2T@&sxa z*Vd&zZY@Az>Vvtn{I#`Y=d6Zsprat}{+X)t1_2$|=!Li^f4AZ4tJK90l;IMcgIzMV zZ6@nEy>WsY!7*~xMGM2W0woV+JV*^0u`#LELU3>Bzr!ZhDk%EQ(PxTU8)D5JW+#^x zM=9~h)^%aab53X>QU;$8-<9P^Wweu4vgQ}*_k zUbQk@qFf9~Z5yjMTd0d7b;E3S$VEgn$dLyut0Ob4dc5KA4COu;cwGg)6U2YD9$T|yI1Q3 z!|LLc1c1F#uNPQ9t#jIu_mVin@Sg{)B^aMs{H*Osl@o(%wQahR_cvU!=*sNX zBMg4ej0dpvm;$QZjBVhAfPwTd^IO&FqXOGgv)nZMRl?}PGCnU_S>-1ZBE)KZN>*@T zSVexIwwWe4#;5{R%dT%JHr2*HI>4aX@0}Sxv#$>Y9foCPbC2Cgfq3|i@a>}$Yim%d zeCWgff`W{Jochx~;?5-*AxWV3T74hPb&%f|`_SoIRHS(VBTPu}4z6I~4d;P;hoto! zQ79&QYos3V_aM?oZU@F11roZOWv#li>7iIN728cki>qU=d>)@EF%?LTVcwp_r*x>Y z8BI4=qnQ0jwP?j&j(fb-qexg@v{%q^Qc#Ee@ezDt@CN^U^`}Qqi3-g7rnM}}t1Rs! z#oSQLmBURqFfL%bA=f^ey;{np!-_c1(NH{12-|FUJocPzTK`sE-GsotyxdYfz?YKU ztv^p+-xq+^i5`RT<#-;31YEY!O$7??Z7+jRx~S_wX2RRq_ZKWbR|gHojmIK9S;!Qq z+S^LHN)<7p*o?*{447!`^2+G56N?Uv1Jkvt9H_QFcH)HV6TT-_#)+5K!_s@2AOOQd z$+DQ9j*C(4JI0G&Kyb%1*LcNJGHzJmZh1_7~2 zCZP5^fr^T0b~mLw9ZF9CF`$g_>;B-H&^Ol5LDmaQombBfj{|SlPZq@2y(@~8{mRWi zHs(jzCVuz4d#>gO%W3UOKsx$?M9^eCSOk<v5}OuKE7#!o_wL=0eJ+MWUX5!s^g$s^<8Ty@k4Eaz*Pzg)($pHrCK-GR4q^ z*MWDrh>0Bh3r$|>x7^kSxUH92h{kXJChoSh3#4}9S;3+971{p#2@GVZH z=dLI%+EDo*gTsa1G)pmG+600;P;}g7770*?T$KCZx>pWa^C~}Sw|q%58B!opCu2O& z+d(Qv8;AutE^{p=IQV38ch3bQ+12)Vgw946chi0h5;|BJ0*qV5Y_-WPgH_}yS>GL1 zY}KmmyA3{}dUwg|ab8ux^vBfl{e5azT8GZoI=Y7s{+JSN(Ri)Si7&uo)c37Np1vRW z{Z`WWu4JPmQWS*W!_Q2wvi+DvV&N16onq7wpe4!bRn;897(bI$^?~#G2)`&38#BY6NKt+rRWw2w{MxPbY>1BF?2AbLP&5r|KgExjk={6WR4I$ zHeNjvhHMWj2UHL{)XB=vkuKtz;eEIpKkNhu&tc}sn;(?#`U4|hXK2pxzv`$9*6?8U z8Y>-BIF_u`Q-J9~4tmmtzu4bkmD8xW*H8%wvFNXuI9yT2cxb-}jJlPSE8*(bG&Xaf z6U7s7Sy?xC>TB+Hiy!X>=l;@p z>7_qL*?gBTN4B_Eq7(5mHPj9UwAy5ZOpgk=*;-r?KHH8?b z?kgaa8N6Q3ygOH%cNx*69a0a(FQ3(GNsfuauEJcERE(=1oCDR+ERC>hPBVkyt) z_V(*n^sn#60TSpTCUIb(a4W^8tOCkl>0Ng1dt~(8uSDL%gX%^}(vTzb?Mm-M+A+G# z*tr?{xg$x)3TnES=^g3_wlxM^f3@2AF}m6UxN$1+E=K~KOTuJ?EoJX!vQwNXn@iw9 zHN8zavOds3$zKM{9oTZzMUZKV96PQImO&Oqk^8k%7-KC&bCN^(X1Sg zQ>osq!VeV!Lca>(lT<|*qTsme>pz4W}f_2Ymhd5Ws|rdR5x>w&Ou^Vy#r*nnhiuen#_?f}>4{gV={R z7>e(n&*|-GN3Svl3U83$S-?y>g;dKe9N$O{ey8{*?eHZ_JM zN{$p(&~uiRxG%2PX>!|gvN(#5L>|N;v<3F~pyP}q*rZM__l?knbPQDR{$TJkdYPnd*1i1kgHEFqfmX7JUMHHEY?)?Hf=)Erkgu__RnO<i+Myi-*OEoa+T#s#wphvro6tA zVPfdooppT90tOQBw8#pzDR1O-3lhMjpO3dQ^w?kBv|O3iWsL`{CsAwsg>wW_>i;=&1d`wSff0wB@5BLz6{c zXb@Uk9S0qVpZK>dT9DO<^O%DXZMCUfaLY7k zEeWCCat*zwT9hA%={)u&SPjZxEtm0Afr>@L>s1j_LCf2Fu+_XUIXd{@!MH*}+_OB;Qc zW-?GNe8|lBm?oa1rr~GECPQJeeRrk1lDk{1uBhW}LB`#OaEd;Fw^%3=pk%N>b%t|T zyRN=W(KXk61h?g!-G1@@Vb{YaMf%mdKSmN z|Fa-Kg8%PL4Yh#D>dQH~o@-3a34Yx>}96t4SBRy>a=!K%-P z@)`S~A7>~;eBCwTc=Bf8sj6LMxKg9fCmtqT0}hbdMKgCUL!*u&CGKyce+3$>Y2*4X zP)798TN|tx;2z=+zC{AO<_K}>-ArF3Lnzkel06&V*gMHY`fxWUseT|s3|H&HLd^#h zsrj-NdUU8opDW^j1po*EIFGTUqPf}s*h-lOSjRiV&}ub8amo z+H^K5e!`yrZ3jvn@p{p=BTQunbG8*A)?hcP$Qkq18%#YT-9W*xZ^j&9XF_ z8oiAKtUrLv-C7TE-m%M?lMD)DK_TGuHw1JpqCr2}A&B8#&{)HjaAAH^2TZ+H z6cyUWglE9HQDVHM$Ksq+`+G-sLb)Eo<|^XSoDC7w(UiqOHG%|mo2^&f;Bx(1T~~&4 ztk9!k&LP2?2+E*WIcI{cz#1sa+0osPJ?=`^zY+rc3xNleNWkZy^}Do4Bp}E2*iKc^ z1N0Z{{sHSh=qEXBRfgY!Jfzh4Q10QsVE0$FZ~B1S7QP^x4dBk=?dM4y)78IX2l@-v zvph(E&~4AZCiP#F`hW9E6$!Lh&lSGQ{muPSKx$uay|C%~I7=yrSAXN`Wc~)yhGLYY z@YJ_|P*Ir{1pdx@Q-F=o7gLpa@x7$_J9lHtqjMcaOw_jb7zh_m<+NV9fiCwIx{$jX9OV2yHDGo)VClUa84;x0jZ2({bF3CrQjh?G64Xtx zDR_gvjEaD!qIM5iFbE}QFL+l65!)UNiVTYX%i+}*6@Kp@x)tz*V>?O8DdWcU1*K@k z-60iVke$LR)V_rWO$(wvLBJbeFPxEp2(LR{L213I zN&zb_hQhcMj|(=csfdAQ*QL*03E!#gjq7Bk7cb;uYuf9kohOfnbojN$6MhAe)R|Rf zv;sE>+R!1IWC8@F>o0@B?Te+ai}^rIwNU4ho-CJ_WeKf?GFl?4kOed>#E+~HGNo-b&VSi0Wi;gRa$^2e#_wU&S4(#**FRD1#DhF}ny3q)% zp|*?sh*VZ$eG^)_tO0Eu6AZ2jy%>`yRoSroXkMj%NND(v2@Zd6n@;7bS13)+S~Q7F z6a>+|6}_P#d}MNeaj`U>`>nn{Ej~&+3=XzmJ*AsM0uHuW?g$NbwV@5O?I3CYf$iYX zu1BVIEG7E_egjeMyfE?ZDbb?kpH0o$ZA8u2u_Y4;_$Bk1b2!kd4UX9`dS8>LANvB_ zGj>vl>A4foo?qHs9Lm+5it8nR7hUJeapTD`5kI0@AD`Fcgr?0sibc>B=PU^JAOYt% zD8_0pKzZY@xo-_j)_@qWm<{b1&l4i+R;M zzH^#&*F#N+YiF}HPVbGoZ-Y^%u&D+5%hmX&FRte)y_aaOxW1hPn%(jSqCSlR%;okire^TZ1DtuT4q@y2Ha6Adov6hn^;J@9*x4sP?UzUp#4nBU}YIL|+3Rga@ z5*r&;ZZocJ3Sbx%w34uAG7ZLVzr~C6+%8V&_IOR`G?B$M=V~4?ZB+8yt!6Oher5z( z#P}(%+HEbAy1W~u)GPm)=|qwZf3wx9>CFIpYJ~(aQVq{pZXS3?^tuBb5*dR{v`;H# z40^1R_3eTW?`nA*)*VED2BVnL;NC7{LIUuY#vIO_lsdeOp}Pkm5R?NvBLdx_mDSC7 z!bF{=4m6quqmRX+BKI)}tqtN$b}gtxM(A3Ztoe&-7RCpBM-|Eia@H>ZH}uE(QxO73 z6&EsaZ4pP0!i75Uv5h@5cN?UtyN!CITVirZ(^Y=I{YnFe_d!5p*Y;0kp^ zDCVV9c#a)S22)>X(5Y136D<-A7JnySC)05mz7qRvx?9nmd01okp3-p2>r^_)#WTj} zZ7?XJIc#gC&g08MSH7`hYKQGDN7;2u>xaOpMb>!09iN|CX616j^|AXCs@zSgAoBCY z<2#Kim=S^_M%#j1UGC#-VYrmPIwF;gi^=0E!ZbMLNO)g(X;<*;?ml~xx|In2W;lm9 zE35xpf&+pGYLoKOpLYD*k6G}8qY!TsH_3|qP|`?(1)?ng{0R4D{Jn(4$Ja_TYgX^( z{qYAV4riKBTLfUY^K0|hxTCp_i&GgFlpDnB2F9m0Co=52J$-@g<*UN++$3a*(M|>p zX4{+q)39xal0<;B?vWeTse89J+J5IDUQJ_;@L`;#2}dxSImq!&3sz;ZPl7;~67RlS z)LH!)JPKoQ1Vv=yOb0$HT{Yi7f2w-?}9_Y0>MFv|n*lw|LF;aw@RYl6M2gq;3UaDlL*LX$F(}oF=3LOmb)_jVJw5;(ys8+BZq>|(0d?lQ zXe$J_>v&m{CWaMwA7KQbCFOYxj%cnE`Sjk@{cvAiacE9?U!Cf}KVSZ#<5~IpB>BCC zZ*bW)B!Jq!o_?l3>(Wf@;908Vy&BA6XZm6v9g!xF9cP)j+vm>XgoaJ(XC6+<`-IsS zG8xgmqm|v?+6lc_L;^7Oo23`$7gjBrTi-0?MzibN+%xLDjz{}-$D6(fhb=fcG<;@j8NjbzUFj17;qaWIPgy(-&eoJ7ZBMdGL@ zNar0>IZym>eqe?ev`Y~y@ug170HK096Zaw3NWd!-^29!l{aRQ1WX9LBumPrD^-@8REoT7fQ?b;O*q7&*mF7Ktt7M`$v1T-F zmbK!4Z}i!{fgc|4a?GW5@ZUHs*DfOPM0sb7hxVKfwoawSU`F(5@Ox5P)l7vRjyFI0 zS8&6h2J}!Z`Hf02+Ew`{=s+!cNeGws#IjlItFvX*wk2A3#n%-`>F|^1GDD|AP3zhd z7+3M(vOcmm4hE?{%wFZSc9uTgycJ!YtacIuj8NrhhQ5Y{f$)1!roPXgs|p^O7;QxX zLME#v&pVl|!>{SO-%9-Yl{x1`5-v&!U8&j-x4vb% zNa|t%iov-hnR5hcX%Bt{}MP6O{9zS<}ohxkTDgi&B(#=Zt-a`T~4?GXV zW){q^_FlE;K?$BED~>ZfsZ|xVAR}2;zIV42;Op*Bj<$F7jD-resRU(?T0bFO{5h0f zSQI?r+Jfaa2-qYn$zF@@os|)GEAS2I4yfY`k~W>R7=AmX8m=6yMvam1W#fxQ4*5!; zJfsv#)K|7tSh=w<#b2#dz_A>~C2$SkQ<*HjR9ip-RJy(Pi<>e!TJ|ccK*GB=>}Ohq zOgv75cCG(RnRWO_9aa8z1-c7FsgQZmcen`7o4K)_0F>Su(ZzzLt$&k~ zlS5v))egOE5$R1z6CfS4iNVGB#TQv+02|94%ZF_rAAX^}DSp7_()$4;_9mPqk!p)- zJ;z3%SC;R50D;%G-a?S&)Su1&S^tFpCAjwe2fU|^4^GMUas-t3EI#+QPu+zh9_-~1 zA-GI^LA?e9PDQ>o+()fD(z<-5^PdQq-->N(WQRpKUN-ud--|8YsY3!ZN#Zx*0>(e( z?o;Y-*<5jEz$t*1UM!K>R1>Y@VqqtCB(n_Z4^sFhhj!a6--Ga!TeM#Hbfuq`9P~db^cB>mh%y zvMC>?uhR+eu9_{m#YR&fo-==Fjwul z4!Z=>6*F#O`#9GC7Z{xpwN2bxuHs+rBo)E_aPpS)o%JC_9(LnlH}aj^4qpD7e|mchyWVntTryUsK%39I6$1>w*<0BCxVxp`qmdxH8JE8a?-w-nprM>d1M_FkexwmzyfML%z6e z#sdVN>Lz&zODqQ0&)$?N|t=Dgce})D2dxbd$PrM$RgeeQ@6bF;upB^{zjM1tl=Tye(XvS8Y&;dOYu3c8jzqmOHC)I zztYyie&Iz)NfzhL3}-E?;78>lbd(5tw8+Fq@)W}I9xmZTgZ@Ztt(vVe5JK>ZeOCm$XSWQx{|CF_F!#96n?q zyeJ@!zRxmUKJ*^{Ny1~pOp+om5&f@8>M62>%iNM4b)|1nc=HndvltE$6U%RgcRl|E zI)7x52kZ?l?vR{Rj+!-$pL;^f@ySVCsN7+SAmtfT9_J z{0KBQj{AMlEX?UpUUi15??BjB;Chg+9U{=B13HFy&}x%9 zhtNA@4hRLELRlDUq_Qf@3%yxGHTn7CGtvcLa9n!vBAwUG@Cv=}IDsKw*XbKTw+lBU~n|*yBEg)|=Opo@qOp5eY`Q>8u zxSRRsO2i5aW2@!s1&feCX{Um*hkQFGeF96v*?Zf8(aK-+OmX0X+W4SxsU5_10um5i zA~=vhP(@(riStb!38*!wAhflibyw0_6&&-}Dzk2t)@^9WlJE#Tx^jlmnG?YbD)!UQ zvn5~|Z>|<%PyMzf3(u_mw54~fKDhl<-W;GhXNQ{%BMv~kP&Vj6kKsoD0q`lx+9)IF z1u_Y^cz!<;;iI!^%EHIvLo zn4*_1y=Of6R$KATkE!uMyMWSQfhGQLp47i5oM9hPjrm6O%%YDeodag z2{mKloXDJhUP9N`Z^Ez+6)qChB_3fBa!l7pB)B`fZgQ`7uXgv$X#0;Czcl8XVJ*T5i3i>7B7+hN{1yKrXh@{M@-0g1f zRwg4zkpK$NSPa4mbq=wcWyAFv3K{|RCw!Sf7+eW;fZreiXV9R34*C!=f!ZO*`mPEx z@|(3`3UHJyBqw-X1)D$ua6wTWJE+89R9K2$uH-ikJ-5mzFA9)mCEg`^54cVO6^Fw* zwmUYJ;r9@zSXmAYd=74ZlV!05@*p{41j6AK{*ujng@309#WBC>VSz-zG7y0lXBNaR@xS2n$n>KH<7vn)t}u3_!x9@1D0Se~nY z;jq673PjoOWj1hEq<{#nYGWMjQtC1j|10bM2S(K|gI66}{sTA<1QhjlY@X%J0>PWb z5*ny|593e~v|@vCJ#k_D8xyW{%h?Nqm3Y%q%F@b1GBjJOw}9`WyR#O=qLPtP@Aq7? zy5UE6SpJ-G>v3#~JqwEMmPmkrMJikth=16#MP++;M7C)KDuug)9R(?#J7^mzA?Ue{vukrfRrz;VwrO64!p3hMU9!jz!0+gnb|9|ZC zN9m6DOf_`kJ=Fj47$)#9c^}(v@p-T}Cp4vLclr0vw#c`;%;#)Ny?~W@?22?u+@KN^&bet}RYEz?e7op6)Wf5mo2S2)wI?4cQ zw*RBY5HmmRk0Jl)`qx8nQ8V-cjk|c~1J5)7l?U{@qWzisf|2x?Dwyw?oP(rX)>mnj z9`awFYcJxAl?5CtFBKjG-1?-Vc{1mIhoyd}5Mljx(LX?0)M#(=qqzxw$CXV{={@M{ zD2Ac={~YLpD!RmX#@W}2t(xIX{`5afZDUp5`!LCY)5VJPOKhztRFl=8dH+W0-vZr4 z(w6n4L5?HxI5%NPfb72i_}M;OBM6D&)YMMzg%H0@bt~}+H=v1}auu!obVHZHT5S~p|$@&6Y<$WlxQGji0M{f@0uLcMx zylxC%f$6wl<9kJJO^B|yt&6olL#C2&j{jK1fboma&74S~ePic3usKr=xGDP-vu|YF z{>s_ie+`mj?hq|vB0J3(yOAbISCs=Eqk+3miyQ!H6xZ9t%s9^n?mK)Jj)A)JFjxe< z5?IvQAeQ(Ns18-PXoSXHg}|v7^fPv${c{cet=kg4NpZu3DzOBla%XnvDLwP;5$*8~ zEfH#GB;i2PB4}TIiQShjqV}r1rJ(d9=fbm!rEf#36!E2-5@W2^XM=(3{Q=VC6FMp(6ZW@xZcEAU6nno&g9D1VB&(<(2E9P)17lD$c*H`j zUXBw|SWFX6*V^I~GH8bDC;W|6a%^ugo`qJ%D9dWIyI z#FO<;5zlz1koyfKph%0@jYCdfQWoz}?43zsT~3nw<~yV|=S0}1E+EuDvp*b>vEGpO(o%Z-oqwK=w&=|z z@tob{j_enw9S1q7RG4e7Z}Fm5!mU%UUWSB#%|~T)Q#Pu^6@^KTevi2iviqa&pupZ# z^M{kIJex)$F*9>M6&14yl8IKIX?#+6AF*Szct!X>0G18jphwHAYjpmZefPihQ$*+S z>^6TW`NN}hXDRKAI0UqKT6od~wRx)8{TaU}G6YTwUZ3bqlY&Yu&Q?k)NaU(|$CzM? z>p)UiM_08&Hf@7?*{1q6q>`+k`>;# z9Gg>I6~v-}T^Pv&3?^iPMOqUPl;zJI!AaF;RSE4BKbmvG;fm{K)MI85kHLu1WVJS4 zawp3xI;Jk2`;3{1V>?+V5y7+_3C1n6qAZwuauhWW{T}uK;!OtZfUSb;FIi)&7*LL_ zc}lM$jMZp<;@Y}|^k619CzLCq)obU#nK7FYvWnVXzI~LiWpNJrVdRlA-=ve%UGIXR zb@V@p31*Y~noq>l=S`hjKUG#H;akx2=&`$4J&CN-s$04y*f4l`WoL|Da}{-zTICzW z(-=EDjrs{6=qRF@#N}lSKFi@IIW_mA1asf=Dss5U47iHfG5bL+q$nb;Ty!RCyP?s3 zBYR;(bitVOqtIb+p%=XA#?wuan#8qYQf6vBE#iWRMys`P#*<07r*>teNx*Z9g`M|d zj7UtF4`%X*hr-WAKzIkGdJ0^?258J<7Esi*B1i>(f8Wv z5WQ{SR6WLVk<&uuoztJ_>)1jHk8CASx&3pE5(i2o6@2vN3d$d0KHcdnSx}F@6@lg` z1C5!Z@`8pp`$p+#5m-2E%y$ksL^50zFvp#K-F-;8dinm@iixvV(D-03kHqNIprFxCMUmw&>4H6$0GHOZ z?dP{(UE8#Ze|)#Y|0EUSpLv-7!+i{sPc@)&*Q+?OEUue=B*1sdk#0{QB$~TW_|M?? zf6hB{u$anydg3@|(T^K_4_r1v0=lzb)`gulo0Y_OqWjWq7ehwcYn}KTbbZ4_@90$; z2Kc_xB|Y}*d4euPcJ#qXq0hLupx|ehk3+FonZ3=TAEQ-Z85+)mL?nQjSg1XVZ}@!C zHm%nueW1r#$XRH5Z;s$$Ds_EGKu;+4U=E8==J89wp`%gjJzu7ys$OT}?fqGthPJwp z8+od>k-pkKSH7HPs>R}y^+CE43%KHVxvML9e!aD=d5Xcg*L(JeC(b*3MP<{4q*7ag z(ID2mOYTd-p3Z_|mAMmvkL^vwLf{Ffn9=h=BgWSft}R>fIP7Z%)vy;s+pE3iLXFut zaSu)k>_2~ecORG1M)kInF*g33b2y8Z!yN;v-CZoUWG8U-s6}F@q)l9Zs&E``^>yDL++5cdU!HnGdE;{VY(lv@KgH_BOZhEr4Hn}E zJ@Z`+uE6er5*9|-Yu_LlwFjzTpVN@{R*y2I1M{fG{XTEH2*W&73t@9=GcH^;_Aajl z1vZ2~Jy^H5wvRA)>`qG@ffG90nuN;EJ!=mwacVZpu3+@ODNmatYYn}XpO_jJ&K)3{ zs3&WoJ26QYt;{HM5-6P~a=9)qKh}TXMf9Bpe zuRi%1I2u}h|NW0S3tJFZEc&~*(wP)-xy+7;#7$>GgXt-i0H-#oQ)?kbrb8mM6q*xC zF4n|sWgS!fR%VV#1mRxlf{5_q{=>z^C|!}hA>~%TMXHXiT+_|iot5fCfxKpDN4RSs zkhoed$K^^c@M_yT&MfccL(c1WvSS39#F{j(E_Hkwa@M^4U1MBDWVy6!p-;tq{I`BK zy18P?S@&~qy=45lhJ|mC6eK<8R&YR_zLLXSWL?AWqO!;<;?Bp@!^ek8ZGJh2v)MbP zRAA{P5nxkA|IQ>qKemmZMH!M%cp8(S5q)jrvHOBZx{PSq;2Xv z%W`XCtn(|WSdt>SLalVSZRJPwu$EqPJ}6Us&IX<5a1gH{TeL_+wEo?%d*35`h6L!Q8^n2jM~uj5yw~6y#1D}xw_vlCyK)TGgKno3uhgffHl$s> z>8}~1Dr(Uf=Zj4cXXIgnxog+?RaO)`UajzgqNs25+KQ89-o%K-y}xqnYck%w*KaPg z+Kg#kDmTM8YxM;Bpvj|bq{76+aSr`-!C69OVz4q=Oe#Osh%(l)x{u~n9IkN7w2c1c zk#^S{J<-Y?(6A}XdI9Hz?4B*YyHJs#y98@2{l4h3u)*|Zm}lH8&3gp`tpk+q*Kaog zsLDLE)f^ngk-0*U+N0eTLRMG0e3A{F>$4C!eofrlgs7%cp53qk2 zV7^V+)@?VWdw$!#BbTyYP-F>nm9K-$M%d*WWqo&iOCxLTG2C=z%bvcRM?0J3K916W zY*86Ps83P{n^Qb2&{99P(Pgy<_;PDA(6TaVP?{^W*$;sm< zGogs>LmZI)M3zW^67Z=Abvi@CBXcq{yEvK~jmPfZ~rg#IhA^hXxTo^aB*)pVqig*_x94u#)}Y`03E-YPsP1d#)f5JthApjL zbN2tFTQLO6cB=tgr3@-&If+hwc?mC_m+j!?r-@=&utdNLjvgWbfzLBg11O<}*Lm5h zf|w$?>SOu+fWqI9VkE9ZF<2YQm!ClG+`mVn;`|NxEg<;0p&&W%HxA0bNBZ&`FfMfQ zG{4I?@$UD-64 z(){L6GW4OhqDvUb-5ccMID5M)XM5?r$6FPq*NoZ2b@BO?R}KPVzzs zKc%k6o@(PWY9>X>&*!;MCN!6LJhDF$cTcD`>bg+k_mmcn&~1ufh299jEsz+g8#LgnZfuAb2UKdraF zAJY0y$s4-|n-Lj@X-(uM7Pubq$)~V0Qp>oZURoI=qAA{+LwkQx^vhQKh-Z!nzXzD@ zhmi^nYkcpV9yCB>+RK&DcyZ6W#%!oLtUT<#?jf1s@hcq5mFia2wG3^lK%;oG0F8i= zd!DU+?v{-l}_Vuk!CuFpopeB}>W^%Am`H+}Lqz@98b_U5!;Cqa_= zJ}ZK(A8d@(7q8s4H#GgY5{@bv`e)Oa*{7;5TnS?|tNW~(Qd}C`C)r;YLdTQu91dfr z^f|q6XnOeso=h1AxlC%(pY&{sf74Jn)GZ3-6C|W1euSXf$>#bW?7eqT6yLrt3X(*U zo2ul7FY zoqO(kx9YvByZ@M(u9{w}yI1%6uGQ=N38=U-$Bz4Rpqw6qG+)K@@+h2LPoC3QR*1ru zo)y|iY@*O|CmVmxQyxAh!pI9(YMMKAo*!~C$~p?sQ?CJ8^su|QLK{5!$WiHU5R?O< zd5EIi)}OUE&%WpNwSLkloI5?WCP|eW61$#u&g%b6QGX(#&c{GP0ygi?lurHB+Fw^{ z*gd)QvP)G_zGY=8OnWb2|G`qPM0g!CNIq0&rg)mSqTJzq%*86@ST7!C=qIUcFiSlTujd-MzwVU3;%$WYyMm>~_A{y@BF`C&M7myiFcnhCc z>Tz}1T2fZ~^b;%kh@@_eFjA`ksfM6lCaU?$^Q;e391WSET2nW#YR?am1)lJP>2Hht z#TtzjP3OxOx4JNvm_tc5L{TPmbF0|t;@6yw;X{*bEGqvKEep=ewTJOZ8(|9#0d#xg zhewKeMuTr8#|H=+PV2uPtQESNJ(=Co)=LW3N)o9#g`zf37U9~r;m2lp=yi_U5!4xBHtVl)*+Dq%8Nl~qj+ZA(H(}%< z|7R6^u)&8%Ntc80N^sS8e{TnOzx6S<@!_Z0J{3M>rHaWpm7-)!`0jL-jlC)7CQw65fsW+3x4itmn5Z^huPJ(8-)CjNrv;m?#qZK%_o;1pWEz(Iz`gcG*# zs+6{lxB|PT_={dsO1o?SM!fwNb4~jBW%jom&HBAlEe6zr0RRfaO)qTd&fw|2)InD( z)eW*@YRY?GFiK?3Ba5TWd#X)-Ih3T+^%re%tC(&G$eblJP33MTY$*1YjA#kg9e>nh zM`ywy<}f1Vo@a8UKtCGkQ<#XeIrT(r-8h zLhCP#A2FXw>V8c4DsDkKX3UU{2{0mNb)@mH394r6msBB&$*U@pL*c8M=L0z5;uUea zeYy!0zff8Vq^ce&*;l`1*{D_`ux(XIR+zqKb2LlvZo5hNYMUlRK0y{VHO@0pheJ8O z(QdW1j^sW3P*ZNNKqc)4##*fws-gs!=_5Ax7ab6$PL0%678|A&rPdaNi|k%MiPd5o z!FjMZSq9RAGWDdZkQogmUzEq%!xb>7&jIHk%5njzZV^wDCo&(GZ>b3jJR%Xc>+0Z{ zms0+*uW#I#b^E2&r-}dNSgUi&cMIz-C_T-($BupT3cFKwgt}Z_M&!Te<^GSziW$BPnzwrn{J7n#NK zH5^n*ParFjP=ScwW=tAN=|Zqo!YwpA&QuLg_g2^0rTVD{lStj2v=8{ z5VrRN%)I{W70DGBACd%rg+ix0(iUefU5=VLXP(00+QT%-Ps=hEf(s?E+HX_3_I@7Mb;T;7JbD&AsQsCe6`M3Ep-`Eqo@!`wNvY#H3D*PH$0W&F@vt`LknEhmMEy^%;bX>2!0Yal@%>oSsr5_kZjCg~138Rhxxy z!x`zuEiZlbvSL0#e{v=f@Z!^mV>S4KJ_qC6f8LHO#;hh)AX%Hwv+!|ZVg6gB9&2$D zJe@ibY|=+Z9d+(?;?Z{a_!(QXg_nXr5(}_y$pGNAlXRB$Cc!ZS0^lrprP=yJER9TZ zHmUMUasXWTRE$v1Alo$=$M>|4A?Ztj+)fXU3O4C(s44}24^1?M;gl~|Fvc~eV#Hy( zvsSing?o2ve_=kVxZ40E8@1i?QGc{fMUUpwH|wxk7HGx@MloyYnD5GVWWzUp;gOL; zDy7;P5#hEM`)ujjN$HPeBS7Exp{<`A8dZ%q9xJBaorYm1%)*}*Z+!~mn)1?@!U3VV z-QgAFMu;c7v%-v;Uer?&4pYxo)Kz+A{F5tUah7&NO%LmtNvx&;5CBT^f-jnAY({;H zirOZI`Oy+QS>$l330YNS+0giLWj&15ZZo<>C$NCn-p|D0iR$k#I>WpDRGf%OI7>Qb zUTbz%Gzx4i zde?qwe$Cz+8)xl}J2K~cOEAocpXti4XDTb$QnUlMHdvDkeP}Xj7sWv-t~A^CT04nO zky!m^e_>esg)#NC{Q-fWpEdu|%f!;{``?uULkjL=vSFso!c$(n9Bx07(pJIcbolyb z)#!ic0+`U>uGket!KivNGzbSl;f4AtlzVtq+roZdV!gv_Z&k@Pel z{;AbT`Iep6$n^WwZ&WM4f*%5m|J;K=fDze0DK9Q36Q`J^FXiPhhvIMZVpF(A-{L9d zj#L2!{`>ZWRrv>ja2dQr{Yg7_Yamx(`8>cuAv3edjYgD9~5+Zn-0K3 zr?-?=y8{w3#PSmyuR(XT>M(ZnUlGyy_gQ zEi>s7IJ5&mmxcq1gaASns0{t5G62Q;kNX#UF!ckKgD?Kn?sP}11!MlV+E0MmmC*t@ zy|Pz#GeO6{JJBvNxfg;5{v!ho41NYP=7`}S0CT&20{~<@fg}&IRd(a^-D3bd`$sQ3 z=m}s);_()*=6~f}@^++SZ;#mJY-G^sZ4sa&FcgGZp_V_e`*oF` z;3#%cRn++(jvUm(iB#7}#mAwc+_8zKLnu3K@^kM=^w z1`dsvF59j8rz11}hYRNo{WVc@%EvV8dT6fkACCMV@4>&C9C%Lu)#U$r%K!fsSQdf6 z!kv3T4g|R*CB*JqeHo`ZIYw`TeinM*3``U7#1 z%)y=j=`U?rL69$lJ|7hSa<_%PQ;O*mQ(lXNt%ivlFpD4sm{ILo>z|zX`Vt7sV-k5) z(J%P%5W<_OdjPRD=yy2^bk{-u7lvZHt8_wsF_QJ1?-AjAVdEDYa{;S*FUH+W=ju!a z?vt8}%gV$6PTdH74@;zmjA5$I_#(y<4O+=js}4X-OD&v~?o^cfjDuS67GZa=yEg&Z zd`Ennx>Jgw8HxY4t^Bt?^rR^a#WVi3)BhPw5Dq1KZe>1Pt_?f}8G^g6Vmu^+tKZ6q z4Ogl72cOUV%1(W8H>*KvA>;8oD9lbGKzkvgPw0>*Ayn3BaA62h-+a~dw$cwy`qiLM z?_%N*?C!7;biL~h@B;hH$=?iEBF$;E6)Ba)fwXG7`8o)lnnUy##zIffPE!>Jz#|17 z>?d|TLFb6IYo7h6g8)X^DCP>NJ%N4aAvt#ROj;)sgW&sT{trNZi5(bPL%oHp89S^FJc9CZpXddal zn&y_84x7{WjlWxmM^4bW)=B-`@D<&Qfkvcgn3MvHj|eS|t+F(23XHJ5#dH2L3^@d9+%>JB&9F@=; zV!|V;)quPGde)_5if=e0;eSx>_ZO%7pL-b$bYG{|XuJSEt`&ACjf`H}Ihz^$9LI_Toqc2re5g2ZQ zuwJ<6H_7GUdC!*O-sAe9aYl^21e}|p zA~!2V4<{im*8!-dU$n$6vTjY0s#AUH2B-Hy864xGnBrLhCf6K2jy7 zEcPLe9Q2P&;=c1fiWwUll~f&l*$roB&owc{7;M~GR>{=v^wCsOpiS{5sYBo$0nH;Kd#AAIA9{9XOt!8)PaaIJXA;}TSsHe#(C zn1?T@7OL76j63sszEyYGjh(3)Wl6xsVfytuMWdtlMU3;QgY(3c;DB~C274lY)#yZ& z4Ay+7Wn_I-tn34bdYPjPe$4BJIzap7++?65s-p5YX-te=%9bi=9QsUE@Vj7ITA>3 z$j znCrtoBK8dte_@281@p{Nr$7oQc;%Oau~NTR9rMX4TjdL7(mN{Y4}OFTV;#h(_5dW- z#q!kptk9uBH%}o-;zC+oX%0VOuvrz|_4s~2EoHvN2v;)yZU~1n_Jh}XZu)uK3t2yUl_?88OY>7HpDm(#iK*&qH|B*X#L}|#xwpzK%5r0<0oP}(j$m! zN5VS_E$z|NYE}VaoU7lxv<7?xVr6pA_9W=&q;E z`RjdOSwyAuDyX(he{az13|X=LWfK^v=Xz<}j@55oAJHex`V+_lE)gt=@%anG#m*#Y z*&6lZCD|=Wz6hR@&NlcmhX*pN+OU5*??t^^Z(}C?3&UA_)_Csw>9d36SAFj9H=2Y{ zT}$I6j?mkDVG>}qf$)yO+)-bKa`HTFUbsy#$i|aQNO##(wW0FK%Pt3lI#IYe_XER7 z?a#$}Wlyg!PEQvGZ$&BvF%78Iz7Yy|C+nNGvo~fs*0-%Vj&?4V!M1z(7*RL=Alm61B)_2^FDTZLDaWv_=R=62)@)juK`e{aixZ%?b9U-wm?e6rDxy4}BR1pG($wE1yNoD$ zd*=c+S`6hNPrgtlshP7Kw$|3x$#a^G@F#q1$$rBm(fC=_RhWaGs8FgOk}%G|qEr?if_QB;O$| ztV}E6+B~*F_CY}QkUukCcps`pwyk#~JbNc_()uUv3yf{Cw++|6^s}exX^i+?XTl7{ z%JT)kH+ewYP2W-N#r{&LFy@1Z8-*Qmur45(BMc&sDnD?6XCB?zLvD14(Nryu-y9`* z<39SD-Eh8|wUP6X?=WY5ja0P|FtLE5B}J;dCS@1lEvK_rN~X?6BHNqnUXljO8IpA= zKXZwnb8K=I@ef~vg8H6ZGo&X##ARcp2w;5pOJf%fo)scTLr#f$&s84L9F z7ex)hKp1H3gbS~^4jNQ6DAJcI-8FB(jyU_c_^p3@l>5cRISZBk>hG`Fa^s4D->l{n zz7XXicQd{C!``HG8<^z8OrvR#<+7Nab`y+_nYf+8RUdV=n-+w|*hXwBmQ2h>QO!;A zjcTL>DsV?cj{poEc_fkJNsM_ug~WD%te z<`jK!Bfeg(3!ys0(dpe)TakMUj=4|W<6T3|UxJUj>5dw198q@K(cQa34pKxv3{`^| zc%3&geZF$h%KfSeO)LmecD%8-6FXFGn$CPVLdiZ;2qJHtbrPX2Mg8+sD$XlSfZ_o_TXTcw~ukTQ1p7NfeNgLb3Pipx+@92G$~18IZ#1 z5yCmd%0-R{utcS^5;&7g>Q$*%HdM9U^Wx-jDAl=aCG|R3kg`a=h#+!UE*DKgc zs|woxjsP9BY>;5A`YSKov4RDAR{29#d1Ry|h(LYb?))9fDWl08&Xumu4Wmnm!M}qs z_pL4_r_g>I`w7_oZYbUSWhb+zi1N!;rzPmilaVLc74MNQ+%(VhIbVq8j%(rxU)r&4 z)@oHeW@92IsUVgx42yL1-~J8im;K008b}SOlVsDNbc;;a#^4}=;*St{;d+<5mjL+Y z1IU(xcq)UJqRj%sH~udSWz@}2(N?}gz8gs>3Z&Yc(sM^S(~!l<`}iYt6)Ie>@||oj z-;sJLY*|T5?Ix1AIT!2J!7UBx=GB=4^T$IhnL<$JH)j5CCjGT@9rI+PsG)DJrO(?x z*Zpc$aM^#H=7Gaqj&alzxh4DUDZS1mJ|-{BQzTO{>A5gCWe%C&}< zAKU4_l?#xF318^b)4RD|>%c1{Nx3dHO#A}z0LDXgP{jE$Vc-N6omS-8(zmDQ!Ww#G z?aO@F-31=Nd?`yN#mQT^ z0YWLfht;=%H&s~L$PR8t_dlH`;Xks5mr`CcvOOGo<~~6-^h}(;4P!JboOs%YmVv>u zG+d?N`|`A5(*bZn0?8ME-fwN*E#fQgSCk5Qq0YmInaol!E_2-#3iX*@MQ4&s#3 zeU5ysq2$^W#I%zaC zR0gtvnY&i008$UJZ-T#mQJ1Hu|^g0Oc-08}JzKYGPO; zU&Mu(KiqTS0h|Q3A8?G@=iUP-vH#Og=yWuRnmsLo?=b4F3V5ksJ-t#kf4H!;*2sR{ z`RBFYZ(@_tU%`lR?ouAe&{+lnr0M@Hlb(+!U#-OTY0zAKk1=9(1ZAo3XwX!skAgooQj(sq&WKgY?3=#K zq8ex683mzf!9JX#B=olvLOyI3jW$nVe51Xoa2q zrOJYlQ(29Bu~6-K!ZXUG6S1#)YBQc(L&I8Sb78J`(nyLsQiNtFtjtgc=FHN+b;z2S zSUIU=%E|hb!K<n5R3nY3TH`lGCD;4a~KqMH4xle&WkW%ptd?Fm07aUj}w zYgC@*;4xAP-2>jmpXo@sKQeed{yn5o30(Dl?v4{JjJRmbrd#mWF8bwI`G&xuGGA2y zV5>jW8rHIK%|vwKpc#?OF=*O#B)pS;dGC(7scELlKrMQlz_7qxn=R~VsvukBC#=}G zUh9QKR8H?j;A3Et-#c<^0LXyy>Rf8qrN{;F(jJ4D9$Ce!W_+{rvBMA$vLfiB?JCju zbw+t#Q~#}``DW+D;Eh>;m0bP_2P`ezEnh{#kdqf_4#8NQb z!HU8%o_~b6NPuw@9BOW1xl`6tZxMfV&AFYqVP!6&%H%@< zt^K8Fdz_9BM%}qHGve*(>XE{^2Hfq@?K787sxf6v`Z3 zFwaA+l;twgBS7G_xNYma~WkUw_EHt9RHp<#!=x1{@nkMV>8ApC5 zHDwV98IaNB+MiTyalCfTc4Cxb>)=Gn6yETzvQ{P)>>0;5ue^wpIARv)lLu50k2WKj z&xYe|O}dqCuF$O)GKV>a(-ojYp|Y&zy!K8qda%*YWbKEzFJk6GLhuLFN?|1AVDD~U z>&%6ybIAa%ZWb&pca}aHo=Ka?A?e_)4r3HSnPU){lm)QrmjBLu-IaLZg zo=+Xops9R{K8RmXLp8mFX+=wbKB0L$ZOZHouAlT5UW_t6Ous&LN z%iPBtegcj~NlR%Ji!?Kj+|abdj%|^wFXgn~O_)1gIl^wbuh$Mqs?-t5CO*PO{?tJX z=~R;b8GOBiEYRH(q38$UkB}#!gH=za)+f*TK0-lZ4d#p}ca_+J=OcEv@rM;lj`ICS zRPYI>!YsDt1fB&NVX1BN*}Q4rigwS=^eg5j>7L{c|h3A;D zE_htO9<5&p_jJav%|hPgnV^DEbJA2k5JF%r`tv7NsW*T;& zN`N3-6Ol8$`r~uH$F{P@WoP2^v~H*79)~BIe_^;wpS}Uc)Ft@^U7#hRg_ao2Ui{#( zX^DG+bGO5#g?)iXTfs*OzU(r?3&%%WRT}L~EhpE|m!|POkJp^k)Rcr5`(CkcfFflc z?8xGKY7oF6tsZm-2BP`BUaisjX3K9ON9q(7HtD{HC(&3*!40}b+54KP%ks5}s+~KU zkQ=3$&g#vD_k=al$&XL-8Xca5S$d{qJVf`Be#KPJNUL_rsPO2Vn7Q_4wpu|gbX~|W znA5!7_j0w@{ITypQ@ogA62|iCP+W?TE~Uqe5787`fBFNZrQbMrh(#f=q*nW*d9R`# z5s)B^BuWci2Sc!%>QLA7^bTmVg=_tMP?3QBJTzKK55})HM%w80OaX)sL04p6%7(~1 z3jp<+J)d+rtar*f+%PJw9S}ts4w7dQgg^sZ1eG zoK1G!wxrd2z5NCiQGg2l=?koj(V}9MTQmEga(lar+|aO@lDWLHb1N~;UTx1KT0q1G7@7{K~pf`~+$ldjoN&0a&nN6Oq+Yo(t;7`CXiT_4Sm8kpa zIbQ6cCFWs@vgo@J0*cCug_Bp;TFS_TPiXn?Ipp@feT5f^-xn)k8FVHtD5g>0>-`;L z4bnWK%F|EMnoT6zB3iv?v-tJ0R4}oe@S?vG(!A?03`&1Fl!`x&>$}o5C@Os6^4c-~ zNqfE$D&8R0`Ryp~;IHjXHq21Fe9=`3@muc-*=y!JZ5s5<6U4_EqG_ z)2WtUzj0nNF8&JBUQRL-1i@sXCU0uY$riy@@^uZVFKc#1y}g;CM6|SeK1u$cKga)i z#DrfNbMX<;Uk9rSYmG~f;2AsmZY5pxow9oW!+RGp_xqQgFEEZ=O}(Q!9nB+r?Lv9b z4tb9H-f@h5t!uk=nmp(9U~A_(oJN014J!A|L;=jMA0S;ip<@a-Jz$Sl1<5f(A>H}9 zmt+}GqOY_LP2!B#6tlEuGx9~!lSdz=iO2eHi9GuKLFxX!@ihprhg@YwHioSqNBcg_ z0#Qdn!R34&SsAvmrd_n3`-I$Cs+WUPEmS6Gsxli6CAH@QNzB2M^_da?ymG)du=HJU zks(iUbNnA&@TxCeQe(xeiA28Pao_gi*Lu*k95#V&ODDhhT9*=BRftk=cVls^#6LJIR21w;sFobE<67Pvz@{ zCPR|K1KO~XQZD^wI*Qpi*s;$Sj?9Wn`TESmfjf+PN!};n9T71T>ep>v(p6oZbS8y z?BwTXt~Ol$F*IU7iNi5;9rUzhc>@VW0-#j%8e8EDYW7_wdZgrkI{pwrL1{(J%iOh*?#f+fSLV z==9@l!qnne{aJ3_~n^+oi5 zm>F%mGPsuP8GQ-Hu>_k8m=PlTAPZ(B^M}G;Dt@sQ&D*4pSPc^5ac3{?n*|{+M!H)Q zddgzVY4#Gn8`6)6lU+SNg*P?FdxDv)AC*Rc?`fb+Jy{^Q@ZJv2`2!hN6t@E1(>9py zMp|>|TLkoerrz>kT3r|~l_i1DJ8Raj!CcGnj$)o-=2d9^bK7r7-Hpl`7%rkH6&5FQ zY^Y=-l4RO{a-;?MF8@1z-8x#s09MtbL13yyxG%#w^S;4=D-Y`QW%~8RC18D30F0x6 z>`QtyaFd7pS=TFghc&pARNv>%9OOq?cZ6Skz?F`9L%Ii3<#5)(AV-fhYgBCC!x+NzWtWV z@OF3*9@w-Q`LxNd@XN-H0zt+!%j0G8K$;K(SAwLq=c4sC&G=s$70J}-XnP)MuXAmCNVWO>x4&tvi4S1lG$}9hfIbU zm2T9GxQu`(zbke0`WV|de~HibgJL6oP8^7TjFPi-#lV#9`Y9xvY95%g89AO%YCG|a zI`D4a|jYzM$id~Qh%UyU@$GPv?6Tb z+N4RyN=jMn=#Lf(Bcu7uW`FNT38vg1>&_p_PwbUMR5F3Qh2=lwEto#P_SKb}_!#tF z>pT$1@Ks*s3`?NGJ63ONWaj|1N!w}=$3Bd1@TjU40567;11eho58l%2ky$SXO7A@L zjn9-QJ4E+@Q^0-qcUevgO(zOB86)oSsxkpOH92t z78qiHJ2YhydxVs%Oz~}lNV?P0T#90-@%_;yl`i_>qkge|C%0&f?^NI35hx*`iu7{X zZ)~hy>j=i?t$%^F;rwALvn(b1KWXk2=Y65foL{WeKm4vB!Ks&6 z!u#XbAW>|v>tInuVXFqu=!d*9nJLyV%&&Y{?!=msc7-N+F&{&V!FD}R>CuHurBnwi zch|dG!$`uED4KLIcO>*$IqR%6^Bydv#{v~_h|QVQ-GTPJEc zJo(Irvv9pLs-|OxakW6>g28Y>S*%D+oc8>{Uov+$jik__)uy5%He&@uaX(4qs4YoO zH~We*w^2VzV3`Nv#KoRg`4EJ^VDaA0#m}ScvkvaKdY=StVd;)rQ-xD zl)X{N{)tfSM|^RLxstYmYG!wRUgk?f@!f<%i)FBS_W}Tefi@Wnf4= zaXZNZs>uS$rMX7Toc>0jr7aSOz#fjBOZlqxbm>|R_gPQ>!Z_YqS*tLbJ#%I^X%L}X z`9v31^B7VVnPxqB+|Wf^Va!4QexhNf{F&A@QMvxM<-iyCn=A68s$sn0_6T`s2^8oN zjdZcC?P)0vpa2>Fk;n?Ts=2h=-uHOFQ&CFh`k>>7!S|eOqrHh<(FYNM17~zoOd6&u9?#lfOOJj~ zN)hqhk^}1fJ;ff5mK7=R%&0Fj624a?=r*Y|L&1ve6XtEjU?t7%8?DfG1Y}-@@l+o- zMvF$vKB2YJ^Y{7;wS|s zkN-Pb6{gBWwL&it&e3a;%4SU_Hn>&CCndbGqWn5Se^w|9=nnjw6Fd-*fK}0g%`^Tb z+@_KO9Bn7iMNT7)?jfd^PnObc#`wyLYqslG+lv28Q0c#n!GHzle~7^g$?dx9u~F0j zNHBwjL-dKyIe#^Ch0j|JXLnZ{Pom9&IHJC`@}$8C!uaAxp}#^+-(Pb`3Qa7n@Y>&v zvpmD%b>r6Z1(vrAw)9hPUi-fc85cWtF6VozBsIk1X*=LvGi!Z=X}pG*y%PhlTV`2+ zBn1GH9^gtH{V|>+=xw)icXo%!_YDFYY~!JYMdM|6sQ=)u$pDo1u?CnImC7>KZ3_+8808YH)4=Vig z+X!h{!LuNsdTpQ(_i5-IUDg2e83BMYj|xIY%P>(Jce)rKAbc+OloeX&g;=5Nl`8P8+-Elx6^RNH$BmnqG5pv-6 z-r3p>%=}JV@t6C|sjTv4@nsl2P^wexpW8+IpPC`2db@ZlFyUD1_<<-;DDcBSokIRU zwgUCL39ZpvW}rnWKtK^Rpnd%h$5;O6`@-AvuiNmi+wh(|7P5qGz*N& zs&UzWj(r2Sr4Y9_VKYK=?B7X82JT+|iGG)Msy7y6^i|>7D$6#I_ul=iTOI6`ZT+ia zNcIHblrnJIx`qG=uaM#*g$l7(UVNz3*WcM&Ep%h7KgwskfefFnpzPl%a(^e{{*#al zNA7=OR36{{hsZDC`S*n5{uqF>N>cpuHUICX=J~6CHe!LVg2BSS@AvP&uM&*^XvzHf zGcSd89*~{+Z=^1PeVp8>{!Qd_1`y-_n{14N^pk3UvK$`tUn&-M17$}!a*^|)1Hf_f zi;X!jDoy$?FMSO{Jy$fl6^Okdopzu6Fx4%B>Uj}>P`qH(?6n-jCiZ= z7i5F8Z)|L$?)i*=^y9}Ai^s}RfFAC~c2^r8rN0bfb0QL=TDInpuAle{`mUM4?&qM! zkCtE3f;vvw&rf97_RVM){0&#{P1pPRy|YL|zKjEhxtWY6F}r==&{m#Fbkvo6LNnCx z>nw;81@JxDQE!7_&g{K)yL6yI`q%YJfn^O+alAjYfx zY_V$Lq8P|iZa0#eD(=}k3A8l(>zVQfTEgmQH3WSj3bu7&k_LzLUUps>huP=5cTelu zXZ=OXemM*{ugn!7d)*UOrL6g+Tcn+~m~s7;P~Q1yDy9c>HB>bYcEYLSiBqCbo7Tr4 z?@8G-)r=plX!2{+NxarFVTxPlixTm*>pCRp4|*v4!mdq8KQ-MWMa}xdkI2wxzZfwp zi{(*u>z$;$xzDzmL-t>Nth8-tZcL_6dJvm#_{FAQwk`9D)48gmgV~=A`7(WaH&f}$ zf~u<@1q=MZZG`pymi;eER#y7RD?j(>^9g)~TvR$NKDD$DK7@W(;-H_7$8XfjlSZr5 zAMcR&lRj#=bb__f!F1juzf5|MnI7y2!b@Q60~1GGc(tAmvR0sQ{t3kXs(Mz55Oo}4`i~=TJs9aDgNi_%ub+hy5_9q{Pt~@l`<8zZ}(b3 z=uu^C&mnOJFfVkz-fA~D4{kcd%UfI|CCDK%Q>GHT?_F)`N6fjLs><6wp0AJEMZlG3 zD}_d47K=r+HkT(r&d=r%vKqB&(Qm9e1fzG*%1G|64(gc$VMZXWG_LUm%QmfKfowSh zbJT%Jose`w4FMP)lKNw^9FH6 zBIaahQMl;rkrd^3M>>I}#g&EvYh%^snh=#`%1^b~5MPfmYP!5CC+1fu*@0|YLIZr zIH!B$EwwHxtQ?iEiF6)_KAIlpHi`LYp9pQ{X~9U7;%>Dnw(y-o4!q9!X}k~-$i4W| z+6mLWXqhYCh&SJgIN($MdG5qwrOO-NyD)ea0XW&&l8EU{6G~WtvyJo8rDcL0BjNLj z7_2!6aE+IQ$&#Z)ji({1KF*+0Mc?^(PJ(CD`wx4hvVvJ{1zH#``uwk8&eTRI-Gbdl ziLXy>(M`bo`J<;5Ap6zrqJwcQ*WT?PVoCwX@&L%m@$aTF5_Qf#c zz4`K*mP3V^BKB*O`eUN6CNI)Rt43(VF`CSLVfFq*Fe3IBs3rIzxiQ2<^3Zm#uN;<` z>J-c(yLaoj{nNzHAAAAg@gIUH`q4My8X3^&1mMC4S_;Twb$q>~>*N;dbI2X2pci3o zOT= zp~c2qvbJA?x=ZT`Ce6%(B}vvqDSN=`09F(M>gS6*hQ)lEF*`Y;3_p4O60CAEMf?0k zO5#tZ=8IxC$|qxdtSe_#QD*q_QvB|4NgfT{RGTDM)^Med=|2WPx#<6PjrX&y_27%m z##=a#aejdBQhsz?ccJa$#8bO$m9rugj4QSyxiG;4`=&WO0t@ORxn~^Io3E2SOz&X* zsov%FL31l9=_%K?I4iBVG?6wkyrI~9Zx!b)-++M(|GSDsRbJEvzuf3AY$^L95f{1G zWEEOzj+VThvK%GD{cD*d*c$!XDmeNGDb(z|-CC0@`1_XhAV9b3*`I^cA7$K5hmYFt}?58oowgyEDSPt93D`l9N0Ij zeGU@*(T;*6pgo2;N?F=T(0MH`zsU8Z!*?lSOKii1xFp!ySS^*8JxuIV5UYH77zxR! z%QIf@$mP-(YWxa47`~zA0FW6KxNZQB8LwU{_T5pf?BT}FD?R%{s`0EoSsE#l4Mx3sX(gCn7AjHonTXekDTG5w z9vFAQ+bLi5Kd+HP2tXSgX!oZTq*F!R1wCwazZ0H4qL2CQ);w56jS!822qH%M0>!2S zAFUK6l$!6kttq&FxNlOXig$D&LwpE9x7Q5_=fH{FdoBTLQZQPrdb%j0t5v_y<5I=h zzdBsLQL>IKUKhMfPm9*z4gta$)7*wujM)QZT9L!&@Ccwo?P*SH&m;rUU^dLsZ-$(@V39N|sgG*V zTO9u!r|?Cte(PD}Hd|w@MOh*7J=JCDQ4!|zI?l}MbGB5M#BUTSb6*cq1Ph46OU2JC z(|i8HpoQSUH1yY$Ynz*!nmZ;IHRkFYcij`Nejj02#fO$wCHLoe!eqnDq;q=ob-J9W zqtcKS`ci6*kyxagOndh&KY3kL?rp()0!3O7TL8D6ywdZ^>|)hPUENuRTu4C4jZU71 zh^%BI3Q(O;6;z#yTap<&R+qmSsLm@{|3l6HpL{-6j2iu$K&a-w>4i2Bo~3mGw&=8f zH?;fDXSR8Xl1kkozkV}_2!{TGcSv&?hj^y0CyOzUB&CLQ2sT8nub+2qg;-9oBbX!n z{R;Lnd+%6h(+qjN13r^(VUM(!r~5#uB^yr@yS)9I@qsjcPqy^bk!J*wsh~2Q^_H4- zZ1GgwOsXfiWk}P#835Pu=rZA>IuouqSOLo{mO>y&FmS6|)~zp~-*m$l)l8E@9B7IFY2uZKmf0$rRt~HN*0Kl5_{?EGhn>Aq`RepzK;Q5+%L$w+@P365{dgoKpD@7CTh)H!FAouZoVkLXq&~YZ)6z}d5ZP8^^`hF!9scS_UP-;F0{Tk=?K5sh4qo$S6^7j z+krXOVnh16K<9Tisx+ssoVKiHyacc3x`!na`6o71;^V(3g>vr54+`PGeHh~x#I|&$ zzDXLAr8LTv4^oRC?;g;K9VlK>bc}V=XEW@QDd3BwZ~K$cJ3s`+*ku?Pih!=XGeJ6DeBZA@U^?jiLJgXzx6O zn(Ed)9*Q7c5D*AGAVNUO8<4J|6loEZ5Kus+MhrDTXadqps1g;ag0v8jUX z1f&R|cj4qc_ndp*`{9^-yQdG=cW_4`5gq3ElO3ZdGp>zxhm zDa&^{1bU`#{7`Z4SAWeu0u!#RtSk?CG}(gPlD>=4xOPf&K^>m`&Qm{XrMVTLrCnM; zL7UZQ``Ge19nRdW*>-8NHxW{;{wz*>0%-blvDzp0HCvKhbUW{R{Zia(C5t9r0i*P= zgb$b*TNDg+QglnizL>?iCEW%C2PrbnP=Lgi#Vzk*ymXqc_!{gKORoIc?QZs-D7vRdvLW80LF!MSq)|7a4p8XELy=c z2u%)x*X3&0ec`>9Kw334#}YYM?l*Na^3=L$74Phin17UiT{t9{BzZSfhS)*(GsZOE z#-49^I+IPOX>DCIEN498mEzM_A!lBl1d0klz%?sQ2!x;$qJVX2Mu*boz*S4xDUw$ErBPvDEI5%*uH+&=PG!;SfI z5FbfXqfHGrtJ0U?sO2i>r(p?^k{h>{%_0?xU-g*fR)jg`Hw-``ing3s7E8IoH7+jb zt~FShvX618d;V-FuusKsl#<~DUaY}ek`i?St(tR%>ASw(en+99ur6rLCmW~#;N75H z))VeSKzgI@d_@Vz?3$O@rCpl!+oNcKB-Z{Fr4vFxof3rZ>SSg@j%|!<;TxrAnUv6i&IcyQ-9CkW z#C6Gap;u<=C!IXF8>4}Ah5N@#8LmY$%d({>7O#!;1VIc{kJpf*u^F`I6CGEz4ol5kr%ekLFhH3z+*;|4G;WkAS%U8QAt8!bT>YziUZg68M7N zn`!2M$DaMmI>kSR1t)DyoJOV`J{(Z{8+LtSZA=F{v&QNmLMa4aK z4qAKnP89hND0-ybl6MG|=H$zOn2=4X;{(M3Oa$*$G$qk6wX8{wxpx* z5MSwXvZoekF*BoI9W7cjWiCts4+Jfc8BQ2~K4<3(_zDjnwT zdpbmlO)nLn3T^gdu7vJpuvMr#6{s3hxq>)dLMJ`1L+Js$ra6Se0L=>Ql%wO_SUs>i zf`M^SP_frKJ(Z=Wc%{xGHo=G>q49i54IxwNg!q)Dkv|Ek$J`>CY_0T>@b>|pmK}%H~&Pt@K5qx5Si4*MIbn;D{LNw?UflOxH9!Fv9(**)J8k#e5ZDz&-rAz zw`}ei_i>rbCQ`O2d*ojROGgF+7X!X@PLG<82WKCK4Z_!81s?RDJecB5!nI&rw(4_H zQd#eD{Epe3q9YaLgM)sW_0cWeRXPxcci1sHnR-1d7dm4b#bZ*LlXv+Oh-51A4F>NVxM+QoR$L#q9LF>6 zU|EqWay&buC0`)?C7p86Xnzt*s?Cn;j`vG@i!)14d)1%p@X2S!ivCuQl876Pg|jnE zLylOD4R!(4stg6Nqj7J{cP(ET9 zF@MC+?*xdGhv4|vE(g#Jdd>=sWA)8auKOCo>C9_wv$skyIIkos0 zGKJv3Z{v<9Lj08GFVl?`8QWKJMK(Lku&%K>?Oy5tf8}A4>EMT9=J0^$gI`@=Bpy{ACL4B! z!~&3cpy#Q3#mQfQL^FuV=E{f=j~}xB85PCj0IsJyPUiqer30LgSU3vGd`E;wzK{_4 z4T8bid-78Dx0N=nBpJ5#*CDbKE0Ubs)3mO>a2}TGDfXGS@1`QudmmL=v6&sF4-cQs z_zkd9@p`dwy!zG2bJ?lg(B)8-ad*YCyZBJMwLt_6#MpH^iB@-E4&;;zRzJrF2T!B& z&jCdG&5ZpQ$BM&qoL<*O69sD?2nSLz`VRxRz)_N{c=nRfZD1$a)g<-n(&1RD*Qs=hP z9fu@OuMqujLca{JSL~jsXMT3NgY`^yjZrC;)KX^LGVjhoe45S8<0!*V-)At1w|5EQ zYgUs@&~5b?uR5#H9!Lp4CI1SV5Pz%NmkwEvLk@Gcr{l_}W`y7E^XKPmb?T=qtLhO+ z^l0}sb!ID;4{!t5f;L7OB?3ejzf-rcPyk{g#Z2U`9`h5JU=}p}Vb}z~(D*Yd*FuXeUA|%;&}Ys&P`$4pagx>F`neE0^TG?o)C2xcr0h zbHG^a+B{AARCSo$i19I)wkDASmKLQgKFgB0aLM(mLaKY8_j<-Rl&>?}GBoRYP~p9g zdM?IO&I-fu1T)n(UgQ%)=7+pqiX{%CsCpH6*&F>aso~YBr{S26hX(@so}iTW5mjy? zhMxiF%W%P@0eao7x1Yo>`>8yU;NoLfkWKHAhsq74b2Rkh}~O#Nl`f>xLN=D*rK~$Z)|6=){m5eV7Bq3&6-K5K6H&8D@SBH zjNiC#Ib|;5PI!bbGL+N3RnO{XNGlNMQ<{e`cS_(bU5S?}gMxZjYXM!S<<^@w}!g*o9$CV_=A4OxXm5G&A8B0 zatL-H3ND$m(ADsw=9%xt8p-l|;UTCcW$a)-JukOYzMH~g`|Fa#E;D=JoR5sWn>54W z%h}o4euj%bUJd_PcNL_TfF^ncrPqoPrrwC>jk7MLa7Gf(0qo?!O2d{P!Xv=mUy{Pk zjR{`}cqSWz35?tm&4-_cHn2C1r#AF#NxZs~?3zr~_X^oWt9gg&oyvCXqA-x7lW zcOsa{|D{#Aq1`2&K#FZcUcNd0+uT25Of`uaG|jn(ZUcG~ps0;`AhrRS88N)LAV-;| zsNwR5ZhKt;xN&e{H-iC<@`Fz}F(rCejqiIIrB=ZZJQ^EHt5D0a&s%bKX9L48uBPEp z>)J-IruH8`3%PZqq8Ec?ZW31y=UXeR-|Ps=U)`VDTTH3{olEUb+0!K6MT?*aR1I`D zf3wf2{4s|dJfU+(DzBp#EP7g}d2n3HxOWQPd*nQz%G{xIr2t(MR=MU=jSN9kh|*cg zjjKH)wV;DpWr9OG%uj%&Rr*k0_zh3WWOU^ib@)0B$%)jMn%elaJnw}m-r*%9*}1;K zPty9NBj)kVMl@|d0QCn}$UmIp-b;S8xID|c$*|Dn@SR&nUrNRPtQ1ZIe#QRboZ}aBCL*=kAqEe$Mk;2m+NMwAx1Td_TYwYl1yI&~ zaxt4w%j8{4nqRTW9SE|O<_q`7Rk{K=!c|jM-~11(Ehf%5qOC9^lVcL#JEGH3Zvtli zKsI9%{~6o+_jkL$E{?q8I!QtP+lJX_V$?M<*pR`~4WMtcw_+|I+9en}md!EvgwqhX zD{o{kjDWKXvG#+n+W|vR3bZ^}1*%A)cxg`guPG>oDlonK522^1A29iT1g{ z`{5h{P?P;3Ul#d&Y5yX2&7=tf50yf9tWM4Wcv1GpkSl!wq_Ya|lHcb5G8f#7Nrhpl zk-T(gt~zmc4p_UVdFB6~{NMZ: ` + self.runCommand(executable: adbPath, arguments: ["pair", fullPairingAddress, self.password]) { [weak self] pairSuccess, pairOutput in + guard let self = self else { return } + print("[ADBPairingManager] adb pair output: \(pairOutput)") + + if !pairSuccess { + DispatchQueue.main.async { + self.status = "Pairing failed: \(pairOutput)" + } + return + } + + DispatchQueue.main.async { + self.status = "Pairing successful! Connecting..." + } + + // 2. Run `adb connect :` + self.runCommand(executable: adbPath, arguments: ["connect", fullConnectAddress]) { connectSuccess, connectOutput in + DispatchQueue.main.async { + if connectSuccess { + self.status = "Device successfully connected!" + AppState.shared.adbConnected = true + AppState.shared.adbPort = UInt16(debuggingPort) + AppState.shared.adbConnectedIP = ip + AppState.shared.adbConnectionResult = "Connected to \(fullConnectAddress)" + } else { + self.status = "Connection failed: \(connectOutput)" + } + } + } + } + } + } + + private func runCommand(executable: String, arguments: [String], completion: @escaping (Bool, String) -> Void) { + let task = Process() + task.executableURL = URL(fileURLWithPath: executable) + task.arguments = arguments + + let pipe = Pipe() + task.standardOutput = pipe + task.standardError = pipe + + do { + try task.run() + task.waitUntilExit() + + let data = pipe.fileHandleForReading.readDataToEndOfFile() + let output = String(data: data, encoding: .utf8) ?? "" + completion(task.terminationStatus == 0, output) + } catch { + completion(false, error.localizedDescription) + } + } +} diff --git a/airsync-mac/Localization/en.json b/airsync-mac/Localization/en.json index 695ad8f6..15d82700 100644 --- a/airsync-mac/Localization/en.json +++ b/airsync-mac/Localization/en.json @@ -138,5 +138,10 @@ "menubar.call.accepted": "Accepted", "settings.menubar.notifications.calls": "Calls", "settings.menubar.calls.plusFeatureMessage": "Ongoing call pill details are available in AirSync+", - "settings.autoStartAtLogin": "Auto start at login" + "settings.autoStartAtLogin": "Auto start at login", + "settings.newDevice": "New device?", + "settings.pairing.howToPair": "How to pair?", + "settings.pairing.instructions": "In order to pair your device, follow these steps:\n\n1. Go to Android device's Settings -> About phone\n2. Tap on \"Build number\" 7 times to unlcok developer options\n3. Go to Settings -> System -> Developer options\n4. Look for \"USB debugging\" and enable\n5. Look for \"Wireless debugging\", enable and go in\n6. Tap on \"Pair device with QR code\" option and scan the above QR code to authenticate the device. If prompted, Select \"Always allow on this network\" if prompted.\n\nDone! You are ready to connect.", + "settings.pairing.troubleshooting": "Troubleshooting", + "settings.pairing.troubleshooting.text": "If pairing is failing, please check the following:\n\n• Ensure both devices are connected to the exact same Wi-Fi network.\n• Check if you have an active VPN on your Mac or Android device, which can block local network traffic (mDNS).\n• Turn Wireless Debugging off and back on in Android Developer options.\n• Check if your router blocks Multicast or local network discovery (AP Isolation)." } diff --git a/airsync-mac/Screens/Settings/ADBPairingSheetView.swift b/airsync-mac/Screens/Settings/ADBPairingSheetView.swift new file mode 100644 index 00000000..f496d83c --- /dev/null +++ b/airsync-mac/Screens/Settings/ADBPairingSheetView.swift @@ -0,0 +1,188 @@ +// +// ADBPairingSheetView.swift +// airsync-mac +// +// Created by Sameera Sandakelum on 2026-05-27. +// + +import SwiftUI +import QRCode +internal import SwiftImageReadWrite + +struct ADBPairingSheetView: View { + @Environment(\.dismiss) private var dismiss + @StateObject private var pairingManager = ADBPairingManager.shared + + @State private var qrImage: CGImage? + @State private var isHowToPairExpanded = false + @State private var isTroubleshootingExpanded = false + + var body: some View { + ZStack { + VisualEffectBlur(material: .hudWindow, blendingMode: .behindWindow) + + VStack { + ScrollView { + VStack(spacing: 20) { + Spacer() + + Text("Pair New ADB Device") + .font(.title2) + .bold() + + if let qrImage = qrImage { + VStack(spacing: 12) { + Image(decorative: qrImage, scale: 1.0) + .resizable() + .interpolation(.none) + .frame(width: 200, height: 200) + .accessibilityLabel("ADB pairing QR Code") + .shadow(radius: 10) + .padding() + .background(Color.black.opacity(0.6), in: RoundedRectangle(cornerRadius: 30)) + + Text(pairingManager.status) + .font(.body) + .foregroundStyle(isErrorStatus ? Color.red : .secondary) + .multilineTextAlignment(.center) + .padding(.horizontal, 16) + } + } else { + ProgressView("Generating QR Code…") + .frame(width: 200, height: 200) + } + + HStack(spacing: 24) { + // "How to pair?" Button + Button(action: { + isHowToPairExpanded.toggle() + if isHowToPairExpanded { + isTroubleshootingExpanded = false + } + }) { + HStack { + Text(L("settings.pairing.howToPair")) + Image(systemName: isHowToPairExpanded ? "chevron.up" : "chevron.down") + } + } + .buttonStyle(.link) + + // "Troubleshooting" Button + Button(action: { + isTroubleshootingExpanded.toggle() + if isTroubleshootingExpanded { + isHowToPairExpanded = false + } + }) { + HStack { + Text(L("settings.pairing.troubleshooting")) + Image(systemName: isTroubleshootingExpanded ? "chevron.up" : "chevron.down") + } + } + .buttonStyle(.link) + } + + if isHowToPairExpanded { + VStack(alignment: .leading, spacing: 12) { + Text(L("settings.pairing.instructions")) + .font(.body) + .multilineTextAlignment(.leading) + .fixedSize(horizontal: false, vertical: false) + + HStack(spacing: 16) { + Spacer() + Image("adb-pair") + .resizable() + .scaledToFit() + .frame(maxWidth: 240, maxHeight: 180) + .cornerRadius(12) + .shadow(radius: 4) + + Image("adb-pair-prompt") + .resizable() + .scaledToFit() + .frame(maxWidth: 240, maxHeight: 180) + .cornerRadius(12) + .shadow(radius: 4) + Spacer() + } + .padding(.vertical, 8) + } + .padding() + .background(Color.white.opacity(0.05)) + .cornerRadius(12) + .frame(maxWidth: .infinity, alignment: .leading) + } + + if isTroubleshootingExpanded { + VStack(alignment: .leading, spacing: 12) { + Text(L("settings.pairing.troubleshooting.text")) + .font(.body) + .multilineTextAlignment(.leading) + .fixedSize(horizontal: false, vertical: false) + } + .padding() + .background(Color.white.opacity(0.05)) + .cornerRadius(12) + .frame(maxWidth: .infinity, alignment: .leading) + } + + Spacer() + } + .padding() + } + + Divider() + + HStack { + Spacer() + + GlassButtonView( + label: "Close", + systemImage: "xmark.circle", + action: { + pairingManager.stopPairing() + dismiss() + } + ) + .keyboardShortcut(.cancelAction) + } + .padding([.horizontal, .bottom]) + } + } + .frame(width: 600, height: 500) + .clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous)) + .shadow(radius: 20) + .onAppear { + pairingManager.startPairing() + generateQRAsync() + } + .onChange(of: pairingManager.pairingString) { _, _ in + generateQRAsync() + } + .onChange(of: pairingManager.status) { _, newStatus in + if newStatus == "Device successfully connected!" { + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { + pairingManager.stopPairing() + dismiss() + } + } + } + } + + private var isErrorStatus: Bool { + let status = pairingManager.status.lowercased() + return status.contains("failed") || status.contains("error") + } + + private func generateQRAsync() { + guard !pairingManager.pairingString.isEmpty else { return } + Task { + if let cgImage = await QRCodeGenerator.generateQRCode(for: pairingManager.pairingString) { + DispatchQueue.main.async { + self.qrImage = cgImage + } + } + } + } +} diff --git a/airsync-mac/Screens/Settings/SyncSettingsView.swift b/airsync-mac/Screens/Settings/SyncSettingsView.swift index 55d3dd29..306b0dd4 100644 --- a/airsync-mac/Screens/Settings/SyncSettingsView.swift +++ b/airsync-mac/Screens/Settings/SyncSettingsView.swift @@ -15,6 +15,7 @@ struct SyncSettingsView: View { @AppStorage("showInControlCenter") private var showInControlCenter = false @State private var showControlCenterInfo = false + @State private var showPairingSheet = false // State for notification permissions @State private var notificationsGranted = false @@ -24,7 +25,18 @@ struct SyncSettingsView: View { ScrollView { VStack(alignment: .leading, spacing: 20) { // 1. Wireless / Wired ADB - headerSection(title: "Connection & ADB", icon: "bolt.horizontal.circle") + HStack { + headerSection(title: "Connection & ADB", icon: "bolt.horizontal.circle") + Spacer() + GlassButtonView( + label: L("settings.newDevice"), + systemImage: "qrcode", + action: { + showPairingSheet = true + } + ) + .padding(.trailing, 8) + } VStack(spacing: 12) { ZStack { HStack { @@ -261,10 +273,12 @@ struct SyncSettingsView: View { } } .padding() - .glassBoxIfAvailable(radius: 18) .sheet(isPresented: $showRemoteSheet) { RemotePermissionView() } + .sheet(isPresented: $showPairingSheet) { + ADBPairingSheetView() + } } .padding() } From 1f503fd44b07c7d22760bde37f28321a9451ee0c Mon Sep 17 00:00:00 2001 From: sameerasw Date: Wed, 27 May 2026 16:48:24 +0530 Subject: [PATCH 03/11] feat: restrict mirror button visibility to Plus users or unlicensed state --- airsync-mac/Screens/MenubarView/MenubarSegments.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/airsync-mac/Screens/MenubarView/MenubarSegments.swift b/airsync-mac/Screens/MenubarView/MenubarSegments.swift index c1976061..60528b18 100644 --- a/airsync-mac/Screens/MenubarView/MenubarSegments.swift +++ b/airsync-mac/Screens/MenubarView/MenubarSegments.swift @@ -84,7 +84,7 @@ struct TopSegmentView: View { } ) - if appState.adbConnected { + if appState.adbConnected && (appState.isPlus || !appState.licenseCheck) { GlassButtonView( label: "Mirror", systemImage: "apps.iphone", From 65dd379286a2328185ff857fe64426d8eb8d2577 Mon Sep 17 00:00:00 2001 From: sameerasw Date: Thu, 28 May 2026 10:19:41 +0530 Subject: [PATCH 04/11] feat: reconnecting device message --- airsync-mac/Core/AppState.swift | 2 + .../Core/WebSocket/WebSocketServer+Ping.swift | 18 ++- .../Core/WebSocket/WebSocketServer.swift | 15 +++ airsync-mac/Screens/HomeScreen/HomeView.swift | 127 ++++++++++++------ .../Screens/HomeScreen/SidebarView.swift | 2 +- 5 files changed, 121 insertions(+), 43 deletions(-) diff --git a/airsync-mac/Core/AppState.swift b/airsync-mac/Core/AppState.swift index 03c65d68..b785d8e7 100644 --- a/airsync-mac/Core/AppState.swift +++ b/airsync-mac/Core/AppState.swift @@ -282,6 +282,7 @@ class AppState: ObservableObject { } } @Published var shouldRefreshQR: Bool = false + @Published var isConnectionWeak: Bool = false @Published var webSocketStatus: WebSocketStatus = .stopped @Published var selectedTab: TabIdentifier = .qr @Published var selectedSettingsTab: SettingsTab = .myMac @@ -964,6 +965,7 @@ class AppState: ObservableObject { // Then locally reset state self.device = nil + self.isConnectionWeak = false self.activeMacIp = nil self.notifications.removeAll() self.status = nil diff --git a/airsync-mac/Core/WebSocket/WebSocketServer+Ping.swift b/airsync-mac/Core/WebSocket/WebSocketServer+Ping.swift index 7011528d..975b0d22 100644 --- a/airsync-mac/Core/WebSocket/WebSocketServer+Ping.swift +++ b/airsync-mac/Core/WebSocket/WebSocketServer+Ping.swift @@ -55,7 +55,23 @@ extension WebSocketServer { let lastDate = self.lastActivity[sessionId] ?? .distantPast self.lock.unlock() - let isStale = now.timeIntervalSince(lastDate) > timeout + let timeSinceLastActivity = now.timeIntervalSince(lastDate) + let isStale = timeSinceLastActivity > timeout + + let isPrimary = (sessionId == primary) + if isPrimary && !isStale { + let isWeak = timeSinceLastActivity > 15.0 + DispatchQueue.main.async { + if AppState.shared.isConnectionWeak != isWeak { + AppState.shared.isConnectionWeak = isWeak + if isWeak { + print("[websocket] Primary session connection is weak. Time since last activity: \(Int(timeSinceLastActivity))s") + } else { + print("[websocket] Primary session connection recovered.") + } + } + } + } if isStale { let isPrimary = (sessionId == primary) diff --git a/airsync-mac/Core/WebSocket/WebSocketServer.swift b/airsync-mac/Core/WebSocket/WebSocketServer.swift index eff27e75..f2930fe1 100644 --- a/airsync-mac/Core/WebSocket/WebSocketServer.swift +++ b/airsync-mac/Core/WebSocket/WebSocketServer.swift @@ -172,6 +172,11 @@ class WebSocketServer: ObservableObject { self.lock.lock() self.lastActivity[ObjectIdentifier(session)] = Date() self.lock.unlock() + DispatchQueue.main.async { + if AppState.shared.isConnectionWeak { + AppState.shared.isConnectionWeak = false + } + } return } @@ -181,6 +186,11 @@ class WebSocketServer: ObservableObject { self.lock.lock() self.lastActivity[ObjectIdentifier(session)] = Date() self.lock.unlock() + DispatchQueue.main.async { + if AppState.shared.isConnectionWeak { + AppState.shared.isConnectionWeak = false + } + } if message.type == .fileChunk || message.type == .fileChunkAck || message.type == .fileTransferComplete || message.type == .fileTransferInit { self.handleMessage(message, session: session) @@ -196,6 +206,11 @@ class WebSocketServer: ObservableObject { self?.lock.lock() self?.lastActivity[ObjectIdentifier(session)] = Date() self?.lock.unlock() + DispatchQueue.main.async { + if AppState.shared.isConnectionWeak { + AppState.shared.isConnectionWeak = false + } + } }, connected: { [weak self] session in guard let self = self else { return } diff --git a/airsync-mac/Screens/HomeScreen/HomeView.swift b/airsync-mac/Screens/HomeScreen/HomeView.swift index b550cf5f..1483bc61 100644 --- a/airsync-mac/Screens/HomeScreen/HomeView.swift +++ b/airsync-mac/Screens/HomeScreen/HomeView.swift @@ -23,51 +23,63 @@ struct HomeView: View { } var body: some View { - NavigationSplitView(columnVisibility: $columnVisibility) { - ZStack { - if appState.selectedTab == .settings { - SettingsSidebarView() - .transition(.opacity.combined(with: .scale)) - } else if appState.device == nil { - QRScannerSidebarView() - .transition(.opacity.combined(with: .scale)) - } else { - SidebarView() - .transition(.opacity.combined(with: .scale)) + ZStack { + NavigationSplitView(columnVisibility: $columnVisibility) { + ZStack { + if appState.selectedTab == .settings { + SettingsSidebarView() + .transition(.opacity.combined(with: .scale)) + } else if appState.device == nil { + QRScannerSidebarView() + .transition(.opacity.combined(with: .scale)) + } else { + SidebarView() + .transition(.opacity.combined(with: .scale)) + } } + .frame(minWidth: 270) + } detail: { + AppContentView() } - .frame(minWidth: 270) - } detail: { - AppContentView() - } - .navigationTitle("") - .background(.background.opacity(appState.windowOpacity)) - .toolbarBackground( - .clear, - for: .windowToolbar - ) - // Show onboarding sheet when needed - .onAppear { - if needsOnboarding { - showOnboarding = true - appState.isOnboardingActive = true + .navigationTitle("") + .background(.background.opacity(appState.windowOpacity)) + .toolbarBackground( + .clear, + for: .windowToolbar + ) + // Show onboarding sheet when needed + .onAppear { + if needsOnboarding { + showOnboarding = true + appState.isOnboardingActive = true + } + updateSidebarVisibility() } - updateSidebarVisibility() - } - .onChange(of: appState.device) { _, _ in - updateSidebarVisibility() - } - .sheet(isPresented: $showOnboarding) { - OnboardingView() - .frame(minWidth: 640, minHeight: 420) - } - .onChange(of: showOnboarding) { oldValue, newValue in - if !newValue { - appState.isOnboardingActive = false + .onChange(of: appState.device) { _, _ in + updateSidebarVisibility() + } + .sheet(isPresented: $showOnboarding) { + OnboardingView() + .frame(minWidth: 640, minHeight: 420) + } + .onChange(of: showOnboarding) { oldValue, newValue in + if !newValue { + appState.isOnboardingActive = false + } + } + .onChange(of: appState.isOnboardingActive) { oldValue, newValue in + // Force view update to refresh window properties + } + + if appState.isConnectionWeak { + VStack { + Spacer() + ConnectionWeakOverlay(appState: appState) + .padding(.bottom, 20) + } + .transition(.move(edge: .bottom).combined(with: .opacity)) + .ignoresSafeArea(.keyboard, edges: .bottom) } - } - .onChange(of: appState.isOnboardingActive) { oldValue, newValue in - // Force view update to refresh window properties } } @@ -78,6 +90,39 @@ struct HomeView: View { } } +struct ConnectionWeakOverlay: View { + @ObservedObject var appState: AppState + @State private var pulse = false + + var body: some View { + HStack(spacing: 12) { + Image(systemName: "wifi.exclamationmark") + .font(.system(size: 14, weight: .bold)) + .foregroundColor(.accentColor) + + Text("Reconnecting to \(appState.device?.name ?? "device")...") + .font(.subheadline) + .fontWeight(.medium) + + + GlassButtonView( + label: "Disconnect", + systemImage: "iphone.slash", + size: .large, + primary: true, + action: { + withAnimation { + appState.disconnectDevice() + } + } + ) + } + .padding(12) + .glassBoxIfAvailable(radius: 24) + .shadow(color: Color.black.opacity(0.2), radius: 10, x: 0, y: 5) + } +} + #Preview { HomeView() } diff --git a/airsync-mac/Screens/HomeScreen/SidebarView.swift b/airsync-mac/Screens/HomeScreen/SidebarView.swift index 061c8880..e1ffbd61 100644 --- a/airsync-mac/Screens/HomeScreen/SidebarView.swift +++ b/airsync-mac/Screens/HomeScreen/SidebarView.swift @@ -23,7 +23,7 @@ struct SidebarView: View { Text(truncated) .font(.title3) } - .padding(6) + .padding(.bottom, 6) if let deviceVersion = appState.device?.version, appState.device?.ipAddress != "BLE", From 55dd7dd5c8255552cd4f2be93e9cf7cbcdbaa2fa Mon Sep 17 00:00:00 2001 From: sameerasw Date: Thu, 28 May 2026 10:25:47 +0530 Subject: [PATCH 05/11] fix: clear found services when stopping the discovery browser --- airsync-mac/Core/QuickShare/NearbyConnectionManager.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/airsync-mac/Core/QuickShare/NearbyConnectionManager.swift b/airsync-mac/Core/QuickShare/NearbyConnectionManager.swift index c67d47ce..df9e6124 100644 --- a/airsync-mac/Core/QuickShare/NearbyConnectionManager.swift +++ b/airsync-mac/Core/QuickShare/NearbyConnectionManager.swift @@ -333,6 +333,7 @@ public class NearbyConnectionManager : NSObject, NetServiceDelegate, InboundNear if discoveryRefCount==0{ browser?.cancel() browser=nil + foundServices.removeAll() } } From c211e557e4008f45b9ea8e3806e86a9dc5753af7 Mon Sep 17 00:00:00 2001 From: sameerasw Date: Thu, 28 May 2026 10:58:33 +0530 Subject: [PATCH 06/11] feat: implement explicit EOF frame signaling and sequential chunk processing in OutboundNearbyConnection --- .../QuickShare/OutboundNearbyConnection.swift | 58 +++++++++++-------- airsync-mac/Localization/en.json | 1 + 2 files changed, 36 insertions(+), 23 deletions(-) diff --git a/airsync-mac/Core/QuickShare/OutboundNearbyConnection.swift b/airsync-mac/Core/QuickShare/OutboundNearbyConnection.swift index 84d05b4b..4f49f332 100644 --- a/airsync-mac/Core/QuickShare/OutboundNearbyConnection.swift +++ b/airsync-mac/Core/QuickShare/OutboundNearbyConnection.swift @@ -394,6 +394,7 @@ public class OutboundNearbyConnection:NearbyConnection{ transfer.payloadHeader.totalSize=Int64(currentTransfer!.totalBytes) transfer.payloadHeader.isSensitive=false currentTransfer!.currentOffset+=Int64(fileBuffer.count) + let isLastChunk = currentTransfer!.currentOffset == currentTransfer!.totalBytes var wrapper=Location_Nearby_Connections_OfflineFrame() wrapper.version = .v1 @@ -402,7 +403,40 @@ public class OutboundNearbyConnection:NearbyConnection{ wrapper.v1.payloadTransfer=transfer try encryptAndSendOfflineFrame(wrapper, completion: { do{ - try self.sendNextFileChunk() + if isLastChunk { + // Signal end of file (yes, all this for one bit) + var eofTransfer=Location_Nearby_Connections_PayloadTransferFrame() + eofTransfer.packetType = .data + eofTransfer.payloadChunk.offset=self.currentTransfer!.currentOffset + eofTransfer.payloadChunk.flags=1 // <- EOF flag + eofTransfer.payloadHeader.id=self.currentTransfer!.payloadID + eofTransfer.payloadHeader.type = .file + eofTransfer.payloadHeader.totalSize=Int64(self.currentTransfer!.totalBytes) + eofTransfer.payloadHeader.isSensitive=false + + var eofWrapper=Location_Nearby_Connections_OfflineFrame() + eofWrapper.version = .v1 + eofWrapper.v1=Location_Nearby_Connections_V1Frame() + eofWrapper.v1.type = .payloadTransfer + eofWrapper.v1.payloadTransfer=eofTransfer + + #if DEBUG + print("sent data chunk, now sending EOF, current transfer: \(String(describing: self.currentTransfer))") + #endif + try self.encryptAndSendOfflineFrame(eofWrapper, completion: { + do { + #if DEBUG + print("EOF sent successfully, calling sendNextFileChunk for next file or clean disconnect") + #endif + try self.sendNextFileChunk() + } catch { + self.lastError=error + self.protocolError() + } + }) + } else { + try self.sendNextFileChunk() + } }catch{ self.lastError=error self.protocolError() @@ -413,28 +447,6 @@ public class OutboundNearbyConnection:NearbyConnection{ #endif totalBytesSent+=Int64(fileBuffer.count) delegate?.outboundConnection(connection: self, transferProgress: Double(totalBytesSent)/Double(totalBytesToSend)) - - if currentTransfer!.currentOffset==currentTransfer!.totalBytes{ - // Signal end of file (yes, all this for one bit) - var transfer=Location_Nearby_Connections_PayloadTransferFrame() - transfer.packetType = .data - transfer.payloadChunk.offset=currentTransfer!.currentOffset - transfer.payloadChunk.flags=1 // <- this one here - transfer.payloadHeader.id=currentTransfer!.payloadID - transfer.payloadHeader.type = .file - transfer.payloadHeader.totalSize=Int64(currentTransfer!.totalBytes) - transfer.payloadHeader.isSensitive=false - - var wrapper=Location_Nearby_Connections_OfflineFrame() - wrapper.version = .v1 - wrapper.v1=Location_Nearby_Connections_V1Frame() - wrapper.v1.type = .payloadTransfer - wrapper.v1.payloadTransfer=transfer - try encryptAndSendOfflineFrame(wrapper) - #if DEBUG - print("sent EOF, current transfer: \(String(describing: currentTransfer))") - #endif - } } private static func sanitizeFileName(name:String)->String{ diff --git a/airsync-mac/Localization/en.json b/airsync-mac/Localization/en.json index 15d82700..024de49a 100644 --- a/airsync-mac/Localization/en.json +++ b/airsync-mac/Localization/en.json @@ -59,6 +59,7 @@ "quickshare.done": "Done", "quickshare.sending": "Sending...", "quickshare.receiving": "Receiving...", + "quickshare.connecting": "Connecting...", "quickshare.confirm_pin": "Confirm PIN on your device", "quickshare.finished": "Transfer Finished!", "quickshare.failed": "Transfer Failed", From 950eeea6325975fb61f3af662157fbae31a9ca76 Mon Sep 17 00:00:00 2001 From: sameerasw Date: Thu, 28 May 2026 13:51:02 +0530 Subject: [PATCH 07/11] feat: Menubar text marquee --- airsync-mac/Components/Text/MarqueeText.swift | 195 ++++++++++++++++++ airsync-mac/Core/AppState.swift | 10 +- airsync-mac/Core/MenuBarManager.swift | 43 ++-- airsync-mac/Localization/af.json | 2 +- airsync-mac/Localization/ar.json | 2 +- airsync-mac/Localization/ca.json | 2 +- airsync-mac/Localization/cs.json | 2 +- airsync-mac/Localization/da.json | 2 +- airsync-mac/Localization/de.json | 2 +- airsync-mac/Localization/el.json | 2 +- airsync-mac/Localization/en.json | 4 +- airsync-mac/Localization/es.json | 2 +- airsync-mac/Localization/fi.json | 2 +- airsync-mac/Localization/fr.json | 2 +- airsync-mac/Localization/he.json | 2 +- airsync-mac/Localization/hi.json | 2 +- airsync-mac/Localization/hu.json | 2 +- airsync-mac/Localization/it.json | 2 +- airsync-mac/Localization/ja.json | 2 +- airsync-mac/Localization/ko.json | 2 +- airsync-mac/Localization/nl.json | 2 +- airsync-mac/Localization/no.json | 2 +- airsync-mac/Localization/pl.json | 2 +- airsync-mac/Localization/pt.json | 2 +- airsync-mac/Localization/ro.json | 2 +- airsync-mac/Localization/ru.json | 2 +- airsync-mac/Localization/si.json | 2 +- airsync-mac/Localization/sk.json | 2 +- airsync-mac/Localization/sr.json | 2 +- airsync-mac/Localization/sv.json | 2 +- airsync-mac/Localization/tr.json | 2 +- airsync-mac/Localization/uk.json | 2 +- airsync-mac/Localization/vi.json | 2 +- airsync-mac/Localization/zh-Hans.json | 2 +- airsync-mac/Localization/zh-Hant.json | 2 +- .../Settings/MenubarSettingsView.swift | 29 ++- 36 files changed, 292 insertions(+), 51 deletions(-) create mode 100644 airsync-mac/Components/Text/MarqueeText.swift diff --git a/airsync-mac/Components/Text/MarqueeText.swift b/airsync-mac/Components/Text/MarqueeText.swift new file mode 100644 index 00000000..c0229d29 --- /dev/null +++ b/airsync-mac/Components/Text/MarqueeText.swift @@ -0,0 +1,195 @@ +// +// MarqueeText.swift +// AirSync +// +// Created by Sameera Sandakelum on 2026-05-28. +// + +import SwiftUI +import AppKit + +/// Seamlessly looping marquee text backed by Core Animation (zero CPU per frame). +/// Falls back to a static view when the text fits within `containerWidth`. +struct MarqueeText: NSViewRepresentable { + let text: String + var fontSize: CGFloat = 12 + var fontWeight: NSFont.Weight = .regular + var containerWidth: CGFloat + /// Scroll speed in points per second. + var speed: Double = 40 + /// Gap between the end of one copy and the start of the next. + var gap: CGFloat = 44 + + func makeNSView(context: Context) -> MarqueeNSView { + MarqueeNSView() + } + + func updateNSView(_ nsView: MarqueeNSView, context: Context) { + nsView.update( + text: text, + fontSize: fontSize, + fontWeight: fontWeight, + containerWidth: containerWidth, + speed: speed, + gap: gap + ) + } + + func sizeThatFits(_ proposal: ProposedViewSize, nsView: MarqueeNSView, context: Context) -> CGSize? { + CGSize(width: containerWidth, height: nsView.contentHeight) + } +} + +// MARK: - NSView + +final class MarqueeNSView: NSView { + private(set) var contentHeight: CGFloat = 16 + + private let clipLayer = CALayer() + private let contentLayer = CALayer() + private let textLayer1 = CATextLayer() + private let textLayer2 = CATextLayer() + + // Track last values to avoid unnecessary redraws + private var lastText = "" + private var lastFontSize: CGFloat = -1 + private var lastFontWeight: NSFont.Weight = .regular + private var lastContainerWidth: CGFloat = -1 + private var lastSpeed: Double = -1 + private var lastGap: CGFloat = -1 + + override init(frame: NSRect) { + super.init(frame: frame) + buildLayers() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + buildLayers() + } + + private func buildLayers() { + wantsLayer = true + layer?.masksToBounds = true + + clipLayer.masksToBounds = true + layer?.addSublayer(clipLayer) + + contentLayer.masksToBounds = false + clipLayer.addSublayer(contentLayer) + + let scale = NSScreen.main?.backingScaleFactor ?? 2.0 + for tl in [textLayer1, textLayer2] { + tl.contentsScale = scale + tl.truncationMode = .none + tl.isWrapped = false + tl.alignmentMode = .left + contentLayer.addSublayer(tl) + } + } + + func update(text: String, fontSize: CGFloat, fontWeight: NSFont.Weight, + containerWidth: CGFloat, speed: Double, gap: CGFloat) { + let changed = text != lastText + || fontSize != lastFontSize + || fontWeight != lastFontWeight + || containerWidth != lastContainerWidth + || speed != lastSpeed + || gap != lastGap + guard changed else { return } + + lastText = text + lastFontSize = fontSize + lastFontWeight = fontWeight + lastContainerWidth = containerWidth + lastSpeed = speed + lastGap = gap + + refresh() + } + + // Called on system dark/light mode switch + override func viewDidChangeEffectiveAppearance() { + super.viewDidChangeEffectiveAppearance() + applyTextColor() + } + + override var intrinsicContentSize: NSSize { + NSSize(width: lastContainerWidth, height: contentHeight) + } + + // MARK: - Layout & Animation + + private func refresh() { + let nsFont = NSFont.systemFont(ofSize: lastFontSize, weight: lastFontWeight) + let attrs: [NSAttributedString.Key: Any] = [.font: nsFont] + let measured = (lastText as NSString).size(withAttributes: attrs) + let tw = ceil(measured.width) + let th = ceil(measured.height) + contentHeight = th + + let loopWidth = tw + lastGap + let needsScroll = tw > lastContainerWidth + + CATransaction.begin() + CATransaction.setDisableActions(true) + + let newFrame = NSRect(x: 0, y: 0, width: lastContainerWidth, height: th) + if frame != newFrame { + frame = newFrame + invalidateIntrinsicContentSize() + } + + clipLayer.frame = CGRect(x: 0, y: 0, width: lastContainerWidth, height: th) + + for tl in [textLayer1, textLayer2] { + tl.string = lastText + tl.font = nsFont + tl.fontSize = lastFontSize + } + + if needsScroll { + contentLayer.frame = CGRect(x: 0, y: 0, width: loopWidth * 2, height: th) + textLayer1.frame = CGRect(x: 0, y: 0, width: tw, height: th) + textLayer2.frame = CGRect(x: loopWidth, y: 0, width: tw, height: th) + textLayer2.isHidden = false + } else { + contentLayer.frame = CGRect(x: 0, y: 0, width: tw, height: th) + textLayer1.frame = CGRect(x: 0, y: 0, width: tw, height: th) + textLayer2.isHidden = true + } + + CATransaction.commit() + + applyTextColor() + + // Restart scroll animation + contentLayer.removeAnimation(forKey: "marquee") + guard needsScroll else { return } + + // Reset model position so beginTime fill works correctly + contentLayer.setValue(0, forKeyPath: "transform.translation.x") + + let anim = CABasicAnimation(keyPath: "transform.translation.x") + anim.fromValue = 0 + anim.toValue = -loopWidth + anim.duration = CFTimeInterval(loopWidth) / lastSpeed + anim.repeatCount = .infinity + anim.isRemovedOnCompletion = false + anim.fillMode = .backwards + anim.beginTime = CACurrentMediaTime() + 1.0 // 1s initial pause + contentLayer.add(anim, forKey: "marquee") + } + + private func applyTextColor() { + var resolved: CGColor = NSColor.labelColor.cgColor + effectiveAppearance.performAsCurrentDrawingAppearance { + resolved = NSColor.labelColor.cgColor + } + CATransaction.begin() + CATransaction.setDisableActions(true) + textLayer1.foregroundColor = resolved + textLayer2.foregroundColor = resolved + CATransaction.commit() + } +} diff --git a/airsync-mac/Core/AppState.swift b/airsync-mac/Core/AppState.swift index b785d8e7..3d1b398e 100644 --- a/airsync-mac/Core/AppState.swift +++ b/airsync-mac/Core/AppState.swift @@ -47,7 +47,8 @@ class AppState: ObservableObject { self.showMenubarDeviceName = UserDefaults.standard.object(forKey: "showMenubarDeviceName") == nil ? true : UserDefaults.standard.bool(forKey: "showMenubarDeviceName") let savedMaxLength = UserDefaults.standard.integer(forKey: "menubarTextMaxLength") - self.menubarTextMaxLength = savedMaxLength > 0 ? savedMaxLength : 30 + // Values < 50 are from the old char-count era; migrate them to the new point-width default + self.menubarTextMaxLength = (savedMaxLength >= 50) ? savedMaxLength : 150 self.showMenubarIcon = UserDefaults.standard.object(forKey: "showMenubarIcon") == nil ? true : UserDefaults.standard.bool(forKey: "showMenubarIcon") self.menubarBatteryStyle = UserDefaults.standard.string(forKey: "menubarBatteryStyle") ?? "both" @@ -59,6 +60,7 @@ class AppState: ObservableObject { self.showMenubarCallDetails = UserDefaults.standard.bool(forKey: "showMenubarCallDetails") && (!licenseCheck || isPlusLoaded) } self.menubarFontSize = UserDefaults.standard.object(forKey: "menubarFontSize") == nil ? 12.0 : UserDefaults.standard.double(forKey: "menubarFontSize") + self.enableMarquee = UserDefaults.standard.bool(forKey: "enableMarquee") self.menubarUnreadBadgeStyle = UserDefaults.standard.string(forKey: "menubarUnreadBadgeStyle") ?? "badge" self.menubarUnreadBadgeColor = UserDefaults.standard.string(forKey: "menubarUnreadBadgeColor") ?? "accent" self.showMenubarPillStroke = UserDefaults.standard.bool(forKey: "showMenubarPillStroke") @@ -343,6 +345,12 @@ class AppState: ObservableObject { } } + @Published var enableMarquee: Bool { + didSet { + UserDefaults.standard.set(enableMarquee, forKey: "enableMarquee") + } + } + @Published var showMenubarIcon: Bool { didSet { UserDefaults.standard.set(showMenubarIcon, forKey: "showMenubarIcon") diff --git a/airsync-mac/Core/MenuBarManager.swift b/airsync-mac/Core/MenuBarManager.swift index 4658b5a9..293d12bc 100644 --- a/airsync-mac/Core/MenuBarManager.swift +++ b/airsync-mac/Core/MenuBarManager.swift @@ -316,8 +316,14 @@ struct MenubarStatusView: View { // 2. Status Text / Details if appState.showMenubarText { if let dragLabel = appState.temporaryDragLabel { - Text(dragLabel) - .font(.system(size: appState.menubarFontSize, weight: .medium)) + if appState.enableMarquee { + MarqueeText(text: dragLabel, fontSize: appState.menubarFontSize, fontWeight: .medium, containerWidth: CGFloat(appState.menubarTextMaxLength)) + } else { + Text(dragLabel) + .font(.system(size: appState.menubarFontSize, weight: .medium)) + .lineLimit(1) + .frame(maxWidth: CGFloat(appState.menubarTextMaxLength), alignment: .leading) + } } else { HStack(spacing: 5) { // Left part: Device Name or Music Info @@ -354,9 +360,7 @@ struct MenubarStatusView: View { .layoutPriority(1) } } else if showMusic, let music = appState.status?.music { - let title = music.title.isEmpty ? "Unknown Title" : music.title - let artist = music.artist.isEmpty ? "Unknown Artist" : music.artist - let musicText = truncate(text: "\(title) - \(artist)") + let musicText = "\(music.title) — \(music.artist)" HStack(spacing: 3) { if appState.showMenubarAlbumArt, @@ -375,14 +379,27 @@ struct MenubarStatusView: View { .font(.system(size: appState.menubarFontSize)) .foregroundColor(.accentColor) } - Text(musicText) - .font(.system(size: appState.menubarFontSize)) + + if appState.enableMarquee { + MarqueeText(text: musicText, fontSize: appState.menubarFontSize, containerWidth: CGFloat(appState.menubarTextMaxLength)) + } else { + Text(musicText) + .font(.system(size: appState.menubarFontSize)) + .lineLimit(1) + .frame(maxWidth: CGFloat(appState.menubarTextMaxLength), alignment: .leading) + } } } else if appState.showMenubarDeviceName { let deviceName = appState.device?.name ?? (bleManager.isAuthenticated ? bleManager.connectedDeviceName : nil) ?? "" if !deviceName.isEmpty { - Text(truncate(text: deviceName)) - .font(.system(size: appState.menubarFontSize, weight: .medium)) + if appState.enableMarquee { + MarqueeText(text: deviceName, fontSize: appState.menubarFontSize, fontWeight: .medium, containerWidth: CGFloat(appState.menubarTextMaxLength)) + } else { + Text(deviceName) + .font(.system(size: appState.menubarFontSize, weight: .medium)) + .lineLimit(1) + .frame(maxWidth: CGFloat(appState.menubarTextMaxLength), alignment: .leading) + } } } @@ -529,11 +546,5 @@ struct MenubarStatusView: View { } } - private func truncate(text: String) -> String { - let maxLength = appState.menubarTextMaxLength - if text.count > maxLength { - return String(text.prefix(maxLength - 1)) + "…" - } - return text - } + } diff --git a/airsync-mac/Localization/af.json b/airsync-mac/Localization/af.json index 0427f383..90d9b014 100644 --- a/airsync-mac/Localization/af.json +++ b/airsync-mac/Localization/af.json @@ -59,7 +59,7 @@ "quickshare.copy": "Kopieer na knipbord", "settings.menubar.showIcon": "Show Menu Bar Icon", "settings.menubar.showText": "Show Menu Bar Text", - "settings.menubar.maxLength": "Max Length", + "settings.menubar.maxLength": "Text Width", "settings.menubar.showDeviceName": "Show Device Name", "settings.menubar.showBattery": "Show Battery Icon", "settings.menubar.showMusic": "Now playing", diff --git a/airsync-mac/Localization/ar.json b/airsync-mac/Localization/ar.json index b662570f..a14042eb 100644 --- a/airsync-mac/Localization/ar.json +++ b/airsync-mac/Localization/ar.json @@ -59,7 +59,7 @@ "quickshare.copy": "نسخ إلى الحافظة", "settings.menubar.showIcon": "Show Menu Bar Icon", "settings.menubar.showText": "Show Menu Bar Text", - "settings.menubar.maxLength": "Max Length", + "settings.menubar.maxLength": "Text Width", "settings.menubar.showDeviceName": "Show Device Name", "settings.menubar.showBattery": "Show Battery Icon", "settings.menubar.showMusic": "Now playing", diff --git a/airsync-mac/Localization/ca.json b/airsync-mac/Localization/ca.json index 013aa8b1..24c74d57 100644 --- a/airsync-mac/Localization/ca.json +++ b/airsync-mac/Localization/ca.json @@ -59,7 +59,7 @@ "quickshare.copy": "Copia al porta-retalls", "settings.menubar.showIcon": "Show Menu Bar Icon", "settings.menubar.showText": "Show Menu Bar Text", - "settings.menubar.maxLength": "Max Length", + "settings.menubar.maxLength": "Text Width", "settings.menubar.showDeviceName": "Show Device Name", "settings.menubar.showBattery": "Show Battery Icon", "settings.menubar.showMusic": "Now playing", diff --git a/airsync-mac/Localization/cs.json b/airsync-mac/Localization/cs.json index e6ceeb51..62b7bfa6 100644 --- a/airsync-mac/Localization/cs.json +++ b/airsync-mac/Localization/cs.json @@ -59,7 +59,7 @@ "quickshare.copy": "Kopírovat do schránky", "settings.menubar.showIcon": "Show Menu Bar Icon", "settings.menubar.showText": "Show Menu Bar Text", - "settings.menubar.maxLength": "Max Length", + "settings.menubar.maxLength": "Text Width", "settings.menubar.showDeviceName": "Show Device Name", "settings.menubar.showBattery": "Show Battery Icon", "settings.menubar.showMusic": "Now playing", diff --git a/airsync-mac/Localization/da.json b/airsync-mac/Localization/da.json index cb517ffc..4dc44784 100644 --- a/airsync-mac/Localization/da.json +++ b/airsync-mac/Localization/da.json @@ -59,7 +59,7 @@ "quickshare.copy": "Kopier til udklipsholder", "settings.menubar.showIcon": "Show Menu Bar Icon", "settings.menubar.showText": "Show Menu Bar Text", - "settings.menubar.maxLength": "Max Length", + "settings.menubar.maxLength": "Text Width", "settings.menubar.showDeviceName": "Show Device Name", "settings.menubar.showBattery": "Show Battery Icon", "settings.menubar.showMusic": "Now playing", diff --git a/airsync-mac/Localization/de.json b/airsync-mac/Localization/de.json index fcc31905..a318e6b7 100644 --- a/airsync-mac/Localization/de.json +++ b/airsync-mac/Localization/de.json @@ -59,7 +59,7 @@ "quickshare.copy": "In die Zwischenablage kopieren", "settings.menubar.showIcon": "Menüleistensymbol anzeigen", "settings.menubar.showText": "Menüleistentext anzeigen", - "settings.menubar.maxLength": "Maximale Länge", + "settings.menubar.maxLength": "Textbreite", "settings.menubar.showDeviceName": "Gerätenamen anzeigen", "settings.menubar.showBattery": "Batteriesymbol anzeigen", "settings.menubar.showMusic": "Jetzt läuft", diff --git a/airsync-mac/Localization/el.json b/airsync-mac/Localization/el.json index 4dae2391..5cc42b68 100644 --- a/airsync-mac/Localization/el.json +++ b/airsync-mac/Localization/el.json @@ -59,7 +59,7 @@ "quickshare.copy": "Αντιγραφή στο πρόχειρο", "settings.menubar.showIcon": "Show Menu Bar Icon", "settings.menubar.showText": "Show Menu Bar Text", - "settings.menubar.maxLength": "Max Length", + "settings.menubar.maxLength": "Text Width", "settings.menubar.showDeviceName": "Show Device Name", "settings.menubar.showBattery": "Show Battery Icon", "settings.menubar.showMusic": "Now playing", diff --git a/airsync-mac/Localization/en.json b/airsync-mac/Localization/en.json index 024de49a..3f50c70b 100644 --- a/airsync-mac/Localization/en.json +++ b/airsync-mac/Localization/en.json @@ -99,7 +99,9 @@ "quickshare.copy": "Copy to clipboard", "settings.menubar.showIcon": "Show Menu Bar Icon", "settings.menubar.showText": "Show Menu Bar Text", - "settings.menubar.maxLength": "Max Length", + "settings.menubar.enableMarquee": "Marquee Text Effect", + "settings.menubar.enableMarquee.info": "Marquee effect might cause increased battery drain.", + "settings.menubar.maxLength": "Text Width", "settings.menubar.showDeviceName": "Show Device Name", "settings.menubar.showBattery": "Show Battery Icon", "settings.menubar.showMusic": "Now playing", diff --git a/airsync-mac/Localization/es.json b/airsync-mac/Localization/es.json index 5bc56489..87500d47 100644 --- a/airsync-mac/Localization/es.json +++ b/airsync-mac/Localization/es.json @@ -59,7 +59,7 @@ "quickshare.copy": "Copiar al portapapeles", "settings.menubar.showIcon": "Mostrar icono de barra de menú", "settings.menubar.showText": "Mostrar texto de barra de menú", - "settings.menubar.maxLength": "Longitud máxima", + "settings.menubar.maxLength": "Ancho del texto", "settings.menubar.showDeviceName": "Mostrar nombre del dispositivo", "settings.menubar.showBattery": "Mostrar icono de batería", "settings.menubar.showMusic": "En reproducción", diff --git a/airsync-mac/Localization/fi.json b/airsync-mac/Localization/fi.json index 6d665951..bc843fe0 100644 --- a/airsync-mac/Localization/fi.json +++ b/airsync-mac/Localization/fi.json @@ -59,7 +59,7 @@ "quickshare.copy": "Kopioi leikepöydälle", "settings.menubar.showIcon": "Show Menu Bar Icon", "settings.menubar.showText": "Show Menu Bar Text", - "settings.menubar.maxLength": "Max Length", + "settings.menubar.maxLength": "Text Width", "settings.menubar.showDeviceName": "Show Device Name", "settings.menubar.showBattery": "Show Battery Icon", "settings.menubar.showMusic": "Now playing", diff --git a/airsync-mac/Localization/fr.json b/airsync-mac/Localization/fr.json index ab7e2c23..817ea28e 100644 --- a/airsync-mac/Localization/fr.json +++ b/airsync-mac/Localization/fr.json @@ -59,7 +59,7 @@ "quickshare.copy": "Copier dans le presse-papiers", "settings.menubar.showIcon": "Afficher l'icône de la barre de menus", "settings.menubar.showText": "Afficher le texte de la barre de menus", - "settings.menubar.maxLength": "Longueur maximale", + "settings.menubar.maxLength": "Largeur du texte", "settings.menubar.showDeviceName": "Afficher le nom de l'appareil", "settings.menubar.showBattery": "Afficher l'icône de batterie", "settings.menubar.showMusic": "En cours de lecture", diff --git a/airsync-mac/Localization/he.json b/airsync-mac/Localization/he.json index 555e82ea..22c7aed4 100644 --- a/airsync-mac/Localization/he.json +++ b/airsync-mac/Localization/he.json @@ -59,7 +59,7 @@ "quickshare.copy": "העתק ללוח", "settings.menubar.showIcon": "Show Menu Bar Icon", "settings.menubar.showText": "Show Menu Bar Text", - "settings.menubar.maxLength": "Max Length", + "settings.menubar.maxLength": "Text Width", "settings.menubar.showDeviceName": "Show Device Name", "settings.menubar.showBattery": "Show Battery Icon", "settings.menubar.showMusic": "Now playing", diff --git a/airsync-mac/Localization/hi.json b/airsync-mac/Localization/hi.json index ccc9e7f9..7aca14a7 100644 --- a/airsync-mac/Localization/hi.json +++ b/airsync-mac/Localization/hi.json @@ -59,7 +59,7 @@ "quickshare.copy": "क्लिपबोर्ड पर कॉपी करें", "settings.menubar.showIcon": "Show Menu Bar Icon", "settings.menubar.showText": "Show Menu Bar Text", - "settings.menubar.maxLength": "Max Length", + "settings.menubar.maxLength": "Text Width", "settings.menubar.showDeviceName": "Show Device Name", "settings.menubar.showBattery": "Show Battery Icon", "settings.menubar.showMusic": "Now playing", diff --git a/airsync-mac/Localization/hu.json b/airsync-mac/Localization/hu.json index 686e944c..26c43356 100644 --- a/airsync-mac/Localization/hu.json +++ b/airsync-mac/Localization/hu.json @@ -59,7 +59,7 @@ "quickshare.copy": "Másolás a vágólapra", "settings.menubar.showIcon": "Show Menu Bar Icon", "settings.menubar.showText": "Show Menu Bar Text", - "settings.menubar.maxLength": "Max Length", + "settings.menubar.maxLength": "Text Width", "settings.menubar.showDeviceName": "Show Device Name", "settings.menubar.showBattery": "Show Battery Icon", "settings.menubar.showMusic": "Now playing", diff --git a/airsync-mac/Localization/it.json b/airsync-mac/Localization/it.json index 7f7c05d7..e36029b7 100644 --- a/airsync-mac/Localization/it.json +++ b/airsync-mac/Localization/it.json @@ -59,7 +59,7 @@ "quickshare.copy": "Copia negli appunti", "settings.menubar.showIcon": "Show Menu Bar Icon", "settings.menubar.showText": "Show Menu Bar Text", - "settings.menubar.maxLength": "Max Length", + "settings.menubar.maxLength": "Text Width", "settings.menubar.showDeviceName": "Show Device Name", "settings.menubar.showBattery": "Show Battery Icon", "settings.menubar.showMusic": "In riproduzione", diff --git a/airsync-mac/Localization/ja.json b/airsync-mac/Localization/ja.json index cd193f93..c855ab6e 100644 --- a/airsync-mac/Localization/ja.json +++ b/airsync-mac/Localization/ja.json @@ -59,7 +59,7 @@ "quickshare.copy": "クリップボードにコピー", "settings.menubar.showIcon": "Show Menu Bar Icon", "settings.menubar.showText": "Show Menu Bar Text", - "settings.menubar.maxLength": "Max Length", + "settings.menubar.maxLength": "Text Width", "settings.menubar.showDeviceName": "Show Device Name", "settings.menubar.showBattery": "Show Battery Icon", "settings.menubar.showMusic": "再生中", diff --git a/airsync-mac/Localization/ko.json b/airsync-mac/Localization/ko.json index 5cba3bdd..d93f5599 100644 --- a/airsync-mac/Localization/ko.json +++ b/airsync-mac/Localization/ko.json @@ -59,7 +59,7 @@ "quickshare.copy": "클립보드에 복사", "settings.menubar.showIcon": "Show Menu Bar Icon", "settings.menubar.showText": "Show Menu Bar Text", - "settings.menubar.maxLength": "Max Length", + "settings.menubar.maxLength": "Text Width", "settings.menubar.showDeviceName": "Show Device Name", "settings.menubar.showBattery": "Show Battery Icon", "settings.menubar.showMusic": "Now playing", diff --git a/airsync-mac/Localization/nl.json b/airsync-mac/Localization/nl.json index 2fd25c28..b23f27d0 100644 --- a/airsync-mac/Localization/nl.json +++ b/airsync-mac/Localization/nl.json @@ -59,7 +59,7 @@ "quickshare.copy": "Kopieer naar klembord", "settings.menubar.showIcon": "Show Menu Bar Icon", "settings.menubar.showText": "Show Menu Bar Text", - "settings.menubar.maxLength": "Max Length", + "settings.menubar.maxLength": "Text Width", "settings.menubar.showDeviceName": "Show Device Name", "settings.menubar.showBattery": "Show Battery Icon", "settings.menubar.showMusic": "Nu afspelen", diff --git a/airsync-mac/Localization/no.json b/airsync-mac/Localization/no.json index a85a5e1d..8ef0a250 100644 --- a/airsync-mac/Localization/no.json +++ b/airsync-mac/Localization/no.json @@ -59,7 +59,7 @@ "quickshare.copy": "Kopier til utklippstavle", "settings.menubar.showIcon": "Show Menu Bar Icon", "settings.menubar.showText": "Show Menu Bar Text", - "settings.menubar.maxLength": "Max Length", + "settings.menubar.maxLength": "Text Width", "settings.menubar.showDeviceName": "Show Device Name", "settings.menubar.showBattery": "Show Battery Icon", "settings.menubar.showMusic": "Now playing", diff --git a/airsync-mac/Localization/pl.json b/airsync-mac/Localization/pl.json index b3a46474..144f9b09 100644 --- a/airsync-mac/Localization/pl.json +++ b/airsync-mac/Localization/pl.json @@ -59,7 +59,7 @@ "quickshare.copy": "Skopiuj do schowka", "settings.menubar.showIcon": "Show Menu Bar Icon", "settings.menubar.showText": "Show Menu Bar Text", - "settings.menubar.maxLength": "Max Length", + "settings.menubar.maxLength": "Text Width", "settings.menubar.showDeviceName": "Show Device Name", "settings.menubar.showBattery": "Show Battery Icon", "settings.menubar.showMusic": "Now playing", diff --git a/airsync-mac/Localization/pt.json b/airsync-mac/Localization/pt.json index dcc9bc53..462258e9 100644 --- a/airsync-mac/Localization/pt.json +++ b/airsync-mac/Localization/pt.json @@ -59,7 +59,7 @@ "quickshare.copy": "Copiar para a área de transferência", "settings.menubar.showIcon": "Show Menu Bar Icon", "settings.menubar.showText": "Show Menu Bar Text", - "settings.menubar.maxLength": "Max Length", + "settings.menubar.maxLength": "Text Width", "settings.menubar.showDeviceName": "Show Device Name", "settings.menubar.showBattery": "Show Battery Icon", "settings.menubar.showMusic": "A reproduzir", diff --git a/airsync-mac/Localization/ro.json b/airsync-mac/Localization/ro.json index 6577a42c..9a35c2b8 100644 --- a/airsync-mac/Localization/ro.json +++ b/airsync-mac/Localization/ro.json @@ -59,7 +59,7 @@ "quickshare.copy": "Copiați în clipboard", "settings.menubar.showIcon": "Show Menu Bar Icon", "settings.menubar.showText": "Show Menu Bar Text", - "settings.menubar.maxLength": "Max Length", + "settings.menubar.maxLength": "Text Width", "settings.menubar.showDeviceName": "Show Device Name", "settings.menubar.showBattery": "Show Battery Icon", "settings.menubar.showMusic": "Now playing", diff --git a/airsync-mac/Localization/ru.json b/airsync-mac/Localization/ru.json index 171679a7..a9f43fc7 100644 --- a/airsync-mac/Localization/ru.json +++ b/airsync-mac/Localization/ru.json @@ -59,7 +59,7 @@ "quickshare.copy": "Копировать в буфер обмена", "settings.menubar.showIcon": "Show Menu Bar Icon", "settings.menubar.showText": "Show Menu Bar Text", - "settings.menubar.maxLength": "Max Length", + "settings.menubar.maxLength": "Text Width", "settings.menubar.showDeviceName": "Show Device Name", "settings.menubar.showBattery": "Show Battery Icon", "settings.menubar.showMusic": "Сейчас играет", diff --git a/airsync-mac/Localization/si.json b/airsync-mac/Localization/si.json index ec9e8e4d..e3893fa4 100644 --- a/airsync-mac/Localization/si.json +++ b/airsync-mac/Localization/si.json @@ -59,7 +59,7 @@ "quickshare.copy": "ක්ලිප්බෝඩ් එකට කොපි කරන්න", "settings.menubar.showIcon": "Show Menu Bar Icon", "settings.menubar.showText": "Show Menu Bar Text", - "settings.menubar.maxLength": "Max Length", + "settings.menubar.maxLength": "Text Width", "settings.menubar.showDeviceName": "Show Device Name", "settings.menubar.showBattery": "Show Battery Icon", "settings.menubar.showMusic": "Now playing", diff --git a/airsync-mac/Localization/sk.json b/airsync-mac/Localization/sk.json index eb3b13d3..d839067d 100644 --- a/airsync-mac/Localization/sk.json +++ b/airsync-mac/Localization/sk.json @@ -59,7 +59,7 @@ "quickshare.copy": "Kopírovať do schránky", "settings.menubar.showIcon": "Show Menu Bar Icon", "settings.menubar.showText": "Show Menu Bar Text", - "settings.menubar.maxLength": "Max Length", + "settings.menubar.maxLength": "Text Width", "settings.menubar.showDeviceName": "Show Device Name", "settings.menubar.showBattery": "Show Battery Icon", "settings.menubar.showMusic": "Now playing", diff --git a/airsync-mac/Localization/sr.json b/airsync-mac/Localization/sr.json index 704b8162..50fb6b64 100644 --- a/airsync-mac/Localization/sr.json +++ b/airsync-mac/Localization/sr.json @@ -59,7 +59,7 @@ "quickshare.copy": "Копирај у привремену меморију", "settings.menubar.showIcon": "Show Menu Bar Icon", "settings.menubar.showText": "Show Menu Bar Text", - "settings.menubar.maxLength": "Max Length", + "settings.menubar.maxLength": "Text Width", "settings.menubar.showDeviceName": "Show Device Name", "settings.menubar.showBattery": "Show Battery Icon", "settings.menubar.showMusic": "Now playing", diff --git a/airsync-mac/Localization/sv.json b/airsync-mac/Localization/sv.json index 3db5a266..e8b50142 100644 --- a/airsync-mac/Localization/sv.json +++ b/airsync-mac/Localization/sv.json @@ -59,7 +59,7 @@ "quickshare.copy": "Kopiera till urklipp", "settings.menubar.showIcon": "Show Menu Bar Icon", "settings.menubar.showText": "Show Menu Bar Text", - "settings.menubar.maxLength": "Max Length", + "settings.menubar.maxLength": "Text Width", "settings.menubar.showDeviceName": "Show Device Name", "settings.menubar.showBattery": "Show Battery Icon", "settings.menubar.showMusic": "Now playing", diff --git a/airsync-mac/Localization/tr.json b/airsync-mac/Localization/tr.json index 6f6bd202..f0674417 100644 --- a/airsync-mac/Localization/tr.json +++ b/airsync-mac/Localization/tr.json @@ -59,7 +59,7 @@ "quickshare.copy": "Panoya kopyala", "settings.menubar.showIcon": "Show Menu Bar Icon", "settings.menubar.showText": "Show Menu Bar Text", - "settings.menubar.maxLength": "Max Length", + "settings.menubar.maxLength": "Text Width", "settings.menubar.showDeviceName": "Show Device Name", "settings.menubar.showBattery": "Show Battery Icon", "settings.menubar.showMusic": "Now playing", diff --git a/airsync-mac/Localization/uk.json b/airsync-mac/Localization/uk.json index 22e587bd..b96520b3 100644 --- a/airsync-mac/Localization/uk.json +++ b/airsync-mac/Localization/uk.json @@ -59,7 +59,7 @@ "quickshare.copy": "Копіювати в буфер обміну", "settings.menubar.showIcon": "Show Menu Bar Icon", "settings.menubar.showText": "Show Menu Bar Text", - "settings.menubar.maxLength": "Max Length", + "settings.menubar.maxLength": "Text Width", "settings.menubar.showDeviceName": "Show Device Name", "settings.menubar.showBattery": "Show Battery Icon", "settings.menubar.showMusic": "Now playing", diff --git a/airsync-mac/Localization/vi.json b/airsync-mac/Localization/vi.json index 21c21488..71eefa22 100644 --- a/airsync-mac/Localization/vi.json +++ b/airsync-mac/Localization/vi.json @@ -59,7 +59,7 @@ "quickshare.copy": "Sao chép vào khay nhớ tạm", "settings.menubar.showIcon": "Show Menu Bar Icon", "settings.menubar.showText": "Show Menu Bar Text", - "settings.menubar.maxLength": "Max Length", + "settings.menubar.maxLength": "Text Width", "settings.menubar.showDeviceName": "Show Device Name", "settings.menubar.showBattery": "Show Battery Icon", "settings.menubar.showMusic": "Now playing", diff --git a/airsync-mac/Localization/zh-Hans.json b/airsync-mac/Localization/zh-Hans.json index ac2b87bd..71f3dcc5 100644 --- a/airsync-mac/Localization/zh-Hans.json +++ b/airsync-mac/Localization/zh-Hans.json @@ -59,7 +59,7 @@ "quickshare.copy": "复制到剪贴板", "settings.menubar.showIcon": "Show Menu Bar Icon", "settings.menubar.showText": "Show Menu Bar Text", - "settings.menubar.maxLength": "Max Length", + "settings.menubar.maxLength": "Text Width", "settings.menubar.showDeviceName": "Show Device Name", "settings.menubar.showBattery": "Show Battery Icon", "settings.menubar.showMusic": "正在播放", diff --git a/airsync-mac/Localization/zh-Hant.json b/airsync-mac/Localization/zh-Hant.json index 77468415..4f4c7b0c 100644 --- a/airsync-mac/Localization/zh-Hant.json +++ b/airsync-mac/Localization/zh-Hant.json @@ -59,7 +59,7 @@ "quickshare.copy": "複製到剪貼簿", "settings.menubar.showIcon": "Show Menu Bar Icon", "settings.menubar.showText": "Show Menu Bar Text", - "settings.menubar.maxLength": "Max Length", + "settings.menubar.maxLength": "Text Width", "settings.menubar.showDeviceName": "Show Device Name", "settings.menubar.showBattery": "Show Battery Icon", "settings.menubar.showMusic": "正在播放", diff --git a/airsync-mac/Screens/Settings/MenubarSettingsView.swift b/airsync-mac/Screens/Settings/MenubarSettingsView.swift index 6e8e3188..b46fa5fe 100644 --- a/airsync-mac/Screens/Settings/MenubarSettingsView.swift +++ b/airsync-mac/Screens/Settings/MenubarSettingsView.swift @@ -4,6 +4,7 @@ struct MenubarSettingsView: View { @ObservedObject var appState = AppState.shared @State private var showingPlusPopover = false @State private var plusPopoverMessage = "" + @State private var showMarqueeInfo = false var body: some View { ScrollView { @@ -51,11 +52,34 @@ struct MenubarSettingsView: View { get: { Double(appState.menubarTextMaxLength) }, set: { appState.menubarTextMaxLength = Int($0) } ), - in: 10...80, - step: 5 + in: 50...300, + step: 10 ) .frame(width: 150) .controlSize(.small) + + Text("\(appState.menubarTextMaxLength)pt") + .font(.system(size: 11, design: .monospaced)) + .foregroundColor(.secondary) + .frame(width: 36, alignment: .trailing) + } + + HStack { + Label(L("settings.menubar.enableMarquee"), systemImage: "play.right.to.left") + Button(action: { showMarqueeInfo = true }) { + Image(systemName: "info.circle") + .foregroundStyle(.secondary) + } + .buttonStyle(.plain) + .alert(L("settings.menubar.enableMarquee"), isPresented: $showMarqueeInfo) { + Button("OK", role: .cancel) {} + } message: { + Text(L("settings.menubar.enableMarquee.info")) + } + + Spacer() + Toggle("", isOn: $appState.enableMarquee) + .toggleStyle(.switch) } HStack { @@ -215,6 +239,7 @@ struct MenubarSettingsView: View { } .padding() .animation(.spring(), value: appState.showMenubarText) + .animation(.spring(), value: appState.enableMarquee) .animation(.spring(), value: appState.showMenubarIcon) .animation(.spring(), value: appState.menubarBatteryStyle) .animation(.spring(), value: appState.showMenubarMusicIcon) From 3ffebba6c1f888b1e7db45f804d6fdf315eb0a53 Mon Sep 17 00:00:00 2001 From: sameerasw Date: Thu, 28 May 2026 13:59:32 +0530 Subject: [PATCH 08/11] fix: Preventing sleep --- .../Core/Discovery/UDPDiscoveryManager.swift | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/airsync-mac/Core/Discovery/UDPDiscoveryManager.swift b/airsync-mac/Core/Discovery/UDPDiscoveryManager.swift index a2add757..63c48174 100644 --- a/airsync-mac/Core/Discovery/UDPDiscoveryManager.swift +++ b/airsync-mac/Core/Discovery/UDPDiscoveryManager.swift @@ -71,7 +71,13 @@ class UDPDiscoveryManager: ObservableObject { // MARK: - Smart Triggers private func startMonitoring() { - // 1. System Wake + // 1. System Sleep / Wake + NSWorkspace.shared.notificationCenter.addObserver( + self, + selector: #selector(handleSystemSleep), + name: NSWorkspace.willSleepNotification, + object: nil + ) NSWorkspace.shared.notificationCenter.addObserver( self, selector: #selector(handleSystemWake), @@ -80,6 +86,10 @@ class UDPDiscoveryManager: ObservableObject { ) // 2. Network Change + setupNetworkPathMonitor() + } + + private func setupNetworkPathMonitor() { networkMonitor = NWPathMonitor() networkMonitor?.pathUpdateHandler = { [weak self] path in guard let self = self else { return } @@ -107,8 +117,22 @@ class UDPDiscoveryManager: ObservableObject { networkMonitor = nil } + @objc private func handleSystemSleep() { + print("[Discovery] System going to sleep – suspending discovery network triggers") + networkChangePendingWork?.cancel() + networkMonitor?.cancel() + networkMonitor = nil + + // Cancel the periodic broadcast timer by temporarily stopping listening + stopListening() + } + @objc private func handleSystemWake() { - print("[Discovery] System wake detected") + print("[Discovery] System wake detected – resuming discovery network triggers") + setupNetworkPathMonitor() + if isListening { + startListening() + } broadcastBurst() } From b70bb956efcb9fea703e47e93595b0178dc8f1aa Mon Sep 17 00:00:00 2001 From: Mudit200408 Date: Thu, 28 May 2026 18:37:21 +0530 Subject: [PATCH 09/11] fix(notification): prevent duplicate notification cards and redundant alert triggers --- airsync-mac/Core/AppState.swift | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/airsync-mac/Core/AppState.swift b/airsync-mac/Core/AppState.swift index 3d1b398e..c4c53ffa 100644 --- a/airsync-mac/Core/AppState.swift +++ b/airsync-mac/Core/AppState.swift @@ -1066,12 +1066,19 @@ class AppState: ObservableObject { func addNotification(_ notif: Notification) { DispatchQueue.main.async { + var contentChanged = true withAnimation { - self.notifications.insert(notif, at: 0) + if let idx = self.notifications.firstIndex(where: { $0.nid == notif.nid }) { + let old = self.notifications[idx] + contentChanged = (old.title != notif.title || old.body != notif.body || old.actions != notif.actions) + self.notifications[idx] = notif + } else { + self.notifications.insert(notif, at: 0) + } } - // Trigger native macOS notification if not silent + // Trigger native macOS notification if not silent and content actually changed/new // Default to alerting if priority is missing (backwards compatibility) - if notif.priority != "silent" { + if notif.priority != "silent" && contentChanged { var appIcon: NSImage? = nil if let iconPath = self.androidApps[notif.package]?.iconUrl { appIcon = NSImage(contentsOfFile: iconPath) From 8da609576bbba210db8f3683d1c43637b5b133a7 Mon Sep 17 00:00:00 2001 From: Mudit200408 Date: Thu, 28 May 2026 18:37:29 +0530 Subject: [PATCH 10/11] fix(notification): prevent false auto-dismissals during Focus mode and DND --- airsync-mac/Core/AppState.swift | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/airsync-mac/Core/AppState.swift b/airsync-mac/Core/AppState.swift index c4c53ffa..16841f0f 100644 --- a/airsync-mac/Core/AppState.swift +++ b/airsync-mac/Core/AppState.swift @@ -1211,17 +1211,22 @@ class AppState: ObservableObject { } func syncWithSystemNotifications() { - UNUserNotificationCenter.current().getDeliveredNotifications { systemNotifs in - let systemNIDs = Set(systemNotifs.map { $0.request.identifier }) + UNUserNotificationCenter.current().getNotificationSettings { settings in + guard settings.authorizationStatus == .authorized else { + return + } + UNUserNotificationCenter.current().getDeliveredNotifications { systemNotifs in + let systemNIDs = Set(systemNotifs.map { $0.request.identifier }) - DispatchQueue.main.async { - // Only sync notifications that were actually posted to system (non-silent) - let currentSystemNIDs = Set(self.notifications.filter { $0.priority != "silent" }.map { $0.nid }) - let removedNIDs = currentSystemNIDs.subtracting(systemNIDs) + DispatchQueue.main.async { + // Only sync notifications that were actually posted to system (non-silent) + let currentSystemNIDs = Set(self.notifications.filter { $0.priority != "silent" }.map { $0.nid }) + let removedNIDs = currentSystemNIDs.subtracting(systemNIDs) - for nid in removedNIDs { - print("[state] (notification) System notification \(nid) was dismissed manually.") - self.removeNotificationById(nid) + for nid in removedNIDs { + print("[state] (notification) System notification \(nid) was dismissed manually.") + self.removeNotificationById(nid) + } } } } From 6901b7be12cfad1bbb93fe4982b0bda0d6006958 Mon Sep 17 00:00:00 2001 From: Mudit200408 Date: Thu, 28 May 2026 18:37:42 +0530 Subject: [PATCH 11/11] fix(notification): make notification hiding local and remove invalid delegate method --- airsync-mac/Core/AppState.swift | 2 +- airsync-mac/Core/Util/NotificationDelegate.swift | 10 ---------- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/airsync-mac/Core/AppState.swift b/airsync-mac/Core/AppState.swift index 16841f0f..7167a03c 100644 --- a/airsync-mac/Core/AppState.swift +++ b/airsync-mac/Core/AppState.swift @@ -951,7 +951,7 @@ class AppState: ObservableObject { withAnimation { self.notifications.removeAll { $0.id == notif.id } } - self.removeNotification(notif) + UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [notif.nid]) } } diff --git a/airsync-mac/Core/Util/NotificationDelegate.swift b/airsync-mac/Core/Util/NotificationDelegate.swift index b2a51ccb..f26ab625 100644 --- a/airsync-mac/Core/Util/NotificationDelegate.swift +++ b/airsync-mac/Core/Util/NotificationDelegate.swift @@ -10,16 +10,6 @@ import UserNotifications @MainActor class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate { - func userNotificationCenter(_ center: UNUserNotificationCenter, - didRemoveDeliveredNotifications identifiers: [String]) { - for nid in identifiers { - print("[notification-delegate] User dismissed system notification with nid: \(nid)") - DispatchQueue.main.async { - AppState.shared.removeNotificationById(nid) - } - } - } - func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {