From 8cab7bac36949b5114cd021165d01aa8bdf12eed Mon Sep 17 00:00:00 2001 From: Lain Iwakura Date: Fri, 8 Aug 2025 15:45:20 +0300 Subject: [PATCH] add pics support --- .../UserInterfaceState.xcuserstate | Bin 0 -> 18941 bytes MobileMkch/APIClient.swift | 97 +++++++++++++----- MobileMkch/AddCommentView.swift | 59 ++++++++++- MobileMkch/CreateThreadView.swift | 59 ++++++++++- MobileMkch/ImagePicker.swift | 57 ++++++++++ MobileMkch/Info.plist | 2 + MobileMkch/MobileMkchApp.swift | 2 +- MobileMkch/ThreadDetailView.swift | 24 +---- 8 files changed, 249 insertions(+), 51 deletions(-) create mode 100644 MobileMkch.xcodeproj/project.xcworkspace/xcuserdata/platon.xcuserdatad/UserInterfaceState.xcuserstate create mode 100644 MobileMkch/ImagePicker.swift diff --git a/MobileMkch.xcodeproj/project.xcworkspace/xcuserdata/platon.xcuserdatad/UserInterfaceState.xcuserstate b/MobileMkch.xcodeproj/project.xcworkspace/xcuserdata/platon.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000000000000000000000000000000000000..525b529f65736ea6378d91e2bb33b4450f68a072 GIT binary patch literal 18941 zcmeHu30PCt*7n|K0F)sK1PGAC3`8J6NJt2vf*^yUAPC~DG(f;8Bs2*sYVAI?Ry*3! z+E%RuYn?jW&aKsI=W4B0J6}87+S=OD4z{-a*ExqUh_~(cJ@1m7pa+fg`*K=DX{5>O^Gp)6!Z7L<*w z$cA#zBvgs2&}3ANYS0u^i|Wu+RF52}5jCM^O@`WQbf=dXf?VTU4w2w zH=_+`Bie*^pq*$Jx(D5h?n4iwN6-^!H+m91i=IQzqnFUj=rB5pj-j{FJLn_yF*<=h zL4RO`F{Ut$8O-7k9E!v6FgzSfaTJ#0SR9AraS~3)88{Q0a27UW3m%Qf-~wET$KhH$ z4>w>ZUWnUpJH8lq;z+y*FU89+hcCld;H&V}__UN6?XU6fL2pw2Y3XV`w=YOUKdiw1QUCI$BSs(5bYE&ZBGTI(jNy zPfw$#(=+Iq^elQdJ%^r4JLvhei@uol(mr|_y_~*;CiGSG)$}#=we$`2E%aUVR{Cyw z8@-+0LGPq@(GSrN(~r=P(vQ)P(@)X+=>7E5^a1)H{V{!l{)GONK1qK@e@>sGzo5UQ zPt)JhKQoBI48=q+kxUdLVPcqgW(1>QQkgWSgehgpm~v)3Gl8jKCNh(lN~VgL!pvZ1 zG7hGZX<`;KZA?3}m|4PfF-w^h%w^0<<|gK5<`!lhb1So+xsB;&0?amMJM$0b0p>C0 zapno;N#-f$8RjMC5c4MU7V|dq4)X!?A@d3I8S@qMHS<056Z0GMJB!(PR>3B)iEI+9 zWL2!19l>f?EvsYoY%*(LQ&}6E!`j(gHjm9`N3o;XVs;W+$yTwG*=n|ioyE>(8`vh+ z$NJe$b`iUnUBWJ_Pj+{67bB3!PoQS@7K}pVN|?+8jP#0Lf4^ zp$Hv7auiD#@;=cwNR#b$o29foXIy4pX=!$*t=wLonU|kioSB`UZMT|pip_cBtPN6m zMQPb|k9VQ3!_nj{^)z+1JKg?Z$B9UfGPa>4q(myDMkA00X_1bwB!q;LFfxn`C*dSw z8%jn7G!mu2XA=KOB1t)UkHo_#D|{Loq=f_fnd)dP@wok7Pg|SQ+mK@^&MUL!XWe|y;>nIhu2u(3U3}H|SnPGe0?2@>-tTI2`CU%m zbXT)~zDQQ#_H}>}@Ob;l`f3VfbEdhRi%Xr2ovp2~iPD%VXS>JSHQwv!nBV_4IEKC+ z=0bO6&92^dhXtoU4b4UwThMeg1I|f?6Mpv9 z3y!Y^E#$}7ismC1T0qof1kn)f7Sx8?ksGxU9Z4nz_=NGv%Ux}Lr&r*yaa~m~Rs;-`6hxxSxSim57NNx`Zu4fMKZ=&3WoS9NWR|E5+#m%r z#Gg*zoXK^iF5p>@+u`jB0M&{alk2Jl<1Vt~pz+jsfC+#w*awGJqPT8!85!A)E+;9x zQsLS7f_099np%UdL{~LP6JdLJA*-2$2aqy!cX(Y8;hR-$zAql2vhSlkf zGhKdXyD(E=KG13Y;wIP+pR*Z+)H%=B#A}Kgj}Lg%{q{iolTEdC0lrs<0=7JuuDuKqyywHYv+479Zg zw9F(xWz#{@T7bEIfWwxeYXN!P1-R=T^cXq-2- zoZo}iqU+I$zfIpDZETq8A73)fVlSOGwZ9LL?QDDh->Ta!Q1~zuJxIo#7)3_1c1Zr<`T~()_A8@~<^`IMhSqsY1EoeQ;2%vT7R+2$7 z1F+V-f>ZLk!qBIJ2I(?ERhjDi-p(d}rxz53?$78dd_J&o!QSpfTX;>i8Fixo>LFRg zOe`du*Hl~4-GZjF65IcPruq|Q5gh9M=mB2A{Db6lqX&uoFBQzApkN;VD+QBVEq!3H zehCSU`tR$P&Afcc7yPmA!$4SgVA${r$X7(<1>`GCmSifJR9Qb`c5{12SFd2;pkPP_ z3kF)LFX|`~-UZiY58B5w&0h2r8AV11(0=qZ8AA$rrb+4aIlVPrXN%M8bvF0eX@P0V zmiV25E~_X73#T1KSpvGW6-@L%nxg{1d;z_2HaKr2LwL+F*w zWUNT>DmpT+kLv}(9`q_t+P4hBNxX*MKpEZWbuz9Sr2s5}!6)&E#8>TZ>*|GBywr;( zsgH&KiQePqejL3^N=aD&V5I?|WI10wwy%1n)3MOmJg&{tv=C-GVn%IW2@fqnRC&DKwqNM=qvO!`UZVVCXfm;kxU|$q>4-?)ue_@A+*%e2kK#8dsjuQDxw5Quf-KCR9BmG*1$qd??Y#s z-_-%gmhY&>>uLu(Ug_xaboyb<&KrlI(|G*TA<_klg%CjVV2ce34orTjv&8`&2(V6s zgPPt~HGBlY$1nHbdcJv8jwP;k*HUM5uUVG`L7^~2e%FuTa2$ao;9W;y2>_(Yb$u&T z>2$XWKy<)60m@6U6p^U{A7UAfo;{#EfMd{#fiGrNcR1Y@Zb0ciDQb{L_pV2!v&BEv zQ|oG-?-zI*E6^+5IDyO}V|$qd>>Kz!ThS@3#v`x>jGYdcVm6sW=8}1&VJl9?20Riz zQ-LurA`U{}^FEP5W#Vc=r{GNVPCZ6^Hvtw3#1CEq%gzrqAZF%p^cA0#sB1 zD|fav^9)@Bo=&avV$kn?U>4pt^GWI8Ftf28W%OVx*y0@0NSa7<56%T%Jwu$poAo>| zM)R{T_VsoxASQr7K?a3FVaWm4ly(7fbTtLvh$}M|z1WS5NDC==3#@%9F2m)pU*quv zT!AOzNnro0@MQQ^gQqk|6PkOyN0D8@BJ$l$7Q8Yoz+5NqaPh|0tS-!@N|GQGx02dHFMAzSgfE^ThG;y7zfO~jkJU0RlzV}=Xs$t`4+oc{8EW1Vs7kdkSaj~^y4z< zlLtehE^~PUxPwP$e@YV^g%|tq0^%VZqbk6U!82$F z&mgnI)#N+BxSu*}$l{1gvl3qp$`LSTa4&=W5x}eX6}eCmcu9CQUW2ck#d|8^N|GgH zaf5Wmzjx54CA^Q^;q@#K2YU^^b~X$($eTU*I=mK?<$zZpe>I?&oXtG%mO*T$ORRYV zzOg}?+UNH5%3?n|43^3OzKNgv-=;r?Z-FIQ2a04pzKxG?H9CAw(BUSp6Lt?`Eg+=J z$+G|Hz1@H})=#bjEfT;RguU$OY;1EiiL#OD^K^Qf_)t@ykn-SN)aOBFFx5C)9llOq zoj_Xxn;|Y|Hx7s~w?QU>SCj$V!*|8UcL0wqd`4Lt|XTS)sr}yJ-o8ni=V># z$SQILS$&?e5p_#Ywg*Ul@YO*<+}He>xQl`vw05uI_ff_U{5pOEzlq<%Z{v6HKk;$= zE`E<(O|Bu=lIzG?ay_|$+(>RBH}Ak7@VF5f@Cp10{uI7H0|H`$&6LDPM%D{k1oo#-HxO3+f z20MtPWGJqOilQWxlx!rM$ele@G!;Y1$z~EDJ$y$ahfE7B$e^C<0KUQMW&H`^R8Lo_ z%g0w75a5=oht%-7nw=0KHZA4@u1BdvB%zWhCB*AYot>`c#x4jjRZFGinHi%u{#KOF zt=c?g6`l1l=@zsIrKUzq1N~HAQ5vAs;G$7lsIMDZf3ot`&N1bac03&S@NdP1Yeti9 zeNpq~fqo`6l1k%OK7~ppcag0D%82%pyTQ}c&ui|5b0QX>&2x@m^_@Oh%qV?PZ*)l- zA6#N9W#?P6Q8{EM*##aeWrTL_Ci?mo;2ht1?SX-GL#VaIC4#CdhZ`itD^9PF8Y{;8 z2PrjR^QMIXs)*N;{9(Y*La<>$9Ni$64#IU*302ys&mJNF7^KgriJ;G@NmM0OMIImz zl863_`iz<(=(C6awmzdAR0{-Zs79)ZYNnjzG4eQhg6!TxwNmr3k6J*UB>Mp=9U#UT zyvM?0vzda^!vC5jvbpFV@{7^uRQ362!EL#iTEutbrF@j1>Lh!}Uh)*#w*^$=83-z* zfD){SZ4!O+Y2b|Vp)5GJa4r`7=~~BP@dyQ^Y;b9B5Vo$#(*ZJ}8B{1#_jti!1l{B6 z0I>nB5;urMXr1>4g5%_64ZOILXYqL7J>xn{DK67XU!mpK2MvI_ggDnjb3n!%iR1VgN`YGVst_3sX>4_ znBMrW=xZuUr$X7F#xwpEah22WXa>1CA_u#v&E#d?2bBTL?Bh?t1r^K!wFSKr zpzfl!l0)QZpYKg=r|#i3^$uz$wTrw$4wF}Vs791R-47BHPmU1%TxhP}lbORm^`Emz z;gRRQDo3l^WjGbgB0GQ)b1heH$nS*LH^>&F{0NOt<(&v@ig@e%GkV_7^QH{ zKsb@5UZ7s;o8y4t3w;2V*Ndlk1D->=W7mLvGgOSu}8jYS!fEVpn@2KyoGt>{%kJL}p&(vA+0r`-8L_Q`b z$S34ea*}*TJ}0Nh7vxKF`d;c+>KyeO^*cI4BV?c{nx+}*cRGX)1?BP;`JThA9Qbwh z9Bw1!9B${Zhr=u26^F0o@O3Z?U=c;Fv#Afn1~D|?Z%`Xx+=bwixIhBd@s)eLeBm7N zJhrZBzLP)tRf&(l&;0F`I;X#%ON6jWa&4y@G?L%t7W{a@hw%d?MA(I>8-E502X&HQ z|Ke#g3?W98i>dQrb(_aK_(N-;OBBob0I0ha2H+OsV-@Ze@R?=Mmk@_s0)a7pYS4^a z9N*wkG)UvmA2u(E!hj{gI8EP3%={rDA2n=q!s%vz@J(ETao{{P3$4LKz%c_2484wn zQ7`Q9z=4=gCNI*C>S_WzoE%bUDDe~i`&zSlPm(zgGMUo9?YDd<60 z8!xh=cQtSXBZU!s*C1W+pBX`MGZ+UUs5YQ~QNrmoIvpe&Jfs!fbOt#?jJ@JQXVF#& zy3t9na7tLTrR3Wl+D7NlcJc#<4ICcZPnu7cL9Ce`MUSS(&;@iMJ(ez_i|KK630+El zBtMa#$yxFX`IVd_zmea`9~?$I>2lG+(G&3}bR}H{B2|r2IE+OD$6H{HV30YMA=pWy1y^U#akbOVQ}USEf9 z1Yd`40%Zt)9Hx1_2Op$BN`efufNlqzOfRI{ILvZ5BtW}qP>G=&4(HYRpExsr!x;*8 z;-}$$K!EO~QaKz3xSn1@cX428Q5~!hPJx0gaP%s$JoIJsO8RmRM{qcj z!%;nyoL)_@Aw3+Ha9GNZ_b+^#|Da!Wl)es0=(R)L9u2xOJic`D#k+Fe&e-adeg|$7 zeItKfqxnhgC#B4K9kn^@lmlE*{~xgGqJ! z>)|ZU(KfXW!ge*mReXYe5{`K3-5l0*(|b6qCHg^iW;#0k5G9A}H&S=7&NKA$f+I;k zM|N^JIY4*OOMr2KeTVRFu*%Eyn-CkK57DpChv`@8BlJ=F82uXkI{gNRM{*eSC&)n> zhm9Og=WqsxGdXPHaMlj`tzLC|T#ORYAMnc798|X1Vw{M>1^@3U+W-B5BKj*{dw$Jf zOHg}$cfm+eaF@^0=Xj3%h5nVpRu0<&^l$X<9M0kJXkHWjTOvga&4hy9V;F{ILO5*a za4v`QK<_cbnBlzM%jfW@zoqy7>tUk58G$;=NRfn*4OM1C!#zhCIgrKRn_; z!6d;VGYP!q7$Zc4=q_^ig$zfqhL%Yd&S;N`agW=R-f|1$}}wj0_xH?qt%L z3?`E?FjIXtOXaG6>VT&516 zdp?Vq%k$N2W)6oZbGSOd%wqsJ)o{3u7pH#<>|vUjR-P48nHI8@ z!&8728JA!>YKQQmOgB3?G&kerMc%`7Fc))pDu)5IP2)x0&vf!4Kb^xf{+7u9O#%L| zhmeml%Rr8o50&FP*8NDmF{Pqx?aXPOyDNWA0jR@V3Z!HxX;}6%U(7A6nDEfGl{fz! z{^sKl3>FPO3YvW8a^`BlJj^QQ3T8F4hPjftio>%x3_CWL!}B-{`*o3kd6?_yZsvLf z2v)#64!}GdoH`5lU!w;@l>ap5`72!WXQ(L10Jk$6foAw)O+Uop`Q6MT9Cks-3w$CzDgdED zIDr{(c76o58nD$uA#Tp>VfMnYz+|{G5@7c9-rQpLfv6%Gqz4oPJ9w6P9$Z`gT{rUr zhuyu=E9L;4FA9{wdoau^%u!x24l}PZM>yQUVGs{*4|9xpjd`8JJ`OMD@G?O%V1Pp+ z9%4|!CxkL`E=0fwGvKVgd2mEzT$fl;sK;N=AJXt(dcK3;)c(o53sYl`bJ*X_yvN~A zVjQqqN12acy*_46@F&gu^#Q$b*EiC34~G|tOZX{s5|)s^bsS(m?YqLse9nBqw|k1i zOS+jaIovh4U1;?i=3C~wnb59J*dq=vg=26t{L8ag%$a7hr7Xv0E6KK$X4~L*gR5h{ z$L;j^8XVqsyG`u-4D$m-pZadriJRTSoMCy1-X_jGGHiQjj!`NZ$a5kL7mvWeJn1eJZ4zJ`e z_=T%Dd;VtkQ4qwS(&?HxL_!_Xr`dKbtS(~es z&#{2;l;}v5f~(&ISH>NFxEBXLQry7T6>!-aE(Ac38m{Ar1$Cv9gyRG^AK@BE8@z=9 zSD(b!1H*hX`zpf`(!g501JuT!*$(L$f_RW<;&;yI+c9Z2#F$IqBv2f4W~nW&cwC;n zIMY&QgFG9%8PZp>b1j+q_Ht`UX-;l&o~@)_*ycIGHY|Cj>{+fFo&_wnj2yxJW=9HF zEQ6bn#+u;RhBdP3Yz7P7%32Oz&*2+5eB%~2i#4+rHk-pYarkBq-@@T_{O-qrtPGC+ zO?CPCHRf;a4=$J{CjMQhsJer{WDhHC9#X3}@W|go>}=<67>X=e=WG=&rqz$@bhS0l zu-Hp-%+_p&^prTSG3b>Zwty{U$8z{q4sYaecVBFr9mkeKG>t7`OW86G18TU9!?*Xa z-j)M6jP z37Ed9*IG^qM$XwOZ2biWPj6zUvopXla(EMm?Qk7;jj~YEjUr<~zaZ+Vf-SioA8hZ1}AQM6= zi-uf~-ua37B5?X#><8`D*a@f4U@#Pfo>|uki5SjiXY;YxxTyGqB!27*2WO#mD+tq_ z2$^0npgGIo?(G|DNMX=L>5~m3Q&Qnw9ozzizFNxL9IZl1O5dp<(9T@J#|_w?6q*a z8;p`TSANljsX&dHvp^T0zq*0hwHJ5`)N|+c@Rpb(mO)PMb&%70BV_fihrHemkk`8z zKLU4*&fv3fYvnim2NgmMgN)rsN(%QzHc|_zCDbxFw!RXwZ#Pg| zAlG&`90MMqPC$0;H*l8rGvwAvMZTtRuNrCU{qFsu4IJJq-~jd_)-ksa1ppLr^uVh| zv;vlpe+pWNW_BTfbx@rxY%5#Fy4VGR0u68&T$bB84A}Ipt!x|iv2NA_H!lIMZ6$!= zz?a_!7YjOgt*ZNT^~ZD9UlPHXfGFk9TjGi8{BU{wFDdy`cY#u06eg~X0NdCu5v)Y( zEw6h3tjwDVS!P`thhJmK8I_#_ERuk9C<7ADN1?I69@8M9X&xjQKS93$A;EX(4JRrX`{6ZRzgIr{~Bn*Eynmi?an zf&D3Dc!(;*5>gs6Bg7f9EabY74Ivvtc7!|_@^HwbA&-ab4tX}@`H&Yw4urfM@=D0J zp=@Y;XhG=2(7Mq2(CMKwLuZFJhqi>y4_y%27U~XN6?$9f1EGgP-wFL^=<(3^LQjT% z7y4^hSXg9OT$m-SFswMNB&ac6W)(*dB_$|Zt4u5X=SK)QxbHcsh zUEx=RUmbpJ_}cKB!`Fqc55GP9>F|@`XTpCA|04oN&=G7zRD?7lIzk>17omvAiYSSg z6R|kr#)vy30ufsx?vB_Vu`}ZNh{F+IMtl|VP2^pX_eSoH+!y(DF2T2yw_sHm|~6QU}jCPht+YKmGA<%=RwE2CCLt%YAt>QIAGF8MQZR zU(|C^FGRf*buj8<2`dSa#7K;i42el%mSjt861yZ%GD21=j(kG;cq=%(Pq{pPkrJqSp zOV7#}S%@r2mMlw=rODD|d9pIucv*#PlB`NrEt?{nC%Z`2C~KCr$mYuy$l7E?wobN1 zcE9XZ*-_d1vQK57$xg|>lzlJzLH3jEtnAllX|z5%C%Q1YIJzXdEV?E7vgobRk43)` zeJc9L7E9{GLpf5;z{ z@0A~vzbXGv{;~WM`N`NRv9n@3Vmo86j=eed&e-nQp4i{xLgQlM65~e3<;4}pwZ&Z= zw;^tO+&|*>#61G7-MuaCbwepmc6@h`_8jz1EAEdF@>Pw{8t ze^n?IYK2CjQzR=!DpD0jMTVkGFBgF~Dr;5)Mrxaf*zEXUn_)c*~@nb@G zLS%v@L6#7c5StL6kdTm+ph_5#piL-Dn47>Q^d!8Ha5_l1HJ+>p3E@!rJy6CX%?H1YAo-HCe=KS=y7X?T(>DJCg4DLzS^ zq)E~xB`1wcs!rmP0!cfQ9!olybSUYKq<51(O8O${XJxoDQW>YzD^rz5WrotCv?_Cy zxyo{7t+HM@PkE8DUFlW&m5Y?il$R(kRdUMJ%5}MQlt-1Pl;>2js(4j`DoLeM zjZkS-MwLZnRpqF1Rr#uN)dbZwszKGJa;rL2UezV4t5nyhu2WsF+Nj#9 z+NRo}+NHWz^{DD`)o#@u)l;hdsspOmRPU>PQZwpEwL~pb$Eah~YPCkKQzxrOs#DeF z>L&FS>O0lD)O*xVsrRd&Q9q}CLH&~Yp!$&dJ@p6bkJKmBpQ=AopHhFR{!0DL2=$1` zBbJTWG2(R%tI5^0XndMQnkAZLnoBg7YHreO&~$5hG)x4b{rENm`Y5gjTCf)f%-KT9ekS&DWM|$7?5OCu%FTGqrQH^Ry0a zlh&zqYkk^I?P6`0_6qH_+O^snv^QyQ*KW{m(r(rUw7awqYoFG>q&=uTq&=*CL;IHY z9qn=Ld)jZbXLSmlK{rlUsw>w`&`s1;>L%-IbhWyvx@o!@x>>q8x_P=~x=VDI>Mql* z(yi8ArMpJAR(GTB7TtQ?9lA}r&ANbYkM14aNqwl^q%YD>)BE(R^_%pM=@00COO_|A zl9Q81CZ{GFlQWX@lSd~PB#%wLH+gsRp5&*J_Zv)x(S|}pk)gy;W*BdnYM5=9YiKYy z4D$^O4DE&vgV%7WVVz;U;da9Y!zROKL%^`bu+^~5u)}bV;aS5AhL;Qn4TlV`8jc!X zH@szd$MCM z{M`6^I!gNjrpHXXO?yrI zP0yH~H@#$f*>u?Ssp)gm7pBvuuT9^XelY!P`o(n4^hXxXqO&wvW3!sFR%LC^I-K>r zIofP7Pc~0C&oa+7H<;&}FE;zli_Bf-W#&uFmzu9J-(bGke5-kb`A&1dyv4lVe8T*# z`A755=3gzyLR&&CVU~DHq9w&*vREuOxVlwnDYBGU$}JNtF3XLUXDx>;?^sS-zP9|F z&1NgI6SMW%=Iny(qU@6F^6ZN2%IxawIoS=_j_l^_*6an@ZQ1VZmDz#pr?TJ8{==GN zO|@oP%~q>5&pOói@pd#(Ge&sdLGk6S;qp0J*_p0S>_Q8v~VY8z&YvnAM)Y-*dB-rZvp46toL6%Gne$%GhdC#5PUd`Wm)lkLk@hsZ z-Ckj@v{&0}?e+E<_SyD%c89&$zS!PnUv6Jv=j@l;SKF_$Uu$1$zrnu2{-FJ3`}_7E zbHj6Eb2D?xbE|VN&%G-5*4#ku&fHzO_vY@-eKz-n+yl9Xa$n7TJNJ0*`?(+Gev*4K z59KB0+4Ed^p1dV_OY>IdU6prj-t~Dm<=v7O$lIQ`EAPI%2l5`zdou5-yr=V?%{!f+ zls`VdCcienA-^TxmEV@{$zPP;mA^dy(tIv|P5uq}w-szF*jeyU!P5mV6}(#TZo#R7 z(*@rYd|&Wm!P$ayg~JLX3Zn{Th4R9;n>3F!s`kjE8Jgrpzv7X8zMrcDDj(J N-s?^DeieT5zW~}qIYR&d literal 0 HcmV?d00001 diff --git a/MobileMkch/APIClient.swift b/MobileMkch/APIClient.swift index a1bbf48..fa72b96 100644 --- a/MobileMkch/APIClient.swift +++ b/MobileMkch/APIClient.swift @@ -1,6 +1,13 @@ import Foundation import Network +struct UploadFile { + let name: String + let filename: String + let mimeType: String + let data: Data +} + class APIClient: ObservableObject { private let baseURL = "https://mkch.pooziqo.xyz" private let apiURL = "https://mkch.pooziqo.xyz/api" @@ -437,21 +444,21 @@ class APIClient: ObservableObject { } } - func createThread(boardCode: String, title: String, text: String, passcode: String, completion: @escaping (Error?) -> Void) { + func createThread(boardCode: String, title: String, text: String, passcode: String, files: [UploadFile] = [], completion: @escaping (Error?) -> Void) { if !passcode.isEmpty { loginWithPasscode(passcode: passcode) { error in if let error = error { completion(error) return } - self.performCreateThread(boardCode: boardCode, title: title, text: text, completion: completion) + self.performCreateThread(boardCode: boardCode, title: title, text: text, files: files, completion: completion) } } else { - performCreateThread(boardCode: boardCode, title: title, text: text, completion: completion) + performCreateThread(boardCode: boardCode, title: title, text: text, files: files, completion: completion) } } - private func performCreateThread(boardCode: String, title: String, text: String, completion: @escaping (Error?) -> Void) { + private func performCreateThread(boardCode: String, title: String, text: String, files: [UploadFile] = [], completion: @escaping (Error?) -> Void) { let formURL = "\(baseURL)/boards/board/\(boardCode)/new" let url = URL(string: formURL)! @@ -485,19 +492,30 @@ class APIClient: ObservableObject { return } - var formData = URLComponents() - formData.queryItems = [ - URLQueryItem(name: "csrfmiddlewaretoken", value: csrfToken), - URLQueryItem(name: "title", value: title), - URLQueryItem(name: "text", value: text) - ] - var postRequest = URLRequest(url: url) postRequest.httpMethod = "POST" - postRequest.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") postRequest.setValue(formURL, forHTTPHeaderField: "Referer") postRequest.setValue(self.userAgent, forHTTPHeaderField: "User-Agent") - postRequest.httpBody = formData.query?.data(using: .utf8) + + if files.isEmpty { + var formData = URLComponents() + formData.queryItems = [ + URLQueryItem(name: "csrfmiddlewaretoken", value: csrfToken), + URLQueryItem(name: "title", value: title), + URLQueryItem(name: "text", value: text) + ] + postRequest.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") + postRequest.httpBody = formData.query?.data(using: .utf8) + } else { + let boundary = "Boundary-\(UUID().uuidString)" + postRequest.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") + let body = self.buildMultipartBody(parameters: [ + "csrfmiddlewaretoken": csrfToken, + "title": title, + "text": text + ], files: files, boundary: boundary) + postRequest.httpBody = body + } self.session.dataTask(with: postRequest) { _, postResponse, postError in DispatchQueue.main.async { @@ -520,21 +538,21 @@ class APIClient: ObservableObject { }.resume() } - func addComment(boardCode: String, threadId: Int, text: String, passcode: String, completion: @escaping (Error?) -> Void) { + func addComment(boardCode: String, threadId: Int, text: String, passcode: String, files: [UploadFile] = [], completion: @escaping (Error?) -> Void) { if !passcode.isEmpty { loginWithPasscode(passcode: passcode) { error in if let error = error { completion(error) return } - self.performAddComment(boardCode: boardCode, threadId: threadId, text: text, completion: completion) + self.performAddComment(boardCode: boardCode, threadId: threadId, text: text, files: files, completion: completion) } } else { - performAddComment(boardCode: boardCode, threadId: threadId, text: text, completion: completion) + performAddComment(boardCode: boardCode, threadId: threadId, text: text, files: files, completion: completion) } } - private func performAddComment(boardCode: String, threadId: Int, text: String, completion: @escaping (Error?) -> Void) { + private func performAddComment(boardCode: String, threadId: Int, text: String, files: [UploadFile] = [], completion: @escaping (Error?) -> Void) { let formURL = "\(baseURL)/boards/board/\(boardCode)/thread/\(threadId)/comment" let url = URL(string: formURL)! @@ -568,18 +586,28 @@ class APIClient: ObservableObject { return } - var formData = URLComponents() - formData.queryItems = [ - URLQueryItem(name: "csrfmiddlewaretoken", value: csrfToken), - URLQueryItem(name: "text", value: text) - ] - var postRequest = URLRequest(url: url) postRequest.httpMethod = "POST" - postRequest.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") postRequest.setValue(formURL, forHTTPHeaderField: "Referer") postRequest.setValue(self.userAgent, forHTTPHeaderField: "User-Agent") - postRequest.httpBody = formData.query?.data(using: .utf8) + + if files.isEmpty { + var formData = URLComponents() + formData.queryItems = [ + URLQueryItem(name: "csrfmiddlewaretoken", value: csrfToken), + URLQueryItem(name: "text", value: text) + ] + postRequest.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") + postRequest.httpBody = formData.query?.data(using: .utf8) + } else { + let boundary = "Boundary-\(UUID().uuidString)" + postRequest.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") + let body = self.buildMultipartBody(parameters: [ + "csrfmiddlewaretoken": csrfToken, + "text": text + ], files: files, boundary: boundary) + postRequest.httpBody = body + } self.session.dataTask(with: postRequest) { _, postResponse, postError in DispatchQueue.main.async { @@ -617,6 +645,25 @@ class APIClient: ObservableObject { return String(html[range]) } + private func buildMultipartBody(parameters: [String: String], files: [UploadFile], boundary: String) -> Data { + var body = Data() + let boundaryPrefix = "--\(boundary)\r\n" + for (key, value) in parameters { + body.append(boundaryPrefix.data(using: .utf8)!) + body.append("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n".data(using: .utf8)!) + body.append("\(value)\r\n".data(using: .utf8)!) + } + for file in files { + body.append(boundaryPrefix.data(using: .utf8)!) + body.append("Content-Disposition: form-data; name=\"\(file.name)\"; filename=\"\(file.filename)\"\r\n".data(using: .utf8)!) + body.append("Content-Type: \(file.mimeType)\r\n\r\n".data(using: .utf8)!) + body.append(file.data) + body.append("\r\n".data(using: .utf8)!) + } + body.append("--\(boundary)--\r\n".data(using: .utf8)!) + return body + } + func checkNewThreads(forBoard boardCode: String, lastKnownThreadId: Int, completion: @escaping (Result<[Thread], Error>) -> Void) { let url = URL(string: "\(apiURL)/board/\(boardCode)")! var request = URLRequest(url: url) diff --git a/MobileMkch/AddCommentView.swift b/MobileMkch/AddCommentView.swift index 95dcf8a..55404bc 100644 --- a/MobileMkch/AddCommentView.swift +++ b/MobileMkch/AddCommentView.swift @@ -1,4 +1,5 @@ import SwiftUI +import PhotosUI struct AddCommentView: View { let boardCode: String @@ -12,6 +13,8 @@ struct AddCommentView: View { @State private var errorMessage: String? @State private var showingSuccess = false @FocusState private var isTextFocused: Bool + @State private var pickedImages: [UIImage] = [] + @State private var showPicker: Bool = false var body: some View { NavigationView { @@ -56,6 +59,38 @@ struct AddCommentView: View { ) } + VStack(alignment: .leading, spacing: 8) { + Text("Фото") + .font(.headline) + .foregroundColor(.primary) + Button { + showPicker = true + } label: { + HStack { + Image(systemName: "photo.on.rectangle") + Text("Выбрать фото") + Spacer() + } + .padding(12) + .background(Color(.secondarySystemBackground)) + .cornerRadius(12) + } + if !pickedImages.isEmpty { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 8) { + ForEach(Array(pickedImages.enumerated()), id: \.offset) { _, img in + Image(uiImage: img) + .resizable() + .scaledToFill() + .frame(width: 64, height: 64) + .clipped() + .cornerRadius(8) + } + } + } + } + } + HStack(spacing: 8) { Image(systemName: settings.passcode.isEmpty ? "exclamationmark.triangle.fill" : "checkmark.circle.fill") .foregroundColor(settings.passcode.isEmpty ? .orange : .green) @@ -126,6 +161,11 @@ struct AddCommentView: View { } message: { Text("Комментарий добавлен") } + .sheet(isPresented: $showPicker) { + ImagePickerView(selectionLimit: 4) { images in + pickedImages = images + } + } } } @@ -139,7 +179,8 @@ struct AddCommentView: View { boardCode: boardCode, threadId: threadId, text: text, - passcode: settings.passcode + passcode: settings.passcode, + files: buildUploadFiles() ) { error in DispatchQueue.main.async { self.isLoading = false @@ -152,6 +193,22 @@ struct AddCommentView: View { } } } + + private func buildUploadFiles() -> [UploadFile] { + var result: [UploadFile] = [] + for (idx, img) in pickedImages.enumerated() { + if let data = img.jpegData(compressionQuality: 0.9) { + let file = UploadFile( + name: "files", + filename: "photo_\(idx + 1).jpg", + mimeType: "image/jpeg", + data: data + ) + result.append(file) + } + } + return result + } } #Preview { diff --git a/MobileMkch/CreateThreadView.swift b/MobileMkch/CreateThreadView.swift index 0ce8b6b..ca95e53 100644 --- a/MobileMkch/CreateThreadView.swift +++ b/MobileMkch/CreateThreadView.swift @@ -1,4 +1,5 @@ import SwiftUI +import PhotosUI struct CreateThreadView: View { let boardCode: String @@ -13,6 +14,8 @@ struct CreateThreadView: View { @State private var showingSuccess = false @FocusState private var titleFocused: Bool @FocusState private var textFocused: Bool + @State private var pickedImages: [UIImage] = [] + @State private var showPicker: Bool = false var body: some View { NavigationView { @@ -41,6 +44,38 @@ struct CreateThreadView: View { ) } + VStack(alignment: .leading, spacing: 8) { + Text("Фото") + .font(.headline) + .foregroundColor(.primary) + Button { + showPicker = true + } label: { + HStack { + Image(systemName: "photo.on.rectangle") + Text("Выбрать фото") + Spacer() + } + .padding(12) + .background(Color(.secondarySystemBackground)) + .cornerRadius(12) + } + if !pickedImages.isEmpty { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 8) { + ForEach(Array(pickedImages.enumerated()), id: \.offset) { _, img in + Image(uiImage: img) + .resizable() + .scaledToFill() + .frame(width: 64, height: 64) + .clipped() + .cornerRadius(8) + } + } + } + } + } + VStack(alignment: .leading, spacing: 8) { HStack { Text("Содержание") @@ -149,6 +184,11 @@ struct CreateThreadView: View { } message: { Text("Тред создан") } + .sheet(isPresented: $showPicker) { + ImagePickerView(selectionLimit: 4) { images in + pickedImages = images + } + } } } @@ -162,7 +202,8 @@ struct CreateThreadView: View { boardCode: boardCode, title: title, text: text, - passcode: settings.passcode + passcode: settings.passcode, + files: buildUploadFiles() ) { error in DispatchQueue.main.async { self.isLoading = false @@ -175,6 +216,22 @@ struct CreateThreadView: View { } } } + + private func buildUploadFiles() -> [UploadFile] { + var result: [UploadFile] = [] + for (idx, img) in pickedImages.enumerated() { + if let data = img.jpegData(compressionQuality: 0.9) { + let file = UploadFile( + name: "files", + filename: "photo_\(idx + 1).jpg", + mimeType: "image/jpeg", + data: data + ) + result.append(file) + } + } + return result + } } #Preview { diff --git a/MobileMkch/ImagePicker.swift b/MobileMkch/ImagePicker.swift new file mode 100644 index 0000000..e1a2228 --- /dev/null +++ b/MobileMkch/ImagePicker.swift @@ -0,0 +1,57 @@ +import SwiftUI +import PhotosUI + +struct ImagePickerView: UIViewControllerRepresentable { + let selectionLimit: Int + let onComplete: ([UIImage]) -> Void + + func makeUIViewController(context: Context) -> PHPickerViewController { + var configuration = PHPickerConfiguration() + configuration.filter = .images + configuration.selectionLimit = selectionLimit + let picker = PHPickerViewController(configuration: configuration) + picker.delegate = context.coordinator + return picker + } + + func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {} + + func makeCoordinator() -> Coordinator { + Coordinator(onComplete: onComplete) + } + + final class Coordinator: NSObject, PHPickerViewControllerDelegate { + let onComplete: ([UIImage]) -> Void + + init(onComplete: @escaping ([UIImage]) -> Void) { + self.onComplete = onComplete + } + + func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { + guard !results.isEmpty else { + picker.dismiss(animated: true) + return + } + let providers = results.map { $0.itemProvider } + var images: [UIImage] = [] + let group = DispatchGroup() + for provider in providers { + if provider.canLoadObject(ofClass: UIImage.self) { + group.enter() + provider.loadObject(ofClass: UIImage.self) { object, _ in + if let img = object as? UIImage { + images.append(img) + } + group.leave() + } + } + } + group.notify(queue: .main) { + self.onComplete(images) + picker.dismiss(animated: true) + } + } + } +} + + diff --git a/MobileMkch/Info.plist b/MobileMkch/Info.plist index 2bd486b..b27b4e4 100644 --- a/MobileMkch/Info.plist +++ b/MobileMkch/Info.plist @@ -8,6 +8,8 @@ NSSupportsLiveActivities + NSPhotoLibraryUsageDescription + Нужно для выбора фото и загрузки вложений UIBackgroundModes background-processing diff --git a/MobileMkch/MobileMkchApp.swift b/MobileMkch/MobileMkchApp.swift index 0fcd0fc..8360add 100644 --- a/MobileMkch/MobileMkchApp.swift +++ b/MobileMkch/MobileMkchApp.swift @@ -37,7 +37,7 @@ struct MobileMkchApp: App { private func handleNotificationLaunch() { if let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene, - let userInfo = scene.session.userInfo { + let _ = scene.session.userInfo { print("Приложение запущено из уведомления") } notificationManager.clearBadge() diff --git a/MobileMkch/ThreadDetailView.swift b/MobileMkch/ThreadDetailView.swift index 372da34..3b7f52c 100644 --- a/MobileMkch/ThreadDetailView.swift +++ b/MobileMkch/ThreadDetailView.swift @@ -89,29 +89,7 @@ struct ThreadDetailView: View { .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { - HStack { - Button("Обновить") { loadThreadDetail() } - if #available(iOS 16.1, *) { - Toggle("", isOn: $activityOn) - .toggleStyle(SwitchToggleStyle(tint: .blue)) - .labelsHidden() - .onChange(of: activityOn) { newValue in - guard settings.liveActivityEnabled else { return } - if newValue { - if let detail = threadDetail { - LiveActivityManager.shared.start(for: detail, comments: comments, settings: settings) - } - } else { - LiveActivityManager.shared.end(threadId: thread.id) - } - } - .onAppear { - if settings.liveActivityEnabled { - activityOn = LiveActivityManager.shared.isActive(threadId: thread.id) - } - } - } - } + Button("Обновить") { loadThreadDetail() } } } .sheet(isPresented: $showingAddComment) {