From 296492a20de01ad4d1358b56bcff68bd1295b28d Mon Sep 17 00:00:00 2001 From: Nikita Aksenov Date: Tue, 5 May 2026 18:18:37 +0300 Subject: [PATCH 001/145] feat: ParkTrack app icon set --- .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 544 -> 5167 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 442 -> 2931 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 721 -> 9190 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 1031 -> 13124 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 1443 -> 18900 bytes assets/icon/app_icon.jpg | Bin 0 -> 100400 bytes ios/Runner.xcodeproj/project.pbxproj | 4 +- .../AppIcon.appiconset/Contents.json | 123 +----------------- .../Icon-App-1024x1024@1x.png | Bin 10932 -> 200312 bytes .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 295 -> 940 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 406 -> 2271 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 450 -> 4051 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 282 -> 1499 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 462 -> 3829 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 704 -> 6700 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 406 -> 2271 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 586 -> 5970 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 862 -> 10350 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 862 -> 10350 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 1674 -> 17738 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 762 -> 5634 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 1226 -> 14219 bytes .../Icon-App-83.5x83.5@2x.png | Bin 1418 -> 15959 bytes pubspec.lock | 56 ++++++++ pubspec.yaml | 6 + 25 files changed, 65 insertions(+), 124 deletions(-) create mode 100644 assets/icon/app_icon.jpg diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index db77bb4b7b0906d62b1847e87f15cdcacf6a4f29..816392b2cfed30f24b952dd6c9bbe36d7354fd39 100644 GIT binary patch literal 5167 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 literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8bpbvhu0Wd6uZuB!w&u2PAxD2eNXD>P5D~Wn-+_Wa#27Xc zC?Zj|6r#X(-D3u$NCt}(Ms06KgJ4FxJVv{GM)!I~&n8Bnc94O7-Hd)cjDZswgC;Qs zO=b+9!WcT8F?0rF7!Uys2bs@gozCP?z~o%U|N3vA*22NaGQG zlg@K`O_XuxvZ&Ks^m&R!`&1=spLvfx7oGDKDwpwW`#iqdw@AL`7MR}m`rwr|mZgU`8P7SBkL78fFf!WnuYWm$5Z0 zNXhDbCv&49sM544K|?c)WrFfiZvCi9h0O)B3Pgg&ebxsLQ05GG~ AQ2+n{ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png index 17987b79bb8a35cc66c3c1fd44f5a5526c1b78be..aaf6333475f58a6051648d23cb2a3ce2cadffe86 100644 GIT binary patch literal 2931 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 literal 442 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 diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index d5f1c8d34e7a88e3f88bea192c3a370d44689c3c..980bbab4598d4e534d7e5edc3de84268b6e1de48 100644 GIT binary patch literal 13124 zcmV-KGrP=*P)Yid-EuG-!`IK~5 z-{sV;bMM*D)lmaD?u6;}+2QH&T|+S{S5FP|FF5<$x}nV{_6kFT5UP#ffPf(Rqx_Fa zP(K48VYjhC5D|gMd1&+r*3%$EAgdP^Flr(5f6H&a+Jl@k3GtL{XP+0b+b3*S zJc$V1Yo0Ofqm77~8boR;AJ3*9Xv62~PMVd=_3%|wH})^+Sh)6~J*zhDOn`_75))9m zK0{5Xw9Rh4a@Id|45N=7;H)lf5D88&8PLmw;vrh%1rjwsfjqO>L{y!z1_1RkPBS-N zy}AGs44MG-Yn0X-+1&P-sX4hBbG|!a)3p0LA6fg8_P4fdOFBm)0&1NyW_Z&VPyP`d z({e>0%!Wn-Bu@?#1Wo|s7UB5M62;A_@N*sr#oM<^;3nQMmXegkRDxyETtcg0EG3*J z>XSC)iEBwox^UR8pfp%(_mq}v>aIQYv&U|l@$Y-@TKbc&JqLT^jvJeRMqfDV6#Xx! z-oxtj6S&}{5d(J?g@i4*!WKlSXte{sRsLD<2da)MB z%^dn8Gc8(j1Y)J$bwJMaKSL>q^BKq%r#KNb5FM=RKfUqq=fBkQ`}JSg{?y7BOU)5U zKrE%Rk=LDmv-nW!w|g1aHOWIwq1ct8^b&;R)=K;u3L05xd)l zoi2sO%!LhiA!f6Oi22mWHZP|#*=N!-DzuGXTeML#m9+!}&79Q#=_!wmZB2b?$NkIy zD{8Dz0@Bht8-3jwH)l>5eM63frrBCjX=SNOCr{WgafWwWjFPl1x%%WKAkXt+nMAR3 zkFajDb}Ccjtihq~-fGOYN0_985eE04H|aMcvWB*I!HVC7^$#VW@n4^RjX8VB4c*{c zT@K(5-wLqTl-f=1zlWYW0Vk9*p>TcVBpWx$F0YlW6{P7^GFQ;4Ug=mt-!n&q6laaS z7an`hu?@|Ao9=z?@t`pR325?_r(M`Pcj!&bOlgg#iGEE|rz7aXyL*b|ktW1t(0D5{@^Ort+fBo$^ zfA{*$9qwix0!nAnY{XYj{b^Uq7*A$-47WB`*x)`v$9T0=|7t?5@F|R2T14>lL$m@E zG{)gUZ0^u2=Uk5)H~*vixrcy;Uo`XF&Y|Wdx|t%=BP51NrJ}SE`-W!^b9mznfD^d8=I~bP#_7F4o3{aq^{9^n$nJWL?e=VbjGhubBCAag+2 z1OdoZ6`ukTq@^sJ(VFVcoAP&fcID@t=lL%?`kWKaJUmFBSCndQD$P(P8k{FR43TSyb#d z@b!zUC50Iu;}|95;81GLC<9=UQ=l4JQKjP}1ou44O!z&Fls0q!0mNJ1!XWjMYyLAp3`#JPAFPwRt54av|5{cJ%Tg6sLo z;_aO1jT0^?Mz^U3^63UCNdFS%A3o>k*8siJ=Y6m{LCsqX!Q~gI0$+J&iPJ zNXa-SovB6683y`tec090fww#NU{}{3Af>_7n2N6~HbPGcFKZjNs{y=h3ZruS#i(xPjlxQh}+DW>d90&gsEts_$75S#yHCEGSM z4vm|TCoJnH96x9RP9Hl1bB0gG%+`q**)$X;7*gEwks6wc+*1K^j=@>78EG3+Hf~Pn zFuL*9{tZ~UekC;z*r zxj?7cKS?yE8uSw{IQ6XEKnqc-3}la9a6W?koGC-YNVMScNvGn%sUODVmNAM5O95Q?G$M;uY;d?9b{LXdA zW`ymrKLT<=jX}Pdnbumy&zyFGzWMO(<}{{|be;((EydF`dZ4&5K`1Hj#Nopc>aB`r zDPYFrT&BRp`eFE+<3El|C!c{j$|%-l5szub<2fllykP7k1S_Lta|o^hDU;iOm%1xq zlt|&PjyW5jI(9zx_8h>1cm9ZbHa&*}W}lTyVaMCZPL7!*a2-UDH9bb0k@rb)yMP$* zQg)ysJ8k}fie%P08;mjf_!-Ae+yyFRNX7Ez)(XlOLZ$txA?L7F69(7t<;myanz^4u zGcqztN~z~oI+n0=!pg%?vNxG8=5}B>KQm|;RX+^hJn3@$^~?+K!{_eD@Ake1=^WBb zD-OCR1*Cb%1o>HFLK#AEY#{$Hj$EnUo%Z*%lityNxE0i536&KHF-cYPYJ<)t5edb1 zChH^dgLzkC{+JoiLD z2Q)5}J*65kLAC7{?&l;SAT2%(%}s;!o@{?Ts%QdF3htruAs#WXA$#LVwjhazLkh|a z*G)VNSI_wrnkc1Y9HM??OvTh-vN%Dmk=RDyZjCJEC|$A1oRXtb&~W*L596FMv+$Fb ze~Y`fzYMN(m0}}_o!PcG;h7@-`WAhQ($O4cg~od9Z`K3UH#d#dkC}6P)0VD-Fc`1G zL0FE!Se`$}QO6}aktfe+G~<>tzl1YKp9IYoV66AiTv-T}LJT9SaO4M8kU2=hpi~3? z{*0?|%J!M~yJf$|e##+bS}`~BV^m7o4)_CYgGO~L*-=4f0lgGRQGjudZj+~VQ@BLW z8ijS{Q7Ykv@Ta6ifei4M&2y|DC)c&%u5+%(_{R8^aFfp(Jgqf<9(&Y4J5W^(qXf4#~Igv10jim#9@w;eqr29$#8~D;6GHr{p)OJ<4h#!fjgU{#4EK*%hG zYQVe>KJxz-n`A1LNr%@~7JI-rf2$PyW1GaFn>r4sv$%B74E+22zXb^mIyY5bel6yX z@^8hraTT|h_%<17{NlW8@x`ZZ!P2e`FenYpG;Iy2_L^1|+LB)G_dgP`$I<|25XY(7 zIPuf7zfNh%%UKSFU>_jnYXRd7Mko7q}fD4rN!d9F_vDtU>(z{Oi!HH7IK`;{=z=RTC%`=26y|w&_-?^3ADw{OrQ( zaPea|;h=dKQXiHlRLZo>0^wsM!yPd0Oc$2)>_fnYU;Lzm zCl^v`F9s9H{qM6C<}<&D=O6nA^spWX+twAlTX1gTs7kYWqpnkDih(Qju`{XcE63=*Db)eN>Jv(s!&R5Xz>Vvp+!aQ6(<7493mN7((5X4O5yn7KL5yMr- z{{{Yg-BP@n+fl^A21#Q#oVBixClyH;D_F zx)9$u>0&gA1~QphhRxP}^a^MuLr&w^aCirP@X~Mb+`f0fB%9PN-<8z#1mjA_fbHey zw(qSjrz$_9pJnmj_9a-bc`2@J`-u3?yvs4HZU{-0)-3WVijq-+ZemjNm?_*a?^C#9 z$!$u@6il;96vz1P?tsS#r;@L@LhzbJ`Ncyje~_|D1)k!8YW{Y<$Ntftmk;&3EkJ3Cceb^evD z+R)jQpa-`t!=jz5#T}=AMa-Re0%eLUnj%j`&RRvShbB0(I${2V891e-4J+EWsd*_b zJIi~H!?P`f^LGRfS^85kTl5#u2el5?y88C|28gK!#?=Uu2-&a%Q>G&Gbn)fm{sMz| zigLQ0msDcx$aEZV{L2du;O_SpLz8hqJUL7Cj3+Erd#C82&n^Cm`2M`liqB6yi=c(= z>S+8OcaWegGNy*BPrMjkdht#WStoCl-cT4PzGIYg9$Rkoj%RNQ0n)QQ{lZ-xq~x>} zvfwB`i+c*GAvKv9Tr&N1aHb=Z<4R~P^}2r9LVY%3$nhM$xAZr-Z_D!_O@k>k6)#;V z$WSn$MvR`{o$_O<^lvFjF%9jh9DV1Nd(lH&e0AD6#EIFbSTQq+`nZy&>t&s7MmTTc z9E@4kjP2$jkP6n1B{PI!?!%$5?n#SaiNae6eb>92v-;G*mL>-lQji_6;PTJgtJ+^*D%J3C7j1$|-v+xXX2kHJhOk9$d~^$3mV%P-$8 zmJa)Y7|}GGDhW-JZ2Zz==7^uKUr0GBLw$&fR!q__1e%7lFg@LnVgC5;BVD9P+CQ49 z38#-cULD(UmM%P00533w_j-w#?21LUa%W*^DYlS(Zd|_Q>hbWY(5&D;6;YJ{c!1 zltS zGE+1$`PWWvBm-%jHDo$|-@hD1J3XLpnEx}XPrTP6&(Bh>OxM*hJxY0xigzg$l3K|L zCMB_1qmC)t7bLYU;u^9ni#yjXLJBPOfrGpD7 znEH?mD|T-X+l@~3tWO`9x<|_OV#zD?;*NL3?rfJBUN@L5M}=9k>W3%jOlE8_!i?6j zYFx9%DM?A8v89>mWZ!L(_t4uA8StLOW)h_Vgnds#VAAk5rONX%2$hqjG^1DctcOr} z%l2h3o@%c#>O1{mxZNep!J{sWPc8wb1c$GdjNpvF`hN6+B(B-#P zm3<&3GcAO{ft|Zesm7BmEG1YOJF-kDgb@V)ee|Fq&`m*3m_$;vq!f}Rj&&W|ERkHW zDpqz+u+aco*|9@#U%%LnRh#H_Q()ZS;aJyIeCuN{j-{WK1Nqa>wwO;mVOm;u-m4sY zMNyZ$_7XmM@$ZhsxNrtG==I7QAe4bFdC-`Q>**(syl_3}gu?_)j z?b$CKq|)w9aqMg!Vj|0kA$yK)8tNjT>TLE(qZ=g>x+4z9u(TBScR%(-G{(2vIOqvi zl{cpA_5zl-P|^Um|8TqDn(R+y$op|D@})sed#Jk|IWm#rDew?IJRCu8*Bdf1JUuw( zDLSC#WJr(mi5y>1v7N`&TexY->(`2!pe9GI5a=N^8~r>rBO{0~dn+W7L(t}p6)G=8OB%$s;qhD2k&=cU`iRuH;l5RvZrEr*x zkhyDf*{=sDS%bU2*}0@Jx#ift1g@We1AT{Ug{C!E40i0s%n1k4kH*Thd)p7n@C_CG z?-=Hifo1m{*D@S$_v{Me$F(<06U2m;5q1Ev2v$#HfV4pGYCiy^OSc^eN4WcZu?quQ zYHOIFCrYrVFb8V6?v5@pu(xX$Cf1FHDJ!hz&+gLtqLtq!-%!V#mNu;I-krZJuN0IL zKug#VEnk>@YDnXytXp(S>o{eK&|r92sei5@_mGUQ*3A?S^zFf6Cf&X6La50F{c;@K zPFvGew8CZbA*&LtFYr4F6-Q-Sl)Dq88ozemb{sQ)bXoQ(*{LDRFnjEDasT#LRgh>{ zhG-B};$S6b!^w!sKEf%ZkGDe5lcW?EUPFzyde1iH)`&BI)Oa;xBvu4PA7bfdUA$@K z{w-y*znX%KiB_%*Uutu?lp4#jX~%&U>LEHzwV+Pxg6O3^8w3ffw6}lh!dPTsex}l3 zM%1^^CnkLu4{Tp*Rg#z2cluP}qFowpo|3m`rS+v`UQ^M`2L~`A+>APLHmCS$u8A z#YKM)lhJ}?n#nC==$aERz`w42+}^>peOu{0PyO#iKRD+~G>bY4Q%vH9b3C)*HIdb2 zRV2L(uV6xmtC`vcWf^#&hk1&e0`7!>FjZ$V_umOcvCE!*2{*LN2z~h7rMT*(3suse z+|`D~aKCn*Y^+GBTiCP(WH4wb^pDnZ85U(!IDJRb?lS@S>m)Y(<$e=yB-Nt zQW?p|D21P1a5b*@uYblG{Q%^(o_#5+fKzfPewV(KrU=u}xR^g=D!zaEl>pO7`A!qs zWvr?p>PjWN-@OMf?sx}0#VS5Ky%K}h9O53=eRr(p7B$?2Rrn9-9{Xa5OyRaw6d@Kv zh%QGhHkc-FdZoMGenxzF%v5y)PKxVA9_>=BK;~JQ%)lgVFn)K@4Y=&dTksCwqpn5B zg@lO4#n+iz=4^40IBVGP`0e@MK$FOjA-HAE3>hmdh)kZgG7>Y(IDWnQ8T8eWIx3kc z7XIU;Hq`>$<0>^?9bLkTe)D@$YiZRCAKQ}4Jg_MH4RD_q!SeN$XH`BdEc(Jvsa zH&B*jJt5P&TqOeMAYzoKtR(CXqX!SKTcUWm<&Z5tg9=(vNc@uPp&J!C*i06>s7;KV~q2K4W5J|%vB##JP%lqD^vseB>asz)e4&dAJQ zEV%Fn+_mx#_{kfO<4`82@;Kry1Hrv$*Nhzrtk`=aR-U zYSr|-<(4P5{tDX!vEZ#gVwaw?aVaJq@0x|hFBST*fJ4EtGYO}=syLR&MM!$kwmvea zPH;P^%096D6=`XoLvWgc#O{ZagW^UFA#viw!D z9%GIL3%HUH`*eB!a50)1@!hHC;)}CCj)rst$*QZe&_X6B)OAp$-Z%8QOorq=_}N=e zfpdEUM80788O4tBBLax4)TFAN;HbzF9p)M+*Dm3+NGOmw@tSGU&8r?0_nq-IwOcG} zZy|&+Z@Dc|{8AEz5~@W`X>=0?jl!*Ge-+nv{}mp6`&seiwpCcYcMD9Z0&4O^vO@Yv zRv^o9F%%ih88sc3wavlDr=5ysy^fg4VRB6oy7XP;G~wCpaY6C{(~4rn-7BBQPJTGF zR)Hw`p)`1i%%uzYhYy?0-Hu$!j2?Pk4=}jxR~{M6K4_oVw4650ybP0ujCHnMEN~MQ zvuiYQW=H0brD=?BY{56?Tt;7?b14pJhIn`9M(pi8h_1dKb$9B}<`#@>X~l%0ZD`Ih z(wqoGVI>essYsa^;>UaWkd)c1a>U1M)w7B#StT0+HbogROlDAXJN-y=sqy~g z$H=ol7Je_rEyTJTN202b^|^Vo6K}%Q&LreAz)x2`il5KF3fYu~jKNf)%Tjp9!qpbE zrM;z9=+rv$c+f~|aaBcRq1@{v>swhtZq;S+^7N5rg}N+o@0-uyfap^>_H|jVSZ=xC zf5*6e5;)VP*{fXiY6tFk6IshpUWhB}CO@?GMSQXAFYuuuZPqQ!3D-7?xa2Z8vX9~; zwo}xg0?Tyn_;TXEJuEk&y~k_#v}Cb8cmg1*= za@_3+Uo>b+7mji>$O~ldU;PYrYyH-pt_5pd;_*s>ebriCh&KHNzJkUoR<)dvRtnM* zzT@ zsY+U%_nKX}bL}E)PFohC5`l1K&nd#zItqdsNdgidpqom#z+pXy8<##P9=zas(mpVV zM}xFS{@e0Lup8Y2dUF_+Q(2?X+h$>8`%O!#bH(5UGrFCu!4^^1@fbEvP!dm(6W4O8 zKCyodE!_Az&YL_3X@en`;;M#patJaUc%RqGBxc=xrYmdqI|n!8p7o2BMw7E;F7nlG zj0YB=GOMZu7^)YZ4I(2^R zTq$X({q76D!MzuM3mT;>*tvdQ9BgB%=1I$iiJM+{06TcMvM;GR990;e1-K_DFrdue zA_4IzDra``loL@X{G?l(9hlUa8upyqS71JNU=1EwzZ9Q2ZoYaRpNPnF0tL-m%QAk8 zx39*3Ze9kVX(%swGCp|B5K(QC4XR`x-BWk122d;cNbWQ#bIK$1hUE|9w9zMEY|C&* zf(cQF9XY_+Y?S{!ju03`P$k)JzM&m!1A)$5FRop3ue?YGn!JsX{1>!DL;TQ?;0MLe z%l}5YH+(Ei6+SBbtzIC2aNGkE}$hev(at65XLfREH@2FGmG3M3LYLAe6XA~Au`@Vee?HT1OfB~_MtpxIlXHfh?`_TL zK}N({5Kj^-Zk$E*|6PV%NCV?*Jv-@NUw>5m=%mk}FC)WPEAjUP&lLuK+*N|GER;?h zkG``E4{uuuO=s#d8768c0#rP1-2QffDQ>-J^@~akB*+hE+IqfgLunsdpc4SaLtIc7B(3bRF(xL+hAPx^9K!Yi0Xh5Mm~;G(I{p4ut_=5W_eB z_zS%B>3>5e-GDUb>V%RJ&YC-O&hx%uqF^!#WVSs`*sclAP5j-ncVi#xm6x#OYf)CS zB7r+UvZB)`E|;*I?RalV{O>)J}j@x4XA#?D-qrSg~YjU7{^-{GQ|WSk6;u=S1eog+kIVHac| zJFREHTp4j|;#UL6zL@&euC2Ij`J?#uM=nLmBwyM}xNE{6&QWqc=K@>BqBtI1zZ?(0 zw;VjJ#q<^h&*KSGDI(sOYgwjIEUXu|9Yw=NkJNR+{la8&h#MPIaM}(W7#h(nYoElW zQ|DpQu(5EeNw_Vu-c!Z8OTO?d-aL-cPu>;Q)zg6+Ubqk0It^r2{=QN1-Z7D@7>D8% zA;RW$>$6^)e~2?azXAM%2K|x`#dHl+w)s8Sq@N@QOeXC0Y6iZs_#W}Zr@k*QVsY{> zLbpx%vFZYP7Px-dL)gW-q2*ZQErtG=D=TiNaJqri6Ml9UjB6>Yyh?sl zcgM@q)x@LkFUP|hmw*w2sfiMq6#-gYe%t{+tV=-QtRZ38IkP16yj3DHWHZ{PQ!U}U zR48VW{@&-Z2o%vRYo5UTi6@E=k2;Px#dC%RnJWCDw%oq)aclPhd~50bFwzX^eyAiw z3K*y4T;JC~{=rtIyqqC71tD)MpwgNf>O@NhIs} zWDJh4FS-l+j85p#(VwcK!RKfNAynK$kwR!KRPADbVr9LCsmTe!eF7MyV?*CQ`p%O7 z6hE8)6>y!YTzBamPi#bGLZuk>OTQN9`1y(_#B=-C0F+XCE#sC9lP}F1U?wPg*G3r! zV*rI+V5D>iXIn{YdRPrWJs;&5Q6tekaImW)ZlwRw;@xMqUFV?M%giJMkF3Pw6vmS|hZL`)*_uZ^o_cK-KDY&wB$lU>9c271{> zf)60b0tu2>dNP7;c>XS&{OLBdHH}miFc^YkZ^8(l+wxO8nfUU;JJCaevYHOYjRDOr zttupzyHrIpbs=`iyr<}SqubW<^huPfrUYIqi&eTAu3L1sc<2*1SP_f~zqv^=oIHyk zKL1<1)w6>bn2HFf3hDT9FV|<~?k1S))ZtcvE;ycnkmFZq!IpYq+GYmWT38XjrM0br zAc|`DAzNEgZ_XH=-MgOdT=@*HnSFtkpb|q{a-Qj?i6^$aCGL24u~gZu8>Yi}RvcDT z6L1UiWV_z9D~Q9sbhkQ&J~xynSV!aGce+%fC+ayMo0gX999_NemtxUh z|1;`Eot^Cw43b6V)CrWswSV{}wzI>O%95&!7-HFpO7{(hGQWoK&lL&9SA8Zs^;y23 zj;hh+z<}^;D0%QO>5#t_o~x32ua&k4d-8ruw+=_o$x&Yrh?FwWx-Ri5Hr9xE^yJz(?cyj9-fTqEn04w0Vq$CVN zPd33^?`(v;K3NyD2bw3`zZkw5V;~j-0`yT>zA(peca+)_J!-Z4^oWQIbh|Gc&AJfm z6%5i6E&q1gx`miAb}}xRGFRQ(ZmQr9155U9z(2e!!&9w$E8Nr8ni&`JyJ}tN_NXc9^x4?6A76dqcC>4` ze0T;EF~U^+6NVD7*ToQAK0e$^?va7qb5xObg60zLYrj!cU$5|sI}_KTgsVP zk%1!Che_-_4x5ea>jXsvK@<$BP7F=L*^fpDc?ru?EwC-skBc7tzbfrAnvvT2kXil;{7LM( zV$d6Sid#Br__4`jNlD4U?R*Tp3?vS?{Y+7Rj~v6tG{|Aw!qR+{yV>w|aqUv;W?dGX z*B|R8^pXwZCUGWN!g6eNjzBLo6OA0E{@xpF+A*~y!L**ytbfb-cz!J^D#|?xE;WFt zg?8tT4OAL;rBIc5g+N25F00oweOnj8%Z&^e#AKm=hNA%N&0eRa_UebW?%hU{n=Ff& z+qUZpJs=dq0E*1kD0BC#*qpGx`OF3(HcXHGHdf?cd>SpJvsCA{oFnbJsbn#!j2KLVvYQKrmCTiSxaC9DvM-(e*lw~AFHV(<^hu3X= zRm>aKM?%zlzlh(*<@!WN=bBawwLB%tEq7B1)MPF@ag+&kZ(FhUH9dQl(=#*X%{l=s5PzSZqMyQE+=AG(E)KhXsx-Z?%aN^2c29yWWyZ zo>mc8WFP%-v>g2MQ?=?jQM zTZOp0WHy%q%pH zP+yalN^_4zhVOZphtmP(buPwQzX05i(4la&u$^+igS%ZYR2tofi}WiyR&Ce4rf-PH15%UMElwB(THzUO{Ge5^a3U-zpoEu%#;q8{MmQzaGvFB zc62_s_JM{I&$zxjgNap}@z3xl1+t9I5L18az{~6Z;5^F{Q1`Zd?L$`Wy1n<5(LZJ) zMP@ZFuL=TD-aQuO*T5`6fkp6_p7^!gkA zwt)gk`>&q=-k@)O>_hD%>(18u1sUv5CloR2GYeOnYRND)hXnH`A#u*+ZHgSkm>7Jp z|AF2oR^A!ZE|7qX&fea>-z@#YpwFN7WY6IAiAY|+>jMc#%_(w(K}A^#83oRTzPtO8 z_Pdw-jnUEFAJjHfCF2^vfj<#0oCt!~F3hbXl)A17&1^o9y}$FWrC*C89+V;=34L4lZ%zGp@%h95 z`lGiV7@PTmL0a;1UPl1phG5hfiSWNUwwWV+653#~EotlFU4!54{7(0Rmw#n+_8O)7 z#j@&k?K#-h_lt#J8F~5KCHrTM{Bc&-hO0}6HG5l28@F4~j)-rrmbX8Nxj(K)J?6xP zpDSTM72%M)o$rnrm52j!=vdW37WH6jv`5x_W$(-DUyh4PY_o8#FDrIDu=M`1Z%=!% z|J1hY+9x)APH<`#(5$3jPIk5Sc$yAA;Eu*44c<(vnAeosot&$$R9Y1`WtpQb4R7wc z4NErNcW~?O&Lnsw_ABgIxqb^)uD@p7M^CstbLR9f?iG0v9R9t?GG#l9ybn zB$}`00`XD8W-5YwHmOxp6@M;7Jih7l%<*mKcTjGYnbtA_Vvx#3mtuE*{VBg!imPsje9TE9Ma(8D*X1Zq zB@ssq6exV)u+JPj6T^*<|7TICU{V}T>+2JMk2n3E@)}_&aB5f41B;lDsTpiYf&y1DCjJKDS_cfege&!SOv8VV<9$(GyPG;1fgZm4b|lhTcPp|u~{y6e3WgN7K{tp`{3>^iXZz@CoI{+?dm e9q~<*!2bu!6^oW^(@*RG0000{( zJaZG%Q-e|yQz{EjrrIztFa`(sgt!6~Yi|1%a`XoT0ojZ}lNrNjb9xjc(B0U1_% zz5^97Xt*%oq$rQy4?0GKNfJ44uvxI)gC`h-NZ|&0-7(qS@?b!5r36oQ}zyZrNO3 zMO=Or+<~>+A&uN&E!^Sl+>xE!QC-|oJv`ApDhqC^EWD|@=#J`=d#Xzxs4ah}w&Jnc z$|q_opQ^2TrnVZ0o~wh<3t%W&flvYGe#$xqda2bR_R zvPYgMcHgjZ5nSA^lJr%;<&0do;O^tDDh~=pIxA#coaCY>&N%M2^tq^U%3DB@ynvKo}b?yu-bFc-u0JHzced$sg7S3zqI(2 z#Km{dPr7I=pQ5>FuK#)QwK?Y`E`B?nP+}U)I#c1+FM*1kNvWG|a(TpksZQ3B@sD~b zpQ2)*V*TdwjFOtHvV|;OsiDqHi=6%)o4b!)x$)%9pGTsE z-JL={-Ffv+T87W(Xpooq<`r*VzWQcgBN$$`u}f>-ZQI1BB8ykN*=e4rIsJx9>z}*o zo~|9I;xof diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 4d6372eebdb28e45604e46eeda8dd24651419bc0..3c0b81fab2218300de2c5f2959012fc52e289652 100644 GIT binary patch literal 18900 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
    0GvI9M4$*tFo8)Y4~>Eq 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 literal 1443 zcmb`G{WsKk6vsdJTdFg%tJav9_E4vzrOaqkWF|A724Nly!y+?N9`YV6wZ}5(X(D_N(?!*n3`|_r0Hc?=PQw&*vnU?QTFY zB_MsH|!j$PP;I}?dppoE_gA(4uc!jV&0!l7_;&p2^pxNo>PEcNJv za5_RT$o2Mf!<+r?&EbHH6nMoTsDOa;mN(wv8RNsHpG)`^ymG-S5By8=l9iVXzN_eG%Xg2@Xeq76tTZ*dGh~Lo9vl;Zfs+W#BydUw zCkZ$o1LqWQO$FC9aKlLl*7x9^0q%0}$OMlp@Kk_jHXOjofdePND+j!A{q!8~Jn+s3 z?~~w@4?egS02}8NuulUA=L~QQfm;MzCGd)XhiftT;+zFO&JVyp2mBww?;QByS_1w! zrQlx%{^cMj0|Bo1FjwY@Q8?Hx0cIPF*@-ZRFpPc#bBw{5@tD(5%sClzIfl8WU~V#u zm5Q;_F!wa$BSpqhN>W@2De?TKWR*!ujY;Yylk_X5#~V!L*Gw~;$%4Q8~Mad z@`-kG?yb$a9cHIApZDVZ^U6Xkp<*4rU82O7%}0jjHlK{id@?-wpN*fCHXyXh(bLt* zPc}H-x0e4E&nQ>y%B-(EL=9}RyC%MyX=upHuFhAk&MLbsF0LP-q`XnH78@fT+pKPW zu72MW`|?8ht^tz$iC}ZwLp4tB;Q49K!QCF3@!iB1qOI=?w z7In!}F~ij(18UYUjnbmC!qKhPo%24?8U1x{7o(+?^Zu0Hx81|FuS?bJ0jgBhEMzf< zCgUq7r2OCB(`XkKcN-TL>u5y#dD6D!)5W?`O5)V^>jb)P)GBdy%t$uUMpf$SNV31$ zb||OojAbvMP?T@$h_ZiFLFVHDmbyMhJF|-_)HX3%m=CDI+ID$0^C>kzxprBW)hw(v zr!Gmda);ICoQyhV_oP5+C%?jcG8v+D@9f?Dk*!BxY}dazmrT@64UrP3hlslANK)bq z$67n83eh}OeW&SV@HG95P|bjfqJ7gw$e+`Hxo!4cx`jdK1bJ>YDSpGKLPZ^1cv$ek zIB?0S<#tX?SJCLWdMd{-ME?$hc7A$zBOdIJ)4!KcAwb=VMov)nK;9z>x~rfT1>dS+ zZ6#`2v@`jgbqq)P22H)Tx2CpmM^o1$B+xT6`(v%5xJ(?j#>Q$+rx_R|7TzDZe{J6q zG1*EcU%tE?!kO%^M;3aM6JN*LAKUVb^xz8-Pxo#jR5(-KBeLJvA@-gxNHx0M-ZJLl z;#JwQoh~9V?`UVo#}{6ka@II>++D@%KqGpMdlQ}?9E*wFcf5(#XQnP$Dk5~%iX^>f z%$y;?M0BLp{O3a(-4A?ewryHrrD%cx#Q^%KY1H zNre$ve+vceSLZcNY4U(RBX&)oZn*Py()h)XkE?PL$!bNb{N5FVI2Y%LKEm%yvpyTP z(1P?z~7YxD~Rf<(a@_y` diff --git a/assets/icon/app_icon.jpg b/assets/icon/app_icon.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2930f32c7dfef75d368c6b99352f355f5b9da3a6 GIT binary patch literal 100400 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 literal 0 HcmV?d00001 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index e4e2bee..29348c4 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -431,7 +431,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; @@ -488,7 +488,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json index d36b1fa..d0d98aa 100644 --- a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,122 +1 @@ -{ - "images" : [ - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@3x.png", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@3x.png", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@3x.png", - "scale" : "3x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@2x.png", - "scale" : "2x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@3x.png", - "scale" : "3x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@1x.png", - "scale" : "1x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@1x.png", - "scale" : "1x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@1x.png", - "scale" : "1x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@2x.png", - "scale" : "2x" - }, - { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon-App-83.5x83.5@2x.png", - "scale" : "2x" - }, - { - "size" : "1024x1024", - "idiom" : "ios-marketing", - "filename" : "Icon-App-1024x1024@1x.png", - "scale" : "1x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} +{"images":[{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@3x.png","scale":"3x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"Icon-App-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"Icon-App-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}} \ No newline at end of file diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png index dc9ada4725e9b0ddb1deab583e5b5102493aa332..6e1677bd312a38d36cc76c8f223480f2ae2aa1a5 100644 GIT binary patch literal 200312 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 literal 10932 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 DIT*_w delta 279 zcmV+y0qFj$2d4s%8Gi!+006pI?LPnj0Blf9R7L;)|5U~J`u_j-{Qm)0oAmqtj@kOz z^8J|I`-|B6ht~R5kG+%I`zf~eztraM`u^bc{`dO)zUlmg)%x%C`E}6wSI77~z4s`y z^XT{f(eM4n?EUff`e@AgO~UxV*5*r_%Uhbj5N)LaQj!wdIe!-b004GLL_t&-)18pX z4udcZ1u-#g(~z+5JN*AY5?>Gw7hsN~k)CYt4dQDFxbs5*_&e@Hj)wtt(&JE<3Eq*D z;_gQLvqXoKv=I*gWqM9C(Tvu0>=?hTbOp9!6k6AF;>f6|S5%jGEE}TA9h)e`Yuiu8 d7)l?o1NFcJg%EAfM$P~L002ovPDHLkV1gzynvVbg diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png index 797d452e458972bab9d994556c8305db4c827017..e575cdf7171e8b5556888c53c81175b349b1031c 100644 GIT binary patch delta 2270 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 delta 390 zcmV;10eSx45tajx8Gi!+006rnNM8T|0E$pdR7L;)|5U~J0au$Tw)XJ){%+3s=lA~6 z@BMVp`S<<*VaoaP`~U3u{%g(ou*=|m)B4`@{`33)?ezIj#Q6OF|6IuUF}e2O>+>eB z?J{?+FLkYu+4_Uk`r_>LHF~flZm0oBf#vr8%vJ>#p~!KNvqGG3)|f1T_)ydeh8$vDceZ>oNbH^|*hJ*t?Yc*1`WB&W>VYVEzu) zq#7;;VjO)t*nbgf(!`OXJBr45rP>>AQr$6c7slJWvbpNW@KTwna6d?PP>hvXCcp=4 zF;=GR@R4E7{4VU^0p4F>v^#A|>07*qoM6N<$g4^lWpa1{> diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png index 6ed2d933e1120817fe9182483a228007b18ab6ae..00b4f8e95801341408f505bf7b94d0d69ac2ce9c 100644 GIT binary patch literal 4051 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 delta 447 zcmV;w0YLus9nJ%g8Gi!+007oyx*7lg0G3cpR7L;)|5U~J0au$Tw)URh`@-w}Xw3Np zS)Ix4{k7)&ujKrh-TO(x_}20L&+q+}+xr1ilg8}*yXgGl_5RcF{f*iBEV%Z~-t4>5 ziGV;=={^- z?sLQGb)?A{hr$_!z8HbH7kH=vM0x-*R~t>;jsO4v^GQTOR7l6|(&r9>FcgO2dg?%> z;=sK?5%;?Pn^T7LL?Y$@5u?06NuIR*0?Yf$Hf5Afk+lM<^ch*jvO$sU*m9J?JI7eI zGFV6+q|w~e;JI~C4Vf^@d>Wvj=fl`^u9x9wd9R%3*Q+)t%S!MU_`id^@& zY{y7-r98lZX0?YrHlfmwb?#}^1b{8g&KzmkE(L>Z&p6kME1_Z%?`+u)^el0!1<0sd p?Eyu!OMLDifi)An*I;?S-wj=m4RYIt!kPd8002ovPDHLkV1lv^^n(BZ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png index 321773cd857a8a0f0c9c7d3dc3f5ff4fb298dc10..f5f0996fce492c8ba422f4258d53180de9124bdd 100644 GIT binary patch literal 6700 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 Zxpx7IC@PekH?;r&002ovPDHLkV1heUYj6Mn diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png index 797d452e458972bab9d994556c8305db4c827017..e575cdf7171e8b5556888c53c81175b349b1031c 100644 GIT binary patch delta 2270 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 delta 390 zcmV;10eSx45tajx8Gi!+006rnNM8T|0E$pdR7L;)|5U~J0au$Tw)XJ){%+3s=lA~6 z@BMVp`S<<*VaoaP`~U3u{%g(ou*=|m)B4`@{`33)?ezIj#Q6OF|6IuUF}e2O>+>eB z?J{?+FLkYu+4_Uk`r_>LHF~flZm0oBf#vr8%vJ>#p~!KNvqGG3)|f1T_)ydeh8$vDceZ>oNbH^|*hJ*t?Yc*1`WB&W>VYVEzu) zq#7;;VjO)t*nbgf(!`OXJBr45rP>>AQr$6c7slJWvbpNW@KTwna6d?PP>hvXCcp=4 zF;=GR@R4E7{4VU^0p4F>v^#A|>07*qoM6N<$g4^lWpa1{> diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png index 502f463a9bc882b461c96aadf492d1729e49e725..68988996991025d927308d895df13a127d2ad52a 100644 GIT binary patch literal 5970 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 Ap8x;= delta 572 zcmV-C0>k~%F3JRu8Gi!+006nq0-pc?0H{z*R7L;)|5U~JDYo_jSDXF*|5nEMy6F5^ z$M}8I`uzU?*Yf=uXr;5|{0m;6_Wb|A>ik^D_|)+I$?g3CSDK^3+eX0mD!2CP`2NN0 z{dLg!a?km&%iyTt`yiax0acdp`~T(l{$a`ZF1YpsRg(cvjDG_-U$Er-fz#Bw>2W$eUI#iU z)Wdgs8Y3U+A$Gd&{+j)d)BmGKx+43U_!tik_YlN)>$7G!hkE!s;%oku3;IwG3U^2k zw?z+HM)jB{@zFhK8P#KMSytSthr+4!c(5c%+^UBn_j%}l|2+O?a>_7qq7W zmx(qtA2nV^tZlLpy_#$U%ZNx5;$`0L&dZ!@e7rFXPGAOup%q`|03hpdtXsPP0000< KMNUMnLSTZPzBPRS diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png index 0ec303439225b78712f49115768196d8d76f6790..ec76a614409b500c7007012aa272ce981ef2ed93 100644 GIT binary patch literal 10350 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 M07*qoM6N<$g5+5q-~a#s delta 850 zcmV-Y1Fih-P~HZR8Gi!+000iU#^3+|0OwFlR7L;)|5U~J09TtSw)Xt~|5(QO`~Ck( z!T0|D|3<*~RmJ%E{r+;#`2ba!klFf7!uJMSo%Q?vP{jByxcAZE>;OrUCbaZYjJo^$ z{nGILmD~Da$@upC{`C6(Ey4dPw)Pyc^>5DkHoEo!QcuK-Jwl-l}t(fQKv z{dds$V#@dygS`PvhX6is7Z+@*x-d;$ zb=6f@U3Jw}_s+W3%*+b9H_vS)-R#9?zrXogeLVI2We2RFTTAL}&3C8PS~<5D&v@UI z+`s*$wqQ=yd$laNUY-|ovcS9~n_90tFUdl#qq0tEUXle|k{Op|DHpSrbxEeZ5~$>o%>OSe z^=41qvh3LlC2xXzu+-2eQoqs1^L>7ylB$bCP);(%(xYZL1 cY5!B-0ft0f?Lgb>C;$Ke07*qoM6N<$fb+j*hF)m)O;AKZMHGXeXvBpiIx$9_7-J?Tf09o#4A~ba`6VWr8RMUA zGD*Z_%%VmTCq|=!fP}ab5Dfx?Aj&S#^iFT@y{hy7_tti+>eljJzeeYuIf-<=s=J)K zoqM)>t`22@Q8OksR81Owa&C0xbfZSAnSH^Wi}M-`H^4+71Rw(bKk*U2p5LQ-Ol5Gpmj=WkE{bxonQ_pM?vZm&dhZeHIDr_e%Mh>(0B-I-5Xxh9ez3vi0k`admID@QPI+ig zh5?l?vg-DuA*LKJ*Xs{nhh{yVIp#tJM4bqxdDXb)Q;kb^Hovg*G1%I?qU+^7Tl$Xm zn8j9K)Bx9xYaBNGnpq#mGlt)2R?(>%5oPZM2X`j8daXLP?s$YN}9CFZzx@k+e?xz{Bz?Ch7$SkD0gh_Zx<+N@jXb|_-q_okx@WELU%Bna-Os%7NcWNU{=`Pw zkZORlHC1}kb+c|{=Z*iGQDsijI8`z=$T7wr0GYfx1JucDSJ?(PZiS^?ZKa^%FPo1P z^U9(W6HAp&kD4)haw z{Dp;&gq)n0PiE4PYG@XI;N(Y7oIT~|yKeu};zD;{>}rl3;HCxB=hR()#xIR3m|F0$ zKv9h-@80^~4Je@tHR61yU;wq23Hql-ed|5bjxF7Aef#$PVW!81$N_G+@T3cdeEhVB z`!%W;^V&=%YpOjdj!_Ab@D!wzTv`y_6LK1gDv>-A%lTwyPa*f6+%9>~A-jEPu$B4$ z2tfZ99j@E!N7F9QmO`+9c;y8(Up#kp|6R{q(YyW7>#F?90UovB)Op&cPyHk0vvoju zyUSFhB_1hNx)3vjRu7Uy#nKSo$bl;=jcSsFNi_fMO(;}w7|GSdlJFFSkK9>?b{0#N z>(eWy4f(>nCot&sdb0Zklzkk*%1-B{noAausZNqDbUD`x@Zs8T^oP6%!5L*x5rJ=SyoGymzl zbO(6Yl#wI5&#wP+rohm^AQJL;M_1@&iC7V721c4&LGi2DE>>fo9gOvk^xh9q z;$#F4G4*)~>}`}4G`e{5cdDLv`H9}PW4#~`aMQIj7xw}VH_%iPiloMYuYoL8;}St3 zOgnO8lE#W0MwuKcisKe z-QWyx?U=@4{iig1Ofv}!SoARb54aF5$VQ2aNwRNpm0ke_uG1z1J7?A}9yX%!R~<)M z`osVanLFuHQ>RhFXp&H&wSYVo9VzKw6uEKa)37?zCp){}W8~OWV59 z;6NYyM30~XKB|KhRmT|`R7{&n5fcp<@hsI5FiEB0 zH?7fY>T9#d##g*6W18TI^GXyI3m~+sq*Q(k6y%j|8Eq}BnXND(dk%8julQn;r%Z80 zUIf(89vsK{19rJjK- zF`x_+1KyYGgSOr-c)jfiyxQ?5Y;NBJTU+-)FB)KI0Gi2-|9&G$sQ$7+j9gC#x$9#wZ4vL33Xl>h0af>a@msn+YisR} z3RJkYQ9_AIQB!Pb1{h3l&})a+WsdcB+lJ&~R~bf<@}R6K@4vo@5;HO;XrNI)oKZgo z?mmAJOsE_I{W?I#&M)tWD%ukPNCxDY#VA}e<}CQp+?xS4hnvyLI0yY2xioV|D1t~AxrSsjaNk8= zflogDLwM})D}swBo83=zqgCQ!Qw)_MJqjO%7|Gx9G7Qdqnt(b{G^-FvQU*aQ<$wtp zU}ga>88HKXF!yFKbPa@-ZGZ!tq?(Kq?^NmdvZw?Nmd;-cH>|o7)^uzKW@LoQznsi! zn(tBRP*5F75({gC%crr4SE7S0uYz#Joi=PN-92v+r!OcpRkr7Uxu6_Q@*4LG&lDg- z081}g3|FnV4YqV2a@&F_JFXaYuNpU5+u}T+BM_s|4-h(*n;@}IUDtqtnM~j@RLjym z^A|y7wi2CADu-$`&0q%8M=AZ$mYW$_=Jc9M26r#`EG&F%33M2JmY5P@xhcB_(8q>5 zd!o?z2sTs)o5@o0=%v&bqiT!-p<1(L-7m~|Kb%-S1`DhJNdA>u%*u-c2RB(`l;}VZ zBiD0jd1}>Y_}Xdjhg&u+1I+-T41ps0VlY&{yhxo)zKG3qj%-CBM6Ov!@eRDj3*gKJ z%frdl(h)U7p+>J1GYd6K z9@i+y492J#gL@qT^lJb&OuHEFd*gZ7(zzRmJl8H)IoAh_Zkv4_RIrRC+$mO6nw877PZ|-l^mM?oUC+RZ1Djz} z+fL}q_X<^P0SWXxK|^H?oH=|dTt4<}xZU2?`b>@=8c{);0?F6y9Mt0+avJc zo6kXkiwtN8GpNR@#dn=ki!8tqD!_`PTVQ4LtMILte@`DfP=5f~1k)0j#Wm}iT4#EVkE!2zO-S#N_ZpYK~ zqxqkPIb%;n#!0=qN^GYe+mj%;r#jD?FpMx*bmj-(;R7!~zrny{@n?R>=5nL$TooOJXL!i^;$k2Io7s0~MGC3mc}x+4UztftjG2ey=|jISe(2 z2yWYOKP=txq@}*V^xF;DrbIr3yUh-~ZuNI*$?WUt#*-JIH_sydTmr5}1P05&$u%S3 z$}#7{eFxV;reHCTk$G({xE$UveqFRRH7pWcY0Z=*S+!V5>|D;=m_Q$ywg3oq%o=8_ z;UFlca4^Y)Z>;+b-1Ej$fS6I*S);IUl@`dEI;&H z2n17z-2dy+31{GIJJ+P9eo7N!iu7?P=}nWyW?I2JkTwvXtYRkzC(jGUo*~Fs zS|D4c$zy`ty8gG&hXo<;N){BCsd5A!hgui20(ieS#I0az@iN;kX zp`1mVKV~NU8 zk_G|AzOBKgjyGuS;cbuwhB?W|L}?kt7!Vd26o>Zk-d)eoKb>_YHB=45jG+mJDTyI+ z+AwSMiJ<2U$Y-PFVc)ORTyj)8XYyGEb+xn`dg@b~g8<{}o1mU$Ax8k&l#5IFK68<1 z_Xa?XVINmqnB_lj!&Rr;71sb0R_)mY*G;{^I;v@hL6Si=5CxN<5wkF1*l^g@+Y+oTa8${7X@P>X(gqb_n=*7XKQe`xA_m7m4Hz2m>|5JxT@ktr62xkC z6o^Jj4(NI(A`w1+_*MGw)cLm3L`oGyZJ35&62b|!BgFu_HuWfUV*5GX#2NLytF7G- z_F!_2)iHIX0`&dyA!CLLr!iG^ac3!%z$4M+m;swxcMGwmRJ1)QDq@fvqJRi59o}uJ zffFk7F^1ywJ%dTLO+bhJdUqcll+*xB$8iP96V8a)47VoPhlVH}nF|k-T7n2t0p{B! ztzm7fsFBuux<(Kq*xh~zdMF2o>KHO{dhb+q1e7bUbsdI0nUnzyA!g-{m>Xy>$3HUC*Ysj7!+;JJv!vYEEpNbh!pV zoj7Se+;tQt)QlDrEL6IR5#?Tuz+iMmT?*+UNs1^Dc@k$bqHC51!$nX!y5t(o`KAf13xb@7kT*@R=o~iQA8P+4FTdh%$6I01rAs(d;53ql4 zV=$&_7&?C;`N>pMv;W7cX z9BU^Nira>{_r=7V}mC0Z+csdK>;IX#YbY%%ytg7GE-02Pv z92JN->b*+SRje|6PxnzVV9dwC$GggKcC9K?2`AN#gzY`erSGkAd2H^0^Hbaaa{7p| zz)TGo6x`DkiFMc}yS+lm;q*~b%>&aQ8g@NQVwrtR7A0-=4s#W8w6WM(xU=N|6c8xu z8>NIvf&|q$4d#rVPTO8zi|TX4DG^ABxMSMp%nO@l2-`KO_S&faLV!F1yw+G>Nj zEr!@5hdQ~Z`pa+cbvL4jV2p(f;-B~Yji#J-S?Xmc9BB~XU1LwdvuY;Ly1u=bF<7L< z#s!}}IJq8S+a&|8m~;+IsA=NPMP5{a2rgQF&ySZry>}DjGco(zfIwJ1tmp;GuH#fg zcvY5}<+MSbG;uQKLwnZ2$4|RVd3Hf%8+IT=3~rrw9bUQOU&V4W%4BkLz+eQ!3l;SB zbFL9Q0;LGPCIcVS$M$U?g9ufJ(~2d*p{*dn`^Gs zQLG3Oe%0*%J$`fUhhhBCk!Y*DQ<)(hS@}?3Cp>px8|u2Acn(@s7WuYQyO+t+-1|gX zThDL$I2kL4Z-8A+I5IrHWK{P(M}Lp%2(WpAJTkm*>r-_5yz7N8lO_cViDURIL72gp z&b=CTx4s2`XxS`0H2F$qO5BvC_*RH;ED`q>7Y%20vc72+Y+#g+~uyVOn(n7Ip;$b_h8y*1XSr2 z$6mP8eHR+=o7Yy^X4pa5fD^%?U^*vAf^l)8hslY`IIvM=@!5V9a(Mryr|7e1T_ya8 zqx>p^W$?i)AcG2~!!Itm8SdQlF#O<^sK+eb zG5;Un`pNT!mnT2K#m6UVj)k~=;sIK8Sn=A^@J6mvl1Ib+iog_dv6s%ZX^lGR3vo zfINKv<;S7mPSdE#>O3iA>p(lv$sn^f6gmqU?i+##gK7+<5fKv0kn`L7NqfEe#XW=t-wq;b;Lwiw(u+P^3lEU(DqfdV(%-kucr-IM)On^rb$}f zgc*(F;eF%Jgbz)>2u>K%h~gF^?&3@cJ*{-`=ppFnJq8)3LH&?g7}qoo8nhu;!A~ICj&uy|<1|H5F-NB0 zJi!k(K4G0?AQtY}BJ>R*u@Av!0Bt5LeA|Nb!m(>0`Bre0+?mRPQ3QBo(!+%}=m_hB z`*y5>Pfx#8gmUm^X0$vUiI1H$TOgQFS&tLP)mwqVEhl%eICaAsl!1acv1kIUeB&k9 z+H*+ACQO8#C~ob@Ty)YrAWCOL*4-$qJ{dY@Km)(iHX+#YAPue)Du_q2`_a}X;Koxf z0$tC9BgqADi#@z;n>`J2^u%HyMIJOpP-gnKrvhQ%}AD>!6GxpLKL zGM|RbV?rf`O_FUR#enn6IsdqK1H9O}3y?8E^2F=uN7I2yuJqpd)w!iFGrq^> zH63)*gr98qBkaw$3EUk+dm7s>1gMy*1tj>BfO~L6vECl;DJS*dJqqp*MaE$0JeQr1 zn_!_S9Z;yaoF%%xU)el^2lo9HKDK{8%p5-*D$VqCMrFA2yY{3fSaFny-p%3Q4QJJeSDne|;U>_@drIq~{D#`s9?M16&468R-DoPoGB$NNj@gOV);^)@= z3Z5DLFJNkZdrZ{HLw~uDYA}l>;SN|wN=^9Uv%iKO&c2Dk9e4zwvVhGsle~gSvN8A_ z%VhHAJjamB6pna)5jHHc^V17ld9k(Y2;B3x$KX@5t`Mj8V_ihua^%5so>)lTIPl4f z@Ziq1u=4P$0ymF57FRCX=b&W28vasg{=DOX2W|67b4ofZ!Dw!uHIUuItdR$hZboIa#Dw;r^8vdTz_%W9zD z*QXje_{YEe93H*m7C=q66)spS=%`f6UF{YH&XSh662_Xs+>T?8$&ek?V?7?m&DOSW_ z*@lzA4Nt@f_V#zdcQ^b2ZkzXE@s0;!iCfL3*ps>ChAZm%{b~1ZSq(3=ziGMo3{a+Y z+Z(M70vp-zyCR`s)JWZRe5b)sO_K640O)U241)qTufQ+T{Q0)kbk)i8V0Pmq%xRXZ zs+(5PM=F1_I9;>l9Kc|IUkiL^PeIic;bDe|9s%e_VYxJ$3Cjq1x0f|7E!>xpZ`Hb)&>^39iUw6Z_Au z`nfnm6@Rf>_@gPQEqxOy4dYN`z&3^2N4N})8b2>dp_8= zdeCX4{ZrO($%IF#PAje}l!}cMd#1Qy)Fk+~S60x%$+O|~5ff35k!REP8QRWs2RmEg z)(yV{aj{)Q1R|e-0OT&g;=@u6V0-ii)}v0GTi^_X@aV7-Fv$Z#Qr%JT;x<)FOGHTe zP15S1<6_~TYKNaCC@)d zcbB3S*Q~u1EWp&;ERlKC^l$ z923480UkrOz5C>!z{|MyrtJ&ocuFWaMxNi2aUj>&;4~zqGj%dvmB&{)pyeamwypVtUz6HXj_GwVIP%QvhMfrO4okiXOsB-i6v38fwVoCHR*StN({aL z;gBza8LIc-GJ8P`7FeehIGNSq@k3kSo>x}GCuc0=VdlbV746~ir*j(IV- zMsb515u&2#c1UVQ>r8?zjXRFfu1eX5eQ1QyR!>8OV-K>vx(^$)4eNPN<`ojvd6M8=rui zXa5~!4HO}fK$>ja0+lJ4@T;BAz+?MgLE(N=hTSNs2p><8s#}6nJ?#LCJmP|A2FFk2 zJ#N629d~Sc6fU1~7MxHwKBR%>li*;k4VJvH3=B~Bxj~|kga5Vir|{$lZv|b;*wN~k&`#ySfJLkBhC|#lQnGcJ zz7il*sG5R1w8m5;0-AzfkPEeI6>AO4?33j})Eo}mD&oJV0Q5fuRgmDQ5PjPP`Ljw! z_b3cFtFgXgAN=z(_rskRd<0CU+5Soe_`%DM!}5b$z%diK)g-z6#J=^b(OjhwMdwth zJ4*$XV%7^~7_qX{TGwa<(?B{MfI41?Dyu3TUr3G>=%+i@z^RR6;bSu{5&4(zc^SU- z;_t;-B9Yeg*-qjsRoW0pomNq((vVUn!<$>SipjEEUnIn2a_Eq*!IKa?BolZK)punc znd~CM=byh%IMFv$4ui#ixfgnwbYd~Zszs+FS?+Wb6g)(fG5g|xxnT+8>Q4iz5_BG} z)jL-c&Or|6S*~d|*r4br?h)CeA$;+#%d8thImc{jMNuEmzFk@;q>V%`rlEx39F9$|+_@7Q;w2kSjK(Fds5o8`*Vb~>VW*FmXdzG$xqwH3y?wb+c@ z_d<6Q#>_}ka%ROhE^fY;y2|+~uF~|pZg%$`g!_i%_jVm_ zYt|dIAuWgPYun@tudKh|CpiOMdFovtlBgw%XkNNVDKperZ!h^6405b23jyjV+p0Ie zwBtE*RwHSIm=sj|zdHuUB`Pd%FwRX6n=s7sAg{SjQFW*eYfF zr79f@A0H7qClwL*v_Xt)`=}_}D&I4VFv*1_8Irs<2hIDWTy-Sm)lA`0**q+#y`}da zINDx&tgx+XD4XZ{QVxQV-=}g6A?WATL-k9+%UtDiiJg>WWB#w;;E)77km{HYw*KJm zecgIKmm_Ug%VTin@VPwVj^&l;HB#Oqql5u;Csi<|G;oe@aTK>Cg}n0xfeDEzsHj{a z!eAY}maFW)A=c$w}l6180X+6=Pr;xE(Iv z4;l_{FBRaa_!9d;C8rKPypz|pZ{ab3OwMfS-@I=HI0M|ibbZ<_ZW7Wc}##)e~$y!0>H$IkvcLanlV zU6%er3s7`UG3Sj31?0&co2*Q@ltPXJpcl-}{L?#r=>B#dx;O24xv6#L@7o&m8%pyQ zjhAMzgfInzF^FlJ_O?)kly%18&zHlf!~J*f+rD>?^Umk^-t)`nzlNVV{{pDYP8a~H z{{wFm)Z}q{*Ru7u%kO*x+`X^)K>eRzUv%uUX}>cWRyY=>a?I$#UKL52tT@McUpdEH zOp5SoNK0wBO&b~iYjoGG-v9Bv2aa^gX+lR=+Mayn(fX0YZtt2kW(g9hg0BCUgtYe9 z<3^s#RFAlqiCACla|b@P_o?;kg5Q;AliKfl?)EA=cSvs5*e?+w6T@s`*gW`70sBP$ zagFgc06TSbnaS@wV;otL3pDDlSh@W(tt+=Ypej;1z+{?c?*lK~GOXgfeAo1mUoiOK{SYvv*g<2t`JNfjRrCDbo7z`yxj(AB$Vi%Gm}d8{*ZuFh zcTeBcec9xn_5m9XZ)4m_8PY@mD07AJ-v;qGOAzcfQ&qv-QT6za>yJLWd1GAq*ifll zjoI?#%MUdiY~3{E>RJEZI!t>v_XM$Q3Yw+xlGVr|4_l9PmI6uSwk+qU%0X&V+r{PQ z=svNuvvy+U-IpZ#Q!{%Tn{y9z{9^5wyPG>(ld4P=3%7Oio}Cr1A70ov=fq3Rg;T%M zUZI^wjC8#KU}z|-kt5sVnVQucY{+Umn5#2tP>%zk;wcQ?su6cPY5am@I#$!t{YP`f z*8kJIY4@hI^`#DW+~3=0_Wyay3jNvFo*aF~iKpgHZF*nNl)6j$sw-w28XKxQkvz~r zIxbApJCJd-PHOOAA!d)>oqMrnN83aF>vsOJWA~x`#gS-E1>%_T7jh<1}BkG!R8aCzf1s;G-Gszzi#WoJfzCi%))yJ-59@>Yq5^u{S zdk;kW*;kKi>9Tmwpf(L{6heAs`|5Q0N|>OTMUK0}&)=ej1lhk)iYJRw7UzaZ?&gVT zU$xClAWdUN71O)A4mR%{TRXgG|N2*-YRqR2wY+)Y(9!-}$+P_b0`!j>cW&I-AOHXW M07*qoM6N<$g5+5q-~a#s delta 850 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 VaRJ^sV37a-002ovPDHLkV1l@jv1$MS delta 1669 zcmV;02739*iUEoZkQsji0000A6ZQZA009nAOjJex|Nm6Q_$jvb0au$<#rXaH|N8y^ zQpEUJ#`ydG|NQ;_MZov||No!f`T$m%O2YR8S)5G5_fNz4Dz^3jOq3U}_WS((M}@Yk z;`?&X`2bOuN5J>Y?fd{!nE+Lo((wHUTb+Q@`6#pUD6sD;y#9YIxAv&v`Mztj67_#M}W?l>kYSliK<%xAp;0j{!}J0!o7bE>q9${Lb$D&h7k=+4=!e zk^n+`0zq>LL1O?lVyea53S5x`Nqqo2YyeuIrQrJj9XjOp{;T5qbim@3`g_s&+~)8ls^*!x)`zdpdY-=YnCk%m00lQm zL_t(|+U?tUa}qHa2JnR4kliI*uA+j)6UD1)OFi*ETJQV%{$Ip5$s+2qo3ygS8z#>m z?et*gH}8bxX(+_U$H&LV$H&LV$H&LV$Hxan>XspRB4U4H8%B_X#pL9(-DAe=?lxbL zn4kzt$i;BbZJW~VzWvMuj!}d81nq`gg>p*e2NkYA@GN1YiDh|6Gw(*BF%cp&Be(gX zh#G@w&WzjwGdGfPnQyMCnWHi%R%?St%A61kVKKRFVWttqViszzvzjv5X|-c zm@zaZZKi)+qnRQzUj#F?*z24%3$p5HbLiBZXT8pz#LneF`X%Le|F96ZWpC% z*y~(0yVyl3o-QhEyC_H1MeELXQR-N)gLBZi{tUg&X0O9#cCOdCX!c#SvgEs{bG^>h zF1n7r&YS-$Vx8|radlC?4pV9qGj=2)TNH3 zd)=De<+^w;>S7m_eFKTvzeaBeir45xY!^m!FmxnljbSS_3o=g(->^wC9%qkR{kbGnW8MfFew_o9h3(hlliq#y*k%L?ae&uM%r9~`WdMAb1z!Ao->&16_|m*l$u+1QK8LY zXlZ6dr5bg}ZF-%(nOABChLC$mp25+YHAH`@x%pmHo8eb&*M=ZfMX6zY#$Kn*Jmf2$ zvreql7Ut7$BO7yQFju73T{LGtQX53vcy?=Zo zHfvr{7d5Nf1$*7xT)3{Q>Z0bev3cR|y-sNJ4Y20b=3ZBQG?%djW?+`9`KruHoHy&e zZjqWJn6qN$%=%0zP@^#ald&?709l}3{n}8;cO)amh(2@f?4yUIhhuT5<#8RJ zhGz4%b$`PJwKPAudsm|at?u;*hGgnAU1)#a+M1!Y z8r8G$I;eBz#}Br*f1{D9iB^x&oOv=)XsHhHspEguV4m*Wd;BL_tNYkabWqRD_jjfl zdQ+pddIPOmnm;^vG6nSZ7MLY@uD(JNRO&RHnV;_f`rZW0N<3G05UNKrvqVp}h7yWi zT^nWQohOsKK}R|BYHUsQ%twE;(?E+Csc|*c zI3X_|KL|iP1ae`rri!IKjRpDH4}ct$CTpr#+S8bjOV?T*16rTxSZ2-0n2}3|&1zKr z8q@-29h9okwI@&$sSX@d_oTYxmRjsX-G7DWY)njQlufl^X|R0S1%7@*Ig_94T#c1clMD4o&%5 z%oM8r?hVuZp9_p#vJFSBIgQO4?|%zb0d6 zShjiCKb16}b!F*C?GJT7yZ(m*9o@Y$t%$%Xri__dd-Kefvn9qDMm6h6#xVx+Cg+n0 ziw57kaxK^uY*z&Mxm!6es?`r3Le1VnLRa6bTZT2E8H0#~OB5&-rb~58jxY85m3f7Z@4_)ZTQ) z0;(uH$tDFMjB%=*JceBw=`@q{zDC<*|KrJA&y#`#al6j%fjQBieW)#-S^wR+Ki%`d zTXz5#Smm@a)9r@Je+X*}f1(P^ATL16w|qk%6u1ZAOR^Poa{4PzC_L(h1uBA;c)j$; zkz+I1KdJKO%1NW2=-Sw}*%4UT`Az=}1g4FF)0PQ_qFYFv;+h&Rs9*%2(?ef53{A5@ zPQ^ESmEg#?ns_=~%MfgY!qN**xV39z+YMGpZACdYR$m|rD>Rr)XAlLF9Fi$S8iGE4 z5;^W2F%YaMV7*xd)*$JkcstCu%3F}i$GR3a*jRIMVOhz|R>6rQPP2i^-KQd{u*w>b zMp#_KJo;=(gZm1bpuC_MN(+mj(69gugiIz4hm*Z9KpD5MBS{P}AQyyMOnRPG!U8Ta z!E`BzUTG7CCz?l{VkIWlpXKHu>X7Z*`w_?~7LXb?)R_sm=(w41#)!#qYVA0vEldOv z1KoKuK(M2;9a=g*ho$YUuypSRNZMI2Jx%kSv!F$4A0{@`_+3d@AU+ry&EsoMx6Cn> zC)!{LPF4d9B|*rbRqfEjj@DpuHwc=`kA)kX&WH1ln+}C&fk`&l2Ks%1iP6!j#;Q@! zRCNqoK5hoQ)2jfJ`suO z)~wn_c;@Uop_ut74gONma0Mem2$kPteGRBCE`#UKxf`xnazCs*yv@E=ln!qnm8oCknts9k|K-`yu)2Cwe=5cVazl>tD# zR8n4-FIn#7naFbskgiGtH4p?{?fO>Z9H=ZRM~)!H#fd`8fDQMoeg+nPvE04l0Wsp6 zCxnnh8{XK{f^Y3^q1&1-qC2Kuil)C50y#rrrS#ImBKS`8JovYd7ezvMP|}t`E&95o zl7qWdW22Q#XPk0)M7Vx#DZ5Wepy7*Pp-ERZoC^lo0fdmddE;+=|1o%B&sv8SzPltl zXFp0I-~*d}PknZZ?wfXn))O+Y$>#pXxU=BLEiXYjOJIk*zXJWoWHe$=2DYuf!`)e^ zFH-{7p(11U6sFfTL48S;OU^4>N1QkSJp9>P@M7DCZdqH<74B!1jvWvm-t-QgQazR~ z88=gj3C{=2&NN&HLZ;*_KsU0FX_G2B|`K98sT9B06>WScdPNWvGN}5W=%t--A9%yQb)X z!aR|=7bW(ldtuS`)o@kQxdMU=_hVP5)Qok?N6}p>c@rr%llC@)S-B}gma7-x&=F4n zU3|k~o3QQ8d__~)v9Ty3ivVx8wYWY#2TYC@^mru=aB*7;UDGr-NRNew%yZ7B%8`(D zd|H?cskiFYAhmK0Fa zCHc~##D1T2v@r)yj)#;q*|idYnnVSZfCWh=wmh^rwkksa6*bkSC9xR6B0Rzv{ymZ& zI{GHXAc_<7v!HEV$vzsiGXREW_ekS~<;8ULcR8AFX|e#O2!>M*DuedaVTo4mY(t%u z5LgcG;X)L$BMSEmrx|Lb0XD;3wo3>7ju7f>7^dUn!o!W|j&@FsFk-j=+-PY-UuJ+u z*c|ktb4L?>C;3L?>*x1{bw~C>MQo=yh#zuS_ zk*E$uzi30~PY*(Oy5G}gT`DfELG$4FRf|_S#fpfWwC0MOem~QIwyp!vR6AC?Y07-S z$u-B(2M4!AZmO}D_?>U&1DsknhDBVAJC3ls`ykk+31~-bCNy6xDsbcxlzc6UQ-2A+ zgeT)Pf^Mcw_-W^EYOEa_)>sUfVzA=kF{i@^2et^Qv=E`CPcmm-MnIR2oh~3 zt%rS?0Y4LvVS?z)-6_z48>Y9x>}(u+xms}e1;DZ$A)>puBWNmN{A$w@xV&MGvj>Ta z!yzu5GvNBk^I%wMDcrvLS?Hpq6Kz6^@_m*Kbp_@0tJ&AW+|koe*m#V}%P}Pr9^bSC zc+n=<*Z`pl<7W--Ia&{xXW5Yn;wKYBD|P-@LESpuw)2XT9s{jid*IcbYw2s_rXzSN z9tMFf9p(RC2AmZO$vWq?ko9SxU{n-14B&4IGQ1QPlu z1=Tztz}mf=VA;OS{w@%Or&U4>ex5@_j=q7D`yha944Y((U~&tKtG;VM_pN^s&L1-c z3eAGhc-bu2zxt9Y_}6Kd!97zi#V<4c)Y07uCOflMUJX@73EDz5#8@`i;%xZA>gU}I zMUjWf>v1=dgX<=_H8p1^UaE&Nsao>Zk`4=Lb4>wX1Q`Rt&i;e&%$8+vZS%a)B1OT4 zOqDT;2sH%>tgT6S=7_Fkog&o=5e6XgGh$&$k#p|N1h6@Gl=c z1^+wm_7KDf&p|%c;(wr_W^<{E=qyoPKU!AuVzU z-i7sib6+d`cFRim`=(iuyRJr|SDXy!sz9dvnYXuhwZIEI*Mh3PuN&k0c@P8z3OPy5 za$$;q@V$-F@MJ>}kOWWMg&T0!y5G=Q$23EIS&hPj1do^>LJ1>t&O@~5*q!QwJ60|9 zDhg^;ujG^YEnbEjLUgfQFdWvIyv`luXmSzmaHC2KMNLj_yQ!Kg8&1){kFvk4ka-<5`nVB{MEJ=cxBsa zxTImakeGx{69oKuCK^m70m=W*M_2CI2v2T(-vg;RfHg0c5Z?#Q1(T%vo^Q1r7nnn0 z`R+@`GvE%tW%biAwSGK|t*G+=3u?{;Y8=0}+(XG;xZ%A=-I4ij(*;um&yJp_ zQe^Q{;Q4BejDQtMlu#QHE_-=hXC?`^z5f&~y6{e4_EAV743i-Qx+R#qS3L{620Hb8 zs`w839dQz^kVN(wFBYd2r#Lby_u+;zgllRw!uQ4d|A>!mSWMqO={yw21ObL3hfG9X zUfQ}Ap5MMkql7`W5Gqc_zn!YQjN&~sps1vTCEsd|388uUO|lqeREBW*X%NCa>t2AF zBPY`2y0HimP0}x}t?MA%xbn#`G7qiU#a2fjw!-`}dOe3aZHpf~b}W+@f#lM2z>C-7 zlUiL^7yAql;rCWNMN7W^@9ce8t+2w;j^LJ6Ps2cF(65jNa|QikgWpgCmebwjRLSCj z$W_0gIdtuwpc2i8d0=S!qt2bU;N#cn_NfImbmf)&o55aqv`~)hC63Qr< z%4F&4B@5tiWuHtYz4V&#ZiqT$`3l6f+~U6%5NC`>iu`lCaJOyk{Sxn8 z^Bn#1%AKVRGZnH^>0xdKXV) z0G|H*eVpGg17?hD_9G^|%LzyMVrCT}$^Fndm%4t2SKhIQo*Jo|Dl(zs z9#^3t(74yrKyc$9o`B_-{s^jy6OIoVNT=Z2%N~Ig+TO3BL>DAGYY?M7a0d?33WLnT zy_Z{E_-M;JtIerPw{_Q|P2@@3eP(Kr6^hQLr%~(;U_&a6Z|(aMu3P#ryl}y7fM~+a z?>zyZbnOYtL$i?Jn^YDeRg%w7cju=jVlNB5fG%o*K>_#+=lnUE)FrF9C_=c77+7!N=Kaf!3nr$r1{8_4B#=uy z{0fDM$k+WeIO6+UQ}-RDWT*KZbUVV94=~SBKrA5WO@8*LRaRfy!TpJY{VV$FOU^|K zAD-1`|6W*Tn&xPhY(A!z0Fmh;dH+!DkqaaU!onlXExZ?ERXW4Pmzfe;aY`!XC#cg&d znwib?q<_8klEJov`}nm`#WlHZ$J=GYO8+tWH_eY_5R3Grf{BKZcz8Sqa3Q~Q9a=(1 zI?@|2@#s}LTnI!^gxHNQwaia$+OtmV7WtLU-gh@HOdROlQuy`B5B67;OmWw4_>+kF zp^FM~R56YgF39!xBN3Eth>)UkK*gS8@&2Br^vb6o*)pV=&f+M5i4T##)bW{bK~>*4*+?@z7T_B-a+ zRdlq!6rAo$rn_Ekc@$s&^s$nL5hqv^kC{vLWfRk7)@ZZ5WcXkvTOgfY46TrxE6Tgd z0>DKt7e(Q4c<`O)aO%&XFnh@Wf=nJj#7pdLG_nlCroQBXk~AL3?&)kzZ`|{C|MvDx cxgOO20Wu&&ON06bMgRZ+07*qoM6N<$f`+K^)&Kwi delta 749 zcmVg;Ps8|O$@u8^{Z_{KM!@$5TAfS6_e#O{MZfpz`2O`0$7~@NRr(1{THzH08y3x{{PYM{eL;T_A9^tcF_4Sxb`8l z_9V3RD6;a(-0A^Pjsi!1?)d#Ap4Tk3^CP0(07;VpJ7@tgQ}z4)*zx@&yZwC9`DV-b z0ZobH_5IB4{KxD3;p_6%|f=bdFhu+F!zMZ2UFj;GUKX7tI;hv3{q~!*pMj75WP_c}> z6)IWvg5_yyg<9Op()eD1hWC19M@?_9_MHec{Z8n3FMs~w_u?Av_yNBmRxVYrpi(M% zFMP21g+hmocQp3ay*Su=qM6He)*HaaTg$E^sym`(t%s3A)x!M+vfjXUBEpK6X9%iU zU!u9jj3(-$dM~sJ%Liy#?|+!6IY#MTau#O6vVj`yh_7%Ni!?!VS+MPTO(_fG+1<#p zqu;A#i+_(N%CmVnYvb>#nA{>Q%3E`Ds7<~jZMywn@h2t>G-LrYy7?Dj{aZqhQd6tzX%(Trn+ z)HNF}%-F{rr=m*0{=a;s#YDL00000NkvXXu0mjfwJ?~` diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png index 8953cba09064923c5daf2d37e7c3c836ccdd794b..a61517251feb867b13187d61546ebd0ed9ea734b 100644 GIT binary patch literal 14219 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 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png index 0467bf12aa4d28f374bb26596605a46dcbb3e7c8..f508d4a103f4bf3fa29e75c2579c522b4b1f67ea 100644 GIT binary patch literal 15959 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

    $7Kfj#bY#R60u0MJgatHrG+DR5HUM zV5|V|Fa#wI-*Mr#VWbqU>U?+OmhOk@-fF*Z|BBa^b#?XUsvV~)0b*8HRPk53&x>Rm@)w9n{2JD*nSsY03!&O{!6;!QO!FIm624~6A+RGQ-mDi-y$ zsIu*5=E_b|=pcz#j0g67EJ3B@oqRMD-a2}I-$Ft};ipK~?A&ekH*dg+Mn88H$YEf?7TpJmESLA!R9VTi0 zDkRb=GI3UHue|~sgxV=#tc1;O!z*|6;SN_uAyODN>swOCz_E7TYlN8boaQs;n2uAy z#y?vlXl}RS%!oH{nH{d|L^Dqgd}6tT``0)tY{Qds= z{r_0T`1}6fwc-8!#-TGX}_?g)CZq4{k!uZ_g@DrQdoW0kI zu+W69&uN^)W`CK&06mMNcYMVF00dG=L_t(|+U?wHQxh>12H+Dm+1+fh+IF>G0SjJM zkQQre1x4|G*Z==(Ot&kCnUrL4I(rf(ucITwmuHf^hXiJTkdTm&kdTm&kdTm&kdP`e zsgWG0BcWCVkVZ&2dUwN`cgM8QJb`Z7Z~e<&Yj2(}>VI$fQI%^ugM`#6By?GeadWcu z0gy9!D`m!H>Bd!JW(@avE8`|5XX(0PN}!8K>`dkavs;rHL+wy96QGNT=S@#7%xtlm zIW!++@*2zm-Py#Zr`DzqsLm!b{iskFNULSqE9A>SqHem>o31A%XL>S_5?=;V_i_y+ z(xxXhnt#r-l1Y8_*h`r?8Tr|)(RAiO)4jQR`13X0mx07C&p@KBP_2s``KEhv^|*8c z$$_T(v6^1Ig=#R}sE{vjA?ErGDZGUsyoJuWdJMc7Nb1^KF)-u<7q zPy$=;)0>vuWuK2hQhswLf!9yg`88u&eBbR8uhod?Nw09AXH}-#qOLLxeT2%C;R)QQ$Za#qp~cM&YVmS4i-*Fpd!cC zBXc?(4wcg>sHmXGd^VdE<5QX{Kyz$;$sCPl(_*-P2Iw?p^C6J2ZC!+UppiK6&y3Kmbv&O!oYF34$0Z;QO!J zOY#!`qyGH<3Pd}Pt@q*A0V=3SVtWKRR8d8Z&@)3qLPA19LPA19LPEUCUoZo%k(yku QW&i*H07*qoM6N<$f+?)ciU0rr diff --git a/pubspec.lock b/pubspec.lock index fdf14cb..d523831 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -25,6 +25,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.13.4" + archive: + dependency: transitive + description: + name: archive + sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff + url: "https://pub.dev" + source: hosted + version: "4.0.9" args: dependency: transitive description: @@ -129,6 +137,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.4" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c + url: "https://pub.dev" + source: hosted + version: "0.4.2" clock: dependency: transitive description: @@ -270,6 +286,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_launcher_icons: + dependency: "direct dev" + description: + name: flutter_launcher_icons + sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7" + url: "https://pub.dev" + source: hosted + version: "0.14.4" flutter_lints: dependency: "direct dev" description: @@ -480,6 +504,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" + image: + dependency: transitive + description: + name: image + sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce + url: "https://pub.dev" + source: hosted + version: "4.8.0" io: dependency: transitive description: @@ -680,6 +712,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" + url: "https://pub.dev" + source: hosted + version: "7.0.2" platform: dependency: transitive description: @@ -704,6 +744,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.2" + posix: + dependency: transitive + description: + name: posix + sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07" + url: "https://pub.dev" + source: hosted + version: "6.5.0" pub_semver: dependency: transitive description: @@ -1005,6 +1053,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.dev" + source: hosted + version: "6.6.1" yaml: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index f71302e..f5c0d9b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -45,6 +45,12 @@ dev_dependencies: freezed: ^2.5.8 json_serializable: ^6.9.4 riverpod_generator: ^2.6.1 + flutter_launcher_icons: ^0.14.3 flutter: uses-material-design: true + +flutter_launcher_icons: + android: true + ios: true + image_path: "assets/icon/app_icon.jpg" From 8df03f42a9ecdef09d7bee4c1a0da5b0dcb47824 Mon Sep 17 00:00:00 2001 From: Ilya Kiselev Date: Fri, 15 May 2026 11:51:12 +0300 Subject: [PATCH 002/145] fix: show zones in all time modes, fix degenerate geometry, update mock coords --- lib/core/network/mock_interceptor.dart | 81 ++++++++++++++++--- lib/data/repositories/zones_repository.dart | 58 +++++++++---- lib/domain/models/zone.dart | 1 + lib/presentation/screens/map/map_screen.dart | 4 +- .../map/widgets/parking_card_sheet.dart | 5 +- .../map/widgets/parking_zone_layer.dart | 19 ++++- 6 files changed, 133 insertions(+), 35 deletions(-) diff --git a/lib/core/network/mock_interceptor.dart b/lib/core/network/mock_interceptor.dart index bb12d51..c02dd72 100644 --- a/lib/core/network/mock_interceptor.dart +++ b/lib/core/network/mock_interceptor.dart @@ -29,11 +29,11 @@ class MockInterceptor extends Interceptor { freeCount: 5, pay: 0, ring: [ - [37.611, 55.7515], - [37.613, 55.7515], - [37.613, 55.7525], - [37.611, 55.7525], - [37.611, 55.7515], + [30.398, 59.733], + [30.402, 59.733], + [30.402, 59.737], + [30.398, 59.737], + [30.398, 59.733], ], ), _mockZone( @@ -42,21 +42,41 @@ class MockInterceptor extends Interceptor { freeCount: 2, pay: 100, ring: [ - [37.621, 55.749], - [37.623, 55.749], - [37.623, 55.750], - [37.621, 55.750], - [37.621, 55.749], + [30.406, 59.739], + [30.410, 59.739], + [30.410, 59.743], + [30.406, 59.743], + [30.406, 59.739], + ], + ), + _mockZone( + zoneId: 103, + zoneType: 'parallel', + freeCount: 0, + pay: 50, + ring: [ + [30.403, 59.735], + [30.403, 59.736], + [30.412, 59.736], + [30.412, 59.735], + [30.403, 59.735], ], ), ])); } if (method == 'GET' && path.startsWith('/occupancy')) { - return handler.resolve(_response(options, [])); + return handler.resolve(_response(options, [ + _mockOccupancy(zoneId: 101, freeCount: 3), + _mockOccupancy(zoneId: 102, freeCount: 0), + _mockOccupancy(zoneId: 103, freeCount: 1), + ])); } if (method == 'GET' && path.startsWith('/forecasts')) { - return handler.resolve(_response(options, [])); + return handler.resolve(_response(options, [ + _mockForecast(zoneId: 101, predictedFreeCount: 4, confidence: 0.85), + _mockForecast(zoneId: 103, predictedFreeCount: 2, confidence: 0.71), + ])); } if (method == 'POST' && path == '/routing/search') { return handler.resolve(_response(options, { @@ -157,4 +177,41 @@ class MockInterceptor extends Interceptor { 'full_name': name, 'global_roles': ['user'], }; + + Map _mockOccupancy({ + required int zoneId, + required int freeCount, + }) => + { + 'zone_id': zoneId, + 'zone_type': 'standard', + 'capacity': 20, + 'free_count': freeCount, + 'confidence': 0.9, + 'pay': 0, + 'geometry': { + 'type': 'Polygon', + 'coordinates': [ + [ + [30.398, 59.733], + [30.402, 59.733], + [30.402, 59.737], + [30.398, 59.737], + [30.398, 59.733], + ] + ], + }, + 'observed_at': '2024-01-01T12:00:00Z', + }; + + Map _mockForecast({ + required int zoneId, + required int predictedFreeCount, + required double confidence, + }) => + { + 'zone_id': zoneId, + 'predicted_free_count': predictedFreeCount, + 'confidence': confidence, + }; } diff --git a/lib/data/repositories/zones_repository.dart b/lib/data/repositories/zones_repository.dart index 99db93e..dfcc231 100644 --- a/lib/data/repositories/zones_repository.dart +++ b/lib/data/repositories/zones_repository.dart @@ -26,11 +26,30 @@ class ZonesRepository { } Future> getZonesFuture(String bbox, DateTime at) async { - final items = await _forecastsApi.getForecastsMap( - bbox: bbox, - at: at.toUtc().toIso8601String(), - ); - return items.map(_mapFromForecast).toList(); + final atStr = at.toUtc().toIso8601String(); + final zonesFuture = _zonesApi.getZones(bbox: bbox, view: 'map'); + final forecastsFuture = _forecastsApi.getForecastsMap(bbox: bbox, at: atStr); + + final zoneDtos = await zonesFuture; + List> forecasts; + try { + forecasts = await forecastsFuture; + } catch (_) { + forecasts = []; + } + + final forecastMap = >{}; + for (final f in forecasts) { + final id = f['zone_id']; + if (id is int) forecastMap[id] = f; + } + + return zoneDtos.map((dto) { + final forecast = forecastMap[dto.zoneId]; + return forecast != null + ? _mapZoneWithForecast(dto, forecast) + : _mapZone(dto).copyWith(hasForecast: false); + }).toList(); } Future getZoneDetails(int zoneId) async { @@ -74,6 +93,24 @@ class ZonesRepository { : null, ); + Zone _mapZoneWithForecast(ZoneMapItemDto dto, Map forecast) => Zone( + zoneId: dto.zoneId, + zoneType: _parseZoneType(dto.zoneType), + capacity: dto.capacity, + freeCount: (forecast['predicted_free_count'] as num?)?.toInt() ?? dto.freeCount, + confidence: (forecast['confidence'] as num?)?.toDouble() ?? dto.confidence, + pay: dto.pay, + geometry: _parseGeometry(dto.geometry), + isActive: dto.isActive, + locationType: _parseLocationType(dto.locationType), + isPrivate: dto.isPrivate, + isAccessible: dto.isAccessible, + confidenceLevel: dto.confidenceLevel, + occupancyUpdatedAt: dto.occupancyUpdatedAt != null + ? DateTime.tryParse(dto.occupancyUpdatedAt!) + : null, + ); + Zone _mapFromOccupancy(Map json) => Zone( zoneId: json['zone_id'] as int, zoneType: _parseZoneType(json['zone_type'] as String? ?? 'standard'), @@ -88,17 +125,6 @@ class ZonesRepository { : null, ); - Zone _mapFromForecast(Map json) => Zone( - zoneId: json['zone_id'] as int, - zoneType: ZoneType.standard, - capacity: json['capacity'] as int? ?? 0, - freeCount: json['predicted_free_count'] as int? ?? 0, - confidence: (json['confidence'] as num?)?.toDouble() ?? 0.0, - pay: 0, - geometry: _parseGeometry(json['geometry'] as Map? ?? {}), - isActive: true, - ); - ZoneType _parseZoneType(String type) => type == 'parallel' ? ZoneType.parallel : ZoneType.standard; diff --git a/lib/domain/models/zone.dart b/lib/domain/models/zone.dart index d3d2bd0..b8b09d7 100644 --- a/lib/domain/models/zone.dart +++ b/lib/domain/models/zone.dart @@ -18,6 +18,7 @@ class Zone with _$Zone { required int pay, required List geometry, @Default(true) bool isActive, + @Default(true) bool hasForecast, LocationType? locationType, bool? isPrivate, bool? isAccessible, diff --git a/lib/presentation/screens/map/map_screen.dart b/lib/presentation/screens/map/map_screen.dart index 420ee85..30e1f4d 100644 --- a/lib/presentation/screens/map/map_screen.dart +++ b/lib/presentation/screens/map/map_screen.dart @@ -426,11 +426,11 @@ class _MapScreenState extends ConsumerState { mapObjects: mapObjects, onMapCreated: (controller) async { _mapController = controller; - const initial = Point(latitude: 55.7558, longitude: 37.6173); + const initial = Point(latitude: 59.7390, longitude: 30.4020); _lastCameraTarget = initial; await controller.moveCamera( CameraUpdate.newCameraPosition( - const CameraPosition(target: initial, zoom: 13), + const CameraPosition(target: initial, zoom: 14), ), ); _fetchZones(); diff --git a/lib/presentation/screens/map/widgets/parking_card_sheet.dart b/lib/presentation/screens/map/widgets/parking_card_sheet.dart index 4b2249c..2aec190 100644 --- a/lib/presentation/screens/map/widgets/parking_card_sheet.dart +++ b/lib/presentation/screens/map/widgets/parking_card_sheet.dart @@ -63,7 +63,10 @@ class _ParkingCardSheet extends StatelessWidget { _InfoRow( icon: Icons.local_parking, label: 'Мест', - value: '${zone.freeCount} / ${zone.capacity} свободно', + value: zone.hasForecast + ? '${zone.freeCount} / ${zone.capacity} свободно' + : 'Нет прогноза', + valueColor: zone.hasForecast ? null : AppColors.textSecondary, ), _InfoRow( icon: Icons.payments_outlined, diff --git a/lib/presentation/screens/map/widgets/parking_zone_layer.dart b/lib/presentation/screens/map/widgets/parking_zone_layer.dart index 783b0a6..c2aa7f3 100644 --- a/lib/presentation/screens/map/widgets/parking_zone_layer.dart +++ b/lib/presentation/screens/map/widgets/parking_zone_layer.dart @@ -8,18 +8,29 @@ List buildZoneMapObjects({ required List zones, required void Function(Zone) onTap, }) { - return zones.map((zone) { + final result = []; + for (final zone in zones) { + if (_isDegenerate(zone.geometry)) continue; final color = _zoneColor(zone); if (zone.zoneType == ZoneType.parallel) { - return _buildParallelLine(zone, color, onTap); + result.add(_buildParallelLine(zone, color, onTap)); } else { - return _buildPolygon(zone, color, onTap); + result.add(_buildPolygon(zone, color, onTap)); } - }).toList(); + } + return result; +} + +bool _isDegenerate(List points) { + if (points.length < 3) return true; + final lat0 = points.first.latitude; + final lon0 = points.first.longitude; + return points.every((p) => p.latitude == lat0 && p.longitude == lon0); } Color _zoneColor(Zone zone) { if (!zone.isActive || zone.geometry.isEmpty) return AppColors.parkingUnknown; + if (!zone.hasForecast) return AppColors.parkingUnknown; if (zone.freeCount == 0) return AppColors.parkingFull; if (zone.freeCount == 1) return AppColors.parkingOne; if (zone.confidence >= kConfidenceThreshold) return AppColors.parkingFewHigh; From 667f886ee1bbb107f666f2f175e1d06bd59c3e2b Mon Sep 17 00:00:00 2001 From: Ilya Kiselev Date: Sun, 17 May 2026 16:44:30 +0300 Subject: [PATCH 003/145] fix: 422 on routing, parallel line direction, zone count labels with clustering, green filter style --- build.yaml | 1 + lib/presentation/screens/map/map_screen.dart | 29 ++- .../map/widgets/parking_zone_layer.dart | 167 +++++++++++++++++- 3 files changed, 187 insertions(+), 10 deletions(-) diff --git a/build.yaml b/build.yaml index 5d6aeda..ab5baeb 100644 --- a/build.yaml +++ b/build.yaml @@ -4,3 +4,4 @@ targets: json_serializable: options: field_rename: snake + include_if_null: false diff --git a/lib/presentation/screens/map/map_screen.dart b/lib/presentation/screens/map/map_screen.dart index 30e1f4d..7b4c345 100644 --- a/lib/presentation/screens/map/map_screen.dart +++ b/lib/presentation/screens/map/map_screen.dart @@ -8,6 +8,7 @@ import 'package:geolocator/geolocator.dart'; import 'package:go_router/go_router.dart'; import 'package:yandex_mapkit/yandex_mapkit.dart'; import '../../providers/zones_provider.dart'; +import '../../../domain/models/zone.dart'; import '../../providers/filters_provider.dart'; import '../../providers/routing_provider.dart'; import '../../providers/time_selector_provider.dart'; @@ -40,6 +41,9 @@ class _MapScreenState extends ConsumerState { bool _isRouteSheetOpen = false; bool _isParkingCardOpen = false; + Map _zoneLabelCache = {}; + Map _zonesById = {}; + @override void initState() { super.initState(); @@ -80,6 +84,23 @@ class _MapScreenState extends ConsumerState { return byteData!.buffer.asUint8List(); } + Future _updateZoneBitmaps(List zones) async { + final newCache = {}; + final newById = {}; + for (final zone in zones) { + if (zone.geometry.length < 3) continue; + final color = zoneColor(zone); + newCache[zone.zoneId] = await buildCountBitmap(zone.freeCount, color); + newById[zone.zoneId] = zone; + } + if (mounted) { + setState(() { + _zoneLabelCache = newCache; + _zonesById = newById; + }); + } + } + Future _buildUserLocationBitmap() async { const size = 48.0; const center = Offset(size / 2, size / 2); @@ -300,6 +321,7 @@ class _MapScreenState extends ConsumerState { ); ref.listen(timeSelectorProvider, (_, __) => _fetchZones()); + ref.listen(filteredZonesProvider, (_, zones) => _updateZoneBitmaps(zones)); ref.listen(rawZonesProvider, (_, next) { next.whenOrNull( @@ -393,6 +415,8 @@ class _MapScreenState extends ConsumerState { final mapObjects = [ ...zoneObjects, + if (_zoneLabelCache.isNotEmpty) + buildZoneLabels(zones: zones, bitmapCache: _zoneLabelCache, zonesById: _zonesById), if (_userPosition != null && _userLocationBytes != null) PlacemarkMapObject( mapId: const MapObjectId('user_location'), @@ -758,7 +782,7 @@ class _FiltersSheet extends ConsumerWidget { maxChildSize: 0.95, builder: (_, controller) => Container( decoration: const BoxDecoration( - color: Colors.white, + color: Color(0xFFE8F5E9), borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), child: ListView( @@ -884,8 +908,9 @@ class _FiltersSheet extends ConsumerWidget { ), const SizedBox(height: 16), FilledButton( + style: FilledButton.styleFrom(backgroundColor: AppColors.primary), onPressed: () => Navigator.pop(context), - child: const Text('Применить'), + child: const Text('Применить', style: TextStyle(color: Colors.white)), ), ], ), diff --git a/lib/presentation/screens/map/widgets/parking_zone_layer.dart b/lib/presentation/screens/map/widgets/parking_zone_layer.dart index c2aa7f3..8ab5085 100644 --- a/lib/presentation/screens/map/widgets/parking_zone_layer.dart +++ b/lib/presentation/screens/map/widgets/parking_zone_layer.dart @@ -1,3 +1,6 @@ +import 'dart:math' as math; +import 'dart:typed_data'; +import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:yandex_mapkit/yandex_mapkit.dart'; import '../../../../domain/models/zone.dart'; @@ -11,7 +14,7 @@ List buildZoneMapObjects({ final result = []; for (final zone in zones) { if (_isDegenerate(zone.geometry)) continue; - final color = _zoneColor(zone); + final color = zoneColor(zone); if (zone.zoneType == ZoneType.parallel) { result.add(_buildParallelLine(zone, color, onTap)); } else { @@ -21,6 +24,128 @@ List buildZoneMapObjects({ return result; } +MapObject buildZoneLabels({ + required List zones, + required Map bitmapCache, + required Map zonesById, +}) { + final placemarks = zones + .where((z) => !_isDegenerate(z.geometry) && bitmapCache.containsKey(z.zoneId)) + .map((zone) => PlacemarkMapObject( + mapId: MapObjectId('zone_label_${zone.zoneId}'), + point: _centroid(zone.geometry), + icon: PlacemarkIcon.single(PlacemarkIconStyle( + image: BitmapDescriptor.fromBytes(bitmapCache[zone.zoneId]!), + scale: 1.0, + )), + )) + .toList(); + + return ClusterizedPlacemarkCollection( + mapId: const MapObjectId('zone_labels'), + placemarks: placemarks, + radius: 60, + minZoom: 15, + onClusterAdded: (collection, cluster) async { + final zoneIds = cluster.placemarks + .map((p) => int.tryParse(p.mapId.value.replaceFirst('zone_label_', ''))) + .whereType() + .toList(); + final totalFree = zoneIds + .map((id) => zonesById[id]?.freeCount ?? 0) + .fold(0, (a, b) => a + b); + final bytes = await buildClusterBitmap(totalFree, cluster.placemarks.length); + return cluster.copyWith( + appearance: cluster.appearance.copyWith( + icon: PlacemarkIcon.single(PlacemarkIconStyle( + image: BitmapDescriptor.fromBytes(bytes), + scale: 1.0, + )), + ), + ); + }, + ); +} + +Future buildCountBitmap(int count, Color color) async { + const size = 36.0; + final recorder = ui.PictureRecorder(); + final canvas = Canvas(recorder); + canvas.drawCircle( + const Offset(size / 2, size / 2), + size / 2 - 1, + Paint()..color = color, + ); + canvas.drawCircle( + const Offset(size / 2, size / 2), + size / 2 - 1, + Paint() + ..color = Colors.white + ..style = PaintingStyle.stroke + ..strokeWidth = 2, + ); + final textPainter = TextPainter( + text: TextSpan( + text: '$count', + style: const TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + textDirection: TextDirection.ltr, + )..layout(); + textPainter.paint( + canvas, + Offset((size - textPainter.width) / 2, (size - textPainter.height) / 2), + ); + final picture = recorder.endRecording(); + final image = await picture.toImage(size.toInt(), size.toInt()); + final data = await image.toByteData(format: ui.ImageByteFormat.png); + return data!.buffer.asUint8List(); +} + +Future buildClusterBitmap(int totalFree, int clusterSize) async { + const size = 46.0; + final recorder = ui.PictureRecorder(); + final canvas = Canvas(recorder); + canvas.drawCircle( + const Offset(size / 2, size / 2), + size / 2 - 1, + Paint()..color = AppColors.primary, + ); + canvas.drawCircle( + const Offset(size / 2, size / 2), + size / 2 - 1, + Paint() + ..color = Colors.white + ..style = PaintingStyle.stroke + ..strokeWidth = 3, + ); + final label = clusterSize <= 1 ? '$totalFree' : '$totalFree\n($clusterSize)'; + final textPainter = TextPainter( + text: TextSpan( + text: label, + style: const TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.bold, + height: 1.2, + ), + ), + textDirection: TextDirection.ltr, + textAlign: TextAlign.center, + )..layout(maxWidth: size - 6); + textPainter.paint( + canvas, + Offset((size - textPainter.width) / 2, (size - textPainter.height) / 2), + ); + final picture = recorder.endRecording(); + final image = await picture.toImage(size.toInt(), size.toInt()); + final data = await image.toByteData(format: ui.ImageByteFormat.png); + return data!.buffer.asUint8List(); +} + bool _isDegenerate(List points) { if (points.length < 3) return true; final lat0 = points.first.latitude; @@ -28,7 +153,7 @@ bool _isDegenerate(List points) { return points.every((p) => p.latitude == lat0 && p.longitude == lon0); } -Color _zoneColor(Zone zone) { +Color zoneColor(Zone zone) { if (!zone.isActive || zone.geometry.isEmpty) return AppColors.parkingUnknown; if (!zone.hasForecast) return AppColors.parkingUnknown; if (zone.freeCount == 0) return AppColors.parkingFull; @@ -44,7 +169,7 @@ MapObject _buildPolygon(Zone zone, Color color, void Function(Zone) onTap) { outerRing: LinearRing(points: zone.geometry), innerRings: [], ), - fillColor: color.withOpacity(0.5), + fillColor: color.withValues(alpha: 0.5), strokeColor: color, strokeWidth: 2, onTap: (_, __) => onTap(zone), @@ -53,12 +178,21 @@ MapObject _buildPolygon(Zone zone, Color color, void Function(Zone) onTap) { MapObject _buildParallelLine(Zone zone, Color color, void Function(Zone) onTap) { final points = zone.geometry; - if (points.length < 4) { - return _buildPolygon(zone, color, onTap); + if (points.length < 4) return _buildPolygon(zone, color, onTap); + + final len01 = _distance(points[0], points[1]); + final len12 = _distance(points[1], points[2]); + + final Point mid1, mid2; + if (len01 <= len12) { + // стороны 0→1 и 2→3 — короткие (торцы ряда) + mid1 = _midpoint(points[0], points[1]); + mid2 = _midpoint(points[2], points[3]); + } else { + // стороны 1→2 и 3→0 — короткие (торцы ряда) + mid1 = _midpoint(points[1], points[2]); + mid2 = _midpoint(points[3], points[0]); } - // Centers of short sides: midpoints of sides 0→1 and 2→3 - final mid1 = _midpoint(points[0], points[1]); - final mid2 = _midpoint(points[2], points[3]); return PolylineMapObject( mapId: MapObjectId('zone_line_${zone.zoneId}'), @@ -69,7 +203,24 @@ MapObject _buildParallelLine(Zone zone, Color color, void Function(Zone) onTap) ); } +double _distance(Point a, Point b) { + final dlat = a.latitude - b.latitude; + final dlon = a.longitude - b.longitude; + return math.sqrt(dlat * dlat + dlon * dlon); +} + Point _midpoint(Point a, Point b) => Point( latitude: (a.latitude + b.latitude) / 2, longitude: (a.longitude + b.longitude) / 2, ); + +Point _centroid(List points) { + final pts = points.length > 1 && + points.first.latitude == points.last.latitude && + points.first.longitude == points.last.longitude + ? points.sublist(0, points.length - 1) + : points; + final lat = pts.map((p) => p.latitude).reduce((a, b) => a + b) / pts.length; + final lon = pts.map((p) => p.longitude).reduce((a, b) => a + b) / pts.length; + return Point(latitude: lat, longitude: lon); +} From d5e6aa7baf153004431b80e81fb527a332de2fe1 Mon Sep 17 00:00:00 2001 From: Ilya Kiselev Date: Sun, 17 May 2026 17:00:02 +0300 Subject: [PATCH 004/145] fix: bigger zone count circles, solid white ring for contrast --- .../map/widgets/parking_zone_layer.dart | 44 ++++++------------- 1 file changed, 13 insertions(+), 31 deletions(-) diff --git a/lib/presentation/screens/map/widgets/parking_zone_layer.dart b/lib/presentation/screens/map/widgets/parking_zone_layer.dart index 8ab5085..cd013c8 100644 --- a/lib/presentation/screens/map/widgets/parking_zone_layer.dart +++ b/lib/presentation/screens/map/widgets/parking_zone_layer.dart @@ -68,28 +68,20 @@ MapObject buildZoneLabels({ } Future buildCountBitmap(int count, Color color) async { - const size = 36.0; + const size = 96.0; + final center = Offset(size / 2, size / 2); final recorder = ui.PictureRecorder(); final canvas = Canvas(recorder); - canvas.drawCircle( - const Offset(size / 2, size / 2), - size / 2 - 1, - Paint()..color = color, - ); - canvas.drawCircle( - const Offset(size / 2, size / 2), - size / 2 - 1, - Paint() - ..color = Colors.white - ..style = PaintingStyle.stroke - ..strokeWidth = 2, - ); + // White outer ring for contrast against any map background + canvas.drawCircle(center, size / 2 - 1, Paint()..color = Colors.white); + // Colored fill + canvas.drawCircle(center, size / 2 - 5, Paint()..color = color); final textPainter = TextPainter( text: TextSpan( text: '$count', style: const TextStyle( color: Colors.white, - fontSize: 14, + fontSize: 36, fontWeight: FontWeight.bold, ), ), @@ -106,36 +98,26 @@ Future buildCountBitmap(int count, Color color) async { } Future buildClusterBitmap(int totalFree, int clusterSize) async { - const size = 46.0; + const size = 112.0; + final center = Offset(size / 2, size / 2); final recorder = ui.PictureRecorder(); final canvas = Canvas(recorder); - canvas.drawCircle( - const Offset(size / 2, size / 2), - size / 2 - 1, - Paint()..color = AppColors.primary, - ); - canvas.drawCircle( - const Offset(size / 2, size / 2), - size / 2 - 1, - Paint() - ..color = Colors.white - ..style = PaintingStyle.stroke - ..strokeWidth = 3, - ); + canvas.drawCircle(center, size / 2 - 1, Paint()..color = Colors.white); + canvas.drawCircle(center, size / 2 - 5, Paint()..color = AppColors.primary); final label = clusterSize <= 1 ? '$totalFree' : '$totalFree\n($clusterSize)'; final textPainter = TextPainter( text: TextSpan( text: label, style: const TextStyle( color: Colors.white, - fontSize: 12, + fontSize: 32, fontWeight: FontWeight.bold, height: 1.2, ), ), textDirection: TextDirection.ltr, textAlign: TextAlign.center, - )..layout(maxWidth: size - 6); + )..layout(maxWidth: size - 10); textPainter.paint( canvas, Offset((size - textPainter.width) / 2, (size - textPainter.height) / 2), From f0a28140fe69323fe026944a7cc4ce5205848254 Mon Sep 17 00:00:00 2001 From: Ilya Kiselev Date: Sun, 17 May 2026 17:03:37 +0300 Subject: [PATCH 005/145] fix: placemark opacity 1.0, was defaulting to 0.5 in yandex_mapkit --- lib/presentation/screens/map/widgets/parking_zone_layer.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/presentation/screens/map/widgets/parking_zone_layer.dart b/lib/presentation/screens/map/widgets/parking_zone_layer.dart index cd013c8..023c9ab 100644 --- a/lib/presentation/screens/map/widgets/parking_zone_layer.dart +++ b/lib/presentation/screens/map/widgets/parking_zone_layer.dart @@ -34,6 +34,7 @@ MapObject buildZoneLabels({ .map((zone) => PlacemarkMapObject( mapId: MapObjectId('zone_label_${zone.zoneId}'), point: _centroid(zone.geometry), + opacity: 1.0, icon: PlacemarkIcon.single(PlacemarkIconStyle( image: BitmapDescriptor.fromBytes(bitmapCache[zone.zoneId]!), scale: 1.0, @@ -57,6 +58,7 @@ MapObject buildZoneLabels({ final bytes = await buildClusterBitmap(totalFree, cluster.placemarks.length); return cluster.copyWith( appearance: cluster.appearance.copyWith( + opacity: 1.0, icon: PlacemarkIcon.single(PlacemarkIconStyle( image: BitmapDescriptor.fromBytes(bytes), scale: 1.0, From 5f43ed8b55b7fd0252bcf82c51f0e2d4dffd2708 Mon Sep 17 00:00:00 2001 From: Ilya Kiselev Date: Sun, 17 May 2026 17:12:24 +0300 Subject: [PATCH 006/145] fix: deeplink fallback to navigator coords, move camera to zone on route --- lib/presentation/screens/map/map_screen.dart | 24 +++++++++++++- .../map/widgets/parking_zone_layer.dart | 4 +-- .../map/widgets/route_preview_sheet.dart | 31 +++++++++++++------ 3 files changed, 47 insertions(+), 12 deletions(-) diff --git a/lib/presentation/screens/map/map_screen.dart b/lib/presentation/screens/map/map_screen.dart index 7b4c345..5466acb 100644 --- a/lib/presentation/screens/map/map_screen.dart +++ b/lib/presentation/screens/map/map_screen.dart @@ -383,10 +383,32 @@ class _MapScreenState extends ConsumerState { routePreview: (route) async { if (_isRouteSheetOpen) return; _isRouteSheetOpen = true; + + final matches = zones.where((z) => z.zoneId == route.selectedZoneId); + double? zoneLat, zoneLon; + if (matches.isNotEmpty) { + final zone = matches.first; + if (zone.geometry.length >= 3) { + final c = centroid(zone.geometry); + zoneLat = c.latitude; + zoneLon = c.longitude; + await _mapController?.moveCamera( + CameraUpdate.newCameraPosition( + CameraPosition(target: c, zoom: 17), + ), + animation: const MapAnimation(duration: 0.8), + ); + } + } + await showModalBottomSheet( context: context, isScrollControlled: true, - builder: (_) => RoutePreviewSheet(route: route), + builder: (_) => RoutePreviewSheet( + route: route, + zoneLat: zoneLat, + zoneLon: zoneLon, + ), ); _isRouteSheetOpen = false; }, diff --git a/lib/presentation/screens/map/widgets/parking_zone_layer.dart b/lib/presentation/screens/map/widgets/parking_zone_layer.dart index 023c9ab..8c549ec 100644 --- a/lib/presentation/screens/map/widgets/parking_zone_layer.dart +++ b/lib/presentation/screens/map/widgets/parking_zone_layer.dart @@ -33,7 +33,7 @@ MapObject buildZoneLabels({ .where((z) => !_isDegenerate(z.geometry) && bitmapCache.containsKey(z.zoneId)) .map((zone) => PlacemarkMapObject( mapId: MapObjectId('zone_label_${zone.zoneId}'), - point: _centroid(zone.geometry), + point: centroid(zone.geometry), opacity: 1.0, icon: PlacemarkIcon.single(PlacemarkIconStyle( image: BitmapDescriptor.fromBytes(bitmapCache[zone.zoneId]!), @@ -198,7 +198,7 @@ Point _midpoint(Point a, Point b) => Point( longitude: (a.longitude + b.longitude) / 2, ); -Point _centroid(List points) { +Point centroid(List points) { final pts = points.length > 1 && points.first.latitude == points.last.latitude && points.first.longitude == points.last.longitude diff --git a/lib/presentation/screens/map/widgets/route_preview_sheet.dart b/lib/presentation/screens/map/widgets/route_preview_sheet.dart index 19c2f33..332a3ef 100644 --- a/lib/presentation/screens/map/widgets/route_preview_sheet.dart +++ b/lib/presentation/screens/map/widgets/route_preview_sheet.dart @@ -6,9 +6,16 @@ import '../../../../domain/models/route_result.dart'; import '../../../providers/routing_provider.dart'; class RoutePreviewSheet extends ConsumerStatefulWidget { - const RoutePreviewSheet({super.key, required this.route}); + const RoutePreviewSheet({ + super.key, + required this.route, + this.zoneLat, + this.zoneLon, + }); final ActiveRoute route; + final double? zoneLat; + final double? zoneLon; @override ConsumerState createState() => _RoutePreviewSheetState(); @@ -18,16 +25,22 @@ class _RoutePreviewSheetState extends ConsumerState { bool _launching = false; Future _launch() async { - final url = widget.route.deeplinkUrl; - if (url == null) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Ссылка на навигатор недоступна')), - ); - return; - } setState(() => _launching = true); try { - await openYandexNavigatorUrl(url); + final url = widget.route.deeplinkUrl; + if (url != null) { + await openYandexNavigatorUrl(url); + return; + } + final lat = widget.zoneLat; + final lon = widget.zoneLon; + if (lat != null && lon != null) { + await openYandexNavigator(lat, lon); + return; + } + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Нет данных для навигации')), + ); } finally { if (mounted) setState(() => _launching = false); } From cd351c8c520c65d58c35184df465e846714e00d7 Mon Sep 17 00:00:00 2001 From: Ilya Kiselev Date: Sun, 17 May 2026 17:17:42 +0300 Subject: [PATCH 007/145] fix: rename filter labels for clarity --- lib/presentation/screens/map/map_screen.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/presentation/screens/map/map_screen.dart b/lib/presentation/screens/map/map_screen.dart index 5466acb..9fc75a9 100644 --- a/lib/presentation/screens/map/map_screen.dart +++ b/lib/presentation/screens/map/map_screen.dart @@ -848,12 +848,12 @@ class _FiltersSheet extends ConsumerWidget { onChanged: (_) => notifier.toggleHidePrivate(), ), SwitchListTile( - title: const Text('Скрыть недоступные'), + title: const Text('Скрыть недоступные для инвалидов'), value: filters.hideInaccessible, onChanged: (_) => notifier.toggleHideInaccessible(), ), SwitchListTile( - title: const Text('Скрыть неактивные'), + title: const Text('Скрыть закрытые'), value: filters.hideInactive, onChanged: (_) => notifier.toggleHideInactive(), ), From d1d9f7956fe6a597cc8b30dd64112b1d8d692187 Mon Sep 17 00:00:00 2001 From: Ilya Kiselev Date: Mon, 18 May 2026 09:09:53 +0300 Subject: [PATCH 008/145] fix: 422 on routing, parallel line direction, zone count labels with clustering, green filter style --- .../providers/routing_provider.dart | 2 +- .../providers/zones_provider.dart | 32 +++++++++---------- lib/presentation/screens/map/map_screen.dart | 26 +++++++++++++-- .../map/widgets/parking_zone_layer.dart | 14 ++++---- 4 files changed, 48 insertions(+), 26 deletions(-) diff --git a/lib/presentation/providers/routing_provider.dart b/lib/presentation/providers/routing_provider.dart index d9b91a7..9b2985e 100644 --- a/lib/presentation/providers/routing_provider.dart +++ b/lib/presentation/providers/routing_provider.dart @@ -61,7 +61,7 @@ class RoutingNotifier extends StateNotifier { destinationLat: destination?.latitude, destinationLon: destination?.longitude, maxPay: filters.maxPayPerHour, - minConfidence: filters.minConfidence, + minConfidence: filters.minConfidence > 0 ? filters.minConfidence : null, minFreeCount: filters.hideNoFreeSpots ? 1 : null, ); state = RoutingState.candidates(candidates); diff --git a/lib/presentation/providers/zones_provider.dart b/lib/presentation/providers/zones_provider.dart index a57175a..8057525 100644 --- a/lib/presentation/providers/zones_provider.dart +++ b/lib/presentation/providers/zones_provider.dart @@ -17,7 +17,7 @@ class ZonesNotifier extends StateNotifier>> { Future fetchZones(String bbox) async { _lastBbox = bbox; - state = const AsyncValue.loading(); + state = const AsyncValue>.loading().copyWithPrevious(state); try { final repo = _ref.read(zonesRepositoryProvider); final timeMode = _ref.read(timeSelectorProvider); @@ -45,22 +45,20 @@ final filteredZonesProvider = Provider>((ref) { final zonesAsync = ref.watch(rawZonesProvider); final filters = ref.watch(filtersProvider); - return zonesAsync.maybeWhen( - data: (zones) => zones.where((z) { - if (filters.hideInactive && !z.isActive) return false; - if (filters.hideNoFreeSpots && z.freeCount == 0) return false; - if (z.confidence < filters.minConfidence) return false; - if (filters.maxPayPerHour != null && z.pay > filters.maxPayPerHour!) return false; - if (filters.hidePrivate && (z.isPrivate ?? false)) return false; - if (filters.hideInaccessible && (z.isAccessible == false)) return false; - if (z.locationType != null) { - final typeKey = _locationTypeKey(z.locationType!); - if (filters.hiddenLocationTypes.contains(typeKey)) return false; - } - return true; - }).toList(), - orElse: () => [], - ); + final zones = zonesAsync.valueOrNull ?? []; + return zones.where((z) { + if (filters.hideInactive && !z.isActive) return false; + if (filters.hideNoFreeSpots && z.freeCount == 0) return false; + if (z.confidence < filters.minConfidence) return false; + if (filters.maxPayPerHour != null && z.pay > filters.maxPayPerHour!) return false; + if (filters.hidePrivate && (z.isPrivate ?? false)) return false; + if (filters.hideInaccessible && (z.isAccessible == true)) return false; + if (z.locationType != null) { + final typeKey = _locationTypeKey(z.locationType!); + if (filters.hiddenLocationTypes.contains(typeKey)) return false; + } + return true; + }).toList(); }); String _locationTypeKey(LocationType type) => switch (type) { diff --git a/lib/presentation/screens/map/map_screen.dart b/lib/presentation/screens/map/map_screen.dart index 9fc75a9..611dbe6 100644 --- a/lib/presentation/screens/map/map_screen.dart +++ b/lib/presentation/screens/map/map_screen.dart @@ -438,7 +438,29 @@ class _MapScreenState extends ConsumerState { final mapObjects = [ ...zoneObjects, if (_zoneLabelCache.isNotEmpty) - buildZoneLabels(zones: zones, bitmapCache: _zoneLabelCache, zonesById: _zonesById), + buildZoneLabels( + zones: zones, + bitmapCache: _zoneLabelCache, + zonesById: _zonesById, + onClusterTap: (_, cluster) { + if (cluster.placemarks.isEmpty) return; + final lats = cluster.placemarks.map((p) => p.point.latitude); + final lons = cluster.placemarks.map((p) => p.point.longitude); + _mapController?.moveCamera( + CameraUpdate.newGeometry(Geometry.fromBoundingBox(BoundingBox( + southWest: Point( + latitude: lats.reduce(math.min), + longitude: lons.reduce(math.min), + ), + northEast: Point( + latitude: lats.reduce(math.max), + longitude: lons.reduce(math.max), + ), + ))), + animation: const MapAnimation(duration: 0.5), + ); + }, + ), if (_userPosition != null && _userLocationBytes != null) PlacemarkMapObject( mapId: const MapObjectId('user_location'), @@ -848,7 +870,7 @@ class _FiltersSheet extends ConsumerWidget { onChanged: (_) => notifier.toggleHidePrivate(), ), SwitchListTile( - title: const Text('Скрыть недоступные для инвалидов'), + title: const Text('Скрыть места для инвалидов'), value: filters.hideInaccessible, onChanged: (_) => notifier.toggleHideInaccessible(), ), diff --git a/lib/presentation/screens/map/widgets/parking_zone_layer.dart b/lib/presentation/screens/map/widgets/parking_zone_layer.dart index 8c549ec..3cac790 100644 --- a/lib/presentation/screens/map/widgets/parking_zone_layer.dart +++ b/lib/presentation/screens/map/widgets/parking_zone_layer.dart @@ -28,6 +28,7 @@ MapObject buildZoneLabels({ required List zones, required Map bitmapCache, required Map zonesById, + ClusterTapCallback? onClusterTap, }) { final placemarks = zones .where((z) => !_isDegenerate(z.geometry) && bitmapCache.containsKey(z.zoneId)) @@ -47,6 +48,7 @@ MapObject buildZoneLabels({ placemarks: placemarks, radius: 60, minZoom: 15, + onClusterTap: onClusterTap, onClusterAdded: (collection, cluster) async { final zoneIds = cluster.placemarks .map((p) => int.tryParse(p.mapId.value.replaceFirst('zone_label_', ''))) @@ -70,20 +72,20 @@ MapObject buildZoneLabels({ } Future buildCountBitmap(int count, Color color) async { - const size = 96.0; + const size = 48.0; final center = Offset(size / 2, size / 2); final recorder = ui.PictureRecorder(); final canvas = Canvas(recorder); // White outer ring for contrast against any map background canvas.drawCircle(center, size / 2 - 1, Paint()..color = Colors.white); // Colored fill - canvas.drawCircle(center, size / 2 - 5, Paint()..color = color); + canvas.drawCircle(center, size / 2 - 3, Paint()..color = color); final textPainter = TextPainter( text: TextSpan( text: '$count', style: const TextStyle( color: Colors.white, - fontSize: 36, + fontSize: 18, fontWeight: FontWeight.bold, ), ), @@ -100,19 +102,19 @@ Future buildCountBitmap(int count, Color color) async { } Future buildClusterBitmap(int totalFree, int clusterSize) async { - const size = 112.0; + const size = 56.0; final center = Offset(size / 2, size / 2); final recorder = ui.PictureRecorder(); final canvas = Canvas(recorder); canvas.drawCircle(center, size / 2 - 1, Paint()..color = Colors.white); - canvas.drawCircle(center, size / 2 - 5, Paint()..color = AppColors.primary); + canvas.drawCircle(center, size / 2 - 3, Paint()..color = AppColors.primary); final label = clusterSize <= 1 ? '$totalFree' : '$totalFree\n($clusterSize)'; final textPainter = TextPainter( text: TextSpan( text: label, style: const TextStyle( color: Colors.white, - fontSize: 32, + fontSize: 16, fontWeight: FontWeight.bold, height: 1.2, ), From d724e64fee3731d88e930e6776711426d477bc7e Mon Sep 17 00:00:00 2001 From: Ilya Kiselev Date: Mon, 18 May 2026 10:09:36 +0300 Subject: [PATCH 009/145] fix: bigger zone count circles, solid white ring for contrast --- lib/presentation/screens/map/map_screen.dart | 14 +++++++---- .../map/widgets/parking_zone_layer.dart | 23 +++++++++++++------ 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/lib/presentation/screens/map/map_screen.dart b/lib/presentation/screens/map/map_screen.dart index 611dbe6..176448f 100644 --- a/lib/presentation/screens/map/map_screen.dart +++ b/lib/presentation/screens/map/map_screen.dart @@ -446,15 +446,21 @@ class _MapScreenState extends ConsumerState { if (cluster.placemarks.isEmpty) return; final lats = cluster.placemarks.map((p) => p.point.latitude); final lons = cluster.placemarks.map((p) => p.point.longitude); + final latMin = lats.reduce(math.min); + final latMax = lats.reduce(math.max); + final lonMin = lons.reduce(math.min); + final lonMax = lons.reduce(math.max); + final latPad = (latMax - latMin) * 0.6 + 0.003; + final lonPad = (lonMax - lonMin) * 0.6 + 0.003; _mapController?.moveCamera( CameraUpdate.newGeometry(Geometry.fromBoundingBox(BoundingBox( southWest: Point( - latitude: lats.reduce(math.min), - longitude: lons.reduce(math.min), + latitude: latMin - latPad, + longitude: lonMin - lonPad, ), northEast: Point( - latitude: lats.reduce(math.max), - longitude: lons.reduce(math.max), + latitude: latMax + latPad, + longitude: lonMax + lonPad, ), ))), animation: const MapAnimation(duration: 0.5), diff --git a/lib/presentation/screens/map/widgets/parking_zone_layer.dart b/lib/presentation/screens/map/widgets/parking_zone_layer.dart index 3cac790..c512b7c 100644 --- a/lib/presentation/screens/map/widgets/parking_zone_layer.dart +++ b/lib/presentation/screens/map/widgets/parking_zone_layer.dart @@ -57,7 +57,16 @@ MapObject buildZoneLabels({ final totalFree = zoneIds .map((id) => zonesById[id]?.freeCount ?? 0) .fold(0, (a, b) => a + b); - final bytes = await buildClusterBitmap(totalFree, cluster.placemarks.length); + final hasForecast = zoneIds.any((id) => zonesById[id]?.hasForecast == true); + final Color clusterColor; + if (!hasForecast) { + clusterColor = AppColors.parkingUnknown; + } else if (totalFree == 0) { + clusterColor = AppColors.parkingFull; + } else { + clusterColor = AppColors.primary; + } + final bytes = await buildClusterBitmap(totalFree, cluster.placemarks.length, clusterColor); return cluster.copyWith( appearance: cluster.appearance.copyWith( opacity: 1.0, @@ -72,7 +81,7 @@ MapObject buildZoneLabels({ } Future buildCountBitmap(int count, Color color) async { - const size = 48.0; + const size = 72.0; final center = Offset(size / 2, size / 2); final recorder = ui.PictureRecorder(); final canvas = Canvas(recorder); @@ -85,7 +94,7 @@ Future buildCountBitmap(int count, Color color) async { text: '$count', style: const TextStyle( color: Colors.white, - fontSize: 18, + fontSize: 28, fontWeight: FontWeight.bold, ), ), @@ -101,20 +110,20 @@ Future buildCountBitmap(int count, Color color) async { return data!.buffer.asUint8List(); } -Future buildClusterBitmap(int totalFree, int clusterSize) async { - const size = 56.0; +Future buildClusterBitmap(int totalFree, int clusterSize, Color color) async { + const size = 84.0; final center = Offset(size / 2, size / 2); final recorder = ui.PictureRecorder(); final canvas = Canvas(recorder); canvas.drawCircle(center, size / 2 - 1, Paint()..color = Colors.white); - canvas.drawCircle(center, size / 2 - 3, Paint()..color = AppColors.primary); + canvas.drawCircle(center, size / 2 - 4, Paint()..color = color); final label = clusterSize <= 1 ? '$totalFree' : '$totalFree\n($clusterSize)'; final textPainter = TextPainter( text: TextSpan( text: label, style: const TextStyle( color: Colors.white, - fontSize: 16, + fontSize: 24, fontWeight: FontWeight.bold, height: 1.2, ), From feedf27f157d38a25967a3e2deaad0b177fb18bf Mon Sep 17 00:00:00 2001 From: Ilya Kiselev Date: Wed, 20 May 2026 11:54:23 +0300 Subject: [PATCH 010/145] feat: route polyline, zone highlight, candidate highlight, card route button --- lib/presentation/screens/map/map_screen.dart | 30 ++++++++- .../screens/map/widgets/candidates_sheet.dart | 25 +++++-- .../map/widgets/parking_card_sheet.dart | 13 ++-- .../map/widgets/parking_zone_layer.dart | 65 ++++++++++++++++--- .../map/widgets/route_preview_sheet.dart | 24 ++++++- 5 files changed, 138 insertions(+), 19 deletions(-) diff --git a/lib/presentation/screens/map/map_screen.dart b/lib/presentation/screens/map/map_screen.dart index 176448f..af3d540 100644 --- a/lib/presentation/screens/map/map_screen.dart +++ b/lib/presentation/screens/map/map_screen.dart @@ -43,6 +43,8 @@ class _MapScreenState extends ConsumerState { Map _zoneLabelCache = {}; Map _zonesById = {}; + List? _routePolyline; + int? _activeRouteZoneId; @override void initState() { @@ -348,7 +350,12 @@ class _MapScreenState extends ConsumerState { ref.listen(routingProvider, (_, next) async { await next.when( - idle: () async {}, + idle: () async { + setState(() { + _routePolyline = null; + _activeRouteZoneId = null; + }); + }, searching: () async {}, error: (message) async { ScaffoldMessenger.of(context).showSnackBar( @@ -401,6 +408,11 @@ class _MapScreenState extends ConsumerState { } } + setState(() { + _routePolyline = route.candidates.firstOrNull?.routePolyline; + _activeRouteZoneId = route.selectedZoneId; + }); + await showModalBottomSheet( context: context, isScrollControlled: true, @@ -422,6 +434,7 @@ class _MapScreenState extends ConsumerState { final zoneObjects = buildZoneMapObjects( zones: zones, + highlightedIds: candidateIds, onTap: (zone) { if (_isSelectingOnMap && candidateIds.contains(zone.zoneId)) { _buildRouteForZone(zone.zoneId); @@ -429,7 +442,11 @@ class _MapScreenState extends ConsumerState { } if (_isParkingCardOpen) return; setState(() => _isParkingCardOpen = true); - showParkingCard(context, zone).then((_) { + showParkingCard( + context, + zone, + onBuildRoute: () => _buildRouteForZone(zone.zoneId), + ).then((_) { if (mounted) setState(() => _isParkingCardOpen = false); }); }, @@ -437,6 +454,15 @@ class _MapScreenState extends ConsumerState { final mapObjects = [ ...zoneObjects, + if (_routePolyline != null && _routePolyline!.length >= 2) + PolylineMapObject( + mapId: const MapObjectId('route_polyline'), + polyline: Polyline(points: _routePolyline!), + strokeColor: Colors.blue, + strokeWidth: 5, + ), + if (_activeRouteZoneId != null) + ...buildHighlightZone(zones, _activeRouteZoneId!), if (_zoneLabelCache.isNotEmpty) buildZoneLabels( zones: zones, diff --git a/lib/presentation/screens/map/widgets/candidates_sheet.dart b/lib/presentation/screens/map/widgets/candidates_sheet.dart index 7b3770c..c9001c5 100644 --- a/lib/presentation/screens/map/widgets/candidates_sheet.dart +++ b/lib/presentation/screens/map/widgets/candidates_sheet.dart @@ -2,6 +2,26 @@ import 'package:flutter/material.dart'; import '../../../../domain/models/route_result.dart'; import '../../../../core/theme/app_colors.dart'; +class _CandidateSubtitle extends StatelessWidget { + final RouteCandidate candidate; + const _CandidateSubtitle({required this.candidate}); + + @override + Widget build(BuildContext context) { + final parts = [ + 'Свободно: ${candidate.freeCount}', + candidate.pay == 0 ? 'бесплатно' : '${candidate.pay} ₽/ч', + ]; + if (candidate.distanceToDestinationMeters != null) + parts.add('${(candidate.distanceToDestinationMeters! / 1000).toStringAsFixed(1)} км'); + if (candidate.durationFromOriginSeconds != null) + parts.add('~${(candidate.durationFromOriginSeconds! / 60).round()} мин'); + if (candidate.predictedFreeCount != null) + parts.add('прогноз: ${candidate.predictedFreeCount}'); + return Text(parts.join(' • ')); + } +} + class CandidatesSheet extends StatefulWidget { const CandidatesSheet({ super.key, @@ -70,10 +90,7 @@ class _CandidatesSheetState extends State { margin: const EdgeInsets.symmetric(vertical: 6), child: ListTile( title: Text('Зона #${candidate.zoneId}'), - subtitle: Text( - 'Свободно: ${candidate.freeCount} • ' - 'Цена: ${candidate.pay == 0 ? "бесплатно" : "${candidate.pay} ₽/ч"}', - ), + subtitle: _CandidateSubtitle(candidate: candidate), trailing: const Icon(Icons.chevron_right), onTap: () => widget.onSelectByList(candidate.zoneId), ), diff --git a/lib/presentation/screens/map/widgets/parking_card_sheet.dart b/lib/presentation/screens/map/widgets/parking_card_sheet.dart index 2aec190..f29ce7c 100644 --- a/lib/presentation/screens/map/widgets/parking_card_sheet.dart +++ b/lib/presentation/screens/map/widgets/parking_card_sheet.dart @@ -2,18 +2,23 @@ import 'package:flutter/material.dart'; import '../../../../domain/models/zone.dart'; import '../../../../core/theme/app_colors.dart'; -Future showParkingCard(BuildContext context, Zone zone) { +Future showParkingCard( + BuildContext context, + Zone zone, { + VoidCallback? onBuildRoute, +}) { return showModalBottomSheet( context: context, isScrollControlled: true, - builder: (_) => _ParkingCardSheet(zone: zone), + builder: (_) => _ParkingCardSheet(zone: zone, onBuildRoute: onBuildRoute), ); } class _ParkingCardSheet extends StatelessWidget { final Zone zone; + final VoidCallback? onBuildRoute; - const _ParkingCardSheet({required this.zone}); + const _ParkingCardSheet({required this.zone, this.onBuildRoute}); @override Widget build(BuildContext context) { @@ -100,7 +105,7 @@ class _ParkingCardSheet extends StatelessWidget { FilledButton.icon( onPressed: () { Navigator.pop(context); - // Store selected zone for routing + onBuildRoute?.call(); }, icon: const Icon(Icons.directions), label: const Text('Построить маршрут'), diff --git a/lib/presentation/screens/map/widgets/parking_zone_layer.dart b/lib/presentation/screens/map/widgets/parking_zone_layer.dart index c512b7c..ba0ac1b 100644 --- a/lib/presentation/screens/map/widgets/parking_zone_layer.dart +++ b/lib/presentation/screens/map/widgets/parking_zone_layer.dart @@ -10,20 +10,67 @@ import '../../../../core/theme/app_colors.dart'; List buildZoneMapObjects({ required List zones, required void Function(Zone) onTap, + Set highlightedIds = const {}, }) { final result = []; for (final zone in zones) { if (_isDegenerate(zone.geometry)) continue; final color = zoneColor(zone); + final highlighted = highlightedIds.contains(zone.zoneId); if (zone.zoneType == ZoneType.parallel) { - result.add(_buildParallelLine(zone, color, onTap)); + result.add(_buildParallelLine(zone, color, onTap, highlighted: highlighted)); } else { - result.add(_buildPolygon(zone, color, onTap)); + result.add(_buildPolygon(zone, color, onTap, highlighted: highlighted)); } } return result; } +List buildHighlightZone(List zones, int zoneId) { + final matches = zones.where((z) => z.zoneId == zoneId && !_isDegenerate(z.geometry)); + if (matches.isEmpty) return []; + final zone = matches.first; + final color = zoneColor(zone); + if (zone.zoneType == ZoneType.parallel) { + final points = zone.geometry; + if (points.length < 4) return []; + final len01 = _distance(points[0], points[1]); + final len12 = _distance(points[1], points[2]); + final Point mid1, mid2; + if (len01 <= len12) { + mid1 = _midpoint(points[0], points[1]); + mid2 = _midpoint(points[2], points[3]); + } else { + mid1 = _midpoint(points[1], points[2]); + mid2 = _midpoint(points[3], points[0]); + } + return [ + PolylineMapObject( + mapId: MapObjectId('zone_highlight_${zone.zoneId}'), + polyline: Polyline(points: [mid1, mid2]), + strokeColor: Colors.white, + strokeWidth: 10, + ), + PolylineMapObject( + mapId: MapObjectId('zone_highlight_inner_${zone.zoneId}'), + polyline: Polyline(points: [mid1, mid2]), + strokeColor: color, + strokeWidth: 6, + ), + ]; + } else { + return [ + PolygonMapObject( + mapId: MapObjectId('zone_highlight_${zone.zoneId}'), + polygon: Polygon(outerRing: LinearRing(points: zone.geometry), innerRings: []), + fillColor: color.withValues(alpha: 0.6), + strokeColor: Colors.white, + strokeWidth: 4, + ), + ]; + } +} + MapObject buildZoneLabels({ required List zones, required Map bitmapCache, @@ -157,7 +204,8 @@ Color zoneColor(Zone zone) { return AppColors.parkingFewLow; } -MapObject _buildPolygon(Zone zone, Color color, void Function(Zone) onTap) { +MapObject _buildPolygon(Zone zone, Color color, void Function(Zone) onTap, + {bool highlighted = false}) { return PolygonMapObject( mapId: MapObjectId('zone_polygon_${zone.zoneId}'), polygon: Polygon( @@ -165,13 +213,14 @@ MapObject _buildPolygon(Zone zone, Color color, void Function(Zone) onTap) { innerRings: [], ), fillColor: color.withValues(alpha: 0.5), - strokeColor: color, - strokeWidth: 2, + strokeColor: highlighted ? Colors.white : color, + strokeWidth: highlighted ? 4 : 2, onTap: (_, __) => onTap(zone), ); } -MapObject _buildParallelLine(Zone zone, Color color, void Function(Zone) onTap) { +MapObject _buildParallelLine(Zone zone, Color color, void Function(Zone) onTap, + {bool highlighted = false}) { final points = zone.geometry; if (points.length < 4) return _buildPolygon(zone, color, onTap); @@ -192,8 +241,8 @@ MapObject _buildParallelLine(Zone zone, Color color, void Function(Zone) onTap) return PolylineMapObject( mapId: MapObjectId('zone_line_${zone.zoneId}'), polyline: Polyline(points: [mid1, mid2]), - strokeColor: color, - strokeWidth: 6, + strokeColor: highlighted ? Colors.white : color, + strokeWidth: highlighted ? 9 : 6, onTap: (_, __) => onTap(zone), ); } diff --git a/lib/presentation/screens/map/widgets/route_preview_sheet.dart b/lib/presentation/screens/map/widgets/route_preview_sheet.dart index 332a3ef..e225ed2 100644 --- a/lib/presentation/screens/map/widgets/route_preview_sheet.dart +++ b/lib/presentation/screens/map/widgets/route_preview_sheet.dart @@ -24,6 +24,16 @@ class RoutePreviewSheet extends ConsumerStatefulWidget { class _RoutePreviewSheetState extends ConsumerState { bool _launching = false; + String _formatArrival(String iso) { + final dt = DateTime.tryParse(iso)?.toLocal(); + if (dt == null) return iso; + final mins = dt.difference(DateTime.now()).inMinutes; + if (mins > 0 && mins <= 90) return 'через $mins мин'; + final hh = dt.hour.toString().padLeft(2, '0'); + final mm = dt.minute.toString().padLeft(2, '0'); + return '$hh:$mm'; + } + Future _launch() async { setState(() => _launching = true); try { @@ -80,7 +90,19 @@ class _RoutePreviewSheetState extends ConsumerState { const SizedBox(height: 10), Text('Зона: #${widget.route.selectedZoneId}'), if (widget.route.arrivalTime != null) - Text('Прибытие: ${widget.route.arrivalTime}'), + Text('Прибытие: ${_formatArrival(widget.route.arrivalTime!)}'), + Builder(builder: (_) { + final c = widget.route.candidates.firstOrNull; + if (c == null) return const SizedBox.shrink(); + final parts = []; + if (c.distanceToDestinationMeters != null) + parts.add('${(c.distanceToDestinationMeters! / 1000).toStringAsFixed(1)} км'); + if (c.durationFromOriginSeconds != null) + parts.add('~${(c.durationFromOriginSeconds! / 60).round()} мин'); + if (parts.isEmpty) return const SizedBox.shrink(); + return Text(parts.join(' • '), + style: const TextStyle(color: Color(0xFF666666))); + }), const SizedBox(height: 18), FilledButton.icon( onPressed: _launching ? null : _launch, From 74f42be0336ef9e35f8daf4cad0a2dcb35789583 Mon Sep 17 00:00:00 2001 From: Ilya Kiselev Date: Sat, 23 May 2026 22:57:11 +0300 Subject: [PATCH 011/145] feat: in-app navigation option, fix duration formatting (hours) --- lib/presentation/screens/map/map_screen.dart | 145 ++++++++++++++++++ .../screens/map/widgets/candidates_sheet.dart | 10 +- .../map/widgets/route_preview_sheet.dart | 80 +++++++--- 3 files changed, 211 insertions(+), 24 deletions(-) diff --git a/lib/presentation/screens/map/map_screen.dart b/lib/presentation/screens/map/map_screen.dart index af3d540..37dd1fb 100644 --- a/lib/presentation/screens/map/map_screen.dart +++ b/lib/presentation/screens/map/map_screen.dart @@ -11,6 +11,7 @@ import '../../providers/zones_provider.dart'; import '../../../domain/models/zone.dart'; import '../../providers/filters_provider.dart'; import '../../providers/routing_provider.dart'; +import '../../../domain/models/route_result.dart'; import '../../providers/time_selector_provider.dart'; import '../../../core/theme/app_colors.dart'; import '../../../core/utils/navigation_deeplink.dart'; @@ -45,6 +46,8 @@ class _MapScreenState extends ConsumerState { Map _zonesById = {}; List? _routePolyline; int? _activeRouteZoneId; + bool _inAppNavMode = false; + DrivingSession? _drivingSession; @override void initState() { @@ -60,6 +63,7 @@ class _MapScreenState extends ConsumerState { @override void dispose() { _debounce?.cancel(); + _drivingSession?.close(); super.dispose(); } @@ -302,6 +306,35 @@ class _MapScreenState extends ConsumerState { ); } + Future _startInAppNavigation(double toLat, double toLon) async { + setState(() => _inAppNavMode = true); + if (_routePolyline != null) return; + final pos = _userPosition; + if (pos == null) return; + await _drivingSession?.close(); + final resultWithSession = YandexDriving.requestRoutes( + points: [ + RequestPoint( + point: Point(latitude: pos.latitude, longitude: pos.longitude), + requestPointType: RequestPointType.wayPoint, + ), + RequestPoint( + point: Point(latitude: toLat, longitude: toLon), + requestPointType: RequestPointType.wayPoint, + ), + ], + drivingOptions: const DrivingOptions(routesCount: 1), + ); + _drivingSession = resultWithSession.session; + final result = await resultWithSession.result; + await resultWithSession.session.close(); + _drivingSession = null; + final route = result.routes?.firstOrNull; + if (route != null && mounted) { + setState(() => _routePolyline = route.geometry); + } + } + void _showFilters(BuildContext context) { showModalBottomSheet( context: context, @@ -354,6 +387,7 @@ class _MapScreenState extends ConsumerState { setState(() { _routePolyline = null; _activeRouteZoneId = null; + _inAppNavMode = false; }); }, searching: () async {}, @@ -420,6 +454,9 @@ class _MapScreenState extends ConsumerState { route: route, zoneLat: zoneLat, zoneLon: zoneLon, + onNavigateInApp: (zoneLat != null && zoneLon != null) + ? () => _startInAppNavigation(zoneLat!, zoneLon!) + : null, ), ); _isRouteSheetOpen = false; @@ -674,6 +711,23 @@ class _MapScreenState extends ConsumerState { ), ), ), + if (_inAppNavMode && _activeRouteZoneId != null) + Positioned( + bottom: 0, + left: 0, + right: 0, + child: _InAppNavBar( + zoneId: _activeRouteZoneId!, + candidate: routingState.maybeWhen( + routePreview: (r) => r.candidates.firstOrNull, + orElse: () => null, + ), + onFinish: () { + ref.read(routingProvider.notifier).reset(); + setState(() => _inAppNavMode = false); + }, + ), + ), if (isRoutingLoading) Positioned( top: 148, @@ -994,3 +1048,94 @@ class _FiltersSheet extends ConsumerWidget { ); } } + +// ─────────────────────────────────────────────────────────────────────────── + +class _InAppNavBar extends StatelessWidget { + const _InAppNavBar({ + required this.zoneId, + required this.onFinish, + this.candidate, + }); + + final int zoneId; + final VoidCallback onFinish; + final RouteCandidate? candidate; + + String _formatDuration(int seconds) { + final mins = (seconds / 60).round(); + if (mins < 60) return '~$mins мин'; + final h = mins ~/ 60; + final m = mins % 60; + return m == 0 ? '~${h}ч' : '~${h}ч ${m}мин'; + } + + @override + Widget build(BuildContext context) { + final parts = []; + if (candidate?.distanceToDestinationMeters != null) + parts.add('${(candidate!.distanceToDestinationMeters! / 1000).toStringAsFixed(1)} км'); + if (candidate?.durationFromOriginSeconds != null) + parts.add(_formatDuration(candidate!.durationFromOriginSeconds!)); + + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.12), + blurRadius: 12, + offset: const Offset(0, -2), + ), + ], + ), + padding: EdgeInsets.fromLTRB( + 20, + 14, + 20, + MediaQuery.of(context).padding.bottom + 14, + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: AppColors.primary.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + ), + child: const Icon(Icons.navigation, color: AppColors.primary, size: 20), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Зона #$zoneId', + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15), + ), + if (parts.isNotEmpty) + Text( + parts.join(' • '), + style: const TextStyle( + color: AppColors.textSecondary, + fontSize: 13, + ), + ), + ], + ), + ), + TextButton( + onPressed: onFinish, + child: const Text( + 'Завершить', + style: TextStyle(color: AppColors.textSecondary), + ), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/screens/map/widgets/candidates_sheet.dart b/lib/presentation/screens/map/widgets/candidates_sheet.dart index c9001c5..7ff1ef9 100644 --- a/lib/presentation/screens/map/widgets/candidates_sheet.dart +++ b/lib/presentation/screens/map/widgets/candidates_sheet.dart @@ -2,6 +2,14 @@ import 'package:flutter/material.dart'; import '../../../../domain/models/route_result.dart'; import '../../../../core/theme/app_colors.dart'; +String _formatDuration(int seconds) { + final mins = (seconds / 60).round(); + if (mins < 60) return '~$mins мин'; + final h = mins ~/ 60; + final m = mins % 60; + return m == 0 ? '~${h}ч' : '~${h}ч ${m}мин'; +} + class _CandidateSubtitle extends StatelessWidget { final RouteCandidate candidate; const _CandidateSubtitle({required this.candidate}); @@ -15,7 +23,7 @@ class _CandidateSubtitle extends StatelessWidget { if (candidate.distanceToDestinationMeters != null) parts.add('${(candidate.distanceToDestinationMeters! / 1000).toStringAsFixed(1)} км'); if (candidate.durationFromOriginSeconds != null) - parts.add('~${(candidate.durationFromOriginSeconds! / 60).round()} мин'); + parts.add(_formatDuration(candidate.durationFromOriginSeconds!)); if (candidate.predictedFreeCount != null) parts.add('прогноз: ${candidate.predictedFreeCount}'); return Text(parts.join(' • ')); diff --git a/lib/presentation/screens/map/widgets/route_preview_sheet.dart b/lib/presentation/screens/map/widgets/route_preview_sheet.dart index e225ed2..d3d344a 100644 --- a/lib/presentation/screens/map/widgets/route_preview_sheet.dart +++ b/lib/presentation/screens/map/widgets/route_preview_sheet.dart @@ -11,11 +11,13 @@ class RoutePreviewSheet extends ConsumerStatefulWidget { required this.route, this.zoneLat, this.zoneLon, + this.onNavigateInApp, }); final ActiveRoute route; final double? zoneLat; final double? zoneLon; + final VoidCallback? onNavigateInApp; @override ConsumerState createState() => _RoutePreviewSheetState(); @@ -34,23 +36,33 @@ class _RoutePreviewSheetState extends ConsumerState { return '$hh:$mm'; } - Future _launch() async { + String _formatDuration(int seconds) { + final mins = (seconds / 60).round(); + if (mins < 60) return '~$mins мин'; + final h = mins ~/ 60; + final m = mins % 60; + return m == 0 ? '~${h}ч' : '~${h}ч ${m}мин'; + } + + Future _launchNavigator() async { setState(() => _launching = true); try { final url = widget.route.deeplinkUrl; - if (url != null) { - await openYandexNavigatorUrl(url); - return; - } final lat = widget.zoneLat; final lon = widget.zoneLon; - if (lat != null && lon != null) { + if (url != null) { + await openYandexNavigatorUrl(url); + } else if (lat != null && lon != null) { await openYandexNavigator(lat, lon); + } else { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Нет данных для навигации')), + ); + } return; } - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Нет данных для навигации')), - ); + if (mounted) Navigator.pop(context); } finally { if (mounted) setState(() => _launching = false); } @@ -58,6 +70,7 @@ class _RoutePreviewSheetState extends ConsumerState { @override Widget build(BuildContext context) { + final candidate = widget.route.candidates.firstOrNull; return DraggableScrollableSheet( expand: false, initialChildSize: 0.42, @@ -91,23 +104,44 @@ class _RoutePreviewSheetState extends ConsumerState { Text('Зона: #${widget.route.selectedZoneId}'), if (widget.route.arrivalTime != null) Text('Прибытие: ${_formatArrival(widget.route.arrivalTime!)}'), - Builder(builder: (_) { - final c = widget.route.candidates.firstOrNull; - if (c == null) return const SizedBox.shrink(); - final parts = []; - if (c.distanceToDestinationMeters != null) - parts.add('${(c.distanceToDestinationMeters! / 1000).toStringAsFixed(1)} км'); - if (c.durationFromOriginSeconds != null) - parts.add('~${(c.durationFromOriginSeconds! / 60).round()} мин'); - if (parts.isEmpty) return const SizedBox.shrink(); - return Text(parts.join(' • '), - style: const TextStyle(color: Color(0xFF666666))); - }), + if (candidate != null) ...[ + Builder(builder: (_) { + final parts = []; + if (candidate.distanceToDestinationMeters != null) + parts.add('${(candidate.distanceToDestinationMeters! / 1000).toStringAsFixed(1)} км'); + if (candidate.durationFromOriginSeconds != null) + parts.add(_formatDuration(candidate.durationFromOriginSeconds!)); + if (parts.isEmpty) return const SizedBox.shrink(); + return Text(parts.join(' • '), + style: const TextStyle(color: Color(0xFF666666))); + }), + ], const SizedBox(height: 18), + if (widget.onNavigateInApp != null) + FilledButton.icon( + onPressed: () { + Navigator.pop(context); + widget.onNavigateInApp!(); + }, + icon: const Icon(Icons.map_outlined), + label: const Text('Маршрут в приложении'), + ), + const SizedBox(height: 8), FilledButton.icon( - onPressed: _launching ? null : _launch, + style: FilledButton.styleFrom( + backgroundColor: widget.onNavigateInApp != null + ? Colors.white + : null, + foregroundColor: widget.onNavigateInApp != null + ? AppColors.primary + : null, + side: widget.onNavigateInApp != null + ? const BorderSide(color: AppColors.primary) + : null, + ), + onPressed: _launching ? null : _launchNavigator, icon: const Icon(Icons.navigation_outlined), - label: Text(_launching ? 'Открываем...' : 'Открыть в навигаторе'), + label: Text(_launching ? 'Открываем...' : 'Яндекс Навигатор'), ), const SizedBox(height: 8), TextButton( From 152eb2da1f026572384cc44462d65ef77a2a70d7 Mon Sep 17 00:00:00 2001 From: Ilya Kiselev Date: Sat, 23 May 2026 23:43:10 +0300 Subject: [PATCH 012/145] feat: in-app navigator with heading-up camera, turns, ETA, speed, traffic --- lib/core/utils/nav_math.dart | 128 ++++++++++ .../providers/navigation_provider.dart | 121 ++++++++++ lib/presentation/screens/map/map_screen.dart | 226 ++++++++---------- .../map/widgets/navigation_overlay.dart | 221 +++++++++++++++++ 4 files changed, 566 insertions(+), 130 deletions(-) create mode 100644 lib/core/utils/nav_math.dart create mode 100644 lib/presentation/providers/navigation_provider.dart create mode 100644 lib/presentation/screens/map/widgets/navigation_overlay.dart diff --git a/lib/core/utils/nav_math.dart b/lib/core/utils/nav_math.dart new file mode 100644 index 0000000..5bfc48f --- /dev/null +++ b/lib/core/utils/nav_math.dart @@ -0,0 +1,128 @@ +import 'dart:math'; +import 'package:yandex_mapkit/yandex_mapkit.dart'; + +const _kEarthRadius = 6371000.0; + +enum TurnDirection { left, slightLeft, straight, slightRight, right, uTurn, arrive } + +class NavTurn { + final TurnDirection direction; + final double distanceMeters; + const NavTurn(this.direction, this.distanceMeters); +} + +double navDistanceM(Point a, Point b) { + final lat1 = a.latitude * pi / 180; + final lat2 = b.latitude * pi / 180; + final dlat = (b.latitude - a.latitude) * pi / 180; + final dlon = (b.longitude - a.longitude) * pi / 180; + final h = sin(dlat / 2) * sin(dlat / 2) + + cos(lat1) * cos(lat2) * sin(dlon / 2) * sin(dlon / 2); + return 2 * _kEarthRadius * asin(sqrt(h.clamp(0.0, 1.0))); +} + +double navBearingDeg(Point a, Point b) { + final lat1 = a.latitude * pi / 180; + final lat2 = b.latitude * pi / 180; + final dlon = (b.longitude - a.longitude) * pi / 180; + final y = sin(dlon) * cos(lat2); + final x = cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(dlon); + return (atan2(y, x) * 180 / pi + 360) % 360; +} + +double _bearingDiff(double to, double from) => (to - from + 540) % 360 - 180; + +({int segment, double t}) closestOnRoute(Point p, List pts) { + int bestSeg = 0; + double bestT = 0; + double bestDist = double.infinity; + for (int i = 0; i < pts.length - 1; i++) { + final a = pts[i]; + final b = pts[i + 1]; + final dx = b.longitude - a.longitude; + final dy = b.latitude - a.latitude; + final len2 = dx * dx + dy * dy; + double t = 0; + if (len2 > 1e-18) { + t = ((p.longitude - a.longitude) * dx + (p.latitude - a.latitude) * dy) / len2; + t = t.clamp(0.0, 1.0); + } + final proj = Point( + latitude: a.latitude + t * dy, + longitude: a.longitude + t * dx, + ); + final d = navDistanceM(p, proj); + if (d < bestDist) { + bestDist = d; + bestSeg = i; + bestT = t; + } + } + return (segment: bestSeg, t: bestT); +} + +double remainingRouteDistance(List pts, int seg, double t) { + if (pts.length < 2 || seg >= pts.length - 1) return 0; + final a = pts[seg]; + final b = pts[seg + 1]; + final proj = Point( + latitude: a.latitude + t * (b.latitude - a.latitude), + longitude: a.longitude + t * (b.longitude - a.longitude), + ); + double dist = navDistanceM(proj, b); + for (int i = seg + 1; i < pts.length - 1; i++) { + dist += navDistanceM(pts[i], pts[i + 1]); + } + return dist; +} + +double perpDistToRoute(Point p, List pts, int seg, double t) { + if (pts.isEmpty || seg >= pts.length - 1) return 0; + final a = pts[seg]; + final b = pts[seg + 1]; + final proj = Point( + latitude: a.latitude + t * (b.latitude - a.latitude), + longitude: a.longitude + t * (b.longitude - a.longitude), + ); + return navDistanceM(p, proj); +} + +NavTurn? findNextTurn(List pts, int fromSeg, Point currentPos) { + if (pts.length < 3) return null; + double accDist = fromSeg < pts.length - 1 + ? navDistanceM(currentPos, pts[fromSeg + 1]) + : 0; + for (int i = max(fromSeg + 1, 1); i < pts.length - 1; i++) { + final b1 = navBearingDeg(pts[i - 1], pts[i]); + final b2 = navBearingDeg(pts[i], pts[i + 1]); + final diff = _bearingDiff(b2, b1); + if (diff.abs() >= 18) { + return NavTurn(_classifyTurn(diff), accDist); + } + accDist += navDistanceM(pts[i], pts[i + 1]); + if (accDist > 3000) break; + } + return null; +} + +TurnDirection _classifyTurn(double diff) { + final a = diff.abs(); + if (a > 150) return TurnDirection.uTurn; + if (diff > 60) return TurnDirection.right; + if (diff > 18) return TurnDirection.slightRight; + if (diff < -60) return TurnDirection.left; + if (diff < -18) return TurnDirection.slightLeft; + return TurnDirection.straight; +} + +String formatNavDistance(double meters) { + if (meters < 950) return '${meters.round()} м'; + return '${(meters / 1000).toStringAsFixed(1)} км'; +} + +String formatNavDuration(int seconds) { + if (seconds < 3600) return '${(seconds / 60).round()} мин'; + final h = seconds ~/ 3600; + final m = (seconds % 3600) ~/ 60; + return m == 0 ? '${h}ч' : '${h}ч ${m}мин'; +} diff --git a/lib/presentation/providers/navigation_provider.dart b/lib/presentation/providers/navigation_provider.dart new file mode 100644 index 0000000..038d995 --- /dev/null +++ b/lib/presentation/providers/navigation_provider.dart @@ -0,0 +1,121 @@ +import 'dart:async'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:yandex_mapkit/yandex_mapkit.dart'; +import '../../core/utils/nav_math.dart'; + +class NavigationData { + final int zoneId; + final List route; + final double remainingMeters; + final int remainingSeconds; + final double speedKmh; + final NavTurn? nextTurn; + final bool isOffRoute; + final Point currentPosition; + final double heading; + + const NavigationData({ + required this.zoneId, + required this.route, + required this.remainingMeters, + required this.remainingSeconds, + required this.speedKmh, + required this.currentPosition, + required this.heading, + this.nextTurn, + this.isOffRoute = false, + }); + + bool get hasArrived => remainingMeters < 30; + + NavigationData copyWith({ + double? remainingMeters, + int? remainingSeconds, + double? speedKmh, + NavTurn? nextTurn, + bool clearTurn = false, + bool? isOffRoute, + Point? currentPosition, + double? heading, + }) { + return NavigationData( + zoneId: zoneId, + route: route, + remainingMeters: remainingMeters ?? this.remainingMeters, + remainingSeconds: remainingSeconds ?? this.remainingSeconds, + speedKmh: speedKmh ?? this.speedKmh, + nextTurn: clearTurn ? null : (nextTurn ?? this.nextTurn), + isOffRoute: isOffRoute ?? this.isOffRoute, + currentPosition: currentPosition ?? this.currentPosition, + heading: heading ?? this.heading, + ); + } +} + +class NavigationNotifier extends Notifier { + StreamSubscription? _sub; + int _segmentIndex = 0; + double _totalSeconds = 0; + double _totalMeters = 0; + + @override + NavigationData? build() => null; + + Future startNavigation({ + required int zoneId, + required List route, + required double totalSeconds, + required double totalMeters, + }) async { + await _sub?.cancel(); + _segmentIndex = 0; + _totalSeconds = totalSeconds; + _totalMeters = totalMeters; + + _sub = Geolocator.getPositionStream( + locationSettings: const LocationSettings( + accuracy: LocationAccuracy.high, + distanceFilter: 5, + ), + ).listen((pos) => _onPosition(pos, zoneId, route)); + } + + void stop() { + _sub?.cancel(); + _sub = null; + state = null; + } + + void _onPosition(Position pos, int zoneId, List route) { + final current = Point(latitude: pos.latitude, longitude: pos.longitude); + final closest = closestOnRoute(current, route); + + if (closest.segment > _segmentIndex) { + _segmentIndex = closest.segment; + } + + final remaining = remainingRouteDistance(route, _segmentIndex, closest.t); + final perp = perpDistToRoute(current, route, _segmentIndex, closest.t); + final remainingSecs = _totalMeters > 0 + ? (_totalSeconds * remaining / _totalMeters).round() + : 0; + + final nextTurn = findNextTurn(route, _segmentIndex, current); + + state = NavigationData( + zoneId: zoneId, + route: route, + remainingMeters: remaining, + remainingSeconds: remainingSecs, + speedKmh: (pos.speed * 3.6).clamp(0, 300), + currentPosition: current, + heading: pos.heading >= 0 ? pos.heading : 0, + nextTurn: nextTurn, + isOffRoute: perp > 60, + ); + } +} + +final navigationProvider = + NotifierProvider(NavigationNotifier.new); diff --git a/lib/presentation/screens/map/map_screen.dart b/lib/presentation/screens/map/map_screen.dart index 37dd1fb..223a27e 100644 --- a/lib/presentation/screens/map/map_screen.dart +++ b/lib/presentation/screens/map/map_screen.dart @@ -11,11 +11,13 @@ import '../../providers/zones_provider.dart'; import '../../../domain/models/zone.dart'; import '../../providers/filters_provider.dart'; import '../../providers/routing_provider.dart'; -import '../../../domain/models/route_result.dart'; +import '../../providers/navigation_provider.dart'; import '../../providers/time_selector_provider.dart'; import '../../../core/theme/app_colors.dart'; +import '../../../core/utils/nav_math.dart'; import '../../../core/utils/navigation_deeplink.dart'; import 'widgets/candidates_sheet.dart'; +import 'widgets/navigation_overlay.dart' show NavigationTurnCard, NavigationBottomBar; import 'widgets/parking_zone_layer.dart'; import 'widgets/time_selector_widget.dart'; import 'widgets/parking_card_sheet.dart'; @@ -46,7 +48,6 @@ class _MapScreenState extends ConsumerState { Map _zonesById = {}; List? _routePolyline; int? _activeRouteZoneId; - bool _inAppNavMode = false; DrivingSession? _drivingSession; @override @@ -306,33 +307,70 @@ class _MapScreenState extends ConsumerState { ); } - Future _startInAppNavigation(double toLat, double toLon) async { - setState(() => _inAppNavMode = true); - if (_routePolyline != null) return; - final pos = _userPosition; + Future _startInAppNavigation({ + required int zoneId, + required double toLat, + required double toLon, + }) async { + final pos = await _getCurrentPosition(); if (pos == null) return; - await _drivingSession?.close(); - final resultWithSession = YandexDriving.requestRoutes( - points: [ - RequestPoint( - point: Point(latitude: pos.latitude, longitude: pos.longitude), - requestPointType: RequestPointType.wayPoint, - ), - RequestPoint( - point: Point(latitude: toLat, longitude: toLon), - requestPointType: RequestPointType.wayPoint, - ), - ], - drivingOptions: const DrivingOptions(routesCount: 1), - ); - _drivingSession = resultWithSession.session; - final result = await resultWithSession.result; - await resultWithSession.session.close(); - _drivingSession = null; - final route = result.routes?.firstOrNull; - if (route != null && mounted) { - setState(() => _routePolyline = route.geometry); + + List route; + double totalSeconds = 0; + double totalMeters = 0; + + // Prefer backend polyline, else call YandexDriving + if (_routePolyline != null && _routePolyline!.length >= 2) { + route = _routePolyline!; + } else { + await _drivingSession?.close(); + final rws = YandexDriving.requestRoutes( + points: [ + RequestPoint( + point: Point(latitude: pos.latitude, longitude: pos.longitude), + requestPointType: RequestPointType.wayPoint, + ), + RequestPoint( + point: Point(latitude: toLat, longitude: toLon), + requestPointType: RequestPointType.wayPoint, + ), + ], + drivingOptions: const DrivingOptions(routesCount: 1), + ); + _drivingSession = rws.session; + final result = await rws.result; + await rws.session.close(); + _drivingSession = null; + final dr = result.routes?.firstOrNull; + if (dr == null || !mounted) return; + route = dr.geometry; + totalSeconds = dr.metadata.weight.timeWithTraffic.value ?? 0; + totalMeters = dr.metadata.weight.distance.value ?? 0; + setState(() => _routePolyline = route); } + + // Build total metrics from nav_math if backend polyline used + if (totalMeters == 0 && route.length >= 2) { + for (int i = 0; i < route.length - 1; i++) { + totalMeters += navDistanceM(route[i], route[i + 1]); + } + totalSeconds = totalMeters / 10; // rough: 36 km/h average + } + + await ref.read(navigationProvider.notifier).startNavigation( + zoneId: zoneId, + route: route, + totalSeconds: totalSeconds, + totalMeters: totalMeters, + ); + + // Enable heading arrow and traffic + await _mapController?.toggleUserLayer( + visible: true, + headingEnabled: true, + autoZoomEnabled: false, + ); + await _mapController?.toggleTrafficLayer(visible: true); } void _showFilters(BuildContext context) { @@ -358,6 +396,19 @@ class _MapScreenState extends ConsumerState { ref.listen(timeSelectorProvider, (_, __) => _fetchZones()); ref.listen(filteredZonesProvider, (_, zones) => _updateZoneBitmaps(zones)); + ref.listen(navigationProvider, (_, nav) { + if (nav == null) return; + _mapController?.moveCamera( + CameraUpdate.newCameraPosition(CameraPosition( + target: nav.currentPosition, + zoom: 17, + azimuth: nav.heading, + tilt: 40, + )), + animation: const MapAnimation(duration: 0.6), + ); + }); + ref.listen(rawZonesProvider, (_, next) { next.whenOrNull( error: (e, _) { @@ -384,10 +435,12 @@ class _MapScreenState extends ConsumerState { ref.listen(routingProvider, (_, next) async { await next.when( idle: () async { + ref.read(navigationProvider.notifier).stop(); + await _mapController?.toggleUserLayer(visible: false); + await _mapController?.toggleTrafficLayer(visible: false); setState(() { _routePolyline = null; _activeRouteZoneId = null; - _inAppNavMode = false; }); }, searching: () async {}, @@ -455,7 +508,11 @@ class _MapScreenState extends ConsumerState { zoneLat: zoneLat, zoneLon: zoneLon, onNavigateInApp: (zoneLat != null && zoneLon != null) - ? () => _startInAppNavigation(zoneLat!, zoneLon!) + ? () => _startInAppNavigation( + zoneId: route.selectedZoneId, + toLat: zoneLat!, + toLon: zoneLon!, + ) : null, ), ); @@ -711,21 +768,20 @@ class _MapScreenState extends ConsumerState { ), ), ), - if (_inAppNavMode && _activeRouteZoneId != null) + if (ref.watch(navigationProvider) != null) + const Positioned( + top: 0, + left: 0, + right: 0, + child: NavigationTurnCard(), + ), + if (ref.watch(navigationProvider) != null) Positioned( bottom: 0, left: 0, right: 0, - child: _InAppNavBar( - zoneId: _activeRouteZoneId!, - candidate: routingState.maybeWhen( - routePreview: (r) => r.candidates.firstOrNull, - orElse: () => null, - ), - onFinish: () { - ref.read(routingProvider.notifier).reset(); - setState(() => _inAppNavMode = false); - }, + child: NavigationBottomBar( + onFinish: () => ref.read(routingProvider.notifier).reset(), ), ), if (isRoutingLoading) @@ -1049,93 +1105,3 @@ class _FiltersSheet extends ConsumerWidget { } } -// ─────────────────────────────────────────────────────────────────────────── - -class _InAppNavBar extends StatelessWidget { - const _InAppNavBar({ - required this.zoneId, - required this.onFinish, - this.candidate, - }); - - final int zoneId; - final VoidCallback onFinish; - final RouteCandidate? candidate; - - String _formatDuration(int seconds) { - final mins = (seconds / 60).round(); - if (mins < 60) return '~$mins мин'; - final h = mins ~/ 60; - final m = mins % 60; - return m == 0 ? '~${h}ч' : '~${h}ч ${m}мин'; - } - - @override - Widget build(BuildContext context) { - final parts = []; - if (candidate?.distanceToDestinationMeters != null) - parts.add('${(candidate!.distanceToDestinationMeters! / 1000).toStringAsFixed(1)} км'); - if (candidate?.durationFromOriginSeconds != null) - parts.add(_formatDuration(candidate!.durationFromOriginSeconds!)); - - return Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.12), - blurRadius: 12, - offset: const Offset(0, -2), - ), - ], - ), - padding: EdgeInsets.fromLTRB( - 20, - 14, - 20, - MediaQuery.of(context).padding.bottom + 14, - ), - child: Row( - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: AppColors.primary.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(8), - ), - child: const Icon(Icons.navigation, color: AppColors.primary, size: 20), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - 'Зона #$zoneId', - style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15), - ), - if (parts.isNotEmpty) - Text( - parts.join(' • '), - style: const TextStyle( - color: AppColors.textSecondary, - fontSize: 13, - ), - ), - ], - ), - ), - TextButton( - onPressed: onFinish, - child: const Text( - 'Завершить', - style: TextStyle(color: AppColors.textSecondary), - ), - ), - ], - ), - ); - } -} diff --git a/lib/presentation/screens/map/widgets/navigation_overlay.dart b/lib/presentation/screens/map/widgets/navigation_overlay.dart new file mode 100644 index 0000000..5f89103 --- /dev/null +++ b/lib/presentation/screens/map/widgets/navigation_overlay.dart @@ -0,0 +1,221 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../../../core/theme/app_colors.dart'; +import '../../../../core/utils/nav_math.dart'; +import '../../../providers/navigation_provider.dart'; + +class NavigationTurnCard extends ConsumerWidget { + const NavigationTurnCard({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final nav = ref.watch(navigationProvider); + if (nav == null) return const SizedBox.shrink(); + return _TurnCard( + turn: nav.nextTurn, + isOffRoute: nav.isOffRoute, + hasArrived: nav.hasArrived, + ); + } +} + +class NavigationBottomBar extends ConsumerWidget { + const NavigationBottomBar({super.key, required this.onFinish}); + + final VoidCallback onFinish; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final nav = ref.watch(navigationProvider); + if (nav == null) return const SizedBox.shrink(); + return _NavBottomBar(nav: nav, onFinish: onFinish); + } +} + +// ─── Turn instruction card ───────────────────────────────────────────────── + +class _TurnCard extends StatelessWidget { + const _TurnCard({required this.turn, required this.isOffRoute, required this.hasArrived}); + + final NavTurn? turn; + final bool isOffRoute; + final bool hasArrived; + + @override + Widget build(BuildContext context) { + final Color bg; + final IconData icon; + final String primary; + final String? secondary; + + if (hasArrived) { + bg = AppColors.primary; + icon = Icons.local_parking; + primary = 'Вы прибыли'; + secondary = null; + } else if (isOffRoute) { + bg = Colors.orange.shade800; + icon = Icons.warning_rounded; + primary = 'Пересчёт маршрута...'; + secondary = null; + } else if (turn != null) { + final t = turn!; + bg = AppColors.primary; + icon = _turnIcon(t.direction); + primary = _turnLabel(t.direction); + secondary = 'через ${formatNavDistance(t.distanceMeters)}'; + } else { + bg = AppColors.primary; + icon = Icons.arrow_upward_rounded; + primary = 'Движение прямо'; + secondary = null; + } + + return Material( + color: bg, + borderRadius: const BorderRadius.vertical(bottom: Radius.circular(16)), + elevation: 6, + child: SafeArea( + bottom: false, + child: Padding( + padding: const EdgeInsets.fromLTRB(20, 12, 20, 16), + child: Row( + children: [ + Icon(icon, color: Colors.white, size: 48), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + primary, + style: const TextStyle( + color: Colors.white, + fontSize: 22, + fontWeight: FontWeight.bold, + height: 1.1, + ), + ), + if (secondary != null) ...[ + const SizedBox(height: 2), + Text( + secondary, + style: const TextStyle( + color: Colors.white70, + fontSize: 16, + ), + ), + ], + ], + ), + ), + ], + ), + ), + ), + ); + } + + IconData _turnIcon(TurnDirection d) => switch (d) { + TurnDirection.left => Icons.turn_left_rounded, + TurnDirection.slightLeft => Icons.turn_slight_left_rounded, + TurnDirection.right => Icons.turn_right_rounded, + TurnDirection.slightRight => Icons.turn_slight_right_rounded, + TurnDirection.uTurn => Icons.u_turn_left_rounded, + TurnDirection.arrive => Icons.local_parking, + TurnDirection.straight => Icons.arrow_upward_rounded, + }; + + String _turnLabel(TurnDirection d) => switch (d) { + TurnDirection.left => 'Поверните налево', + TurnDirection.slightLeft => 'Держитесь левее', + TurnDirection.right => 'Поверните направо', + TurnDirection.slightRight => 'Держитесь правее', + TurnDirection.uTurn => 'Выполните разворот', + TurnDirection.arrive => 'Вы прибыли', + TurnDirection.straight => 'Движение прямо', + }; +} + +// ─── Bottom stats bar ────────────────────────────────────────────────────── + +class _NavBottomBar extends StatelessWidget { + const _NavBottomBar({required this.nav, required this.onFinish}); + + final NavigationData nav; + final VoidCallback onFinish; + + @override + Widget build(BuildContext context) { + final speedColor = nav.speedKmh > 90 ? Colors.red.shade600 : AppColors.onSurface; + return Container( + color: Colors.white, + padding: EdgeInsets.fromLTRB( + 20, 10, 8, MediaQuery.of(context).padding.bottom + 10, + ), + child: Row( + children: [ + _StatCell( + value: formatNavDuration(nav.remainingSeconds), + label: 'времени', + ), + const SizedBox(width: 24), + _StatCell( + value: formatNavDistance(nav.remainingMeters), + label: 'до цели', + ), + const SizedBox(width: 24), + _StatCell( + value: '${nav.speedKmh.round()}', + label: 'км/ч', + valueColor: speedColor, + ), + const Spacer(), + TextButton( + onPressed: onFinish, + child: const Text( + 'Завершить', + style: TextStyle(color: AppColors.textSecondary, fontSize: 15), + ), + ), + ], + ), + ); + } +} + +class _StatCell extends StatelessWidget { + const _StatCell({required this.value, required this.label, this.valueColor}); + + final String value; + final String label; + final Color? valueColor; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + value, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: valueColor ?? AppColors.onSurface, + height: 1, + ), + ), + Text( + label, + style: const TextStyle( + fontSize: 12, + color: AppColors.textSecondary, + height: 1.2, + ), + ), + ], + ); + } +} From a931a081bbc9e96eee544861fabb32f21a935762 Mon Sep 17 00:00:00 2001 From: Ilya Kiselev Date: Sat, 23 May 2026 23:52:46 +0300 Subject: [PATCH 013/145] fix: nav bar overflow, camera to position on start, finish confirmation --- lib/presentation/screens/map/map_screen.dart | 10 ++++ .../map/widgets/navigation_overlay.dart | 58 ++++++++++++++----- 2 files changed, 52 insertions(+), 16 deletions(-) diff --git a/lib/presentation/screens/map/map_screen.dart b/lib/presentation/screens/map/map_screen.dart index 223a27e..6b07a33 100644 --- a/lib/presentation/screens/map/map_screen.dart +++ b/lib/presentation/screens/map/map_screen.dart @@ -315,6 +315,16 @@ class _MapScreenState extends ConsumerState { final pos = await _getCurrentPosition(); if (pos == null) return; + // Immediately fly camera to user's current position + await _mapController?.moveCamera( + CameraUpdate.newCameraPosition(CameraPosition( + target: Point(latitude: pos.latitude, longitude: pos.longitude), + zoom: 17, + tilt: 40, + )), + animation: const MapAnimation(duration: 0.8), + ); + List route; double totalSeconds = 0; double totalMeters = 0; diff --git a/lib/presentation/screens/map/widgets/navigation_overlay.dart b/lib/presentation/screens/map/widgets/navigation_overlay.dart index 5f89103..96434e3 100644 --- a/lib/presentation/screens/map/widgets/navigation_overlay.dart +++ b/lib/presentation/screens/map/widgets/navigation_overlay.dart @@ -146,37 +146,63 @@ class _NavBottomBar extends StatelessWidget { final NavigationData nav; final VoidCallback onFinish; + void _confirmFinish(BuildContext context) { + showDialog( + context: context, + builder: (_) => AlertDialog( + title: const Text('Завершить маршрут?'), + content: const Text('Навигация будет остановлена.'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Нет'), + ), + FilledButton( + onPressed: () { + Navigator.pop(context); + onFinish(); + }, + child: const Text('Завершить'), + ), + ], + ), + ); + } + @override Widget build(BuildContext context) { final speedColor = nav.speedKmh > 90 ? Colors.red.shade600 : AppColors.onSurface; return Container( color: Colors.white, padding: EdgeInsets.fromLTRB( - 20, 10, 8, MediaQuery.of(context).padding.bottom + 10, + 12, 10, 4, MediaQuery.of(context).padding.bottom + 10, ), child: Row( children: [ - _StatCell( - value: formatNavDuration(nav.remainingSeconds), - label: 'времени', + Expanded( + child: _StatCell( + value: formatNavDuration(nav.remainingSeconds), + label: 'времени', + ), ), - const SizedBox(width: 24), - _StatCell( - value: formatNavDistance(nav.remainingMeters), - label: 'до цели', + Expanded( + child: _StatCell( + value: formatNavDistance(nav.remainingMeters), + label: 'до цели', + ), ), - const SizedBox(width: 24), - _StatCell( - value: '${nav.speedKmh.round()}', - label: 'км/ч', - valueColor: speedColor, + Expanded( + child: _StatCell( + value: '${nav.speedKmh.round()}', + label: 'км/ч', + valueColor: speedColor, + ), ), - const Spacer(), TextButton( - onPressed: onFinish, + onPressed: () => _confirmFinish(context), child: const Text( 'Завершить', - style: TextStyle(color: AppColors.textSecondary, fontSize: 15), + style: TextStyle(color: AppColors.textSecondary, fontSize: 14), ), ), ], From 1d2c2d60555546b973bf6c666c5cb245893be0fb Mon Sep 17 00:00:00 2001 From: Ilya Kiselev Date: Sun, 24 May 2026 00:23:31 +0300 Subject: [PATCH 014/145] feat: off-route recalc, auto-stop, nav UI hiding, loading, background gps --- android/app/src/main/AndroidManifest.xml | 7 + .../providers/navigation_provider.dart | 197 ++++++++++++++---- lib/presentation/screens/map/map_screen.dart | 177 ++++++++++------ .../map/widgets/navigation_overlay.dart | 12 +- 4 files changed, 285 insertions(+), 108 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 74c93c7..2ceeed0 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -2,6 +2,9 @@ + + + + diff --git a/lib/presentation/providers/navigation_provider.dart b/lib/presentation/providers/navigation_provider.dart index 038d995..8801e02 100644 --- a/lib/presentation/providers/navigation_provider.dart +++ b/lib/presentation/providers/navigation_provider.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:geolocator/geolocator.dart'; import 'package:yandex_mapkit/yandex_mapkit.dart'; @@ -12,6 +13,7 @@ class NavigationData { final double speedKmh; final NavTurn? nextTurn; final bool isOffRoute; + final bool isRecalculating; final Point currentPosition; final double heading; @@ -25,39 +27,25 @@ class NavigationData { required this.heading, this.nextTurn, this.isOffRoute = false, + this.isRecalculating = false, }); bool get hasArrived => remainingMeters < 30; - - NavigationData copyWith({ - double? remainingMeters, - int? remainingSeconds, - double? speedKmh, - NavTurn? nextTurn, - bool clearTurn = false, - bool? isOffRoute, - Point? currentPosition, - double? heading, - }) { - return NavigationData( - zoneId: zoneId, - route: route, - remainingMeters: remainingMeters ?? this.remainingMeters, - remainingSeconds: remainingSeconds ?? this.remainingSeconds, - speedKmh: speedKmh ?? this.speedKmh, - nextTurn: clearTurn ? null : (nextTurn ?? this.nextTurn), - isOffRoute: isOffRoute ?? this.isOffRoute, - currentPosition: currentPosition ?? this.currentPosition, - heading: heading ?? this.heading, - ); - } } class NavigationNotifier extends Notifier { StreamSubscription? _sub; + Timer? _recalcDebounce; + Timer? _arrivalTimer; + DrivingSession? _drivingSession; + int _segmentIndex = 0; double _totalSeconds = 0; double _totalMeters = 0; + List _route = []; + double _destLat = 0; + double _destLon = 0; + int _zoneId = 0; @override NavigationData? build() => null; @@ -67,53 +55,186 @@ class NavigationNotifier extends Notifier { required List route, required double totalSeconds, required double totalMeters, + required double destLat, + required double destLon, }) async { + _cleanupTimers(); await _sub?.cancel(); + _segmentIndex = 0; _totalSeconds = totalSeconds; _totalMeters = totalMeters; + _route = route; + _destLat = destLat; + _destLon = destLon; + _zoneId = zoneId; _sub = Geolocator.getPositionStream( - locationSettings: const LocationSettings( - accuracy: LocationAccuracy.high, - distanceFilter: 5, - ), - ).listen((pos) => _onPosition(pos, zoneId, route)); + locationSettings: _buildLocationSettings(), + ).listen(_onPosition); } void stop() { + _cleanupTimers(); _sub?.cancel(); _sub = null; + _drivingSession?.close(); + _drivingSession = null; state = null; } - void _onPosition(Position pos, int zoneId, List route) { + LocationSettings _buildLocationSettings() { + if (Platform.isAndroid) { + return AndroidSettings( + accuracy: LocationAccuracy.high, + distanceFilter: 5, + foregroundNotificationConfig: const ForegroundNotificationConfig( + notificationTitle: 'ParkTrack', + notificationText: 'Навигация активна', + enableWakeLock: true, + ), + ); + } + return const LocationSettings( + accuracy: LocationAccuracy.high, + distanceFilter: 5, + ); + } + + void _cleanupTimers() { + _recalcDebounce?.cancel(); + _recalcDebounce = null; + _arrivalTimer?.cancel(); + _arrivalTimer = null; + } + + void _onPosition(Position pos) { final current = Point(latitude: pos.latitude, longitude: pos.longitude); - final closest = closestOnRoute(current, route); + final closest = closestOnRoute(current, _route); if (closest.segment > _segmentIndex) { _segmentIndex = closest.segment; } - final remaining = remainingRouteDistance(route, _segmentIndex, closest.t); - final perp = perpDistToRoute(current, route, _segmentIndex, closest.t); + final remaining = remainingRouteDistance(_route, _segmentIndex, closest.t); + final perp = perpDistToRoute(current, _route, _segmentIndex, closest.t); final remainingSecs = _totalMeters > 0 ? (_totalSeconds * remaining / _totalMeters).round() : 0; - - final nextTurn = findNextTurn(route, _segmentIndex, current); + final nextTurn = findNextTurn(_route, _segmentIndex, current); + final isOff = perp > 60; state = NavigationData( - zoneId: zoneId, - route: route, + zoneId: _zoneId, + route: _route, remainingMeters: remaining, remainingSeconds: remainingSecs, - speedKmh: (pos.speed * 3.6).clamp(0, 300), + speedKmh: (pos.speed * 3.6).clamp(0.0, 300.0), currentPosition: current, heading: pos.heading >= 0 ? pos.heading : 0, nextTurn: nextTurn, - isOffRoute: perp > 60, + isOffRoute: isOff, + isRecalculating: state?.isRecalculating ?? false, ); + + // Auto-stop 3 s after arrival + if (remaining < 30) { + _recalcDebounce?.cancel(); + _recalcDebounce = null; + _arrivalTimer ??= Timer(const Duration(seconds: 3), stop); + } else { + if (_arrivalTimer != null) { + _arrivalTimer?.cancel(); + _arrivalTimer = null; + } + + // Debounced off-route recalculation + if (isOff) { + _recalcDebounce ??= Timer(const Duration(seconds: 5), () => _recalculate(current)); + } else { + _recalcDebounce?.cancel(); + _recalcDebounce = null; + } + } + } + + Future _recalculate(Point from) async { + _recalcDebounce = null; + final current = state; + if (current == null) return; + + state = NavigationData( + zoneId: current.zoneId, + route: current.route, + remainingMeters: current.remainingMeters, + remainingSeconds: current.remainingSeconds, + speedKmh: current.speedKmh, + currentPosition: current.currentPosition, + heading: current.heading, + isOffRoute: true, + isRecalculating: true, + ); + + try { + await _drivingSession?.close(); + final rws = YandexDriving.requestRoutes( + points: [ + RequestPoint(point: from, requestPointType: RequestPointType.wayPoint), + RequestPoint( + point: Point(latitude: _destLat, longitude: _destLon), + requestPointType: RequestPointType.wayPoint, + ), + ], + drivingOptions: const DrivingOptions(routesCount: 1), + ); + _drivingSession = rws.session; + final result = await rws.result; + await rws.session.close(); + _drivingSession = null; + + final dr = result.routes?.firstOrNull; + if (dr == null || state == null) return; + + _route = dr.geometry; + _segmentIndex = 0; + _totalSeconds = dr.metadata.weight.timeWithTraffic.value ?? _totalSeconds; + _totalMeters = dr.metadata.weight.distance.value ?? _totalMeters; + + if (_totalMeters == 0 && _route.length >= 2) { + for (int i = 0; i < _route.length - 1; i++) { + _totalMeters += navDistanceM(_route[i], _route[i + 1]); + } + _totalSeconds = _totalMeters / 10; + } + + final s = state!; + state = NavigationData( + zoneId: s.zoneId, + route: _route, + remainingMeters: _totalMeters, + remainingSeconds: _totalSeconds.round(), + speedKmh: s.speedKmh, + currentPosition: s.currentPosition, + heading: s.heading, + isOffRoute: false, + isRecalculating: false, + ); + } catch (_) { + final s = state; + if (s != null) { + state = NavigationData( + zoneId: s.zoneId, + route: s.route, + remainingMeters: s.remainingMeters, + remainingSeconds: s.remainingSeconds, + speedKmh: s.speedKmh, + currentPosition: s.currentPosition, + heading: s.heading, + isOffRoute: true, + isRecalculating: false, + ); + } + } } } diff --git a/lib/presentation/screens/map/map_screen.dart b/lib/presentation/screens/map/map_screen.dart index 6b07a33..bcb0bcf 100644 --- a/lib/presentation/screens/map/map_screen.dart +++ b/lib/presentation/screens/map/map_screen.dart @@ -49,6 +49,7 @@ class _MapScreenState extends ConsumerState { List? _routePolyline; int? _activeRouteZoneId; DrivingSession? _drivingSession; + bool _navBuilding = false; @override void initState() { @@ -312,75 +313,85 @@ class _MapScreenState extends ConsumerState { required double toLat, required double toLon, }) async { - final pos = await _getCurrentPosition(); - if (pos == null) return; + if (!mounted) return; + setState(() => _navBuilding = true); + try { + final pos = await _getCurrentPosition(); + if (pos == null) return; - // Immediately fly camera to user's current position - await _mapController?.moveCamera( - CameraUpdate.newCameraPosition(CameraPosition( - target: Point(latitude: pos.latitude, longitude: pos.longitude), - zoom: 17, - tilt: 40, - )), - animation: const MapAnimation(duration: 0.8), - ); + await _mapController?.moveCamera( + CameraUpdate.newCameraPosition(CameraPosition( + target: Point(latitude: pos.latitude, longitude: pos.longitude), + zoom: 17, + tilt: 40, + )), + animation: const MapAnimation(duration: 0.8), + ); - List route; - double totalSeconds = 0; - double totalMeters = 0; + List route; + double totalSeconds = 0; + double totalMeters = 0; - // Prefer backend polyline, else call YandexDriving - if (_routePolyline != null && _routePolyline!.length >= 2) { - route = _routePolyline!; - } else { - await _drivingSession?.close(); - final rws = YandexDriving.requestRoutes( - points: [ - RequestPoint( - point: Point(latitude: pos.latitude, longitude: pos.longitude), - requestPointType: RequestPointType.wayPoint, - ), - RequestPoint( - point: Point(latitude: toLat, longitude: toLon), - requestPointType: RequestPointType.wayPoint, - ), - ], - drivingOptions: const DrivingOptions(routesCount: 1), - ); - _drivingSession = rws.session; - final result = await rws.result; - await rws.session.close(); - _drivingSession = null; - final dr = result.routes?.firstOrNull; - if (dr == null || !mounted) return; - route = dr.geometry; - totalSeconds = dr.metadata.weight.timeWithTraffic.value ?? 0; - totalMeters = dr.metadata.weight.distance.value ?? 0; - setState(() => _routePolyline = route); - } + if (_routePolyline != null && _routePolyline!.length >= 2) { + route = _routePolyline!; + } else { + await _drivingSession?.close(); + final rws = YandexDriving.requestRoutes( + points: [ + RequestPoint( + point: Point(latitude: pos.latitude, longitude: pos.longitude), + requestPointType: RequestPointType.wayPoint, + ), + RequestPoint( + point: Point(latitude: toLat, longitude: toLon), + requestPointType: RequestPointType.wayPoint, + ), + ], + drivingOptions: const DrivingOptions(routesCount: 1), + ); + _drivingSession = rws.session; + final result = await rws.result; + await rws.session.close(); + _drivingSession = null; + final dr = result.routes?.firstOrNull; + if (dr == null || !mounted) return; + route = dr.geometry; + totalSeconds = dr.metadata.weight.timeWithTraffic.value ?? 0; + totalMeters = dr.metadata.weight.distance.value ?? 0; + setState(() => _routePolyline = route); + } - // Build total metrics from nav_math if backend polyline used - if (totalMeters == 0 && route.length >= 2) { - for (int i = 0; i < route.length - 1; i++) { - totalMeters += navDistanceM(route[i], route[i + 1]); + if (totalMeters == 0 && route.length >= 2) { + for (int i = 0; i < route.length - 1; i++) { + totalMeters += navDistanceM(route[i], route[i + 1]); + } + totalSeconds = totalMeters / 10; } - totalSeconds = totalMeters / 10; // rough: 36 km/h average - } - await ref.read(navigationProvider.notifier).startNavigation( - zoneId: zoneId, - route: route, - totalSeconds: totalSeconds, - totalMeters: totalMeters, - ); + await ref.read(navigationProvider.notifier).startNavigation( + zoneId: zoneId, + route: route, + totalSeconds: totalSeconds, + totalMeters: totalMeters, + destLat: toLat, + destLon: toLon, + ); - // Enable heading arrow and traffic - await _mapController?.toggleUserLayer( - visible: true, - headingEnabled: true, - autoZoomEnabled: false, - ); - await _mapController?.toggleTrafficLayer(visible: true); + await _mapController?.toggleUserLayer( + visible: true, + headingEnabled: true, + autoZoomEnabled: false, + ); + await _mapController?.toggleTrafficLayer(visible: true); + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Не удалось построить маршрут: $e')), + ); + } + } finally { + if (mounted) setState(() => _navBuilding = false); + } } void _showFilters(BuildContext context) { @@ -398,6 +409,7 @@ class _MapScreenState extends ConsumerState { final zonesAsync = ref.watch(rawZonesProvider); final routingState = ref.watch(routingProvider); final destination = ref.watch(destinationProvider); + final isNavigating = ref.watch(navigationProvider) != null; final isRoutingLoading = routingState.maybeWhen( searching: () => true, orElse: () => false, @@ -406,8 +418,11 @@ class _MapScreenState extends ConsumerState { ref.listen(timeSelectorProvider, (_, __) => _fetchZones()); ref.listen(filteredZonesProvider, (_, zones) => _updateZoneBitmaps(zones)); - ref.listen(navigationProvider, (_, nav) { + ref.listen(navigationProvider, (prev, nav) { if (nav == null) return; + if (prev?.route != nav.route) { + setState(() => _routePolyline = nav.route); + } _mapController?.moveCamera( CameraUpdate.newCameraPosition(CameraPosition( target: nav.currentPosition, @@ -642,6 +657,7 @@ class _MapScreenState extends ConsumerState { onCameraPositionChanged: _onCameraPositionChanged, ), // ─── Top bar ─────────────────────────────────────────────── + if (!isNavigating) SafeArea( child: Padding( padding: const EdgeInsets.all(12), @@ -697,7 +713,7 @@ class _MapScreenState extends ConsumerState { ), ), // ─── Нижняя строка: FAB слева + кнопки справа ───────────── - if (destination == null) + if (destination == null && !isNavigating) Positioned( bottom: bottomInset + 12, left: 12, @@ -740,7 +756,7 @@ class _MapScreenState extends ConsumerState { ), ), // ─── Таймлайн: полная ширина, своя строка выше кнопок ────── - if (destination == null) + if (destination == null && !isNavigating) Positioned( bottom: bottomInset + 76, left: 0, @@ -748,7 +764,7 @@ class _MapScreenState extends ConsumerState { child: const TimeSelectorWidget(), ), // ─── Карточка назначения ──────────────────────────────────── - if (destination != null) + if (destination != null && !isNavigating) Positioned( bottom: bottomInset + 76, left: 12, @@ -778,14 +794,14 @@ class _MapScreenState extends ConsumerState { ), ), ), - if (ref.watch(navigationProvider) != null) + if (isNavigating) const Positioned( top: 0, left: 0, right: 0, child: NavigationTurnCard(), ), - if (ref.watch(navigationProvider) != null) + if (isNavigating) Positioned( bottom: 0, left: 0, @@ -794,6 +810,31 @@ class _MapScreenState extends ConsumerState { onFinish: () => ref.read(routingProvider.notifier).reset(), ), ), + if (_navBuilding) + const Positioned.fill( + child: ColoredBox( + color: Color(0x55000000), + child: Center( + child: Card( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 20, vertical: 14), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator(strokeWidth: 2), + ), + SizedBox(width: 12), + Text('Строим маршрут...'), + ], + ), + ), + ), + ), + ), + ), if (isRoutingLoading) Positioned( top: 148, diff --git a/lib/presentation/screens/map/widgets/navigation_overlay.dart b/lib/presentation/screens/map/widgets/navigation_overlay.dart index 96434e3..469cb15 100644 --- a/lib/presentation/screens/map/widgets/navigation_overlay.dart +++ b/lib/presentation/screens/map/widgets/navigation_overlay.dart @@ -78,10 +78,18 @@ class _TurnCard extends StatelessWidget { child: SafeArea( bottom: false, child: Padding( - padding: const EdgeInsets.fromLTRB(20, 12, 20, 16), + padding: const EdgeInsets.fromLTRB(16, 12, 20, 16), child: Row( children: [ - Icon(icon, color: Colors.white, size: 48), + Container( + width: 64, + height: 64, + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + child: Icon(icon, color: bg, size: 38), + ), const SizedBox(width: 16), Expanded( child: Column( From 97495081df75c65dc00c76c753434679687f37b4 Mon Sep 17 00:00:00 2001 From: Ilya Kiselev Date: Sun, 24 May 2026 00:31:46 +0300 Subject: [PATCH 015/145] feat: open map at current location on startup --- lib/presentation/screens/map/map_screen.dart | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/lib/presentation/screens/map/map_screen.dart b/lib/presentation/screens/map/map_screen.dart index bcb0bcf..943665b 100644 --- a/lib/presentation/screens/map/map_screen.dart +++ b/lib/presentation/screens/map/map_screen.dart @@ -645,14 +645,26 @@ class _MapScreenState extends ConsumerState { mapObjects: mapObjects, onMapCreated: (controller) async { _mapController = controller; - const initial = Point(latitude: 59.7390, longitude: 30.4020); - _lastCameraTarget = initial; + const fallback = Point(latitude: 59.7390, longitude: 30.4020); + _lastCameraTarget = fallback; await controller.moveCamera( CameraUpdate.newCameraPosition( - const CameraPosition(target: initial, zoom: 14), + const CameraPosition(target: fallback, zoom: 14), ), ); _fetchZones(); + final pos = await _getCurrentPosition(); + if (pos != null && mounted) { + final target = Point(latitude: pos.latitude, longitude: pos.longitude); + _lastCameraTarget = target; + await controller.moveCamera( + CameraUpdate.newCameraPosition( + CameraPosition(target: target, zoom: 15), + ), + animation: const MapAnimation(duration: 0.8), + ); + _fetchZones(); + } }, onCameraPositionChanged: _onCameraPositionChanged, ), From 51d4dcc3f7a30f872848ef2015260a064f9f0f04 Mon Sep 17 00:00:00 2001 From: Ilya Kiselev Date: Mon, 25 May 2026 10:58:05 +0300 Subject: [PATCH 016/145] fix: nav arrow icon, destination in-app nav, smooth GPS jitter --- .../providers/navigation_provider.dart | 27 ++++++- lib/presentation/screens/map/map_screen.dart | 81 ++++++++++++++++--- 2 files changed, 93 insertions(+), 15 deletions(-) diff --git a/lib/presentation/providers/navigation_provider.dart b/lib/presentation/providers/navigation_provider.dart index 8801e02..11a96eb 100644 --- a/lib/presentation/providers/navigation_provider.dart +++ b/lib/presentation/providers/navigation_provider.dart @@ -47,6 +47,10 @@ class NavigationNotifier extends Notifier { double _destLon = 0; int _zoneId = 0; + double? _smoothLat; + double? _smoothLon; + double? _smoothHeading; + @override NavigationData? build() => null; @@ -68,6 +72,9 @@ class NavigationNotifier extends Notifier { _destLat = destLat; _destLon = destLon; _zoneId = zoneId; + _smoothLat = null; + _smoothLon = null; + _smoothHeading = null; _sub = Geolocator.getPositionStream( locationSettings: _buildLocationSettings(), @@ -108,8 +115,24 @@ class NavigationNotifier extends Notifier { _arrivalTimer = null; } + static const _kAlpha = 0.3; + + double _ema(double raw, double? prev) => + prev == null ? raw : prev * (1 - _kAlpha) + raw * _kAlpha; + + double _emaCircular(double raw, double? prev) { + if (prev == null) return raw; + final diff = ((raw - prev + 540) % 360) - 180; + return (prev + _kAlpha * diff + 360) % 360; + } + void _onPosition(Position pos) { - final current = Point(latitude: pos.latitude, longitude: pos.longitude); + _smoothLat = _ema(pos.latitude, _smoothLat); + _smoothLon = _ema(pos.longitude, _smoothLon); + final rawHeading = pos.heading >= 0 ? pos.heading : 0.0; + _smoothHeading = _emaCircular(rawHeading, _smoothHeading); + + final current = Point(latitude: _smoothLat!, longitude: _smoothLon!); final closest = closestOnRoute(current, _route); if (closest.segment > _segmentIndex) { @@ -131,7 +154,7 @@ class NavigationNotifier extends Notifier { remainingSeconds: remainingSecs, speedKmh: (pos.speed * 3.6).clamp(0.0, 300.0), currentPosition: current, - heading: pos.heading >= 0 ? pos.heading : 0, + heading: _smoothHeading!, nextTurn: nextTurn, isOffRoute: isOff, isRecalculating: state?.isRecalculating ?? false, diff --git a/lib/presentation/screens/map/map_screen.dart b/lib/presentation/screens/map/map_screen.dart index 943665b..99224f5 100644 --- a/lib/presentation/screens/map/map_screen.dart +++ b/lib/presentation/screens/map/map_screen.dart @@ -38,6 +38,7 @@ class _MapScreenState extends ConsumerState { double _currentAzimuth = 0; Uint8List? _destinationPinBytes; Uint8List? _userLocationBytes; + Uint8List? _navArrowBytes; bool _isSelectingOnMap = false; bool _isCandidatesSheetOpen = false; @@ -60,6 +61,9 @@ class _MapScreenState extends ConsumerState { _buildUserLocationBitmap().then((b) { if (mounted) setState(() => _userLocationBytes = b); }); + _buildNavArrowBitmap().then((b) { + if (mounted) setState(() => _navArrowBytes = b); + }); } @override @@ -122,6 +126,36 @@ class _MapScreenState extends ConsumerState { return byteData!.buffer.asUint8List(); } + Future _buildNavArrowBitmap() async { + const size = 56.0; + final recorder = ui.PictureRecorder(); + final canvas = Canvas(recorder); + canvas.drawCircle( + const Offset(size / 2, size / 2), + size / 2 - 1, + Paint()..color = AppColors.primary, + ); + canvas.drawCircle( + const Offset(size / 2, size / 2), + size / 2 - 1, + Paint() + ..color = Colors.white + ..style = PaintingStyle.stroke + ..strokeWidth = 2.5, + ); + final path = Path() + ..moveTo(size / 2, 9) + ..lineTo(size * 0.7, size * 0.66) + ..lineTo(size / 2, size * 0.53) + ..lineTo(size * 0.3, size * 0.66) + ..close(); + canvas.drawPath(path, Paint()..color = Colors.white); + final picture = recorder.endRecording(); + final image = await picture.toImage(size.toInt(), size.toInt()); + final byteData = await image.toByteData(format: ui.ImageByteFormat.png); + return byteData!.buffer.asUint8List(); + } + void _onCameraPositionChanged(CameraPosition position, _, __) { _lastCameraTarget = position.target; _debounce?.cancel(); @@ -377,11 +411,7 @@ class _MapScreenState extends ConsumerState { destLon: toLon, ); - await _mapController?.toggleUserLayer( - visible: true, - headingEnabled: true, - autoZoomEnabled: false, - ); + await _mapController?.toggleUserLayer(visible: false); await _mapController?.toggleTrafficLayer(visible: true); } catch (e) { if (mounted) { @@ -409,7 +439,8 @@ class _MapScreenState extends ConsumerState { final zonesAsync = ref.watch(rawZonesProvider); final routingState = ref.watch(routingProvider); final destination = ref.watch(destinationProvider); - final isNavigating = ref.watch(navigationProvider) != null; + final navState = ref.watch(navigationProvider); + final isNavigating = navState != null; final isRoutingLoading = routingState.maybeWhen( searching: () => true, orElse: () => false, @@ -612,7 +643,7 @@ class _MapScreenState extends ConsumerState { ); }, ), - if (_userPosition != null && _userLocationBytes != null) + if (_userPosition != null && _userLocationBytes != null && !isNavigating) PlacemarkMapObject( mapId: const MapObjectId('user_location'), point: Point( @@ -624,6 +655,16 @@ class _MapScreenState extends ConsumerState { scale: 1.0, )), ), + if (isNavigating && _navArrowBytes != null) + PlacemarkMapObject( + mapId: const MapObjectId('nav_arrow'), + point: navState!.currentPosition, + opacity: 1.0, + icon: PlacemarkIcon.single(PlacemarkIconStyle( + image: BitmapDescriptor.fromBytes(_navArrowBytes!), + scale: 1.0, + )), + ), if (destination != null && _destinationPinBytes != null) PlacemarkMapObject( mapId: const MapObjectId('destination_pin'), @@ -786,6 +827,10 @@ class _MapScreenState extends ConsumerState { onFindParking: isRoutingLoading ? null : _findParking, onNavigate: () => openYandexNavigator( destination.latitude, destination.longitude), + onNavigateInApp: () => _startInAppNavigation( + zoneId: 0, + toLat: destination.latitude, + toLon: destination.longitude), onClear: () { ref.read(destinationProvider.notifier).state = null; ref.read(routingProvider.notifier).reset(); @@ -896,12 +941,14 @@ class _DestinationCard extends StatelessWidget { required this.destination, required this.onFindParking, required this.onNavigate, + required this.onNavigateInApp, required this.onClear, }); final Destination destination; final VoidCallback? onFindParking; final VoidCallback onNavigate; + final VoidCallback onNavigateInApp; final VoidCallback onClear; @override @@ -947,14 +994,23 @@ class _DestinationCard extends StatelessWidget { ], ), const SizedBox(height: 10), + FilledButton.icon( + onPressed: onFindParking, + icon: const Icon(Icons.local_parking, size: 16), + label: const Text('Искать парковку рядом'), + style: FilledButton.styleFrom( + textStyle: const TextStyle(fontSize: 13), + padding: const EdgeInsets.symmetric(vertical: 10), + ), + ), + const SizedBox(height: 8), Row( children: [ Expanded( - flex: 3, child: FilledButton.icon( - onPressed: onFindParking, - icon: const Icon(Icons.local_parking, size: 16), - label: const Text('Искать парковку рядом'), + onPressed: onNavigateInApp, + icon: const Icon(Icons.map_outlined, size: 16), + label: const Text('В приложении'), style: FilledButton.styleFrom( textStyle: const TextStyle(fontSize: 13), padding: const EdgeInsets.symmetric(vertical: 10), @@ -963,14 +1019,13 @@ class _DestinationCard extends StatelessWidget { ), const SizedBox(width: 8), Expanded( - flex: 2, child: FilledButton.tonal( onPressed: onNavigate, style: FilledButton.styleFrom( textStyle: const TextStyle(fontSize: 13), padding: const EdgeInsets.symmetric(vertical: 10), ), - child: const Text('Ехать сюда'), + child: const Text('Яндекс'), ), ), ], From 8ee36a7403baa4c10114eec8af626087b2e425eb Mon Sep 17 00:00:00 2001 From: Ilya Kiselev Date: Mon, 25 May 2026 11:12:27 +0300 Subject: [PATCH 017/145] fix: nav arrow bigger, no circle, with shadow --- lib/presentation/screens/map/map_screen.dart | 47 +++++++++++++------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/lib/presentation/screens/map/map_screen.dart b/lib/presentation/screens/map/map_screen.dart index 99224f5..1bc3503 100644 --- a/lib/presentation/screens/map/map_screen.dart +++ b/lib/presentation/screens/map/map_screen.dart @@ -127,29 +127,42 @@ class _MapScreenState extends ConsumerState { } Future _buildNavArrowBitmap() async { - const size = 56.0; + const size = 80.0; final recorder = ui.PictureRecorder(); final canvas = Canvas(recorder); - canvas.drawCircle( - const Offset(size / 2, size / 2), - size / 2 - 1, - Paint()..color = AppColors.primary, + + // Drop shadow + canvas.drawPath( + Path() + ..moveTo(size / 2, 4) + ..lineTo(size * 0.78, size * 0.72) + ..lineTo(size / 2, size * 0.56) + ..lineTo(size * 0.22, size * 0.72) + ..close(), + Paint() + ..color = const Color(0x44000000) + ..maskFilter = const ui.MaskFilter.blur(ui.BlurStyle.normal, 4), ); - canvas.drawCircle( - const Offset(size / 2, size / 2), - size / 2 - 1, + + // Arrow fill + final arrowPath = Path() + ..moveTo(size / 2, 4) + ..lineTo(size * 0.78, size * 0.72) + ..lineTo(size / 2, size * 0.56) + ..lineTo(size * 0.22, size * 0.72) + ..close(); + canvas.drawPath(arrowPath, Paint()..color = AppColors.primary); + + // White outline for contrast + canvas.drawPath( + arrowPath, Paint() ..color = Colors.white - ..style = PaintingStyle.stroke - ..strokeWidth = 2.5, + ..style = ui.PaintingStyle.stroke + ..strokeWidth = 2.5 + ..strokeJoin = ui.StrokeJoin.round, ); - final path = Path() - ..moveTo(size / 2, 9) - ..lineTo(size * 0.7, size * 0.66) - ..lineTo(size / 2, size * 0.53) - ..lineTo(size * 0.3, size * 0.66) - ..close(); - canvas.drawPath(path, Paint()..color = Colors.white); + final picture = recorder.endRecording(); final image = await picture.toImage(size.toInt(), size.toInt()); final byteData = await image.toByteData(format: ui.ImageByteFormat.png); From bd5d85545d27353109e6b082c14edbd0394751a9 Mon Sep 17 00:00:00 2001 From: Ilya Kiselev Date: Mon, 25 May 2026 13:17:29 +0300 Subject: [PATCH 018/145] fix: hide zone count circles when no forecast --- lib/presentation/screens/map/map_screen.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/presentation/screens/map/map_screen.dart b/lib/presentation/screens/map/map_screen.dart index 1bc3503..f1c95f2 100644 --- a/lib/presentation/screens/map/map_screen.dart +++ b/lib/presentation/screens/map/map_screen.dart @@ -101,6 +101,7 @@ class _MapScreenState extends ConsumerState { final newById = {}; for (final zone in zones) { if (zone.geometry.length < 3) continue; + if (!zone.hasForecast) continue; final color = zoneColor(zone); newCache[zone.zoneId] = await buildCountBitmap(zone.freeCount, color); newById[zone.zoneId] = zone; From 6adaeff786aa91bcf0c3e361823b120d1c1034e8 Mon Sep 17 00:00:00 2001 From: Ilya Kiselev Date: Mon, 25 May 2026 13:19:14 +0300 Subject: [PATCH 019/145] fix: hide count number in zone circles when no forecast --- lib/presentation/screens/map/map_screen.dart | 6 ++-- .../map/widgets/parking_zone_layer.dart | 34 +++++++++---------- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/lib/presentation/screens/map/map_screen.dart b/lib/presentation/screens/map/map_screen.dart index f1c95f2..16c249e 100644 --- a/lib/presentation/screens/map/map_screen.dart +++ b/lib/presentation/screens/map/map_screen.dart @@ -101,9 +101,11 @@ class _MapScreenState extends ConsumerState { final newById = {}; for (final zone in zones) { if (zone.geometry.length < 3) continue; - if (!zone.hasForecast) continue; final color = zoneColor(zone); - newCache[zone.zoneId] = await buildCountBitmap(zone.freeCount, color); + newCache[zone.zoneId] = await buildCountBitmap( + zone.hasForecast ? zone.freeCount : null, + color, + ); newById[zone.zoneId] = zone; } if (mounted) { diff --git a/lib/presentation/screens/map/widgets/parking_zone_layer.dart b/lib/presentation/screens/map/widgets/parking_zone_layer.dart index ba0ac1b..514e900 100644 --- a/lib/presentation/screens/map/widgets/parking_zone_layer.dart +++ b/lib/presentation/screens/map/widgets/parking_zone_layer.dart @@ -127,30 +127,30 @@ MapObject buildZoneLabels({ ); } -Future buildCountBitmap(int count, Color color) async { +Future buildCountBitmap(int? count, Color color) async { const size = 72.0; final center = Offset(size / 2, size / 2); final recorder = ui.PictureRecorder(); final canvas = Canvas(recorder); - // White outer ring for contrast against any map background canvas.drawCircle(center, size / 2 - 1, Paint()..color = Colors.white); - // Colored fill canvas.drawCircle(center, size / 2 - 3, Paint()..color = color); - final textPainter = TextPainter( - text: TextSpan( - text: '$count', - style: const TextStyle( - color: Colors.white, - fontSize: 28, - fontWeight: FontWeight.bold, + if (count != null) { + final textPainter = TextPainter( + text: TextSpan( + text: '$count', + style: const TextStyle( + color: Colors.white, + fontSize: 28, + fontWeight: FontWeight.bold, + ), ), - ), - textDirection: TextDirection.ltr, - )..layout(); - textPainter.paint( - canvas, - Offset((size - textPainter.width) / 2, (size - textPainter.height) / 2), - ); + textDirection: TextDirection.ltr, + )..layout(); + textPainter.paint( + canvas, + Offset((size - textPainter.width) / 2, (size - textPainter.height) / 2), + ); + } final picture = recorder.endRecording(); final image = await picture.toImage(size.toInt(), size.toInt()); final data = await image.toByteData(format: ui.ImageByteFormat.png); From 825215d71f503a87dfaa41c66ca13ceffaac3b11 Mon Sep 17 00:00:00 2001 From: Ilya Kiselev Date: Mon, 25 May 2026 13:22:36 +0300 Subject: [PATCH 020/145] fix: hide count for grey (unknown) zone circles --- lib/presentation/screens/map/map_screen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/presentation/screens/map/map_screen.dart b/lib/presentation/screens/map/map_screen.dart index 16c249e..caef445 100644 --- a/lib/presentation/screens/map/map_screen.dart +++ b/lib/presentation/screens/map/map_screen.dart @@ -103,7 +103,7 @@ class _MapScreenState extends ConsumerState { if (zone.geometry.length < 3) continue; final color = zoneColor(zone); newCache[zone.zoneId] = await buildCountBitmap( - zone.hasForecast ? zone.freeCount : null, + color == AppColors.parkingUnknown ? null : zone.freeCount, color, ); newById[zone.zoneId] = zone; From 27e580c7e6f00405b4148214873afd156f972bb5 Mon Sep 17 00:00:00 2001 From: Ilya Kiselev Date: Mon, 25 May 2026 13:26:48 +0300 Subject: [PATCH 021/145] fix: grey clusters show only zone count, no free count --- .../map/widgets/parking_zone_layer.dart | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/lib/presentation/screens/map/widgets/parking_zone_layer.dart b/lib/presentation/screens/map/widgets/parking_zone_layer.dart index 514e900..f96be3f 100644 --- a/lib/presentation/screens/map/widgets/parking_zone_layer.dart +++ b/lib/presentation/screens/map/widgets/parking_zone_layer.dart @@ -101,12 +101,15 @@ MapObject buildZoneLabels({ .map((p) => int.tryParse(p.mapId.value.replaceFirst('zone_label_', ''))) .whereType() .toList(); - final totalFree = zoneIds - .map((id) => zonesById[id]?.freeCount ?? 0) - .fold(0, (a, b) => a + b); final hasForecast = zoneIds.any((id) => zonesById[id]?.hasForecast == true); + final int? totalFree = hasForecast + ? zoneIds + .where((id) => zonesById[id]?.hasForecast == true) + .map((id) => zonesById[id]?.freeCount ?? 0) + .fold(0, (a, b) => a + b) + : null; final Color clusterColor; - if (!hasForecast) { + if (totalFree == null) { clusterColor = AppColors.parkingUnknown; } else if (totalFree == 0) { clusterColor = AppColors.parkingFull; @@ -157,14 +160,21 @@ Future buildCountBitmap(int? count, Color color) async { return data!.buffer.asUint8List(); } -Future buildClusterBitmap(int totalFree, int clusterSize, Color color) async { +Future buildClusterBitmap(int? totalFree, int clusterSize, Color color) async { const size = 84.0; final center = Offset(size / 2, size / 2); final recorder = ui.PictureRecorder(); final canvas = Canvas(recorder); canvas.drawCircle(center, size / 2 - 1, Paint()..color = Colors.white); canvas.drawCircle(center, size / 2 - 4, Paint()..color = color); - final label = clusterSize <= 1 ? '$totalFree' : '$totalFree\n($clusterSize)'; + final String label; + if (totalFree == null) { + label = '($clusterSize)'; + } else if (clusterSize <= 1) { + label = '$totalFree'; + } else { + label = '$totalFree\n($clusterSize)'; + } final textPainter = TextPainter( text: TextSpan( text: label, From f1ebdeda94abb9ba8d50f093ab6a989dc4faf631 Mon Sep 17 00:00:00 2001 From: Ilya Kiselev Date: Mon, 25 May 2026 13:29:54 +0300 Subject: [PATCH 022/145] fix: cancel stale bitmap build on repeated zone updates --- lib/presentation/screens/map/map_screen.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/presentation/screens/map/map_screen.dart b/lib/presentation/screens/map/map_screen.dart index caef445..c6df0b0 100644 --- a/lib/presentation/screens/map/map_screen.dart +++ b/lib/presentation/screens/map/map_screen.dart @@ -47,6 +47,7 @@ class _MapScreenState extends ConsumerState { Map _zoneLabelCache = {}; Map _zonesById = {}; + int _bitmapGeneration = 0; List? _routePolyline; int? _activeRouteZoneId; DrivingSession? _drivingSession; @@ -97,9 +98,11 @@ class _MapScreenState extends ConsumerState { } Future _updateZoneBitmaps(List zones) async { + final generation = ++_bitmapGeneration; final newCache = {}; final newById = {}; for (final zone in zones) { + if (_bitmapGeneration != generation) return; if (zone.geometry.length < 3) continue; final color = zoneColor(zone); newCache[zone.zoneId] = await buildCountBitmap( @@ -108,6 +111,7 @@ class _MapScreenState extends ConsumerState { ); newById[zone.zoneId] = zone; } + if (_bitmapGeneration != generation) return; if (mounted) { setState(() { _zoneLabelCache = newCache; From 6cf9d06b07c95b08947050ee2c0f7399c6846e8f Mon Sep 17 00:00:00 2001 From: Ilya Kiselev Date: Mon, 25 May 2026 13:56:08 +0300 Subject: [PATCH 023/145] fix: skip bitmap rebuild when zone data unchanged --- lib/presentation/screens/map/map_screen.dart | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/presentation/screens/map/map_screen.dart b/lib/presentation/screens/map/map_screen.dart index c6df0b0..a835322 100644 --- a/lib/presentation/screens/map/map_screen.dart +++ b/lib/presentation/screens/map/map_screen.dart @@ -98,6 +98,15 @@ class _MapScreenState extends ConsumerState { } Future _updateZoneBitmaps(List zones) async { + final same = zones.length == _zonesById.length && + zones.every((z) { + final prev = _zonesById[z.zoneId]; + return prev != null && + prev.freeCount == z.freeCount && + prev.hasForecast == z.hasForecast && + prev.isActive == z.isActive; + }); + if (same) return; final generation = ++_bitmapGeneration; final newCache = {}; final newById = {}; From b51c5ee551b5ff3c67b357d19d3fb4d840e1864f Mon Sep 17 00:00:00 2001 From: Ilya Kiselev Date: Mon, 25 May 2026 14:35:50 +0300 Subject: [PATCH 024/145] fix: show server response body on routing 422 error --- lib/presentation/providers/routing_provider.dart | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/presentation/providers/routing_provider.dart b/lib/presentation/providers/routing_provider.dart index 9b2985e..d0aa200 100644 --- a/lib/presentation/providers/routing_provider.dart +++ b/lib/presentation/providers/routing_provider.dart @@ -1,3 +1,4 @@ +import 'package:dio/dio.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import '../../domain/models/route_result.dart'; @@ -66,7 +67,11 @@ class RoutingNotifier extends StateNotifier { ); state = RoutingState.candidates(candidates); } catch (e) { - state = RoutingState.error(e.toString()); + if (e is DioException && e.response != null) { + state = RoutingState.error('${e.response!.statusCode}: ${e.response!.data}'); + } else { + state = RoutingState.error(e.toString()); + } } } @@ -87,7 +92,11 @@ class RoutingNotifier extends StateNotifier { ); state = RoutingState.routePreview(route); } catch (e) { - state = RoutingState.error(e.toString()); + if (e is DioException && e.response != null) { + state = RoutingState.error('${e.response!.statusCode}: ${e.response!.data}'); + } else { + state = RoutingState.error(e.toString()); + } } } From 690a1076094972c8b40c8d9dc4d3e7f590356ee6 Mon Sep 17 00:00:00 2001 From: Ilya Kiselev Date: Mon, 25 May 2026 20:53:57 +0300 Subject: [PATCH 025/145] fix: strip null fields from routing request body --- lib/data/api/routing_api.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/data/api/routing_api.dart b/lib/data/api/routing_api.dart index 2fc6362..20b2de6 100644 --- a/lib/data/api/routing_api.dart +++ b/lib/data/api/routing_api.dart @@ -9,7 +9,8 @@ class RoutingApi { Future searchParking( RoutingSearchRequestDto request, ) async { - final response = await _dio.post('/routing/search', data: request.toJson()); + final data = request.toJson()..removeWhere((_, v) => v == null); + final response = await _dio.post('/routing/search', data: data); return RoutingSearchResponseDto.fromJson( response.data as Map); } @@ -17,7 +18,7 @@ class RoutingApi { Future createRoute(RoutingSearchRequestDto request, {int? selectedZoneId}) async { final data = { - ...request.toJson(), + ...request.toJson()..removeWhere((_, v) => v == null), if (selectedZoneId != null) 'selected_zone_id': selectedZoneId, }; final response = await _dio.post('/routing/new', data: data); From a82ca610232ba3be7ad3806a216b0509bb92438b Mon Sep 17 00:00:00 2001 From: Ilya Kiselev Date: Tue, 26 May 2026 14:56:28 +0300 Subject: [PATCH 026/145] feat: pass use_forecast=true to routing API in future time mode --- lib/data/repositories/routing_repository.dart | 4 ++++ lib/presentation/providers/routing_provider.dart | 7 +++++++ 2 files changed, 11 insertions(+) diff --git a/lib/data/repositories/routing_repository.dart b/lib/data/repositories/routing_repository.dart index e4ada81..b4ab528 100644 --- a/lib/data/repositories/routing_repository.dart +++ b/lib/data/repositories/routing_repository.dart @@ -16,6 +16,7 @@ class RoutingRepository { int? maxPay, int? minFreeCount, double? minConfidence, + bool? useForecast, }) async { final request = RoutingSearchRequestDto( mode: destinationLat != null ? 'route_to_destination' : 'find_parking', @@ -26,6 +27,7 @@ class RoutingRepository { maxPay: maxPay, minFreeCount: minFreeCount, minConfidence: minConfidence, + useForecast: useForecast, ); final response = await _api.searchParking(request); return response.candidates.map(_mapCandidate).toList(); @@ -37,6 +39,7 @@ class RoutingRepository { double? destinationLat, double? destinationLon, required int selectedZoneId, + bool? useForecast, }) async { final request = RoutingSearchRequestDto( mode: destinationLat != null ? 'route_to_destination' : 'find_parking', @@ -44,6 +47,7 @@ class RoutingRepository { destination: destinationLat != null ? LocationDto(latitude: destinationLat, longitude: destinationLon!) : null, + useForecast: useForecast, ); final dto = await _api.createRoute(request, selectedZoneId: selectedZoneId); return _mapRoute(dto); diff --git a/lib/presentation/providers/routing_provider.dart b/lib/presentation/providers/routing_provider.dart index d0aa200..c0646a3 100644 --- a/lib/presentation/providers/routing_provider.dart +++ b/lib/presentation/providers/routing_provider.dart @@ -4,6 +4,7 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import '../../domain/models/route_result.dart'; import 'app_providers.dart'; import 'filters_provider.dart'; +import 'time_selector_provider.dart'; part 'routing_provider.freezed.dart'; @@ -56,6 +57,8 @@ class RoutingNotifier extends StateNotifier { try { final destination = _ref.read(destinationProvider); final filters = _ref.read(filtersProvider); + final timeMode = _ref.read(timeSelectorProvider); + final useForecast = timeMode.maybeWhen(future: (_) => true, orElse: () => null); final candidates = await _ref.read(routingRepositoryProvider).searchParking( originLat: originLat, originLon: originLon, @@ -64,6 +67,7 @@ class RoutingNotifier extends StateNotifier { maxPay: filters.maxPayPerHour, minConfidence: filters.minConfidence > 0 ? filters.minConfidence : null, minFreeCount: filters.hideNoFreeSpots ? 1 : null, + useForecast: useForecast, ); state = RoutingState.candidates(candidates); } catch (e) { @@ -83,12 +87,15 @@ class RoutingNotifier extends StateNotifier { state = const RoutingState.searching(); try { final destination = _ref.read(destinationProvider); + final timeMode = _ref.read(timeSelectorProvider); + final useForecast = timeMode.maybeWhen(future: (_) => true, orElse: () => null); final route = await _ref.read(routingRepositoryProvider).createRoute( originLat: originLat, originLon: originLon, destinationLat: destination?.latitude, destinationLon: destination?.longitude, selectedZoneId: selectedZoneId, + useForecast: useForecast, ); state = RoutingState.routePreview(route); } catch (e) { From 3a0ddee5834a90df4e533e9972fd878566dd2716 Mon Sep 17 00:00:00 2001 From: Ilya Kiselev Date: Tue, 26 May 2026 19:00:37 +0300 Subject: [PATCH 027/145] fix: tap on zone circle opens parking card; fix zone_id type for ML forecast matching --- lib/data/repositories/zones_repository.dart | 5 +++-- lib/presentation/screens/map/map_screen.dart | 15 +++++++++++++++ .../screens/map/widgets/parking_zone_layer.dart | 2 ++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/lib/data/repositories/zones_repository.dart b/lib/data/repositories/zones_repository.dart index dfcc231..cd68c5b 100644 --- a/lib/data/repositories/zones_repository.dart +++ b/lib/data/repositories/zones_repository.dart @@ -40,8 +40,9 @@ class ZonesRepository { final forecastMap = >{}; for (final f in forecasts) { - final id = f['zone_id']; - if (id is int) forecastMap[id] = f; + final rawId = f['zone_id']; + final id = rawId is int ? rawId : int.tryParse(rawId?.toString() ?? ''); + if (id != null) forecastMap[id] = f; } return zoneDtos.map((dto) { diff --git a/lib/presentation/screens/map/map_screen.dart b/lib/presentation/screens/map/map_screen.dart index a835322..78c603d 100644 --- a/lib/presentation/screens/map/map_screen.dart +++ b/lib/presentation/screens/map/map_screen.dart @@ -647,6 +647,21 @@ class _MapScreenState extends ConsumerState { zones: zones, bitmapCache: _zoneLabelCache, zonesById: _zonesById, + onZoneTap: (zone) { + if (_isSelectingOnMap && candidateIds.contains(zone.zoneId)) { + _buildRouteForZone(zone.zoneId); + return; + } + if (_isParkingCardOpen) return; + setState(() => _isParkingCardOpen = true); + showParkingCard( + context, + zone, + onBuildRoute: () => _buildRouteForZone(zone.zoneId), + ).then((_) { + if (mounted) setState(() => _isParkingCardOpen = false); + }); + }, onClusterTap: (_, cluster) { if (cluster.placemarks.isEmpty) return; final lats = cluster.placemarks.map((p) => p.point.latitude); diff --git a/lib/presentation/screens/map/widgets/parking_zone_layer.dart b/lib/presentation/screens/map/widgets/parking_zone_layer.dart index f96be3f..2e65b7c 100644 --- a/lib/presentation/screens/map/widgets/parking_zone_layer.dart +++ b/lib/presentation/screens/map/widgets/parking_zone_layer.dart @@ -76,6 +76,7 @@ MapObject buildZoneLabels({ required Map bitmapCache, required Map zonesById, ClusterTapCallback? onClusterTap, + void Function(Zone)? onZoneTap, }) { final placemarks = zones .where((z) => !_isDegenerate(z.geometry) && bitmapCache.containsKey(z.zoneId)) @@ -87,6 +88,7 @@ MapObject buildZoneLabels({ image: BitmapDescriptor.fromBytes(bitmapCache[zone.zoneId]!), scale: 1.0, )), + onTap: onZoneTap != null ? (_, __) => onZoneTap(zone) : null, )) .toList(); From cd950a74af967264b9f154484dbb21513dc0622c Mon Sep 17 00:00:00 2001 From: Ilya Kiselev Date: Tue, 26 May 2026 19:09:20 +0300 Subject: [PATCH 028/145] fix: stop navigation provider on finish route confirmation --- lib/presentation/screens/map/map_screen.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/presentation/screens/map/map_screen.dart b/lib/presentation/screens/map/map_screen.dart index 78c603d..decb477 100644 --- a/lib/presentation/screens/map/map_screen.dart +++ b/lib/presentation/screens/map/map_screen.dart @@ -908,7 +908,10 @@ class _MapScreenState extends ConsumerState { left: 0, right: 0, child: NavigationBottomBar( - onFinish: () => ref.read(routingProvider.notifier).reset(), + onFinish: () { + ref.read(navigationProvider.notifier).stop(); + ref.read(routingProvider.notifier).reset(); + }, ), ), if (_navBuilding) From 5c115bd2183ae271e1f82aae0167b8704dce453f Mon Sep 17 00:00:00 2001 From: Ilya Kiselev Date: Thu, 28 May 2026 09:32:42 +0300 Subject: [PATCH 029/145] feat: forecast info in parking card, stale warning, min free count filter, fix cache on time change --- lib/data/repositories/zones_repository.dart | 22 ++-- lib/domain/models/zone.dart | 2 + .../providers/filters_provider.dart | 3 + .../providers/zones_provider.dart | 1 + lib/presentation/screens/map/map_screen.dart | 29 ++++- .../map/widgets/parking_card_sheet.dart | 118 ++++++++++++++++-- 6 files changed, 158 insertions(+), 17 deletions(-) diff --git a/lib/data/repositories/zones_repository.dart b/lib/data/repositories/zones_repository.dart index cd68c5b..f271746 100644 --- a/lib/data/repositories/zones_repository.dart +++ b/lib/data/repositories/zones_repository.dart @@ -97,19 +97,27 @@ class ZonesRepository { Zone _mapZoneWithForecast(ZoneMapItemDto dto, Map forecast) => Zone( zoneId: dto.zoneId, zoneType: _parseZoneType(dto.zoneType), - capacity: dto.capacity, + capacity: (forecast['capacity'] as num?)?.toInt() ?? dto.capacity, freeCount: (forecast['predicted_free_count'] as num?)?.toInt() ?? dto.freeCount, confidence: (forecast['confidence'] as num?)?.toDouble() ?? dto.confidence, - pay: dto.pay, - geometry: _parseGeometry(dto.geometry), - isActive: dto.isActive, - locationType: _parseLocationType(dto.locationType), + pay: (forecast['pay'] as num?)?.toInt() ?? dto.pay, + geometry: forecast['geometry'] != null + ? _parseGeometry(forecast['geometry'] as Map) + : _parseGeometry(dto.geometry), + isActive: forecast['is_active'] as bool? ?? dto.isActive, + locationType: _parseLocationType(forecast['location_type'] as String? ?? dto.locationType), isPrivate: dto.isPrivate, - isAccessible: dto.isAccessible, - confidenceLevel: dto.confidenceLevel, + isAccessible: forecast['is_accessible'] as bool? ?? dto.isAccessible, + confidenceLevel: forecast['confidence_level'] as String? ?? dto.confidenceLevel, occupancyUpdatedAt: dto.occupancyUpdatedAt != null ? DateTime.tryParse(dto.occupancyUpdatedAt!) : null, + forecastFor: forecast['predicted_for'] != null + ? DateTime.tryParse(forecast['predicted_for'] as String) + : null, + forecastGeneratedAt: forecast['generated_at'] != null + ? DateTime.tryParse(forecast['generated_at'] as String) + : null, ); Zone _mapFromOccupancy(Map json) => Zone( diff --git a/lib/domain/models/zone.dart b/lib/domain/models/zone.dart index b8b09d7..c2afc48 100644 --- a/lib/domain/models/zone.dart +++ b/lib/domain/models/zone.dart @@ -24,5 +24,7 @@ class Zone with _$Zone { bool? isAccessible, String? confidenceLevel, DateTime? occupancyUpdatedAt, + DateTime? forecastFor, + DateTime? forecastGeneratedAt, }) = _Zone; } diff --git a/lib/presentation/providers/filters_provider.dart b/lib/presentation/providers/filters_provider.dart index 3bbfb50..663faab 100644 --- a/lib/presentation/providers/filters_provider.dart +++ b/lib/presentation/providers/filters_provider.dart @@ -13,6 +13,7 @@ class FiltersState with _$FiltersState { @Default(false) bool hideInaccessible, @Default({}) Set hiddenLocationTypes, @Default(true) bool hideInactive, + @Default(0) int minFreeCount, }) = _FiltersState; } @@ -46,6 +47,8 @@ class FiltersNotifier extends StateNotifier { void toggleHideInactive() => state = state.copyWith(hideInactive: !state.hideInactive); + void setMinFreeCount(int value) => state = state.copyWith(minFreeCount: value); + void reset() => state = const FiltersState(); } diff --git a/lib/presentation/providers/zones_provider.dart b/lib/presentation/providers/zones_provider.dart index 8057525..7edd317 100644 --- a/lib/presentation/providers/zones_provider.dart +++ b/lib/presentation/providers/zones_provider.dart @@ -49,6 +49,7 @@ final filteredZonesProvider = Provider>((ref) { return zones.where((z) { if (filters.hideInactive && !z.isActive) return false; if (filters.hideNoFreeSpots && z.freeCount == 0) return false; + if (filters.minFreeCount > 0 && z.freeCount < filters.minFreeCount) return false; if (z.confidence < filters.minConfidence) return false; if (filters.maxPayPerHour != null && z.pay > filters.maxPayPerHour!) return false; if (filters.hidePrivate && (z.isPrivate ?? false)) return false; diff --git a/lib/presentation/screens/map/map_screen.dart b/lib/presentation/screens/map/map_screen.dart index decb477..9899b47 100644 --- a/lib/presentation/screens/map/map_screen.dart +++ b/lib/presentation/screens/map/map_screen.dart @@ -194,8 +194,12 @@ class _MapScreenState extends ConsumerState { } } - Future _fetchZones() async { + Future _fetchZones({bool clearCache = false}) async { if (_mapController == null) return; + if (clearCache) { + _zoneLabelCache.clear(); + _zonesById.clear(); + } try { final visibleRegion = await _mapController!.getVisibleRegion(); final bbox = @@ -475,7 +479,7 @@ class _MapScreenState extends ConsumerState { orElse: () => false, ); - ref.listen(timeSelectorProvider, (_, __) => _fetchZones()); + ref.listen(timeSelectorProvider, (_, __) => _fetchZones(clearCache: true)); ref.listen(filteredZonesProvider, (_, zones) => _updateZoneBitmaps(zones)); ref.listen(navigationProvider, (prev, nav) { @@ -1238,6 +1242,27 @@ class _FiltersSheet extends ConsumerWidget { ], ), const Divider(), + const Text( + 'Минимум свободных мест', + style: TextStyle(fontWeight: FontWeight.w600), + ), + const SizedBox(height: 4), + Text( + filters.minFreeCount == 0 ? 'Любое' : '${filters.minFreeCount}+', + style: const TextStyle( + color: AppColors.textSecondary, + fontWeight: FontWeight.w500, + ), + ), + Slider( + value: filters.minFreeCount.toDouble(), + min: 0, + max: 20, + divisions: 20, + label: filters.minFreeCount == 0 ? 'Любое' : '${filters.minFreeCount}+', + onChanged: (v) => notifier.setMinFreeCount(v.round()), + ), + const Divider(), const Text( 'Тип парковки', style: TextStyle(fontWeight: FontWeight.w600), diff --git a/lib/presentation/screens/map/widgets/parking_card_sheet.dart b/lib/presentation/screens/map/widgets/parking_card_sheet.dart index f29ce7c..d2648b5 100644 --- a/lib/presentation/screens/map/widgets/parking_card_sheet.dart +++ b/lib/presentation/screens/map/widgets/parking_card_sheet.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../../domain/models/zone.dart'; import '../../../../core/theme/app_colors.dart'; +import '../../../../presentation/providers/time_selector_provider.dart'; Future showParkingCard( BuildContext context, @@ -14,19 +16,23 @@ Future showParkingCard( ); } -class _ParkingCardSheet extends StatelessWidget { +class _ParkingCardSheet extends ConsumerWidget { final Zone zone; final VoidCallback? onBuildRoute; const _ParkingCardSheet({required this.zone, this.onBuildRoute}); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final timeMode = ref.watch(timeSelectorProvider); + final isFuture = timeMode.maybeWhen(future: (_) => true, orElse: () => false); + final userSelectedAt = timeMode.maybeWhen(future: (at) => at, orElse: () => null); + return DraggableScrollableSheet( expand: false, initialChildSize: 0.45, minChildSize: 0.3, - maxChildSize: 0.7, + maxChildSize: 0.75, builder: (_, controller) => Container( decoration: const BoxDecoration( color: Colors.white, @@ -73,6 +79,18 @@ class _ParkingCardSheet extends StatelessWidget { : 'Нет прогноза', valueColor: zone.hasForecast ? null : AppColors.textSecondary, ), + if (isFuture && zone.hasForecast && zone.forecastFor != null) + _InfoRow( + icon: Icons.schedule, + label: 'Прогноз на', + value: _formatDateTime(zone.forecastFor!), + ), + if (isFuture && zone.hasForecast && zone.forecastGeneratedAt != null) + _InfoRow( + icon: Icons.build_circle_outlined, + label: 'Создан', + value: _formatDateTime(zone.forecastGeneratedAt!), + ), _InfoRow( icon: Icons.payments_outlined, label: 'Стоимость', @@ -95,11 +113,20 @@ class _ParkingCardSheet extends StatelessWidget { label: 'Постановка', value: zone.zoneType == ZoneType.parallel ? 'Параллельная' : 'Обычная', ), - if (zone.occupancyUpdatedAt != null) + if (!isFuture && zone.occupancyUpdatedAt != null) _InfoRow( icon: Icons.update, label: 'Обновлено', - value: _formatTime(zone.occupancyUpdatedAt!), + value: _formatDateTime(zone.occupancyUpdatedAt!), + ), + if (isFuture && zone.forecastFor != null && userSelectedAt != null) + _StaleForecastBanner( + forecastFor: zone.forecastFor!, + userSelectedAt: userSelectedAt, + onSnap: () { + ref.read(timeSelectorProvider.notifier).setFuture(zone.forecastFor!); + Navigator.pop(context); + }, ), const SizedBox(height: 20), FilledButton.icon( @@ -124,9 +151,84 @@ class _ParkingCardSheet extends StatelessWidget { LocationType.multilevel => 'Многоуровневая', }; - String _formatTime(DateTime dt) { - final local = dt.toLocal(); - return '${local.hour.toString().padLeft(2, '0')}:${local.minute.toString().padLeft(2, '0')}'; + static String _formatTime(DateTime dt) { + final l = dt.toLocal(); + return '${l.hour.toString().padLeft(2, '0')}:${l.minute.toString().padLeft(2, '0')}'; + } + + static String _formatDateTime(DateTime dt) { + final l = dt.toLocal(); + final now = DateTime.now(); + final timeStr = _formatTime(l); + if (l.year == now.year && l.month == now.month && l.day == now.day) { + return 'Сегодня $timeStr'; + } + return '${l.day} ${_monthName(l.month)} $timeStr'; + } + + static String _monthName(int m) => const [ + '', + 'янв', + 'фев', + 'мар', + 'апр', + 'май', + 'июн', + 'июл', + 'авг', + 'сен', + 'окт', + 'ноя', + 'дек' + ][m]; +} + +class _StaleForecastBanner extends StatelessWidget { + final DateTime forecastFor; + final DateTime userSelectedAt; + final VoidCallback onSnap; + + const _StaleForecastBanner({ + required this.forecastFor, + required this.userSelectedAt, + required this.onSnap, + }); + + @override + Widget build(BuildContext context) { + final diffMin = userSelectedAt.difference(forecastFor).inMinutes.abs(); + if (diffMin <= 30) return const SizedBox.shrink(); + return Container( + margin: const EdgeInsets.only(top: 12), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.orange.shade50, + borderRadius: BorderRadius.circular(10), + border: Border.all(color: Colors.orange.shade300), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Прогноз на ${_ParkingCardSheet._formatTime(forecastFor)}, ' + 'вы выбрали ${_ParkingCardSheet._formatTime(userSelectedAt)}', + style: TextStyle(color: Colors.orange.shade900, fontSize: 13), + ), + const SizedBox(height: 8), + SizedBox( + width: double.infinity, + child: OutlinedButton( + style: OutlinedButton.styleFrom( + foregroundColor: Colors.orange.shade800, + side: BorderSide(color: Colors.orange.shade400), + ), + onPressed: onSnap, + child: const Text('Открыть ближайшее доступное'), + ), + ), + ], + ), + ); } } From d1f1f3b24773cbdf14058de2b06551eb47e96063 Mon Sep 17 00:00:00 2001 From: Ilya Kiselev Date: Thu, 28 May 2026 13:14:07 +0300 Subject: [PATCH 030/145] feat: friendly error snackbar with details dialog --- lib/core/utils/error_snackbar.dart | 95 ++++++++++++++++++++ lib/presentation/screens/map/map_screen.dart | 17 ++-- 2 files changed, 101 insertions(+), 11 deletions(-) create mode 100644 lib/core/utils/error_snackbar.dart diff --git a/lib/core/utils/error_snackbar.dart b/lib/core/utils/error_snackbar.dart new file mode 100644 index 0000000..918ba73 --- /dev/null +++ b/lib/core/utils/error_snackbar.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +void showErrorSnackBar( + BuildContext context, + String message, { + Object? error, + StackTrace? stackTrace, +}) { + final details = _buildDetails(error, stackTrace); + ScaffoldMessenger.of(context) + ..hideCurrentSnackBar() + ..showSnackBar( + SnackBar( + content: Row( + children: [ + const Icon(Icons.error_outline, color: Colors.white70, size: 18), + const SizedBox(width: 8), + Expanded(child: Text(message)), + ], + ), + action: details != null + ? SnackBarAction( + label: 'Подробнее', + onPressed: () => _showDetailsDialog(context, message, details), + ) + : null, + behavior: SnackBarBehavior.floating, + duration: const Duration(seconds: 6), + ), + ); +} + +String? _buildDetails(Object? error, StackTrace? stackTrace) { + if (error == null) return null; + final buffer = StringBuffer(error.toString()); + if (stackTrace != null) { + buffer.writeln('\n--- Stack Trace ---'); + final lines = stackTrace.toString().split('\n').take(12); + buffer.writeAll(lines, '\n'); + } + return buffer.toString(); +} + +void _showDetailsDialog(BuildContext context, String message, String details) { + showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text('Детали ошибки'), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(message, style: const TextStyle(fontWeight: FontWeight.w600)), + const SizedBox(height: 12), + Container( + constraints: const BoxConstraints(maxHeight: 320), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(8), + ), + child: SingleChildScrollView( + padding: const EdgeInsets.all(12), + child: SelectableText( + details, + style: const TextStyle(fontSize: 12, fontFamily: 'monospace'), + ), + ), + ), + ], + ), + actions: [ + TextButton.icon( + icon: const Icon(Icons.copy, size: 16), + label: const Text('Копировать'), + onPressed: () { + Clipboard.setData(ClipboardData(text: details)); + Navigator.pop(ctx); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Скопировано в буфер'), + duration: Duration(seconds: 2), + behavior: SnackBarBehavior.floating, + ), + ); + }, + ), + FilledButton( + onPressed: () => Navigator.pop(ctx), + child: const Text('Закрыть'), + ), + ], + ), + ); +} diff --git a/lib/presentation/screens/map/map_screen.dart b/lib/presentation/screens/map/map_screen.dart index 9899b47..cab49f5 100644 --- a/lib/presentation/screens/map/map_screen.dart +++ b/lib/presentation/screens/map/map_screen.dart @@ -16,6 +16,7 @@ import '../../providers/time_selector_provider.dart'; import '../../../core/theme/app_colors.dart'; import '../../../core/utils/nav_math.dart'; import '../../../core/utils/navigation_deeplink.dart'; +import '../../../core/utils/error_snackbar.dart'; import 'widgets/candidates_sheet.dart'; import 'widgets/navigation_overlay.dart' show NavigationTurnCard, NavigationBottomBar; import 'widgets/parking_zone_layer.dart'; @@ -446,11 +447,9 @@ class _MapScreenState extends ConsumerState { await _mapController?.toggleUserLayer(visible: false); await _mapController?.toggleTrafficLayer(visible: true); - } catch (e) { + } catch (e, st) { if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Не удалось построить маршрут: $e')), - ); + showErrorSnackBar(context, 'Не удалось построить маршрут', error: e, stackTrace: st); } } finally { if (mounted) setState(() => _navBuilding = false); @@ -500,10 +499,8 @@ class _MapScreenState extends ConsumerState { ref.listen(rawZonesProvider, (_, next) { next.whenOrNull( - error: (e, _) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Ошибка загрузки зон: $e')), - ); + error: (e, st) { + showErrorSnackBar(context, 'Не удалось загрузить парковки', error: e, stackTrace: st); }, ); }); @@ -534,9 +531,7 @@ class _MapScreenState extends ConsumerState { }, searching: () async {}, error: (message) async { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Ошибка маршрута: $message')), - ); + showErrorSnackBar(context, 'Ошибка при построении маршрута', error: message); }, candidates: (candidates) async { if (_isCandidatesSheetOpen || candidates.isEmpty) return; From f537c30ae4fb6a89f48bde49a3eaeaae87796695 Mon Sep 17 00:00:00 2001 From: Ilya Kiselev Date: Thu, 28 May 2026 13:28:25 +0300 Subject: [PATCH 031/145] fix: debounce zone fetch on timeline scroll --- lib/presentation/screens/map/map_screen.dart | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/presentation/screens/map/map_screen.dart b/lib/presentation/screens/map/map_screen.dart index cab49f5..875e3c0 100644 --- a/lib/presentation/screens/map/map_screen.dart +++ b/lib/presentation/screens/map/map_screen.dart @@ -34,6 +34,7 @@ class MapScreen extends ConsumerStatefulWidget { class _MapScreenState extends ConsumerState { YandexMapController? _mapController; Timer? _debounce; + Timer? _timeDebounce; Position? _userPosition; Point? _lastCameraTarget; double _currentAzimuth = 0; @@ -71,6 +72,7 @@ class _MapScreenState extends ConsumerState { @override void dispose() { _debounce?.cancel(); + _timeDebounce?.cancel(); _drivingSession?.close(); super.dispose(); } @@ -478,7 +480,10 @@ class _MapScreenState extends ConsumerState { orElse: () => false, ); - ref.listen(timeSelectorProvider, (_, __) => _fetchZones(clearCache: true)); + ref.listen(timeSelectorProvider, (_, __) { + _timeDebounce?.cancel(); + _timeDebounce = Timer(const Duration(milliseconds: 600), () => _fetchZones(clearCache: true)); + }); ref.listen(filteredZonesProvider, (_, zones) => _updateZoneBitmaps(zones)); ref.listen(navigationProvider, (prev, nav) { From 2fed2ee82b849ed15d534a49b5b19cbb243d9f75 Mon Sep 17 00:00:00 2001 From: Nikita Aksenov Date: Fri, 29 May 2026 01:13:57 +0300 Subject: [PATCH 032/145] feat: always use forecasts for parking zones search --- lib/presentation/providers/routing_provider.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/presentation/providers/routing_provider.dart b/lib/presentation/providers/routing_provider.dart index c0646a3..2e32554 100644 --- a/lib/presentation/providers/routing_provider.dart +++ b/lib/presentation/providers/routing_provider.dart @@ -57,8 +57,7 @@ class RoutingNotifier extends StateNotifier { try { final destination = _ref.read(destinationProvider); final filters = _ref.read(filtersProvider); - final timeMode = _ref.read(timeSelectorProvider); - final useForecast = timeMode.maybeWhen(future: (_) => true, orElse: () => null); + final useForecast = true; final candidates = await _ref.read(routingRepositoryProvider).searchParking( originLat: originLat, originLon: originLon, From 13596abd13089e901fcc9c88370b72f5f1abcf30 Mon Sep 17 00:00:00 2001 From: Ilya Kiselev Date: Fri, 29 May 2026 08:15:45 +0300 Subject: [PATCH 033/145] fix: set Petrozavodsk as default map position, remove location error snackbars --- lib/presentation/screens/map/map_screen.dart | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/lib/presentation/screens/map/map_screen.dart b/lib/presentation/screens/map/map_screen.dart index 875e3c0..1f8a454 100644 --- a/lib/presentation/screens/map/map_screen.dart +++ b/lib/presentation/screens/map/map_screen.dart @@ -238,18 +238,8 @@ class _MapScreenState extends ConsumerState { _userPosition = pos; return pos; } on TimeoutException { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Не удалось быстро получить геопозицию')), - ); - } return null; } catch (_) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Не удалось получить геопозицию')), - ); - } return null; } } @@ -734,7 +724,7 @@ class _MapScreenState extends ConsumerState { mapObjects: mapObjects, onMapCreated: (controller) async { _mapController = controller; - const fallback = Point(latitude: 59.7390, longitude: 30.4020); + const fallback = Point(latitude: 61.789114, longitude: 34.359757); _lastCameraTarget = fallback; await controller.moveCamera( CameraUpdate.newCameraPosition( From 6bcf4b4373d864232007365f500a18f80a8f5e2f Mon Sep 17 00:00:00 2001 From: Nikita Aksenov Date: Fri, 29 May 2026 10:08:34 +0300 Subject: [PATCH 034/145] feat: start location moved to Petrozavodsk --- lib/presentation/screens/map/map_screen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/presentation/screens/map/map_screen.dart b/lib/presentation/screens/map/map_screen.dart index 1f8a454..5e9ff1a 100644 --- a/lib/presentation/screens/map/map_screen.dart +++ b/lib/presentation/screens/map/map_screen.dart @@ -734,7 +734,7 @@ class _MapScreenState extends ConsumerState { _fetchZones(); final pos = await _getCurrentPosition(); if (pos != null && mounted) { - final target = Point(latitude: pos.latitude, longitude: pos.longitude); + final target = Point(latitude: 61.789114, longitude: 34.359757); _lastCameraTarget = target; await controller.moveCamera( CameraUpdate.newCameraPosition( From 81071d07b052104960117895461d02ff6155053b Mon Sep 17 00:00:00 2001 From: Ilya Kiselev Date: Sat, 30 May 2026 20:24:45 +0300 Subject: [PATCH 035/145] feat: date/time picker, clear zones on time change, prefetch bbox, wrap_content card, confidence ru, nav panel fixes --- .../providers/zones_provider.dart | 4 + lib/presentation/screens/map/map_screen.dart | 25 +- .../screens/map/widgets/candidates_sheet.dart | 4 +- .../map/widgets/navigation_overlay.dart | 15 +- .../map/widgets/parking_card_sheet.dart | 251 ++++++++------ .../map/widgets/time_selector_widget.dart | 307 ++++++++---------- 6 files changed, 311 insertions(+), 295 deletions(-) diff --git a/lib/presentation/providers/zones_provider.dart b/lib/presentation/providers/zones_provider.dart index 7edd317..42bd6f5 100644 --- a/lib/presentation/providers/zones_provider.dart +++ b/lib/presentation/providers/zones_provider.dart @@ -36,6 +36,10 @@ class ZonesNotifier extends StateNotifier>> { if (_lastBbox != null) await fetchZones(_lastBbox!); } + void clearZones() { + state = const AsyncValue.loading(); + } + void setErrorState(Object error, StackTrace stackTrace) { state = AsyncValue.error(error, stackTrace); } diff --git a/lib/presentation/screens/map/map_screen.dart b/lib/presentation/screens/map/map_screen.dart index 5e9ff1a..b0eff15 100644 --- a/lib/presentation/screens/map/map_screen.dart +++ b/lib/presentation/screens/map/map_screen.dart @@ -205,9 +205,13 @@ class _MapScreenState extends ConsumerState { } try { final visibleRegion = await _mapController!.getVisibleRegion(); + final bottomLeft = visibleRegion.bottomLeft; + final topRight = visibleRegion.topRight; + final dLon = (topRight.longitude - bottomLeft.longitude) * 0.5; + final dLat = (topRight.latitude - bottomLeft.latitude) * 0.5; final bbox = - '${visibleRegion.bottomLeft.longitude},${visibleRegion.bottomLeft.latitude},' - '${visibleRegion.topRight.longitude},${visibleRegion.topRight.latitude}'; + '${bottomLeft.longitude - dLon},${bottomLeft.latitude - dLat},' + '${topRight.longitude + dLon},${topRight.latitude + dLat}'; await ref.read(rawZonesProvider.notifier).fetchZones(bbox); } catch (e, st) { ref.read(rawZonesProvider.notifier).setErrorState(e, st); @@ -471,8 +475,14 @@ class _MapScreenState extends ConsumerState { ); ref.listen(timeSelectorProvider, (_, __) { + ref.read(rawZonesProvider.notifier).clearZones(); + _zoneLabelCache.clear(); + _zonesById.clear(); _timeDebounce?.cancel(); - _timeDebounce = Timer(const Duration(milliseconds: 600), () => _fetchZones(clearCache: true)); + _timeDebounce = Timer( + const Duration(milliseconds: 600), + () => _fetchZones(clearCache: false), + ); }); ref.listen(filteredZonesProvider, (_, zones) => _updateZoneBitmaps(zones)); @@ -807,14 +817,14 @@ class _MapScreenState extends ConsumerState { if (destination == null && !isNavigating) Positioned( bottom: bottomInset + 12, - left: 12, + right: 12, child: FloatingActionButton.extended( heroTag: 'find_parking', onPressed: isRoutingLoading ? null : _findParking, backgroundColor: AppColors.primary, icon: const Icon(Icons.local_parking, color: Colors.white), label: Text( - isRoutingLoading ? 'Ищем...' : 'Где припарковаться?', + isRoutingLoading ? 'Ищем...' : 'Припарковаться', style: const TextStyle(color: Colors.white), ), ), @@ -846,12 +856,11 @@ class _MapScreenState extends ConsumerState { ), ), ), - // ─── Таймлайн: полная ширина, своя строка выше кнопок ────── + // ─── Селектор времени: левый край, выше кнопок ───────── if (destination == null && !isNavigating) Positioned( bottom: bottomInset + 76, - left: 0, - right: 0, + left: 12, child: const TimeSelectorWidget(), ), // ─── Карточка назначения ──────────────────────────────────── diff --git a/lib/presentation/screens/map/widgets/candidates_sheet.dart b/lib/presentation/screens/map/widgets/candidates_sheet.dart index 7ff1ef9..a5d29cd 100644 --- a/lib/presentation/screens/map/widgets/candidates_sheet.dart +++ b/lib/presentation/screens/map/widgets/candidates_sheet.dart @@ -7,7 +7,7 @@ String _formatDuration(int seconds) { if (mins < 60) return '~$mins мин'; final h = mins ~/ 60; final m = mins % 60; - return m == 0 ? '~${h}ч' : '~${h}ч ${m}мин'; + return m == 0 ? '~${h}ч' : '~${h}ч ${m}мин'; } class _CandidateSubtitle extends StatelessWidget { @@ -26,7 +26,7 @@ class _CandidateSubtitle extends StatelessWidget { parts.add(_formatDuration(candidate.durationFromOriginSeconds!)); if (candidate.predictedFreeCount != null) parts.add('прогноз: ${candidate.predictedFreeCount}'); - return Text(parts.join(' • ')); + return Text(parts.join(' • '), overflow: TextOverflow.ellipsis, maxLines: 2); } } diff --git a/lib/presentation/screens/map/widgets/navigation_overlay.dart b/lib/presentation/screens/map/widgets/navigation_overlay.dart index 469cb15..15ad5cd 100644 --- a/lib/presentation/screens/map/widgets/navigation_overlay.dart +++ b/lib/presentation/screens/map/widgets/navigation_overlay.dart @@ -193,12 +193,14 @@ class _NavBottomBar extends StatelessWidget { label: 'времени', ), ), + const SizedBox(width: 12), Expanded( child: _StatCell( value: formatNavDistance(nav.remainingMeters), label: 'до цели', ), ), + const SizedBox(width: 12), Expanded( child: _StatCell( value: '${nav.speedKmh.round()}', @@ -206,12 +208,12 @@ class _NavBottomBar extends StatelessWidget { valueColor: speedColor, ), ), - TextButton( + IconButton( onPressed: () => _confirmFinish(context), - child: const Text( - 'Завершить', - style: TextStyle(color: AppColors.textSecondary, fontSize: 14), - ), + icon: const Icon(Icons.stop_circle_outlined), + color: AppColors.textSecondary, + iconSize: 28, + tooltip: 'Завершить', ), ], ), @@ -234,6 +236,9 @@ class _StatCell extends StatelessWidget { children: [ Text( value, + maxLines: 1, + softWrap: false, + overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, diff --git a/lib/presentation/screens/map/widgets/parking_card_sheet.dart b/lib/presentation/screens/map/widgets/parking_card_sheet.dart index d2648b5..e03823e 100644 --- a/lib/presentation/screens/map/widgets/parking_card_sheet.dart +++ b/lib/presentation/screens/map/widgets/parking_card_sheet.dart @@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../../domain/models/zone.dart'; import '../../../../core/theme/app_colors.dart'; import '../../../../presentation/providers/time_selector_provider.dart'; +import '../../../../presentation/providers/zones_provider.dart'; Future showParkingCard( BuildContext context, @@ -28,121 +29,161 @@ class _ParkingCardSheet extends ConsumerWidget { final isFuture = timeMode.maybeWhen(future: (_) => true, orElse: () => false); final userSelectedAt = timeMode.maybeWhen(future: (at) => at, orElse: () => null); - return DraggableScrollableSheet( - expand: false, - initialChildSize: 0.45, - minChildSize: 0.3, - maxChildSize: 0.75, - builder: (_, controller) => Container( - decoration: const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.vertical(top: Radius.circular(20)), - ), - child: ListView( - controller: controller, + final zonesAsync = ref.watch(rawZonesProvider); + final isLoading = zonesAsync is AsyncLoading; + final currentZone = zonesAsync.valueOrNull + ?.where((z) => z.zoneId == zone.zoneId) + .firstOrNull ?? + zone; + + return Padding( + padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), + child: SingleChildScrollView( + child: Container( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), padding: const EdgeInsets.fromLTRB(20, 12, 20, 24), - children: [ - Center( - child: Container( - width: 40, - height: 4, - decoration: BoxDecoration( - color: Colors.grey[300], - borderRadius: BorderRadius.circular(2), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Center( + child: Container( + width: 40, + height: 4, + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(2), + ), ), ), - ), - const SizedBox(height: 16), - Row( - children: [ - Expanded( - child: Text( - 'Парковка #${zone.zoneId}', - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: Text( + 'Парковка #${currentZone.zoneId}', + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ), + if (currentZone.isPrivate == true) + _Badge('Частная', Colors.orange), + if (currentZone.isAccessible == true) + _Badge('♿', AppColors.primary), + ], + ), + const SizedBox(height: 12), + if (isLoading) + const Padding( + padding: EdgeInsets.symmetric(vertical: 24), + child: Center( + child: Text( + 'Данные загружаются...', + style: TextStyle(color: AppColors.textSecondary), ), ), + ) + else ...[ + _InfoRow( + icon: Icons.local_parking, + label: 'Мест', + value: currentZone.hasForecast + ? '${currentZone.freeCount} / ${currentZone.capacity} свободно' + : 'Нет прогноза', + valueColor: + currentZone.hasForecast ? null : AppColors.textSecondary, + ), + if (isFuture && + currentZone.hasForecast && + currentZone.forecastFor != null) + _InfoRow( + icon: Icons.schedule, + label: 'Прогноз на', + value: _formatDateTime(currentZone.forecastFor!), + ), + if (isFuture && + currentZone.hasForecast && + currentZone.forecastGeneratedAt != null) + _InfoRow( + icon: Icons.build_circle_outlined, + label: 'Создан', + value: _formatDateTime(currentZone.forecastGeneratedAt!), + ), + _InfoRow( + icon: Icons.payments_outlined, + label: 'Стоимость', + value: currentZone.pay == 0 + ? 'Бесплатно' + : '${currentZone.pay} ₽/ч', + valueColor: + currentZone.pay == 0 ? AppColors.primary : null, ), - if (zone.isPrivate == true) - _Badge('Частная', Colors.orange), - if (zone.isAccessible == true) - _Badge('♿', AppColors.primary), + _InfoRow( + icon: Icons.verified_outlined, + label: 'Уверенность', + value: _confidenceLabel(currentZone), + ), + if (currentZone.locationType != null) + _InfoRow( + icon: Icons.place_outlined, + label: 'Тип', + value: _locationTypeLabel(currentZone.locationType!), + ), + _InfoRow( + icon: Icons.directions_car_outlined, + label: 'Постановка', + value: currentZone.zoneType == ZoneType.parallel + ? 'Параллельная' + : 'Обычная', + ), + if (!isFuture && currentZone.occupancyUpdatedAt != null) + _InfoRow( + icon: Icons.update, + label: 'Обновлено', + value: _formatDateTime(currentZone.occupancyUpdatedAt!), + ), + if (isFuture && + currentZone.forecastFor != null && + userSelectedAt != null) + _StaleForecastBanner( + forecastFor: currentZone.forecastFor!, + userSelectedAt: userSelectedAt, + onSnap: () { + ref + .read(timeSelectorProvider.notifier) + .setFuture(currentZone.forecastFor!); + Navigator.pop(context); + }, + ), ], - ), - const SizedBox(height: 12), - _InfoRow( - icon: Icons.local_parking, - label: 'Мест', - value: zone.hasForecast - ? '${zone.freeCount} / ${zone.capacity} свободно' - : 'Нет прогноза', - valueColor: zone.hasForecast ? null : AppColors.textSecondary, - ), - if (isFuture && zone.hasForecast && zone.forecastFor != null) - _InfoRow( - icon: Icons.schedule, - label: 'Прогноз на', - value: _formatDateTime(zone.forecastFor!), - ), - if (isFuture && zone.hasForecast && zone.forecastGeneratedAt != null) - _InfoRow( - icon: Icons.build_circle_outlined, - label: 'Создан', - value: _formatDateTime(zone.forecastGeneratedAt!), - ), - _InfoRow( - icon: Icons.payments_outlined, - label: 'Стоимость', - value: zone.pay == 0 ? 'Бесплатно' : '${zone.pay} ₽/ч', - valueColor: zone.pay == 0 ? AppColors.primary : null, - ), - _InfoRow( - icon: Icons.verified_outlined, - label: 'Уверенность', - value: zone.confidenceLevel ?? '${(zone.confidence * 100).round()}%', - ), - if (zone.locationType != null) - _InfoRow( - icon: Icons.place_outlined, - label: 'Тип', - value: _locationTypeLabel(zone.locationType!), - ), - _InfoRow( - icon: Icons.directions_car_outlined, - label: 'Постановка', - value: zone.zoneType == ZoneType.parallel ? 'Параллельная' : 'Обычная', - ), - if (!isFuture && zone.occupancyUpdatedAt != null) - _InfoRow( - icon: Icons.update, - label: 'Обновлено', - value: _formatDateTime(zone.occupancyUpdatedAt!), - ), - if (isFuture && zone.forecastFor != null && userSelectedAt != null) - _StaleForecastBanner( - forecastFor: zone.forecastFor!, - userSelectedAt: userSelectedAt, - onSnap: () { - ref.read(timeSelectorProvider.notifier).setFuture(zone.forecastFor!); + const SizedBox(height: 20), + FilledButton.icon( + onPressed: () { Navigator.pop(context); + onBuildRoute?.call(); }, + icon: const Icon(Icons.directions), + label: const Text('Построить маршрут'), ), - const SizedBox(height: 20), - FilledButton.icon( - onPressed: () { - Navigator.pop(context); - onBuildRoute?.call(); - }, - icon: const Icon(Icons.directions), - label: const Text('Построить маршрут'), - ), - ], + ], + ), ), ), ); } + String _confidenceLabel(Zone z) { + const labels = {'low': 'низкая', 'medium': 'средняя', 'high': 'высокая'}; + if (z.confidenceLevel != null) { + return labels[z.confidenceLevel!.toLowerCase()] ?? z.confidenceLevel!; + } + return '${(z.confidence * 100).round()}%'; + } + String _locationTypeLabel(LocationType type) => switch (type) { LocationType.street => 'Уличная', LocationType.yard => 'Дворовая', @@ -254,7 +295,8 @@ class _InfoRow extends StatelessWidget { Icon(icon, size: 20, color: AppColors.textSecondary), const SizedBox(width: 12), Text(label, - style: const TextStyle(color: AppColors.textSecondary, fontSize: 14)), + style: + const TextStyle(color: AppColors.textSecondary, fontSize: 14)), const Spacer(), Text( value, @@ -282,12 +324,13 @@ class _Badge extends StatelessWidget { margin: const EdgeInsets.only(left: 8), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( - color: color.withOpacity(0.15), + color: color.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(8), - border: Border.all(color: color.withOpacity(0.5)), + border: Border.all(color: color.withValues(alpha: 0.5)), ), child: Text(text, - style: TextStyle(color: color, fontSize: 12, fontWeight: FontWeight.w600)), + style: TextStyle( + color: color, fontSize: 12, fontWeight: FontWeight.w600)), ); } } diff --git a/lib/presentation/screens/map/widgets/time_selector_widget.dart b/lib/presentation/screens/map/widgets/time_selector_widget.dart index 62f1080..ce56bfb 100644 --- a/lib/presentation/screens/map/widgets/time_selector_widget.dart +++ b/lib/presentation/screens/map/widgets/time_selector_widget.dart @@ -11,210 +11,165 @@ class TimeSelectorWidget extends ConsumerStatefulWidget { } class _TimeSelectorWidgetState extends ConsumerState { - static const _step = Duration(minutes: 30); - static const _hoursBefore = 24; - static const _hoursAfter = 24; + DateTime? _selected; - late final List _ticks; - late final int _centerIndex; - late final PageController _controller; - int _selectedIndex = 0; + bool get _isNow => _selected == null; - @override - void initState() { - super.initState(); - final now = DateTime.now(); - final alignedNow = DateTime( - now.year, - now.month, - now.day, - now.hour, - now.minute < 30 ? 0 : 30, - ); - _ticks = [ - for (var i = -(_hoursBefore * 2); i <= _hoursAfter * 2; i++) - alignedNow.add(_step * i), - ]; - _centerIndex = _hoursBefore * 2; - _selectedIndex = _centerIndex; - _controller = PageController( - viewportFraction: 0.17, - initialPage: _centerIndex, - ); - - _controller.addListener(() { - final p = _controller.page; - if (p == null) return; - final rounded = p.round().clamp(0, _ticks.length - 1); - if (rounded != _selectedIndex) { - setState(() => _selectedIndex = rounded); - } - }); - - WidgetsBinding.instance.addPostFrameCallback((_) { - _applyTimeMode(_centerIndex); - }); - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); + void _resetToNow() { + setState(() => _selected = null); + ref.read(timeSelectorProvider.notifier).setNow(); } - void _applyTimeMode(int index) { - final selected = _ticks[index]; - final now = _ticks[_centerIndex]; + void _apply(DateTime dt) { + setState(() => _selected = dt); + final now = DateTime.now(); + final diff = dt.difference(now).inMinutes; final notifier = ref.read(timeSelectorProvider.notifier); - final diff = selected.difference(now).inMinutes; if (diff.abs() < 15) { notifier.setNow(); } else if (diff < 0) { - notifier.setPast(selected); + notifier.setPast(dt); } else { - notifier.setFuture(selected); + notifier.setFuture(dt); } } - void _resetToNow() { - _controller.animateToPage( - _centerIndex, - duration: const Duration(milliseconds: 400), - curve: Curves.easeInOut, + Future _pickDate() async { + final base = _selected ?? DateTime.now(); + final picked = await showDatePicker( + context: context, + initialDate: base, + firstDate: DateTime.now().subtract(const Duration(days: 30)), + lastDate: DateTime.now().add(const Duration(days: 30)), ); - setState(() => _selectedIndex = _centerIndex); - _applyTimeMode(_centerIndex); + if (picked == null) return; + final current = _selected ?? DateTime.now(); + _apply(DateTime(picked.year, picked.month, picked.day, current.hour, current.minute)); } - void _selectIndex(int index) { - _controller.animateToPage( - index, - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, + Future _pickTime() async { + final base = _selected ?? DateTime.now(); + final picked = await showTimePicker( + context: context, + initialTime: TimeOfDay(hour: base.hour, minute: base.minute), ); - setState(() => _selectedIndex = index); - _applyTimeMode(index); + if (picked == null) return; + final current = _selected ?? DateTime.now(); + _apply(DateTime(current.year, current.month, current.day, picked.hour, picked.minute)); + } + + String _dateLabel() { + if (_isNow) { + final now = DateTime.now(); + return _dayLabel(now); + } + return _dayLabel(_selected!); + } + + String _dayLabel(DateTime dt) { + final now = DateTime.now(); + if (dt.year == now.year && dt.month == now.month && dt.day == now.day) return 'Сегодня'; + final tomorrow = now.add(const Duration(days: 1)); + if (dt.year == tomorrow.year && dt.month == tomorrow.month && dt.day == tomorrow.day) { + return 'Завтра'; + } + const months = ['', 'янв', 'фев', 'мар', 'апр', 'май', 'июн', 'июл', 'авг', 'сен', 'окт', 'ноя', 'дек']; + return '${dt.day} ${months[dt.month]}'; + } + + String _timeLabel() { + if (_isNow) return 'сейчас'; + final h = _selected!.hour.toString().padLeft(2, '0'); + final m = _selected!.minute.toString().padLeft(2, '0'); + return '$h:$m'; } @override Widget build(BuildContext context) { - final isOffCenter = _selectedIndex != _centerIndex; - - return Column( + return Row( mainAxisSize: MainAxisSize.min, children: [ - AnimatedOpacity( - opacity: isOffCenter ? 1.0 : 0.0, - duration: const Duration(milliseconds: 200), - child: IgnorePointer( - ignoring: !isOffCenter, - child: Padding( - padding: const EdgeInsets.only(bottom: 4), - child: Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(20), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.1), - blurRadius: 6, - ), - ], - ), - child: TextButton.icon( - onPressed: _resetToNow, - icon: const Icon(Icons.restore, size: 16), - label: const Text('Сейчас'), - style: TextButton.styleFrom( - foregroundColor: AppColors.primary, - padding: - const EdgeInsets.symmetric(horizontal: 14, vertical: 6), - textStyle: const TextStyle( - fontSize: 13, fontWeight: FontWeight.w600), - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - minimumSize: Size.zero, - ), - ), - ), - ), + if (!_isNow) ...[ + _Chip( + label: 'Сейчас', + icon: Icons.restore, + onTap: _resetToNow, + active: false, ), + const SizedBox(width: 6), + ], + _Chip( + label: _dateLabel(), + icon: Icons.calendar_today_outlined, + onTap: _pickDate, + active: !_isNow, ), - Container( - height: 62, - margin: const EdgeInsets.symmetric(horizontal: 12), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.1), - blurRadius: 8, - ), - ], - ), - child: PageView.builder( - controller: _controller, - physics: const BouncingScrollPhysics(), - itemCount: _ticks.length, - onPageChanged: (index) => _applyTimeMode(index), - itemBuilder: (_, index) { - final tick = _ticks[index]; - final isSelected = index == _selectedIndex; - final isNow = index == _centerIndex; - return GestureDetector( - onTap: () => _selectIndex(index), - child: Center( - child: AnimatedContainer( - duration: const Duration(milliseconds: 120), - padding: - const EdgeInsets.symmetric(horizontal: 8, vertical: 8), - decoration: BoxDecoration( - color: isSelected - ? AppColors.primary.withValues(alpha: 0.12) - : Colors.transparent, - borderRadius: BorderRadius.circular(10), - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - width: isSelected ? 3 : 2, - height: isSelected ? 14 : 10, - decoration: BoxDecoration( - color: isSelected - ? AppColors.primary - : Colors.grey[400], - borderRadius: BorderRadius.circular(2), - ), - ), - const SizedBox(height: 4), - Text( - isNow ? 'Сейчас' : _formatTick(tick), - style: TextStyle( - fontSize: 10, - color: isSelected - ? AppColors.primary - : AppColors.textSecondary, - fontWeight: isSelected - ? FontWeight.w700 - : FontWeight.w500, - ), - ), - ], - ), - ), - ), - ); - }, - ), + const SizedBox(width: 6), + _Chip( + label: _timeLabel(), + icon: Icons.access_time_outlined, + onTap: _pickTime, + active: !_isNow, ), ], ); } +} + +class _Chip extends StatelessWidget { + const _Chip({ + required this.label, + required this.icon, + required this.onTap, + required this.active, + }); + + final String label; + final IconData icon; + final VoidCallback onTap; + final bool active; - String _formatTick(DateTime dt) { - final hh = dt.hour.toString().padLeft(2, '0'); - final mm = dt.minute.toString().padLeft(2, '0'); - return '$hh:$mm'; + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: active ? AppColors.primary.withValues(alpha: 0.1) : Colors.white, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: active ? AppColors.primary : Colors.grey.shade300, + width: 1, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.08), + blurRadius: 4, + offset: const Offset(0, 1), + ), + ], + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + icon, + size: 14, + color: active ? AppColors.primary : AppColors.textSecondary, + ), + const SizedBox(width: 5), + Text( + label, + style: TextStyle( + fontSize: 13, + fontWeight: active ? FontWeight.w600 : FontWeight.w500, + color: active ? AppColors.primary : AppColors.onSurface, + ), + ), + ], + ), + ), + ); } } From f096979c82f657c0623bbe0a8c1e18b47498682f Mon Sep 17 00:00:00 2001 From: Ilya Kiselev Date: Sat, 30 May 2026 20:33:44 +0300 Subject: [PATCH 036/145] fix: reactive time selector, Russian locale for date/time pickers, solid chip background --- lib/main.dart | 8 + .../map/widgets/time_selector_widget.dart | 173 +++++++++--------- pubspec.lock | 13 ++ pubspec.yaml | 2 + 4 files changed, 107 insertions(+), 89 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index dd5ea4e..aabd5a0 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'core/router/app_router.dart'; import 'core/theme/app_theme.dart'; @@ -20,6 +21,13 @@ class ParkTrackApp extends ConsumerWidget { theme: AppTheme.light, routerConfig: router, debugShowCheckedModeBanner: false, + locale: const Locale('ru', 'RU'), + localizationsDelegates: const [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [Locale('ru', 'RU'), Locale('en')], ); } } diff --git a/lib/presentation/screens/map/widgets/time_selector_widget.dart b/lib/presentation/screens/map/widgets/time_selector_widget.dart index ce56bfb..1ccddae 100644 --- a/lib/presentation/screens/map/widgets/time_selector_widget.dart +++ b/lib/presentation/screens/map/widgets/time_selector_widget.dart @@ -3,25 +3,76 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../../presentation/providers/time_selector_provider.dart'; import '../../../../core/theme/app_colors.dart'; -class TimeSelectorWidget extends ConsumerStatefulWidget { +class TimeSelectorWidget extends ConsumerWidget { const TimeSelectorWidget({super.key}); @override - ConsumerState createState() => _TimeSelectorWidgetState(); -} + Widget build(BuildContext context, WidgetRef ref) { + final timeMode = ref.watch(timeSelectorProvider); + final isNow = timeMode.maybeWhen(now: () => true, orElse: () => false); + final selectedDt = timeMode.maybeWhen( + future: (at) => at, + past: (at) => at, + orElse: () => null, + ); -class _TimeSelectorWidgetState extends ConsumerState { - DateTime? _selected; + Future pickDate() async { + final base = selectedDt ?? DateTime.now(); + final picked = await showDatePicker( + context: context, + initialDate: base, + firstDate: DateTime.now().subtract(const Duration(days: 30)), + lastDate: DateTime.now().add(const Duration(days: 30)), + locale: const Locale('ru', 'RU'), + ); + if (picked == null || !context.mounted) return; + final current = selectedDt ?? DateTime.now(); + final dt = DateTime(picked.year, picked.month, picked.day, current.hour, current.minute); + _apply(ref, dt); + } - bool get _isNow => _selected == null; + Future pickTime() async { + final base = selectedDt ?? DateTime.now(); + final picked = await showTimePicker( + context: context, + initialTime: TimeOfDay(hour: base.hour, minute: base.minute), + ); + if (picked == null || !context.mounted) return; + final current = selectedDt ?? DateTime.now(); + final dt = DateTime(current.year, current.month, current.day, picked.hour, picked.minute); + _apply(ref, dt); + } - void _resetToNow() { - setState(() => _selected = null); - ref.read(timeSelectorProvider.notifier).setNow(); + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (!isNow) ...[ + _Chip( + label: 'Сейчас', + icon: Icons.restore, + active: false, + onTap: () => ref.read(timeSelectorProvider.notifier).setNow(), + ), + const SizedBox(width: 6), + ], + _Chip( + label: _dateLabel(selectedDt), + icon: Icons.calendar_today_outlined, + active: !isNow, + onTap: pickDate, + ), + const SizedBox(width: 6), + _Chip( + label: _timeLabel(selectedDt), + icon: Icons.access_time_outlined, + active: !isNow, + onTap: pickTime, + ), + ], + ); } - void _apply(DateTime dt) { - setState(() => _selected = dt); + void _apply(WidgetRef ref, DateTime dt) { final now = DateTime.now(); final diff = dt.difference(now).inMinutes; final notifier = ref.read(timeSelectorProvider.notifier); @@ -34,36 +85,12 @@ class _TimeSelectorWidgetState extends ConsumerState { } } - Future _pickDate() async { - final base = _selected ?? DateTime.now(); - final picked = await showDatePicker( - context: context, - initialDate: base, - firstDate: DateTime.now().subtract(const Duration(days: 30)), - lastDate: DateTime.now().add(const Duration(days: 30)), - ); - if (picked == null) return; - final current = _selected ?? DateTime.now(); - _apply(DateTime(picked.year, picked.month, picked.day, current.hour, current.minute)); - } - - Future _pickTime() async { - final base = _selected ?? DateTime.now(); - final picked = await showTimePicker( - context: context, - initialTime: TimeOfDay(hour: base.hour, minute: base.minute), - ); - if (picked == null) return; - final current = _selected ?? DateTime.now(); - _apply(DateTime(current.year, current.month, current.day, picked.hour, picked.minute)); - } - - String _dateLabel() { - if (_isNow) { + String _dateLabel(DateTime? dt) { + if (dt == null) { final now = DateTime.now(); return _dayLabel(now); } - return _dayLabel(_selected!); + return _dayLabel(dt); } String _dayLabel(DateTime dt) { @@ -77,94 +104,62 @@ class _TimeSelectorWidgetState extends ConsumerState { return '${dt.day} ${months[dt.month]}'; } - String _timeLabel() { - if (_isNow) return 'сейчас'; - final h = _selected!.hour.toString().padLeft(2, '0'); - final m = _selected!.minute.toString().padLeft(2, '0'); + String _timeLabel(DateTime? dt) { + if (dt == null) return 'сейчас'; + final h = dt.hour.toString().padLeft(2, '0'); + final m = dt.minute.toString().padLeft(2, '0'); return '$h:$m'; } - - @override - Widget build(BuildContext context) { - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (!_isNow) ...[ - _Chip( - label: 'Сейчас', - icon: Icons.restore, - onTap: _resetToNow, - active: false, - ), - const SizedBox(width: 6), - ], - _Chip( - label: _dateLabel(), - icon: Icons.calendar_today_outlined, - onTap: _pickDate, - active: !_isNow, - ), - const SizedBox(width: 6), - _Chip( - label: _timeLabel(), - icon: Icons.access_time_outlined, - onTap: _pickTime, - active: !_isNow, - ), - ], - ); - } } class _Chip extends StatelessWidget { const _Chip({ required this.label, required this.icon, - required this.onTap, required this.active, + required this.onTap, }); final String label; final IconData icon; - final VoidCallback onTap; final bool active; + final VoidCallback onTap; @override Widget build(BuildContext context) { + final bg = active ? AppColors.primary : Colors.white; + final fg = active ? Colors.white : AppColors.onSurface; + final iconColor = active ? Colors.white : AppColors.textSecondary; + return GestureDetector( onTap: onTap, child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( - color: active ? AppColors.primary.withValues(alpha: 0.1) : Colors.white, + color: bg, borderRadius: BorderRadius.circular(20), border: Border.all( - color: active ? AppColors.primary : Colors.grey.shade300, - width: 1, + color: active ? AppColors.primary : Colors.grey.shade400, ), - boxShadow: [ + boxShadow: const [ BoxShadow( - color: Colors.black.withValues(alpha: 0.08), - blurRadius: 4, - offset: const Offset(0, 1), + color: Color(0x33000000), + blurRadius: 6, + offset: Offset(0, 2), ), ], ), child: Row( mainAxisSize: MainAxisSize.min, children: [ - Icon( - icon, - size: 14, - color: active ? AppColors.primary : AppColors.textSecondary, - ), + Icon(icon, size: 14, color: iconColor), const SizedBox(width: 5), Text( label, style: TextStyle( fontSize: 13, - fontWeight: active ? FontWeight.w600 : FontWeight.w500, - color: active ? AppColors.primary : AppColors.onSurface, + fontWeight: FontWeight.w600, + color: fg, ), ), ], diff --git a/pubspec.lock b/pubspec.lock index d523831..6d3939b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -302,6 +302,11 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.0" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -512,6 +517,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.8.0" + intl: + dependency: transitive + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" io: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index f5c0d9b..3e3be73 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,6 +10,8 @@ environment: dependencies: flutter: sdk: flutter + flutter_localizations: + sdk: flutter # State management flutter_riverpod: ^2.6.1 From 780c45a100d6fa4bae0031b499600fb34ade263e Mon Sep 17 00:00:00 2001 From: Ilya Kiselev Date: Sat, 30 May 2026 20:43:44 +0300 Subject: [PATCH 037/145] feat: compact time picker sheet with day selector and time drum, fix stale chip label --- .../map/widgets/time_selector_widget.dart | 239 +++++++++++++----- 1 file changed, 182 insertions(+), 57 deletions(-) diff --git a/lib/presentation/screens/map/widgets/time_selector_widget.dart b/lib/presentation/screens/map/widgets/time_selector_widget.dart index 1ccddae..a6d0656 100644 --- a/lib/presentation/screens/map/widgets/time_selector_widget.dart +++ b/lib/presentation/screens/map/widgets/time_selector_widget.dart @@ -1,3 +1,4 @@ +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../../presentation/providers/time_selector_provider.dart'; @@ -16,31 +17,16 @@ class TimeSelectorWidget extends ConsumerWidget { orElse: () => null, ); - Future pickDate() async { - final base = selectedDt ?? DateTime.now(); - final picked = await showDatePicker( + void openPicker() { + showModalBottomSheet( context: context, - initialDate: base, - firstDate: DateTime.now().subtract(const Duration(days: 30)), - lastDate: DateTime.now().add(const Duration(days: 30)), - locale: const Locale('ru', 'RU'), - ); - if (picked == null || !context.mounted) return; - final current = selectedDt ?? DateTime.now(); - final dt = DateTime(picked.year, picked.month, picked.day, current.hour, current.minute); - _apply(ref, dt); - } - - Future pickTime() async { - final base = selectedDt ?? DateTime.now(); - final picked = await showTimePicker( - context: context, - initialTime: TimeOfDay(hour: base.hour, minute: base.minute), + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (_) => _TimePickerSheet( + initial: selectedDt, + onApply: (dt) => _apply(ref, dt), + ), ); - if (picked == null || !context.mounted) return; - final current = selectedDt ?? DateTime.now(); - final dt = DateTime(current.year, current.month, current.day, picked.hour, picked.minute); - _apply(ref, dt); } return Row( @@ -56,62 +42,201 @@ class TimeSelectorWidget extends ConsumerWidget { const SizedBox(width: 6), ], _Chip( - label: _dateLabel(selectedDt), - icon: Icons.calendar_today_outlined, - active: !isNow, - onTap: pickDate, - ), - const SizedBox(width: 6), - _Chip( - label: _timeLabel(selectedDt), + label: isNow ? 'Время' : _selectionLabel(selectedDt!), icon: Icons.access_time_outlined, active: !isNow, - onTap: pickTime, + onTap: openPicker, ), ], ); } - void _apply(WidgetRef ref, DateTime dt) { + void _apply(WidgetRef ref, DateTime? dt) { + if (dt == null) { + ref.read(timeSelectorProvider.notifier).setNow(); + return; + } final now = DateTime.now(); final diff = dt.difference(now).inMinutes; - final notifier = ref.read(timeSelectorProvider.notifier); if (diff.abs() < 15) { - notifier.setNow(); + ref.read(timeSelectorProvider.notifier).setNow(); } else if (diff < 0) { - notifier.setPast(dt); + ref.read(timeSelectorProvider.notifier).setPast(dt); } else { - notifier.setFuture(dt); - } - } - - String _dateLabel(DateTime? dt) { - if (dt == null) { - final now = DateTime.now(); - return _dayLabel(now); + ref.read(timeSelectorProvider.notifier).setFuture(dt); } - return _dayLabel(dt); } - String _dayLabel(DateTime dt) { + String _selectionLabel(DateTime dt) { final now = DateTime.now(); - if (dt.year == now.year && dt.month == now.month && dt.day == now.day) return 'Сегодня'; - final tomorrow = now.add(const Duration(days: 1)); - if (dt.year == tomorrow.year && dt.month == tomorrow.month && dt.day == tomorrow.day) { - return 'Завтра'; - } + final tomorrow = DateTime(now.year, now.month, now.day + 1); + final yesterday = DateTime(now.year, now.month, now.day - 1); + final dtDay = DateTime(dt.year, dt.month, dt.day); + final h = dt.hour.toString().padLeft(2, '0'); + final m = dt.minute.toString().padLeft(2, '0'); + final time = '$h:$m'; + if (dtDay == DateTime(now.year, now.month, now.day)) return 'Сегодня $time'; + if (dtDay == DateTime(tomorrow.year, tomorrow.month, tomorrow.day)) return 'Завтра $time'; + if (dtDay == DateTime(yesterday.year, yesterday.month, yesterday.day)) return 'Вчера $time'; const months = ['', 'янв', 'фев', 'мар', 'апр', 'май', 'июн', 'июл', 'авг', 'сен', 'окт', 'ноя', 'дек']; - return '${dt.day} ${months[dt.month]}'; + return '${dt.day} ${months[dt.month]} $time'; } +} - String _timeLabel(DateTime? dt) { - if (dt == null) return 'сейчас'; - final h = dt.hour.toString().padLeft(2, '0'); - final m = dt.minute.toString().padLeft(2, '0'); - return '$h:$m'; +// ─── Bottom sheet ─────────────────────────────────────────────────────────── + +class _TimePickerSheet extends StatefulWidget { + final DateTime? initial; + final void Function(DateTime? dt) onApply; + + const _TimePickerSheet({required this.initial, required this.onApply}); + + @override + State<_TimePickerSheet> createState() => _TimePickerSheetState(); +} + +class _TimePickerSheetState extends State<_TimePickerSheet> { + static const _step = 30; + + late int _dayOffset; // -1 вчера, 0 сегодня, 1 завтра + late DateTime _time; + + @override + void initState() { + super.initState(); + final base = widget.initial ?? DateTime.now(); + final today = _dayOnly(DateTime.now()); + final baseDay = _dayOnly(base); + _dayOffset = baseDay.difference(today).inDays.clamp(-1, 1); + _time = _snapTo30(base); + } + + static DateTime _dayOnly(DateTime dt) => DateTime(dt.year, dt.month, dt.day); + + static DateTime _snapTo30(DateTime dt) { + final snapped = ((dt.minute / _step).round() * _step) % 60; + final hourAdd = ((dt.minute / _step).round() * _step) ~/ 60; + return DateTime(dt.year, dt.month, dt.day, (dt.hour + hourAdd) % 24, snapped); + } + + DateTime get _result { + final base = _dayOnly(DateTime.now()).add(Duration(days: _dayOffset)); + return DateTime(base.year, base.month, base.day, _time.hour, _time.minute); + } + + @override + Widget build(BuildContext context) { + final days = [ + (-1, 'Вчера'), + (0, 'Сегодня'), + (1, 'Завтра'), + ]; + + return Container( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + padding: EdgeInsets.fromLTRB( + 20, 16, 20, MediaQuery.of(context).padding.bottom + 20, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 40, height: 4, + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(2), + ), + ), + const SizedBox(height: 16), + const Text( + 'Выбрать время', + style: TextStyle(fontSize: 17, fontWeight: FontWeight.w600), + ), + const SizedBox(height: 16), + // День + Row( + children: days.map((pair) { + final (offset, label) = pair; + final sel = _dayOffset == offset; + return Expanded( + child: GestureDetector( + onTap: () => setState(() => _dayOffset = offset), + child: AnimatedContainer( + duration: const Duration(milliseconds: 150), + margin: const EdgeInsets.symmetric(horizontal: 4), + padding: const EdgeInsets.symmetric(vertical: 10), + decoration: BoxDecoration( + color: sel ? AppColors.primary : Colors.grey.shade100, + borderRadius: BorderRadius.circular(10), + ), + child: Center( + child: Text( + label, + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 14, + color: sel ? Colors.white : AppColors.onSurface, + ), + ), + ), + ), + ), + ); + }).toList(), + ), + const SizedBox(height: 8), + // Барабан времени + SizedBox( + height: 150, + child: CupertinoDatePicker( + mode: CupertinoDatePickerMode.time, + use24hFormat: true, + minuteInterval: _step, + initialDateTime: _time, + onDateTimeChanged: (dt) { + setState(() => _time = DateTime( + _time.year, _time.month, _time.day, + dt.hour, dt.minute, + )); + }, + ), + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: () { + Navigator.pop(context); + widget.onApply(null); + }, + child: const Text('Сейчас'), + ), + ), + const SizedBox(width: 12), + Expanded( + flex: 2, + child: FilledButton( + onPressed: () { + Navigator.pop(context); + widget.onApply(_result); + }, + child: const Text('Применить'), + ), + ), + ], + ), + ], + ), + ); } } +// ─── Chip ─────────────────────────────────────────────────────────────────── + class _Chip extends StatelessWidget { const _Chip({ required this.label, From 2e0d7f30b8e76c9ba71b92921d39e956331d1207 Mon Sep 17 00:00:00 2001 From: Ilya Kiselev Date: Sat, 30 May 2026 20:52:15 +0300 Subject: [PATCH 038/145] fix: confidence labels capitalized with very_low/very_high, bottom row layout in one line --- lib/presentation/screens/map/map_screen.dart | 41 ++++++++++--------- .../map/widgets/parking_card_sheet.dart | 14 ++++++- 2 files changed, 33 insertions(+), 22 deletions(-) diff --git a/lib/presentation/screens/map/map_screen.dart b/lib/presentation/screens/map/map_screen.dart index b0eff15..774cb64 100644 --- a/lib/presentation/screens/map/map_screen.dart +++ b/lib/presentation/screens/map/map_screen.dart @@ -813,23 +813,7 @@ class _MapScreenState extends ConsumerState { ), ), ), - // ─── Нижняя строка: FAB слева + кнопки справа ───────────── - if (destination == null && !isNavigating) - Positioned( - bottom: bottomInset + 12, - right: 12, - child: FloatingActionButton.extended( - heroTag: 'find_parking', - onPressed: isRoutingLoading ? null : _findParking, - backgroundColor: AppColors.primary, - icon: const Icon(Icons.local_parking, color: Colors.white), - label: Text( - isRoutingLoading ? 'Ищем...' : 'Припарковаться', - style: const TextStyle(color: Colors.white), - ), - ), - ), - Positioned( +Positioned( right: 12, top: 0, bottom: 0, @@ -856,12 +840,29 @@ class _MapScreenState extends ConsumerState { ), ), ), - // ─── Селектор времени: левый край, выше кнопок ───────── + // ─── Нижняя строка: селектор времени + Припарковаться ──── if (destination == null && !isNavigating) Positioned( - bottom: bottomInset + 76, + bottom: bottomInset + 12, left: 12, - child: const TimeSelectorWidget(), + right: 12, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const TimeSelectorWidget(), + const Spacer(), + FloatingActionButton.extended( + heroTag: 'find_parking', + onPressed: isRoutingLoading ? null : _findParking, + backgroundColor: AppColors.primary, + icon: const Icon(Icons.local_parking, color: Colors.white), + label: Text( + isRoutingLoading ? 'Ищем...' : 'Припарковаться', + style: const TextStyle(color: Colors.white), + ), + ), + ], + ), ), // ─── Карточка назначения ──────────────────────────────────── if (destination != null && !isNavigating) diff --git a/lib/presentation/screens/map/widgets/parking_card_sheet.dart b/lib/presentation/screens/map/widgets/parking_card_sheet.dart index e03823e..26e6c41 100644 --- a/lib/presentation/screens/map/widgets/parking_card_sheet.dart +++ b/lib/presentation/screens/map/widgets/parking_card_sheet.dart @@ -177,9 +177,19 @@ class _ParkingCardSheet extends ConsumerWidget { } String _confidenceLabel(Zone z) { - const labels = {'low': 'низкая', 'medium': 'средняя', 'high': 'высокая'}; + const labels = { + 'very_low': 'Очень низкая', + 'low': 'Низкая', + 'medium': 'Средняя', + 'high': 'Высокая', + 'very_high': 'Очень высокая', + }; if (z.confidenceLevel != null) { - return labels[z.confidenceLevel!.toLowerCase()] ?? z.confidenceLevel!; + final key = z.confidenceLevel!.toLowerCase(); + final mapped = labels[key]; + if (mapped != null) return mapped; + final raw = z.confidenceLevel!; + return raw[0].toUpperCase() + raw.substring(1).toLowerCase(); } return '${(z.confidence * 100).round()}%'; } From cd95108799ace7196abbdbe07261f2df4be55d63 Mon Sep 17 00:00:00 2001 From: Ilya Kiselev Date: Sat, 30 May 2026 20:55:06 +0300 Subject: [PATCH 039/145] =?UTF-8?q?fix:=20move=20=D0=A1=D0=B5=D0=B9=D1=87?= =?UTF-8?q?=D0=B0=D1=81=20chip=20above=20time=20chip=20to=20prevent=20row?= =?UTF-8?q?=20overflow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/presentation/screens/map/map_screen.dart | 2 +- .../screens/map/widgets/time_selector_widget.dart | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/presentation/screens/map/map_screen.dart b/lib/presentation/screens/map/map_screen.dart index 774cb64..d9c48c9 100644 --- a/lib/presentation/screens/map/map_screen.dart +++ b/lib/presentation/screens/map/map_screen.dart @@ -847,7 +847,7 @@ Positioned( left: 12, right: 12, child: Row( - crossAxisAlignment: CrossAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, children: [ const TimeSelectorWidget(), const Spacer(), diff --git a/lib/presentation/screens/map/widgets/time_selector_widget.dart b/lib/presentation/screens/map/widgets/time_selector_widget.dart index a6d0656..a9384bb 100644 --- a/lib/presentation/screens/map/widgets/time_selector_widget.dart +++ b/lib/presentation/screens/map/widgets/time_selector_widget.dart @@ -29,8 +29,9 @@ class TimeSelectorWidget extends ConsumerWidget { ); } - return Row( + return Column( mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, children: [ if (!isNow) ...[ _Chip( @@ -39,7 +40,7 @@ class TimeSelectorWidget extends ConsumerWidget { active: false, onTap: () => ref.read(timeSelectorProvider.notifier).setNow(), ), - const SizedBox(width: 6), + const SizedBox(height: 6), ], _Chip( label: isNow ? 'Время' : _selectionLabel(selectedDt!), From 212f65bd6aeeb8b7fcc18998384937caf2f4e317 Mon Sep 17 00:00:00 2001 From: Ilya Kiselev Date: Sat, 30 May 2026 20:57:17 +0300 Subject: [PATCH 040/145] fix: larger time selector chips, more padding from screen edge --- lib/presentation/screens/map/map_screen.dart | 6 +++--- .../map/widgets/time_selector_widget.dart | 17 +++++++++-------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/lib/presentation/screens/map/map_screen.dart b/lib/presentation/screens/map/map_screen.dart index d9c48c9..b560d46 100644 --- a/lib/presentation/screens/map/map_screen.dart +++ b/lib/presentation/screens/map/map_screen.dart @@ -843,9 +843,9 @@ Positioned( // ─── Нижняя строка: селектор времени + Припарковаться ──── if (destination == null && !isNavigating) Positioned( - bottom: bottomInset + 12, - left: 12, - right: 12, + bottom: bottomInset + 20, + left: 16, + right: 16, child: Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ diff --git a/lib/presentation/screens/map/widgets/time_selector_widget.dart b/lib/presentation/screens/map/widgets/time_selector_widget.dart index a9384bb..44ba6c2 100644 --- a/lib/presentation/screens/map/widgets/time_selector_widget.dart +++ b/lib/presentation/screens/map/widgets/time_selector_widget.dart @@ -260,30 +260,31 @@ class _Chip extends StatelessWidget { return GestureDetector( onTap: onTap, child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( color: bg, - borderRadius: BorderRadius.circular(20), + borderRadius: BorderRadius.circular(24), border: Border.all( color: active ? AppColors.primary : Colors.grey.shade400, + width: 1.5, ), boxShadow: const [ BoxShadow( - color: Color(0x33000000), - blurRadius: 6, - offset: Offset(0, 2), + color: Color(0x44000000), + blurRadius: 8, + offset: Offset(0, 3), ), ], ), child: Row( mainAxisSize: MainAxisSize.min, children: [ - Icon(icon, size: 14, color: iconColor), - const SizedBox(width: 5), + Icon(icon, size: 17, color: iconColor), + const SizedBox(width: 7), Text( label, style: TextStyle( - fontSize: 13, + fontSize: 15, fontWeight: FontWeight.w600, color: fg, ), From d353ae31077590db318700ef4713654ac023ceaa Mon Sep 17 00:00:00 2001 From: Ilya Kiselev Date: Sat, 30 May 2026 21:02:03 +0300 Subject: [PATCH 041/145] fix: time chip outlined style to reduce visual competition with primary FAB --- .../map/widgets/time_selector_widget.dart | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/lib/presentation/screens/map/widgets/time_selector_widget.dart b/lib/presentation/screens/map/widgets/time_selector_widget.dart index 44ba6c2..d895e8a 100644 --- a/lib/presentation/screens/map/widgets/time_selector_widget.dart +++ b/lib/presentation/screens/map/widgets/time_selector_widget.dart @@ -253,24 +253,21 @@ class _Chip extends StatelessWidget { @override Widget build(BuildContext context) { - final bg = active ? AppColors.primary : Colors.white; - final fg = active ? Colors.white : AppColors.onSurface; - final iconColor = active ? Colors.white : AppColors.textSecondary; + final borderColor = active ? AppColors.primary : Colors.grey.shade400; + final textColor = active ? AppColors.primary : AppColors.onSurface; + final iconColor = active ? AppColors.primary : AppColors.textSecondary; return GestureDetector( onTap: onTap, child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( - color: bg, + color: Colors.white, borderRadius: BorderRadius.circular(24), - border: Border.all( - color: active ? AppColors.primary : Colors.grey.shade400, - width: 1.5, - ), + border: Border.all(color: borderColor, width: active ? 1.5 : 1.0), boxShadow: const [ BoxShadow( - color: Color(0x44000000), + color: Color(0x33000000), blurRadius: 8, offset: Offset(0, 3), ), @@ -286,7 +283,7 @@ class _Chip extends StatelessWidget { style: TextStyle( fontSize: 15, fontWeight: FontWeight.w600, - color: fg, + color: textColor, ), ), ], From 44660c10d1e61b0725561a58c40a6f9b0076163b Mon Sep 17 00:00:00 2001 From: Ilya Kiselev Date: Sat, 30 May 2026 21:07:56 +0300 Subject: [PATCH 042/145] fix: smaller chips to fix overflow, more gap between chip and FAB --- lib/presentation/screens/map/map_screen.dart | 2 +- .../screens/map/widgets/time_selector_widget.dart | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/presentation/screens/map/map_screen.dart b/lib/presentation/screens/map/map_screen.dart index b560d46..132bd79 100644 --- a/lib/presentation/screens/map/map_screen.dart +++ b/lib/presentation/screens/map/map_screen.dart @@ -850,7 +850,7 @@ Positioned( crossAxisAlignment: CrossAxisAlignment.end, children: [ const TimeSelectorWidget(), - const Spacer(), + const SizedBox(width: 16), FloatingActionButton.extended( heroTag: 'find_parking', onPressed: isRoutingLoading ? null : _findParking, diff --git a/lib/presentation/screens/map/widgets/time_selector_widget.dart b/lib/presentation/screens/map/widgets/time_selector_widget.dart index d895e8a..a4355c0 100644 --- a/lib/presentation/screens/map/widgets/time_selector_widget.dart +++ b/lib/presentation/screens/map/widgets/time_selector_widget.dart @@ -260,10 +260,10 @@ class _Chip extends StatelessWidget { return GestureDetector( onTap: onTap, child: Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + padding: const EdgeInsets.symmetric(horizontal: 13, vertical: 10), decoration: BoxDecoration( color: Colors.white, - borderRadius: BorderRadius.circular(24), + borderRadius: BorderRadius.circular(20), border: Border.all(color: borderColor, width: active ? 1.5 : 1.0), boxShadow: const [ BoxShadow( @@ -276,12 +276,12 @@ class _Chip extends StatelessWidget { child: Row( mainAxisSize: MainAxisSize.min, children: [ - Icon(icon, size: 17, color: iconColor), - const SizedBox(width: 7), + Icon(icon, size: 15, color: iconColor), + const SizedBox(width: 6), Text( label, style: TextStyle( - fontSize: 15, + fontSize: 14, fontWeight: FontWeight.w600, color: textColor, ), From 0328399a430870a5506f1ec0446449c086dba88a Mon Sep 17 00:00:00 2001 From: Ilya Kiselev Date: Sat, 30 May 2026 21:11:45 +0300 Subject: [PATCH 043/145] fix: separate Positioned for time chip and FAB, no overflow, FAB always at right edge --- lib/presentation/screens/map/map_screen.dart | 33 ++++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/lib/presentation/screens/map/map_screen.dart b/lib/presentation/screens/map/map_screen.dart index 132bd79..56dcc63 100644 --- a/lib/presentation/screens/map/map_screen.dart +++ b/lib/presentation/screens/map/map_screen.dart @@ -840,28 +840,27 @@ Positioned( ), ), ), - // ─── Нижняя строка: селектор времени + Припарковаться ──── + // ─── Селектор времени: всегда левый край ────────────── if (destination == null && !isNavigating) Positioned( bottom: bottomInset + 20, left: 16, + child: const TimeSelectorWidget(), + ), + // ─── FAB: всегда правый край ─────────────────────────── + if (destination == null && !isNavigating) + Positioned( + bottom: bottomInset + 20, right: 16, - child: Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - const TimeSelectorWidget(), - const SizedBox(width: 16), - FloatingActionButton.extended( - heroTag: 'find_parking', - onPressed: isRoutingLoading ? null : _findParking, - backgroundColor: AppColors.primary, - icon: const Icon(Icons.local_parking, color: Colors.white), - label: Text( - isRoutingLoading ? 'Ищем...' : 'Припарковаться', - style: const TextStyle(color: Colors.white), - ), - ), - ], + child: FloatingActionButton.extended( + heroTag: 'find_parking', + onPressed: isRoutingLoading ? null : _findParking, + backgroundColor: AppColors.primary, + icon: const Icon(Icons.local_parking, color: Colors.white), + label: Text( + isRoutingLoading ? 'Ищем...' : 'Припарковаться', + style: const TextStyle(color: Colors.white), + ), ), ), // ─── Карточка назначения ──────────────────────────────────── From ac9f71e9754231c84754983d76fe2d1c9f00969b Mon Sep 17 00:00:00 2001 From: Ilya Kiselev Date: Sat, 30 May 2026 21:13:59 +0300 Subject: [PATCH 044/145] fix: show confidence always as percentage --- .../map/widgets/parking_card_sheet.dart | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/lib/presentation/screens/map/widgets/parking_card_sheet.dart b/lib/presentation/screens/map/widgets/parking_card_sheet.dart index 26e6c41..8c28531 100644 --- a/lib/presentation/screens/map/widgets/parking_card_sheet.dart +++ b/lib/presentation/screens/map/widgets/parking_card_sheet.dart @@ -176,23 +176,7 @@ class _ParkingCardSheet extends ConsumerWidget { ); } - String _confidenceLabel(Zone z) { - const labels = { - 'very_low': 'Очень низкая', - 'low': 'Низкая', - 'medium': 'Средняя', - 'high': 'Высокая', - 'very_high': 'Очень высокая', - }; - if (z.confidenceLevel != null) { - final key = z.confidenceLevel!.toLowerCase(); - final mapped = labels[key]; - if (mapped != null) return mapped; - final raw = z.confidenceLevel!; - return raw[0].toUpperCase() + raw.substring(1).toLowerCase(); - } - return '${(z.confidence * 100).round()}%'; - } + String _confidenceLabel(Zone z) => '${(z.confidence * 100).round()}%'; String _locationTypeLabel(LocationType type) => switch (type) { LocationType.street => 'Уличная', From 4ab9e2e42aea694ba72619ca85caa6f4229e5e9a Mon Sep 17 00:00:00 2001 From: Ilya Kiselev Date: Sat, 30 May 2026 21:25:53 +0300 Subject: [PATCH 045/145] fix: past mode now merges base zones with occupancy data like future mode does --- lib/data/repositories/zones_repository.dart | 57 ++++++++++++++------- 1 file changed, 38 insertions(+), 19 deletions(-) diff --git a/lib/data/repositories/zones_repository.dart b/lib/data/repositories/zones_repository.dart index f271746..7c94a08 100644 --- a/lib/data/repositories/zones_repository.dart +++ b/lib/data/repositories/zones_repository.dart @@ -18,11 +18,44 @@ class ZonesRepository { } Future> getZonesPast(String bbox, DateTime at) async { - final items = await _occupancyApi.getOccupancyMap( - bbox: bbox, - at: at.toUtc().toIso8601String(), - ); - return items.map(_mapFromOccupancy).toList(); + final atStr = at.toUtc().toIso8601String(); + final zonesFuture = _zonesApi.getZones(bbox: bbox, view: 'map'); + List> items; + try { + items = await _occupancyApi.getOccupancyMap(bbox: bbox, at: atStr); + } catch (_) { + items = []; + } + + final occupancyMap = >{}; + for (final item in items) { + final rawId = item['zone_id']; + final id = rawId is int ? rawId : int.tryParse(rawId?.toString() ?? ''); + if (id != null) occupancyMap[id] = item; + } + + final zoneDtos = await zonesFuture; + return zoneDtos.map((dto) { + final occ = occupancyMap[dto.zoneId]; + if (occ == null) return _mapZone(dto).copyWith(hasForecast: false); + return Zone( + zoneId: dto.zoneId, + zoneType: _parseZoneType(dto.zoneType), + capacity: occ['capacity'] as int? ?? dto.capacity, + freeCount: occ['free_count'] as int? ?? dto.freeCount, + confidence: (occ['confidence'] as num?)?.toDouble() ?? dto.confidence, + confidenceLevel: occ['confidence_level'] as String? ?? dto.confidenceLevel, + pay: dto.pay, + geometry: _parseGeometry(dto.geometry), + isActive: occ['is_active'] as bool? ?? dto.isActive, + locationType: _parseLocationType(occ['location_type'] as String? ?? dto.locationType), + isPrivate: dto.isPrivate, + isAccessible: occ['is_accessible'] as bool? ?? dto.isAccessible, + occupancyUpdatedAt: occ['observed_at'] != null + ? DateTime.tryParse(occ['observed_at'] as String) + : null, + ); + }).toList(); } Future> getZonesFuture(String bbox, DateTime at) async { @@ -120,20 +153,6 @@ class ZonesRepository { : null, ); - Zone _mapFromOccupancy(Map json) => Zone( - zoneId: json['zone_id'] as int, - zoneType: _parseZoneType(json['zone_type'] as String? ?? 'standard'), - capacity: json['capacity'] as int? ?? 0, - freeCount: json['free_count'] as int? ?? 0, - confidence: (json['confidence'] as num?)?.toDouble() ?? 0.0, - pay: json['pay'] as int? ?? 0, - geometry: _parseGeometry(json['geometry'] as Map? ?? {}), - isActive: true, - occupancyUpdatedAt: json['observed_at'] != null - ? DateTime.tryParse(json['observed_at'] as String) - : null, - ); - ZoneType _parseZoneType(String type) => type == 'parallel' ? ZoneType.parallel : ZoneType.standard; From ae0a6537b7185888108abd1ab501884b1249c63c Mon Sep 17 00:00:00 2001 From: Ilya Kiselev Date: Sat, 30 May 2026 21:28:31 +0300 Subject: [PATCH 046/145] fix: hide confidence row in past mode --- .../screens/map/widgets/parking_card_sheet.dart | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/presentation/screens/map/widgets/parking_card_sheet.dart b/lib/presentation/screens/map/widgets/parking_card_sheet.dart index 8c28531..edfafdd 100644 --- a/lib/presentation/screens/map/widgets/parking_card_sheet.dart +++ b/lib/presentation/screens/map/widgets/parking_card_sheet.dart @@ -27,6 +27,7 @@ class _ParkingCardSheet extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final timeMode = ref.watch(timeSelectorProvider); final isFuture = timeMode.maybeWhen(future: (_) => true, orElse: () => false); + final isPast = timeMode.maybeWhen(past: (_) => true, orElse: () => false); final userSelectedAt = timeMode.maybeWhen(future: (at) => at, orElse: () => null); final zonesAsync = ref.watch(rawZonesProvider); @@ -122,11 +123,12 @@ class _ParkingCardSheet extends ConsumerWidget { valueColor: currentZone.pay == 0 ? AppColors.primary : null, ), - _InfoRow( - icon: Icons.verified_outlined, - label: 'Уверенность', - value: _confidenceLabel(currentZone), - ), + if (!isPast) + _InfoRow( + icon: Icons.verified_outlined, + label: 'Уверенность', + value: _confidenceLabel(currentZone), + ), if (currentZone.locationType != null) _InfoRow( icon: Icons.place_outlined, From fa114bf5c1c1750b24a3a8d32c970cd9eaeb5151 Mon Sep 17 00:00:00 2001 From: Ilya Kiselev Date: Mon, 1 Jun 2026 23:38:55 +0300 Subject: [PATCH 047/145] feat: password reset screen and fix parking card bottom padding --- lib/core/router/app_router.dart | 5 +- lib/data/api/auth_api.dart | 3 + lib/data/repositories/auth_repository.dart | 10 ++ lib/presentation/providers/auth_provider.dart | 3 + .../screens/auth/login_screen.dart | 4 + .../screens/auth/password_reset_screen.dart | 105 ++++++++++++++++++ .../map/widgets/parking_card_sheet.dart | 5 +- 7 files changed, 133 insertions(+), 2 deletions(-) create mode 100644 lib/presentation/screens/auth/password_reset_screen.dart diff --git a/lib/core/router/app_router.dart b/lib/core/router/app_router.dart index 6a4c35e..df9aef1 100644 --- a/lib/core/router/app_router.dart +++ b/lib/core/router/app_router.dart @@ -7,6 +7,7 @@ import '../../presentation/screens/auth/register_screen.dart'; import '../../presentation/screens/map/map_screen.dart'; import '../../presentation/screens/profile/profile_screen.dart'; import '../../presentation/screens/search/search_screen.dart'; +import '../../presentation/screens/auth/password_reset_screen.dart'; final routerProvider = Provider((ref) { final authState = ref.watch(authStateProvider); @@ -24,11 +25,12 @@ final routerProvider = Provider((ref) { final isLoginRoute = state.matchedLocation == '/login'; final isRegisterRoute = state.matchedLocation == '/register'; + final isPasswordResetRoute = state.matchedLocation == '/password-reset'; final isSplashRoute = state.matchedLocation == '/'; if (isUnauth) { if (isSplashRoute) return '/login'; - if (!isLoginRoute && !isRegisterRoute) return '/login'; + if (!isLoginRoute && !isRegisterRoute && !isPasswordResetRoute) return '/login'; } if (isAuth && (isSplashRoute || isLoginRoute || isRegisterRoute)) return '/map'; @@ -42,6 +44,7 @@ final routerProvider = Provider((ref) { GoRoute(path: '/map', builder: (_, __) => const MapScreen()), GoRoute(path: '/profile', builder: (_, __) => const ProfileScreen()), GoRoute(path: '/search', builder: (_, __) => const SearchScreen()), + GoRoute(path: '/password-reset', builder: (_, __) => const PasswordResetScreen()), ], ); }); diff --git a/lib/data/api/auth_api.dart b/lib/data/api/auth_api.dart index cbb78b1..73eff74 100644 --- a/lib/data/api/auth_api.dart +++ b/lib/data/api/auth_api.dart @@ -22,4 +22,7 @@ class AuthApi { final response = await _dio.get('/auth/me'); return UserDto.fromJson(response.data as Map); } + + Future requestPasswordReset(String email) => + _dio.post('/auth/password-reset/request', data: {'email': email}); } diff --git a/lib/data/repositories/auth_repository.dart b/lib/data/repositories/auth_repository.dart index 45f0dc7..fce1426 100644 --- a/lib/data/repositories/auth_repository.dart +++ b/lib/data/repositories/auth_repository.dart @@ -59,6 +59,16 @@ class AuthRepository { } } + Future requestPasswordReset(String email) async { + try { + await _api.requestPasswordReset(email); + } on DioException catch (e) { + throw ApiException.fromStatusCode(e.response?.statusCode ?? 0); + } catch (e) { + throw ApiException('Ошибка: $e'); + } + } + User _mapUser(UserDto dto) => User( userId: dto.userId, email: dto.email, diff --git a/lib/presentation/providers/auth_provider.dart b/lib/presentation/providers/auth_provider.dart index f4464c8..0cdaeca 100644 --- a/lib/presentation/providers/auth_provider.dart +++ b/lib/presentation/providers/auth_provider.dart @@ -76,6 +76,9 @@ class AuthNotifier extends StateNotifier { await _ref.read(authRepositoryProvider).logout(); state = const AuthState.unauthenticated(); } + + Future requestPasswordReset(String email) => + _ref.read(authRepositoryProvider).requestPasswordReset(email); } final authStateProvider = StateNotifierProvider( diff --git a/lib/presentation/screens/auth/login_screen.dart b/lib/presentation/screens/auth/login_screen.dart index 9469c12..1be1a40 100644 --- a/lib/presentation/screens/auth/login_screen.dart +++ b/lib/presentation/screens/auth/login_screen.dart @@ -116,6 +116,10 @@ class _LoginScreenState extends ConsumerState { onPressed: () => context.push('/register'), child: const Text('Нет аккаунта? Зарегистрироваться'), ), + TextButton( + onPressed: () => context.push('/password-reset'), + child: const Text('Забыли пароль?'), + ), ], ), ), diff --git a/lib/presentation/screens/auth/password_reset_screen.dart b/lib/presentation/screens/auth/password_reset_screen.dart new file mode 100644 index 0000000..b30af2b --- /dev/null +++ b/lib/presentation/screens/auth/password_reset_screen.dart @@ -0,0 +1,105 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import '../../providers/auth_provider.dart'; +import '../../../core/network/api_exception.dart'; + +class PasswordResetScreen extends ConsumerStatefulWidget { + const PasswordResetScreen({super.key}); + + @override + ConsumerState createState() => _PasswordResetScreenState(); +} + +class _PasswordResetScreenState extends ConsumerState { + final _formKey = GlobalKey(); + final _emailCtrl = TextEditingController(); + bool _loading = false; + + @override + void dispose() { + _emailCtrl.dispose(); + super.dispose(); + } + + Future _submit() async { + if (!_formKey.currentState!.validate()) return; + final email = _emailCtrl.text.trim(); + setState(() => _loading = true); + try { + await ref.read(authStateProvider.notifier).requestPasswordReset(email); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Ссылка для сброса отправлена на $email')), + ); + context.pop(); + } + } on ApiException catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(e.message), backgroundColor: Colors.red), + ); + } + } finally { + if (mounted) setState(() => _loading = false); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Сброс пароля')), + body: SafeArea( + child: Center( + child: SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Icon(Icons.lock_reset_outlined, size: 64, color: Color(0xFF2E7D32)), + const SizedBox(height: 16), + Text( + 'Введите email, указанный при регистрации. Мы отправим ссылку для сброса пароля.', + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(height: 32), + TextFormField( + controller: _emailCtrl, + decoration: const InputDecoration( + labelText: 'Email', + prefixIcon: Icon(Icons.email_outlined), + border: OutlineInputBorder(), + ), + keyboardType: TextInputType.emailAddress, + validator: (v) => + v == null || v.isEmpty ? 'Введите email' : null, + ), + const SizedBox(height: 24), + FilledButton( + onPressed: _loading ? null : _submit, + style: FilledButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + ), + child: _loading + ? const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : const Text('Отправить'), + ), + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/presentation/screens/map/widgets/parking_card_sheet.dart b/lib/presentation/screens/map/widgets/parking_card_sheet.dart index edfafdd..cba9e5c 100644 --- a/lib/presentation/screens/map/widgets/parking_card_sheet.dart +++ b/lib/presentation/screens/map/widgets/parking_card_sheet.dart @@ -38,7 +38,10 @@ class _ParkingCardSheet extends ConsumerWidget { zone; return Padding( - padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom + + MediaQuery.of(context).padding.bottom, + ), child: SingleChildScrollView( child: Container( decoration: const BoxDecoration( From 4c1d20479b2502246c247ac8ff762b927c665072 Mon Sep 17 00:00:00 2001 From: Ilya Kiselev Date: Fri, 5 Jun 2026 13:04:10 +0300 Subject: [PATCH 048/145] fix: name field required in registration --- lib/presentation/screens/auth/register_screen.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/presentation/screens/auth/register_screen.dart b/lib/presentation/screens/auth/register_screen.dart index d1e2892..9407248 100644 --- a/lib/presentation/screens/auth/register_screen.dart +++ b/lib/presentation/screens/auth/register_screen.dart @@ -34,7 +34,7 @@ class _RegisterScreenState extends ConsumerState { await ref.read(authStateProvider.notifier).register( _emailCtrl.text.trim(), _passwordCtrl.text, - fullName: _nameCtrl.text.trim().isEmpty ? null : _nameCtrl.text.trim(), + fullName: _nameCtrl.text.trim(), ); } on ApiException catch (e) { if (mounted) { @@ -62,10 +62,11 @@ class _RegisterScreenState extends ConsumerState { TextFormField( controller: _nameCtrl, decoration: const InputDecoration( - labelText: 'Имя (необязательно)', + labelText: 'Имя', prefixIcon: Icon(Icons.person_outlined), border: OutlineInputBorder(), ), + validator: (v) => v == null || v.trim().isEmpty ? 'Введите имя' : null, ), const SizedBox(height: 16), TextFormField( From fa5c8cea285f59618213f3ffad678ca66d264d02 Mon Sep 17 00:00:00 2001 From: Nikita Aksenov Date: Thu, 16 Jul 2026 00:19:33 +0300 Subject: [PATCH 049/145] docs: add Google Play app badge --- README.md | 74 ++++++++++++++++++++++++++++++++++++------ README_RU.md | 71 ++++++++++++++++++++++++++++++++++++++++ docs/google-play.webp | Bin 0 -> 5150 bytes 3 files changed, 135 insertions(+), 10 deletions(-) create mode 100644 README_RU.md create mode 100644 docs/google-play.webp diff --git a/README.md b/README.md index 1e9ddd0..8169cff 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,71 @@ -# mobile +# ParkTrack Mobile App -A new Flutter project. +[Русская версия](README_RU.md) | **English Version** + +ParkTrack is a modern mobile application designed to simplify the process of finding and managing parking spaces. Built with Flutter, it offers a seamless and intuitive experience for drivers navigating urban environments. + + +Get it on GooglePlay + + +## Key Features + +- **Interactive Map**: Visualize parking spots around you using Yandex Mapkit integration. +- **Real-time Navigation**: Find the best route to your selected parking spot. +- **Smart Search**: Search for parking by address or name. +- **User Authentication**: Secure sign-in and profile management. +- **Session Management**: Automated handling of expired sessions for enhanced security. +- **Deep Linking**: Quick access to specific app sections via universal links. + +## Tech Stack + +- **Framework**: [Flutter](https://flutter.dev) +- **State Management**: [Riverpod](https://riverpod.dev) (with code generation) +- **Navigation**: [GoRouter](https://pub.dev/packages/go_router) +- **Networking**: [Dio](https://pub.dev/packages/dio) with custom interceptors for Auth and Logging. +- **Maps**: [Yandex Mapkit SDK](https://pub.dev/packages/yandex_mapkit) +- **Data Modeling**: [Freezed](https://pub.dev/packages/freezed) & [JSON Serializable](https://pub.dev/packages/json_serializable) +- **Storage**: [Flutter Secure Storage](https://pub.dev/packages/flutter_secure_storage) + +## Project Structure + +The project follows a clean architecture approach: +- `lib/core`: Shared utilities, networking, themes, and routing. +- `lib/data`: Data sources, repositories implementations, and DTOs. +- `lib/domain`: Business logic, entities, and repository interfaces. +- `lib/presentation`: UI layer organized by screens (Map, Auth, Search, Profile) and Riverpod providers. ## Getting Started -This project is a starting point for a Flutter application. +### Prerequisites + +- Flutter SDK (latest stable version) +- Android Studio / VS Code +- Yandex Mapkit API Key + +### Installation + +1. **Clone the repository:** + ```bash + git clone https://github.com/ParkTrack-Project/mobile-app.git + cd mobile-app + ``` + +2. **Install dependencies:** + ```bash + flutter pub get + ``` + +3. **Generate code:** + ```bash + flutter pub run build_runner build --delete-conflicting-outputs + ``` -A few resources to get you started if this is your first Flutter project: +4. **Run the application:** + ```bash + flutter run + ``` -- [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter) -- [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) -- [Flutter learning resources](https://docs.flutter.dev/reference/learning-resources) +## License -For help getting started with Flutter development, view the -[online documentation](https://docs.flutter.dev/), which offers tutorials, -samples, guidance on mobile development, and a full API reference. +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. diff --git a/README_RU.md b/README_RU.md new file mode 100644 index 0000000..d32d95d --- /dev/null +++ b/README_RU.md @@ -0,0 +1,71 @@ +# Мобильное приложение ParkTrack + +**Русская версия** | [English Version](README.md) + +ParkTrack — это современное мобильное приложение, разработанное для упрощения процесса поиска и управления парковочными местами. Созданное с использованием Flutter, оно предлагает удобный и интуитивно понятный интерфейс для водителей в городских условиях. + + +Get it on GooglePlay + + +## Основные возможности + +- **Интерактивная карта**: Визуализация парковочных мест вокруг вас с помощью интеграции Yandex Mapkit. +- **Навигация в реальном времени**: Поиск оптимального маршрута к выбранному парковочному месту. +- **Умный поиск**: Поиск парковки по адресу или названию. +- **Аутентификация пользователей**: Безопасный вход и управление профилем. +- **Управление сессиями**: Автоматическая обработка истекших сессий для повышения безопасности. +- **Deep Linking**: Быстрый доступ к разделам приложения через универсальные ссылки. + +## Технологический стек + +- **Framework**: [Flutter](https://flutter.dev) +- **State Management**: [Riverpod](https://riverpod.dev) (с генерацией кода) +- **Navigation**: [GoRouter](https://pub.dev/packages/go_router) +- **Networking**: [Dio](https://pub.dev/packages/dio) с кастомными интерцепторами для Auth и логирования. +- **Maps**: [Yandex Mapkit SDK](https://pub.dev/packages/yandex_mapkit) +- **Data Modeling**: [Freezed](https://pub.dev/packages/freezed) & [JSON Serializable](https://pub.dev/packages/json_serializable) +- **Storage**: [Flutter Secure Storage](https://pub.dev/packages/flutter_secure_storage) + +## Структура проекта + +Проект следует принципам чистой архитектуры: +- `lib/core`: Общие утилиты, работа с сетью, темы и роутинг. +- `lib/data`: Источники данных, реализации репозиториев и DTO. +- `lib/domain`: Бизнес-логика, сущности и интерфейсы репозиториев. +- `lib/presentation`: Слой UI, организованный по экранам (Map, Auth, Search, Profile) и провайдеры Riverpod. + +## Начало работы + +### Требования + +- Flutter SDK (последняя стабильная версия) +- Android Studio / VS Code +- API ключ Yandex Mapkit + +### Установка + +1. **Клонируйте репозиторий:** + ```bash + git clone https://github.com/ParkTrack-Project/mobile-app.git + cd mobile-app + ``` + +2. **Установите зависимости:** + ```bash + flutter pub get + ``` + +3. **Сгенерируйте код:** + ```bash + flutter pub run build_runner build --delete-conflicting-outputs + ``` + +4. **Запустите приложение:** + ```bash + flutter run + ``` + +## Лицензия + +Этот проект распространяется под лицензией MIT — подробности см. в файле [LICENSE](LICENSE). diff --git a/docs/google-play.webp b/docs/google-play.webp new file mode 100644 index 0000000000000000000000000000000000000000..57b16c868519db131537e98cbec04e3bb8efa1d8 GIT binary patch literal 5150 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 Date: Thu, 16 Jul 2026 14:54:44 +0300 Subject: [PATCH 050/145] ci: publishing releases to Google Play --- .github/workflows/publish-google-play.yml | 200 +++++++++++++++++++++ distribution/releases/1.0.0/whatsnew-en-US | 8 + distribution/releases/1.0.0/whatsnew-ru-RU | 8 + distribution/releases/1.1.0/whatsnew-en-US | 7 + distribution/releases/1.1.0/whatsnew-ru-RU | 7 + 5 files changed, 230 insertions(+) create mode 100644 .github/workflows/publish-google-play.yml create mode 100644 distribution/releases/1.0.0/whatsnew-en-US create mode 100644 distribution/releases/1.0.0/whatsnew-ru-RU create mode 100644 distribution/releases/1.1.0/whatsnew-en-US create mode 100644 distribution/releases/1.1.0/whatsnew-ru-RU diff --git a/.github/workflows/publish-google-play.yml b/.github/workflows/publish-google-play.yml new file mode 100644 index 0000000..af8becc --- /dev/null +++ b/.github/workflows/publish-google-play.yml @@ -0,0 +1,200 @@ +name: Publish Android to Google Play + +on: + release: + types: + - published + +permissions: + contents: read + +concurrency: + group: google-play-publish + cancel-in-progress: false + +env: + PACKAGE_NAME: com.parktrack.mobile + +jobs: + publish: + name: Build and publish AAB + runs-on: ubuntu-latest + + environment: + name: google-play-release + + steps: + - name: Checkout repository + uses: actions/checkout@v7.0.0 + + - name: Set up Java + uses: actions/setup-java@v5.5.0 + with: + distribution: temurin + java-version: "17" + cache: gradle + + - name: Set up Flutter + uses: subosito/flutter-action@v2.23.0 + with: + channel: stable + cache: true + + - name: Show versions + run: | + flutter --version + java --version + + - name: Install dependencies + run: flutter pub get + + - name: Validate release version + env: + RELEASE_TAG: ${{ github.event.release.tag_name }} + run: | + set -euo pipefail + + TAG_VERSION="${RELEASE_TAG#v}" + APP_VERSION="$(awk '/^version:/ {print $2; exit}' pubspec.yaml)" + VERSION_NAME="${APP_VERSION%%+*}" + VERSION_CODE="${APP_VERSION#*+}" + + if [ -z "$APP_VERSION" ]; then + echo "Version is missing in pubspec.yaml" + exit 1 + fi + + if [ "$TAG_VERSION" != "$VERSION_NAME" ]; then + echo "GitHub tag version: $TAG_VERSION" + echo "pubspec.yaml version: $VERSION_NAME" + echo "Release tag must match pubspec.yaml version" + exit 1 + fi + + if ! [[ "$VERSION_CODE" =~ ^[0-9]+$ ]]; then + echo "Invalid Android version code: $VERSION_CODE" + exit 1 + fi + + echo "Release version: $VERSION_NAME" + echo "Android version code: $VERSION_CODE" + + - name: Restore Android signing key + env: + KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} + STORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} + KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} + KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} + run: | + set -euo pipefail + + : "${KEYSTORE_BASE64:?ANDROID_KEYSTORE_BASE64 is missing}" + : "${STORE_PASSWORD:?ANDROID_KEYSTORE_PASSWORD is missing}" + : "${KEY_PASSWORD:?ANDROID_KEY_PASSWORD is missing}" + : "${KEY_ALIAS:?ANDROID_KEY_ALIAS is missing}" + + mkdir -p android/app + + printf '%s' "$KEYSTORE_BASE64" \ + | base64 --decode \ + > android/app/upload-keystore.jks + + printf '%s\n' \ + "storePassword=$STORE_PASSWORD" \ + "keyPassword=$KEY_PASSWORD" \ + "keyAlias=$KEY_ALIAS" \ + "storeFile=upload-keystore.jks" \ + > android/key.properties + + - name: Verify signing key + env: + STORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} + KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} + run: | + keytool -list \ + -keystore android/app/upload-keystore.jks \ + -storepass "$STORE_PASSWORD" \ + -alias "$KEY_ALIAS" \ + > /dev/null + + echo "Signing key is valid" + + - name: Analyze project + run: flutter analyze + + - name: Run tests + run: | + if [ -d test ]; then + flutter test + else + echo "No test directory, skipping tests" + fi + + - name: Build signed Android App Bundle + run: flutter build appbundle --release + + - name: Check generated bundle + run: | + ls -lh build/app/outputs/bundle/release/ + test -f build/app/outputs/bundle/release/app-release.aab + + - name: Prepare Google Play release notes + env: + RELEASE_TAG: ${{ github.event.release.tag_name }} + run: | + set -euo pipefail + + VERSION="${RELEASE_TAG#v}" + SOURCE_DIR="distribution/releases/$VERSION" + TARGET_DIR="build/google-play/whatsnew" + + echo "Preparing release notes for version: $VERSION" + + test -f "$SOURCE_DIR/whatsnew-ru-RU" + test -f "$SOURCE_DIR/whatsnew-en-US" + + mkdir -p "$TARGET_DIR" + + cp "$SOURCE_DIR/whatsnew-ru-RU" \ + "$TARGET_DIR/whatsnew-ru-RU" + + cp "$SOURCE_DIR/whatsnew-en-US" \ + "$TARGET_DIR/whatsnew-en-US" + + - name: Validate Google Play release notes + run: | + python3 - <<'PY' + from pathlib import Path + + directory = Path("build/google-play/whatsnew") + required_locales = ("ru-RU", "en-US") + + for locale in required_locales: + path = directory / f"whatsnew-{locale}" + + if not path.exists(): + raise SystemExit(f"Missing release notes: {path}") + + text = path.read_text(encoding="utf-8").strip() + + if not text: + raise SystemExit(f"Release notes are empty: {path}") + + if len(text) > 500: + raise SystemExit( + f"{path} contains {len(text)} characters; maximum is 500" + ) + + print(f"{locale}: {len(text)} characters") + PY + + - name: Upload to Google Play + uses: r0adkll/upload-google-play@v1.1.5 + with: + serviceAccountJsonPlainText: ${{ secrets.PLAY_SERVICE_ACCOUNT_JSON }} + packageName: ${{ env.PACKAGE_NAME }} + releaseFiles: build/app/outputs/bundle/release/app-release.aab + tracks: alpha + status: completed + releaseName: ${{ github.event.release.tag_name }} + whatsNewDirectory: build/google-play/what \ No newline at end of file diff --git a/distribution/releases/1.0.0/whatsnew-en-US b/distribution/releases/1.0.0/whatsnew-en-US new file mode 100644 index 0000000..a1b902f --- /dev/null +++ b/distribution/releases/1.0.0/whatsnew-en-US @@ -0,0 +1,8 @@ +Real-time parking occupancy view +Historical occupancy data and forecasts for the next 24 hours +Flexible filtering by price, parking type, and number of available spaces +Search for the best parking option near your destination +"Where to park?" mode: instant search for the nearest available parking +Route planning to selected parking locations +Deep links to routes in Yandex Maps +In-app navigation \ No newline at end of file diff --git a/distribution/releases/1.0.0/whatsnew-ru-RU b/distribution/releases/1.0.0/whatsnew-ru-RU new file mode 100644 index 0000000..74bcae3 --- /dev/null +++ b/distribution/releases/1.0.0/whatsnew-ru-RU @@ -0,0 +1,8 @@ +Просмотр загруженности парковок в реальном времени +Загруженность парковок в прошлом и прогноз загруженности на следующие 24 часа +Гибкая фильтрация по цене, типу парковки, количеству свободных мест +Поиск ближайшей к пункту назначения оптимальной парковки +Режим "Где припарковаться?": немедленный поиск ближайшей свободной парковки +Построение маршрутов к найденным парковкам +Deeplink'и маршрутов в Яндекс Карты +Встроенная навигация внутри приложения \ No newline at end of file diff --git a/distribution/releases/1.1.0/whatsnew-en-US b/distribution/releases/1.1.0/whatsnew-en-US new file mode 100644 index 0000000..87194d2 --- /dev/null +++ b/distribution/releases/1.1.0/whatsnew-en-US @@ -0,0 +1,7 @@ +New time selector designed with forecast generation in mind +Fixed bugs with loading data on zone congestion +Improved display of download status +Fixed ETA display on route selection and navigation screens +Fixed display of the parking card (optimal height adjusted) +Name made a required field on the registration screen +Confidence classes replaced with numeric values \ No newline at end of file diff --git a/distribution/releases/1.1.0/whatsnew-ru-RU b/distribution/releases/1.1.0/whatsnew-ru-RU new file mode 100644 index 0000000..147bdcc --- /dev/null +++ b/distribution/releases/1.1.0/whatsnew-ru-RU @@ -0,0 +1,7 @@ +Новый селектор времени, разработанный с учетом особенностей генерации прогнозов +Исправлены баги с загрузкой данных о загруженности зон +Улучшено отображение статуса загрузки +Исправлено отображение ETA на экранах выбора маршрута и навигации +Исправлено отображение карточки парковки (настроена оптимальная высота) +Имя сделано обязательным полем на экране регистрации +Классы уверенности заменены на числовые значения \ No newline at end of file From 25c19d7adf28a9092e02e92aecad6b10d45e1aa6 Mon Sep 17 00:00:00 2001 From: Nikita Aksenov Date: Thu, 16 Jul 2026 19:15:45 +0300 Subject: [PATCH 051/145] ci: web app deploy at m.parktrack.live --- .github/workflows/deploy-web.yml | 52 ++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 .github/workflows/deploy-web.yml diff --git a/.github/workflows/deploy-web.yml b/.github/workflows/deploy-web.yml new file mode 100644 index 0000000..57fd450 --- /dev/null +++ b/.github/workflows/deploy-web.yml @@ -0,0 +1,52 @@ +name: Deploy web to GitHub Pages + +on: + push: + branches: + - main + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: true + +jobs: + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: '3.41.7' + channel: stable + cache: true + + - name: Install dependencies + run: flutter pub get + + - name: Build web + run: flutter build web --release --base-href / + + - name: Configure GitHub Pages + uses: actions/configure-pages@v5 + + - name: Upload GitHub Pages artifact + uses: actions/upload-pages-artifact@v4 + with: + path: build/web + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 From 54234942b11cb0c731177b212e7ef01d4ce5f3d9 Mon Sep 17 00:00:00 2001 From: Nikita Aksenov Date: Thu, 16 Jul 2026 20:03:47 +0300 Subject: [PATCH 052/145] feat: add settings, profile editing, dark theme, English, web app and macOS support --- .metadata | 6 + android/app/build.gradle.kts | 22 +- .../com/parktrack/mobile/MainActivity.kt | 28 +- .../com/parktrack/mobile/MainApplication.kt | 10 + lib/core/localization/app_localizations.dart | 632 +++++++++++++ lib/core/network/api_exception.dart | 29 +- lib/core/network/auth_interceptor.dart | 71 +- lib/core/network/dio_client.dart | 4 +- lib/core/network/mock_interceptor.dart | 4 +- lib/core/router/app_router.dart | 17 +- lib/core/storage/settings_storage.dart | 31 + lib/core/storage/token_storage.dart | 17 + lib/core/theme/app_colors.dart | 8 +- lib/core/theme/app_theme.dart | 39 + lib/core/utils/error_snackbar.dart | 27 +- lib/core/utils/nav_math.dart | 17 +- lib/data/api/auth_api.dart | 5 + lib/data/api/routing_api.dart | 6 +- lib/data/api/zones_api.dart | 12 +- lib/data/repositories/auth_repository.dart | 24 +- lib/main.dart | 7 +- lib/presentation/providers/auth_provider.dart | 5 + .../providers/navigation_provider.dart | 12 +- .../providers/settings_provider.dart | 77 ++ .../screens/auth/login_screen.dart | 27 +- .../screens/auth/password_reset_screen.dart | 23 +- .../screens/auth/register_screen.dart | 35 +- lib/presentation/screens/map/map_screen.dart | 830 ++++++++++-------- .../screens/map/widgets/candidates_sheet.dart | 70 +- .../map/widgets/navigation_overlay.dart | 73 +- .../map/widgets/parking_card_sheet.dart | 134 ++- .../map/widgets/parking_zone_layer.dart | 6 +- .../map/widgets/route_preview_sheet.dart | 58 +- .../map/widgets/time_selector_widget.dart | 98 ++- .../screens/profile/edit_profile_screen.dart | 125 +++ .../screens/profile/profile_screen.dart | 232 ++++- .../screens/search/search_screen.dart | 57 +- web/favicon.png | Bin 0 -> 18900 bytes 38 files changed, 2159 insertions(+), 719 deletions(-) create mode 100644 lib/core/localization/app_localizations.dart create mode 100644 lib/core/storage/settings_storage.dart create mode 100644 lib/presentation/providers/settings_provider.dart create mode 100644 lib/presentation/screens/profile/edit_profile_screen.dart create mode 100644 web/favicon.png diff --git a/.metadata b/.metadata index cc2cda6..5feb463 100644 --- a/.metadata +++ b/.metadata @@ -21,6 +21,12 @@ migration: - platform: ios create_revision: cc0734ac716fbb8b90f3f9db8020958b1553afa7 base_revision: cc0734ac716fbb8b90f3f9db8020958b1553afa7 + - platform: macos + create_revision: cc0734ac716fbb8b90f3f9db8020958b1553afa7 + base_revision: cc0734ac716fbb8b90f3f9db8020958b1553afa7 + - platform: web + create_revision: cc0734ac716fbb8b90f3f9db8020958b1553afa7 + base_revision: cc0734ac716fbb8b90f3f9db8020958b1553afa7 # User provided section diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index b89e5eb..26d75b5 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -1,3 +1,5 @@ +import java.util.Properties +import java.io.FileInputStream plugins { id("com.android.application") id("kotlin-android") @@ -5,11 +7,16 @@ plugins { id("dev.flutter.flutter-gradle-plugin") } +val keystoreProperties = Properties() +val keystorePropertiesFile = rootProject.file("key.properties") +if (keystorePropertiesFile.exists()) { + keystoreProperties.load(FileInputStream(keystorePropertiesFile)) +} + android { namespace = "com.parktrack.mobile" compileSdk = flutter.compileSdkVersion - ndkVersion = flutter.ndkVersion - + ndkVersion = "30.0.14904198" compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 @@ -30,11 +37,20 @@ android { versionName = flutter.versionName } + signingConfigs { + create("release") { + keyAlias = keystoreProperties["keyAlias"] as String + keyPassword = keystoreProperties["keyPassword"] as String + storeFile = keystoreProperties["storeFile"]?.let { file(it) } + storePassword = keystoreProperties["storePassword"] as String + } + } + buildTypes { release { // TODO: Add your own signing config for the release build. // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig = signingConfigs.getByName("debug") + signingConfig = signingConfigs.getByName("release") } } } diff --git a/android/app/src/main/kotlin/com/parktrack/mobile/MainActivity.kt b/android/app/src/main/kotlin/com/parktrack/mobile/MainActivity.kt index 733ee59..b6d9b6e 100644 --- a/android/app/src/main/kotlin/com/parktrack/mobile/MainActivity.kt +++ b/android/app/src/main/kotlin/com/parktrack/mobile/MainActivity.kt @@ -1,5 +1,31 @@ package com.parktrack.mobile +import android.content.Context +import com.yandex.mapkit.MapKitFactory import io.flutter.embedding.android.FlutterActivity +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.plugin.common.MethodChannel -class MainActivity : FlutterActivity() +class MainActivity : FlutterActivity() { + override fun configureFlutterEngine(flutterEngine: FlutterEngine) { + super.configureFlutterEngine(flutterEngine) + MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "com.parktrack.mobile/mapkit").setMethodCallHandler { call, result -> + if (call.method == "setLocale") { + val locale = call.argument("locale") + if (locale != null) { + // Save locale for the next startup + val prefs = getSharedPreferences("app_settings", Context.MODE_PRIVATE) + prefs.edit().putString("map_locale", locale).apply() + + // We don't call setLocale here to avoid crashes if already initialized. + // The user will be notified that a restart is required. + result.success(null) + } else { + result.error("INVALID_LOCALE", "Locale is null", null) + } + } else { + result.notImplemented() + } + } + } +} diff --git a/android/app/src/main/kotlin/com/parktrack/mobile/MainApplication.kt b/android/app/src/main/kotlin/com/parktrack/mobile/MainApplication.kt index 13a4540..2a6eefa 100644 --- a/android/app/src/main/kotlin/com/parktrack/mobile/MainApplication.kt +++ b/android/app/src/main/kotlin/com/parktrack/mobile/MainApplication.kt @@ -1,11 +1,21 @@ package com.parktrack.mobile import android.app.Application +import android.content.Context import com.yandex.mapkit.MapKitFactory class MainApplication : Application() { override fun onCreate() { super.onCreate() + + // Read saved locale from SharedPreferences BEFORE initialization + val prefs = getSharedPreferences("app_settings", Context.MODE_PRIVATE) + val savedLocale = prefs.getString("map_locale", null) + + if (savedLocale != null) { + MapKitFactory.setLocale(savedLocale) + } + MapKitFactory.setApiKey("3f833508-456e-4d01-a150-751294a1c879") } } diff --git a/lib/core/localization/app_localizations.dart b/lib/core/localization/app_localizations.dart new file mode 100644 index 0000000..297d825 --- /dev/null +++ b/lib/core/localization/app_localizations.dart @@ -0,0 +1,632 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../presentation/providers/settings_provider.dart'; + +class AppStrings { + final String appTitle; + final String findParking; + final String searching; + final String routeReady; + final String selectParking; + final String parkingZone; + final String free; + final String pay; + final String hourSign; + final String capacity; + final String forecast; + final String confidence; + final String type; + final String parkingType; + final String updatedAt; + final String searchPlaceholder; + final String filters; + final String apply; + final String reset; + final String logout; + final String settings; + final String appearance; + final String theme; + final String language; + final String info; + final String privacyPolicy; + final String github; + final String themeLight; + final String themeDark; + final String themeSystem; + final String langRussian; + final String langEnglish; + final String langSystem; + final String account; + final String notAuthenticated; + final String logoutConfirm; + final String cancel; + final String inAppRoute; + final String yandexNavigator; + final String buildRoute; + final String searchParkingNear; + final String selectedPlace; + final String myLocation; + final String now; + final String time; + final String today; + final String tomorrow; + final String yesterday; + final String street; + final String yard; + final String openLot; + final String underground; + final String multilevel; + final String parallel; + final String regular; + final String login; + final String password; + final String signIn; + final String noAccount; + final String signUp; + final String forgotPassword; + final String emailRequired; + final String passwordTooShort; + final String name; + final String nameRequired; + final String invalidEmail; + final String createAccount; + final String alreadyHaveAccount; + final String registration; + final String resetPassword; + final String sendInstructions; + final String hideFull; + final String hidePrivate; + final String hideInaccessible; + final String hideInactive; + final String minConfidence; + final String limitPrice; + final String minFreeSpots; + final String any; + final String locationType; + final String searchNoResults; + final String searchError; + final String searchHint; + final String errorDetails; + final String errorCreatingRoute; + final String errorLoadingZones; + final String copy; + final String close; + final String moreInfo; + final String copiedToClipboard; + final String locationPermissionDenied; + final String locationPermissionReason; + final String pickZoneOnMap; + final String navigationActive; + final String unauthorized; + final String forbidden; + final String notFound; + final String emailAlreadyExists; + final String invalidData; + final String serverError; + final String requestError; + final String searchTimeout; + final String pointLookupError; + final String pointLookupTimeout; + final String restartRequiredForMap; + final String private; + final String loadingData; + final String noForecast; + final String forWord; + final String generated; + final String jumpToClosest; + final String forecastFor; + final String youPicked; + final String arrival; + final String list; + final String map; + final String pickOnMapInstruction; + final String pickOnMapAction; + final String recalculating; + final String straightAhead; + final String finish; + final String finishConfirmTitle; + final String finishConfirmContent; + final String timeLabel; + final String distanceLabel; + final String speedLabel; + final String selectTheme; + final String selectLanguage; + final String metersSign; + final String kmSign; + final String minutesSign; + final String turnLeft; + final String keepLeft; + final String turnRight; + final String keepRight; + final String uTurn; + final String inWord; + final List monthNames; + final String dataErasure; + final String editProfile; + final String save; + final String profileUpdated; + final String desktopMapUnavailable; + final String openWebVersion; + + AppStrings({ + required this.appTitle, + required this.findParking, + required this.searching, + required this.routeReady, + required this.selectParking, + required this.parkingZone, + required this.free, + required this.pay, + required this.hourSign, + required this.capacity, + required this.forecast, + required this.confidence, + required this.type, + required this.parkingType, + required this.updatedAt, + required this.searchPlaceholder, + required this.filters, + required this.apply, + required this.reset, + required this.logout, + required this.settings, + required this.appearance, + required this.theme, + required this.language, + required this.info, + required this.privacyPolicy, + required this.github, + required this.themeLight, + required this.themeDark, + required this.themeSystem, + required this.langRussian, + required this.langEnglish, + required this.langSystem, + required this.account, + required this.notAuthenticated, + required this.logoutConfirm, + required this.cancel, + required this.inAppRoute, + required this.yandexNavigator, + required this.buildRoute, + required this.searchParkingNear, + required this.selectedPlace, + required this.myLocation, + required this.now, + required this.time, + required this.today, + required this.tomorrow, + required this.yesterday, + required this.street, + required this.yard, + required this.openLot, + required this.underground, + required this.multilevel, + required this.parallel, + required this.regular, + required this.login, + required this.password, + required this.signIn, + required this.noAccount, + required this.signUp, + required this.forgotPassword, + required this.emailRequired, + required this.passwordTooShort, + required this.name, + required this.nameRequired, + required this.invalidEmail, + required this.createAccount, + required this.alreadyHaveAccount, + required this.registration, + required this.resetPassword, + required this.sendInstructions, + required this.hideFull, + required this.hidePrivate, + required this.hideInaccessible, + required this.hideInactive, + required this.minConfidence, + required this.limitPrice, + required this.minFreeSpots, + required this.any, + required this.locationType, + required this.searchNoResults, + required this.searchError, + required this.searchHint, + required this.errorDetails, + required this.errorCreatingRoute, + required this.errorLoadingZones, + required this.copy, + required this.close, + required this.moreInfo, + required this.copiedToClipboard, + required this.locationPermissionDenied, + required this.locationPermissionReason, + required this.pickZoneOnMap, + required this.navigationActive, + required this.unauthorized, + required this.forbidden, + required this.notFound, + required this.emailAlreadyExists, + required this.invalidData, + required this.serverError, + required this.requestError, + required this.searchTimeout, + required this.pointLookupError, + required this.pointLookupTimeout, + required this.restartRequiredForMap, + required this.private, + required this.loadingData, + required this.noForecast, + required this.forWord, + required this.generated, + required this.jumpToClosest, + required this.forecastFor, + required this.youPicked, + required this.arrival, + required this.list, + required this.map, + required this.pickOnMapInstruction, + required this.pickOnMapAction, + required this.recalculating, + required this.straightAhead, + required this.finish, + required this.finishConfirmTitle, + required this.finishConfirmContent, + required this.timeLabel, + required this.distanceLabel, + required this.speedLabel, + required this.selectTheme, + required this.selectLanguage, + required this.metersSign, + required this.kmSign, + required this.minutesSign, + required this.turnLeft, + required this.keepLeft, + required this.turnRight, + required this.keepRight, + required this.uTurn, + required this.inWord, + required this.monthNames, + required this.dataErasure, + required this.editProfile, + required this.save, + required this.profileUpdated, + required this.desktopMapUnavailable, + required this.openWebVersion, + }); + + static AppStrings ru = AppStrings( + appTitle: 'ParkTrack', + findParking: 'Припарковаться', + searching: 'Ищем...', + routeReady: 'Маршрут готов', + selectParking: 'Выберите парковку', + parkingZone: 'Парковка', + free: 'Свободно', + pay: 'Стоимость', + hourSign: 'ч', + capacity: 'Мест', + forecast: 'Прогноз', + confidence: 'Уверенность', + type: 'Тип', + parkingType: 'Постановка', + updatedAt: 'Обновлено', + searchPlaceholder: 'Найти место назначения', + filters: 'Фильтры', + apply: 'Применить', + reset: 'Сбросить', + logout: 'Выйти', + settings: 'Настройки', + appearance: 'Внешний вид', + theme: 'Тема', + language: 'Язык', + info: 'Информация', + privacyPolicy: 'Политика конфиденциальности', + github: 'GitHub', + themeLight: 'Светлая', + themeDark: 'Тёмная', + themeSystem: 'Системная', + langRussian: 'Русский', + langEnglish: 'English', + langSystem: 'Системный', + account: 'Аккаунт', + notAuthenticated: 'Не авторизован', + logoutConfirm: 'Вы уверены, что хотите выйти из аккаунта?', + cancel: 'Отмена', + inAppRoute: 'В приложении', + yandexNavigator: 'Яндекс', + buildRoute: 'Построить маршрут', + searchParkingNear: 'Искать парковку рядом', + selectedPlace: 'Выбранное место', + myLocation: 'Мое местоположение', + now: 'Сейчас', + time: 'Время', + today: 'Сегодня', + tomorrow: 'Завтра', + yesterday: 'Вчера', + street: 'Уличная', + yard: 'Дворовая', + openLot: 'Открытая стоянка', + underground: 'Подземная', + multilevel: 'Многоуровневая', + parallel: 'Параллельная', + regular: 'Обычная', + login: 'Email', + password: 'Пароль', + signIn: 'Войти', + noAccount: 'Нет аккаунта? Зарегистрироваться', + signUp: 'Зарегистрироваться', + forgotPassword: 'Забыли пароль?', + emailRequired: 'Введите email', + passwordTooShort: 'Минимум 6 символов', + name: 'Имя', + nameRequired: 'Введите имя', + invalidEmail: 'Неверный email', + createAccount: 'Создать аккаунт', + alreadyHaveAccount: 'Уже есть аккаунт? Войти', + registration: 'Регистрация', + resetPassword: 'Восстановление пароля', + sendInstructions: 'Отправить инструкции', + hideFull: 'Скрыть занятые', + hidePrivate: 'Скрыть частные', + hideInaccessible: 'Скрыть места для инвалидов', + hideInactive: 'Скрыть неактивные', + minConfidence: 'Минимальная уверенность', + limitPrice: 'Ограничить стоимость', + minFreeSpots: 'Минимум свободных мест', + any: 'Любое', + locationType: 'Тип парковки', + searchNoResults: 'Ничего не найдено', + searchError: 'Ошибка поиска', + searchHint: 'Введите адрес или место', + errorDetails: 'Детали ошибки', + errorCreatingRoute: 'Ошибка при построении маршрута', + errorLoadingZones: 'Не удалось загрузить парковки', + copy: 'Копировать', + close: 'Закрыть', + moreInfo: 'Подробнее', + copiedToClipboard: 'Скопировано в буфер', + locationPermissionDenied: 'Нет доступа к геолокации', + locationPermissionReason: + 'Для поиска парковок рядом с вами необходим доступ к местоположению.', + pickZoneOnMap: 'Выберите парковочную зону на карте', + navigationActive: 'Навигация активна', + unauthorized: 'Не авторизован', + forbidden: 'Нет доступа', + notFound: 'Не найдено', + emailAlreadyExists: 'Пользователь с таким email уже существует', + invalidData: 'Неверные данные', + serverError: 'Ошибка сервера', + requestError: 'Ошибка запроса', + searchTimeout: 'Поиск занял слишком много времени', + pointLookupError: 'Не удалось определить координаты места', + pointLookupTimeout: 'Определение точки заняло слишком много времени', + restartRequiredForMap: + 'Перезапустите приложение, чтобы обновить названия на карте', + private: 'Частная', + loadingData: 'Загрузка данных...', + noForecast: 'Нет прогноза', + forWord: 'на', + generated: 'Создан', + jumpToClosest: 'Открыть ближайшее доступное', + forecastFor: 'Прогноз на', + youPicked: 'вы выбрали', + arrival: 'Прибытие', + list: 'Список', + map: 'Карта', + pickOnMapInstruction: + 'Нажмите «Выбирать на карте», затем тапните подходящую зону на карте.', + pickOnMapAction: 'Выбирать на карте', + recalculating: 'Пересчёт маршрута...', + straightAhead: 'Движение прямо', + finish: 'Завершить', + finishConfirmTitle: 'Завершить маршрут?', + finishConfirmContent: 'Навигация будет остановлена.', + timeLabel: 'времени', + distanceLabel: 'до цели', + speedLabel: 'км/ч', + selectTheme: 'Выберите тему', + selectLanguage: 'Выберите язык', + metersSign: 'м', + kmSign: 'км', + minutesSign: 'мин', + turnLeft: 'Поверните налево', + keepLeft: 'Держитесь левее', + turnRight: 'Поверните направо', + keepRight: 'Держитесь правее', + uTurn: 'Выполните разворот', + inWord: 'через', + monthNames: [ + '', + 'янв', + 'фев', + 'мар', + 'апр', + 'май', + 'июн', + 'июл', + 'авг', + 'сен', + 'окт', + 'ноя', + 'дек', + ], + dataErasure: 'Удаление данных', + editProfile: 'Редактировать профиль', + save: 'Сохранить', + profileUpdated: 'Профиль обновлен', + desktopMapUnavailable: + 'Яндекс Карты пока не поддерживают нативные приложения Flutter для macOS.', + openWebVersion: 'Открыть веб-версию', + ); + + static AppStrings en = AppStrings( + appTitle: 'ParkTrack', + findParking: 'Park Now', + searching: 'Searching...', + routeReady: 'Route Ready', + selectParking: 'Select Parking', + parkingZone: 'Parking', + free: 'Free', + pay: 'Price', + hourSign: 'h', + capacity: 'Capacity', + forecast: 'Forecast', + confidence: 'Confidence', + type: 'Type', + parkingType: 'Layout', + updatedAt: 'Updated', + searchPlaceholder: 'Find destination', + filters: 'Filters', + apply: 'Apply', + reset: 'Reset', + logout: 'Logout', + settings: 'Settings', + appearance: 'Appearance', + theme: 'Theme', + language: 'Language', + info: 'Information', + privacyPolicy: 'Privacy Policy', + github: 'GitHub', + themeLight: 'Light', + themeDark: 'Dark', + themeSystem: 'System', + langRussian: 'Russian', + langEnglish: 'English', + langSystem: 'System', + account: 'Account', + notAuthenticated: 'Not authenticated', + logoutConfirm: 'Are you sure you want to sign out?', + cancel: 'Cancel', + inAppRoute: 'In-app', + yandexNavigator: 'Yandex', + buildRoute: 'Build Route', + searchParkingNear: 'Search parking nearby', + selectedPlace: 'Selected place', + myLocation: 'My location', + now: 'Now', + time: 'Time', + today: 'Today', + tomorrow: 'Tomorrow', + yesterday: 'Yesterday', + street: 'Street', + yard: 'Yard', + openLot: 'Open Lot', + underground: 'Underground', + multilevel: 'Multilevel', + parallel: 'Parallel', + regular: 'Regular', + login: 'Email', + password: 'Password', + signIn: 'Sign In', + noAccount: 'No account? Sign Up', + signUp: 'Sign Up', + forgotPassword: 'Forgot password?', + emailRequired: 'Email is required', + passwordTooShort: 'Min 6 characters', + name: 'Name', + nameRequired: 'Name is required', + invalidEmail: 'Invalid email', + createAccount: 'Create Account', + alreadyHaveAccount: 'Already have an account? Sign In', + registration: 'Registration', + resetPassword: 'Reset Password', + sendInstructions: 'Send Instructions', + hideFull: 'Hide full', + hidePrivate: 'Hide private', + hideInaccessible: 'Hide disabled spots', + hideInactive: 'Hide inactive', + minConfidence: 'Min confidence', + limitPrice: 'Limit price', + minFreeSpots: 'Min free spots', + any: 'Any', + locationType: 'Parking type', + searchNoResults: 'No results found', + searchError: 'Search error', + searchHint: 'Enter address or place', + errorDetails: 'Error details', + errorCreatingRoute: 'Error while creating route', + errorLoadingZones: 'Failed to load parking zones', + copy: 'Copy', + close: 'Close', + moreInfo: 'Details', + copiedToClipboard: 'Copied to clipboard', + locationPermissionDenied: 'Location access denied', + locationPermissionReason: + 'Location access is required to find parking near you.', + pickZoneOnMap: 'Select a parking zone on the map', + navigationActive: 'Navigation active', + unauthorized: 'Unauthorized', + forbidden: 'Forbidden', + notFound: 'Not found', + emailAlreadyExists: 'User with this email already exists', + invalidData: 'Invalid data', + serverError: 'Server error', + requestError: 'Request error', + searchTimeout: 'Search timeout', + pointLookupError: 'Could not determine coordinates', + pointLookupTimeout: 'Point lookup timeout', + restartRequiredForMap: 'Restart the app to update map labels', + private: 'Private', + loadingData: 'Loading data...', + noForecast: 'No forecast', + forWord: 'for', + generated: 'Generated', + jumpToClosest: 'Jump to closest available', + forecastFor: 'Forecast for', + youPicked: 'you picked', + arrival: 'Arrival', + list: 'List', + map: 'Map', + pickOnMapInstruction: + 'Tap "Pick on Map", then tap a parking zone on the map.', + pickOnMapAction: 'Pick on Map', + recalculating: 'Recalculating...', + straightAhead: 'Straight ahead', + finish: 'Finish', + finishConfirmTitle: 'Finish route?', + finishConfirmContent: 'Navigation will be stopped.', + timeLabel: 'time', + distanceLabel: 'to goal', + speedLabel: 'km/h', + selectTheme: 'Select theme', + selectLanguage: 'Select language', + metersSign: 'm', + kmSign: 'km', + minutesSign: 'min', + turnLeft: 'Turn left', + keepLeft: 'Keep left', + turnRight: 'Turn right', + keepRight: 'Keep right', + uTurn: 'U-turn', + inWord: 'in', + monthNames: [ + '', + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'May', + 'Jun', + 'Jul', + 'Aug', + 'Sep', + 'Oct', + 'Nov', + 'Dec', + ], + dataErasure: 'Data Erasure', + editProfile: 'Edit Profile', + save: 'Save', + profileUpdated: 'Profile updated', + desktopMapUnavailable: + 'Yandex Maps does not currently support native Flutter apps for macOS.', + openWebVersion: 'Open web version', + ); +} + +final l10nProvider = Provider((ref) { + final settings = ref.watch(settingsProvider); + final locale = settings.locale ?? const Locale('ru'); + return locale.languageCode == 'en' ? AppStrings.en : AppStrings.ru; +}); diff --git a/lib/core/network/api_exception.dart b/lib/core/network/api_exception.dart index 25f3736..bad27c4 100644 --- a/lib/core/network/api_exception.dart +++ b/lib/core/network/api_exception.dart @@ -1,3 +1,5 @@ +import '../localization/app_localizations.dart'; + class ApiException implements Exception { final int? statusCode; final String message; @@ -5,14 +7,27 @@ class ApiException implements Exception { const ApiException(this.message, {this.statusCode}); factory ApiException.fromStatusCode(int code) { + // We return a technical message by default, but it will be localized in UI return switch (code) { - 401 => const ApiException('Не авторизован', statusCode: 401), - 403 => const ApiException('Нет доступа', statusCode: 403), - 404 => const ApiException('Не найдено', statusCode: 404), - 409 => const ApiException('Пользователь с таким email уже существует', statusCode: 409), - 422 => const ApiException('Неверные данные', statusCode: 422), - 500 => const ApiException('Ошибка сервера', statusCode: 500), - _ => ApiException('Ошибка запроса ($code)', statusCode: code), + 401 => ApiException('Unauthorized', statusCode: 401), + 403 => ApiException('Forbidden', statusCode: 403), + 404 => ApiException('Not Found', statusCode: 404), + 409 => ApiException('Conflict', statusCode: 409), + 422 => ApiException('Invalid Data', statusCode: 422), + 500 => ApiException('Server Error', statusCode: 500), + _ => ApiException('Request Error ($code)', statusCode: code), + }; + } + + String getLocalizedMessage(AppStrings s) { + return switch (statusCode) { + 401 => s.unauthorized, + 403 => s.forbidden, + 404 => s.notFound, + 409 => s.emailAlreadyExists, + 422 => s.invalidData, + 500 => s.serverError, + _ => '${s.requestError} ($statusCode)', }; } diff --git a/lib/core/network/auth_interceptor.dart b/lib/core/network/auth_interceptor.dart index 585a286..a62a86b 100644 --- a/lib/core/network/auth_interceptor.dart +++ b/lib/core/network/auth_interceptor.dart @@ -1,9 +1,12 @@ import 'package:dio/dio.dart'; import '../storage/token_storage.dart'; import '../storage/session_expired_notifier.dart'; +import '../constants.dart'; class AuthInterceptor extends Interceptor { final TokenStorage _tokenStorage; + bool _isRefreshing = false; + final List _deferredHandlers = []; AuthInterceptor(this._tokenStorage); @@ -20,11 +23,73 @@ class AuthInterceptor extends Interceptor { } @override - void onError(DioException err, ErrorInterceptorHandler handler) { + Future onError(DioException err, ErrorInterceptorHandler handler) async { if (err.response?.statusCode == 401) { - _tokenStorage.clearTokens(); - sessionExpiredNotifier.value = true; + final login = await _tokenStorage.getLogin(); + final password = await _tokenStorage.getPassword(); + + if (login != null && password != null) { + if (_isRefreshing) { + _deferredHandlers.add(handler); + return; + } + + _isRefreshing = true; + + try { + // Use a fresh Dio instance to avoid interceptor recursion + final refreshDio = _createRefreshDio(); + final response = await refreshDio.post('/auth/login', data: { + 'login': login, + 'password': password, + }); + + final newToken = response.data['access_token'] as String; + await _tokenStorage.saveAccessToken(newToken); + + // Retry the original request + final retryResponse = await _retry(err.requestOptions, newToken); + handler.resolve(retryResponse); + + // For simplicity, we let deferred handlers fail, + // but a robust implementation would retry them as well. + for (final h in _deferredHandlers) { + h.next(err); + } + _deferredHandlers.clear(); + return; + } catch (e) { + await _tokenStorage.clearAll(); + sessionExpiredNotifier.value = true; + for (final h in _deferredHandlers) { + h.next(err); + } + _deferredHandlers.clear(); + } finally { + _isRefreshing = false; + } + } else { + await _tokenStorage.clearAll(); + sessionExpiredNotifier.value = true; + } } handler.next(err); } + + Dio _createRefreshDio() { + return Dio(BaseOptions(baseUrl: kBaseUrl)); + } + + Future> _retry(RequestOptions requestOptions, String token) { + final options = Options( + method: requestOptions.method, + headers: Map.from(requestOptions.headers)..['Authorization'] = 'Bearer $token', + ); + return _createRefreshDio().request( + requestOptions.path, + data: requestOptions.data, + queryParameters: requestOptions.queryParameters, + options: options, + ); + } } diff --git a/lib/core/network/dio_client.dart b/lib/core/network/dio_client.dart index 0a47390..78d29da 100644 --- a/lib/core/network/dio_client.dart +++ b/lib/core/network/dio_client.dart @@ -7,8 +7,8 @@ import 'mock_interceptor.dart'; Dio createDio(TokenStorage tokenStorage) { final dio = Dio(BaseOptions( baseUrl: kBaseUrl, - connectTimeout: const Duration(seconds: 10), - receiveTimeout: const Duration(seconds: 15), + connectTimeout: const Duration(seconds: 15), + receiveTimeout: const Duration(seconds: 20), headers: {'Content-Type': 'application/json'}, )); diff --git a/lib/core/network/mock_interceptor.dart b/lib/core/network/mock_interceptor.dart index c02dd72..d1b25d5 100644 --- a/lib/core/network/mock_interceptor.dart +++ b/lib/core/network/mock_interceptor.dart @@ -9,7 +9,7 @@ class MockInterceptor extends Interceptor { final path = options.path; if (method == 'POST' && path == '/auth/register') { - return handler.resolve(_response(options, _authPayload('registered@mock.dev', 'Новый пользователь'))); + return handler.resolve(_response(options, _authPayload('registered@mock.dev', 'New User'))); } if (method == 'POST' && path == '/auth/login') { return handler.resolve(_response(options, _authPayload('user@mock.dev', 'Mock User'))); @@ -140,7 +140,7 @@ class MockInterceptor extends Interceptor { } Map _authPayload(String email, String name) => { - 'access_token': 'mock-token-123', + 'access_token': 'mock-token-${DateTime.now().millisecondsSinceEpoch}', 'expires_in': 86400, 'user': _userPayload(email, name), }; diff --git a/lib/core/router/app_router.dart b/lib/core/router/app_router.dart index df9aef1..410f4ad 100644 --- a/lib/core/router/app_router.dart +++ b/lib/core/router/app_router.dart @@ -6,6 +6,7 @@ import '../../presentation/screens/auth/login_screen.dart'; import '../../presentation/screens/auth/register_screen.dart'; import '../../presentation/screens/map/map_screen.dart'; import '../../presentation/screens/profile/profile_screen.dart'; +import '../../presentation/screens/profile/edit_profile_screen.dart'; import '../../presentation/screens/search/search_screen.dart'; import '../../presentation/screens/auth/password_reset_screen.dart'; @@ -38,13 +39,15 @@ final routerProvider = Provider((ref) { return null; }, routes: [ - GoRoute(path: '/', builder: (_, __) => const SplashScreen()), - GoRoute(path: '/login', builder: (_, __) => const LoginScreen()), - GoRoute(path: '/register', builder: (_, __) => const RegisterScreen()), - GoRoute(path: '/map', builder: (_, __) => const MapScreen()), - GoRoute(path: '/profile', builder: (_, __) => const ProfileScreen()), - GoRoute(path: '/search', builder: (_, __) => const SearchScreen()), - GoRoute(path: '/password-reset', builder: (_, __) => const PasswordResetScreen()), + GoRoute(path: '/', builder: (_, _) => const SplashScreen()), + GoRoute(path: '/login', builder: (_, _) => const LoginScreen()), + GoRoute(path: '/register', builder: (_, _) => const RegisterScreen()), + GoRoute(path: '/map', builder: (_, _) => const MapScreen()), + GoRoute(path: '/profile', builder: (_, _) => const ProfileScreen(), routes: [ + GoRoute(path: 'edit', builder: (_, _) => const EditProfileScreen()), + ]), + GoRoute(path: '/search', builder: (_, _) => const SearchScreen()), + GoRoute(path: '/password-reset', builder: (_, _) => const PasswordResetScreen()), ], ); }); diff --git a/lib/core/storage/settings_storage.dart b/lib/core/storage/settings_storage.dart new file mode 100644 index 0000000..b250295 --- /dev/null +++ b/lib/core/storage/settings_storage.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; + +class SettingsStorage { + static const _keyTheme = 'app_theme'; + static const _keyLanguage = 'app_language'; + static const _storage = FlutterSecureStorage( + aOptions: AndroidOptions(encryptedSharedPreferences: true), + ); + + Future saveThemeMode(ThemeMode mode) => + _storage.write(key: _keyTheme, value: mode.name); + + Future getThemeMode() async { + final value = await _storage.read(key: _keyTheme); + return ThemeMode.values.firstWhere( + (e) => e.name == value, + orElse: () => ThemeMode.system, + ); + } + + Future saveLanguage(String? languageCode) async { + if (languageCode == null) { + await _storage.delete(key: _keyLanguage); + } else { + await _storage.write(key: _keyLanguage, value: languageCode); + } + } + + Future getLanguage() => _storage.read(key: _keyLanguage); +} diff --git a/lib/core/storage/token_storage.dart b/lib/core/storage/token_storage.dart index d0d1f0d..0fe3bd8 100644 --- a/lib/core/storage/token_storage.dart +++ b/lib/core/storage/token_storage.dart @@ -2,6 +2,8 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart'; class TokenStorage { static const _keyAccess = 'access_token'; + static const _keyLogin = 'user_login'; + static const _keyPassword = 'user_password'; static const _storage = FlutterSecureStorage( aOptions: AndroidOptions(encryptedSharedPreferences: true), ); @@ -11,6 +13,21 @@ class TokenStorage { Future getAccessToken() => _storage.read(key: _keyAccess); + Future saveCredentials(String login, String password) async { + await _storage.write(key: _keyLogin, value: login); + await _storage.write(key: _keyPassword, value: password); + print("Credentials saved."); + } + + Future getLogin() => _storage.read(key: _keyLogin); + Future getPassword() => _storage.read(key: _keyPassword); + + Future clearAll() async { + await _storage.delete(key: _keyAccess); + await _storage.delete(key: _keyLogin); + await _storage.delete(key: _keyPassword); + } + Future clearTokens() => _storage.delete(key: _keyAccess); Future hasToken() async => (await getAccessToken()) != null; diff --git a/lib/core/theme/app_colors.dart b/lib/core/theme/app_colors.dart index db85244..31bd908 100644 --- a/lib/core/theme/app_colors.dart +++ b/lib/core/theme/app_colors.dart @@ -4,12 +4,18 @@ class AppColors { AppColors._(); static const Color primary = Color(0xFF2E7D32); - static const Color primaryLight = Color(0xFF81C784); + static const Color primaryLight = Color(0xFF2E7D32); static const Color background = Color(0xFFF5F5F5); static const Color surface = Color(0xFFFFFFFF); static const Color onSurface = Color(0xFF212121); static const Color textSecondary = Color(0xFF757575); + // Dark Theme Colors + static const Color backgroundDark = Color(0xFF121212); + static const Color surfaceDark = Color(0xFF1E1E1E); + static const Color onSurfaceDark = Color(0xFFE1E1E1); + static const Color textSecondaryDark = Color(0xFFAAAAAA); + // Semantic parking colors static const Color parkingFull = Color(0xFFE53935); static const Color parkingOne = Color(0xFFFDD835); diff --git a/lib/core/theme/app_theme.dart b/lib/core/theme/app_theme.dart index 2c2f28a..31ce36b 100644 --- a/lib/core/theme/app_theme.dart +++ b/lib/core/theme/app_theme.dart @@ -27,5 +27,44 @@ class AppTheme { borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), ), + filledButtonTheme: FilledButtonThemeData( + style: FilledButton.styleFrom( + backgroundColor: AppColors.primary, + foregroundColor: Colors.white, + ), + ), + ); + + static ThemeData get dark => ThemeData( + useMaterial3: true, + brightness: Brightness.dark, + colorScheme: ColorScheme.fromSeed( + seedColor: AppColors.primary, + brightness: Brightness.dark, + surface: AppColors.surfaceDark, + ), + scaffoldBackgroundColor: AppColors.backgroundDark, + appBarTheme: const AppBarTheme( + backgroundColor: AppColors.surfaceDark, + elevation: 0, + centerTitle: true, + titleTextStyle: TextStyle( + color: AppColors.onSurfaceDark, + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + bottomSheetTheme: const BottomSheetThemeData( + backgroundColor: AppColors.surfaceDark, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + ), + filledButtonTheme: FilledButtonThemeData( + style: FilledButton.styleFrom( + backgroundColor: AppColors.primary, + foregroundColor: Colors.white, + ), + ), ); } diff --git a/lib/core/utils/error_snackbar.dart b/lib/core/utils/error_snackbar.dart index 918ba73..8b411e7 100644 --- a/lib/core/utils/error_snackbar.dart +++ b/lib/core/utils/error_snackbar.dart @@ -1,13 +1,18 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import '../localization/app_localizations.dart'; void showErrorSnackBar( BuildContext context, String message, { Object? error, StackTrace? stackTrace, + AppStrings? s, }) { final details = _buildDetails(error, stackTrace); + + final label = s?.moreInfo ?? 'Details'; + ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar( @@ -21,8 +26,8 @@ void showErrorSnackBar( ), action: details != null ? SnackBarAction( - label: 'Подробнее', - onPressed: () => _showDetailsDialog(context, message, details), + label: label, + onPressed: () => _showDetailsDialog(context, message, details, s), ) : null, behavior: SnackBarBehavior.floating, @@ -42,11 +47,11 @@ String? _buildDetails(Object? error, StackTrace? stackTrace) { return buffer.toString(); } -void _showDetailsDialog(BuildContext context, String message, String details) { +void _showDetailsDialog(BuildContext context, String message, String details, AppStrings? s) { showDialog( context: context, builder: (ctx) => AlertDialog( - title: const Text('Детали ошибки'), + title: Text(s?.errorDetails ?? 'Error Details'), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -56,7 +61,9 @@ void _showDetailsDialog(BuildContext context, String message, String details) { Container( constraints: const BoxConstraints(maxHeight: 320), decoration: BoxDecoration( - color: Colors.grey.shade100, + color: Theme.of(context).brightness == Brightness.dark + ? Colors.grey.shade800 + : Colors.grey.shade100, borderRadius: BorderRadius.circular(8), ), child: SingleChildScrollView( @@ -72,14 +79,14 @@ void _showDetailsDialog(BuildContext context, String message, String details) { actions: [ TextButton.icon( icon: const Icon(Icons.copy, size: 16), - label: const Text('Копировать'), + label: Text(s?.copy ?? 'Copy'), onPressed: () { Clipboard.setData(ClipboardData(text: details)); Navigator.pop(ctx); ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Скопировано в буфер'), - duration: Duration(seconds: 2), + SnackBar( + content: Text(s?.copiedToClipboard ?? 'Copied to clipboard'), + duration: const Duration(seconds: 2), behavior: SnackBarBehavior.floating, ), ); @@ -87,7 +94,7 @@ void _showDetailsDialog(BuildContext context, String message, String details) { ), FilledButton( onPressed: () => Navigator.pop(ctx), - child: const Text('Закрыть'), + child: Text(s?.close ?? 'Close'), ), ], ), diff --git a/lib/core/utils/nav_math.dart b/lib/core/utils/nav_math.dart index 5bfc48f..9cb5497 100644 --- a/lib/core/utils/nav_math.dart +++ b/lib/core/utils/nav_math.dart @@ -1,5 +1,6 @@ import 'dart:math'; import 'package:yandex_mapkit/yandex_mapkit.dart'; +import '../localization/app_localizations.dart'; const _kEarthRadius = 6371000.0; @@ -115,14 +116,18 @@ TurnDirection _classifyTurn(double diff) { return TurnDirection.straight; } -String formatNavDistance(double meters) { - if (meters < 950) return '${meters.round()} м'; - return '${(meters / 1000).toStringAsFixed(1)} км'; +String formatNavDistance(num meters, [AppStrings? s]) { + final metersSign = s?.metersSign ?? 'm'; + final kmSign = s?.kmSign ?? 'km'; + if (meters < 950) return '${meters.round()} $metersSign'; + return '${(meters / 1000).toStringAsFixed(1)} $kmSign'; } -String formatNavDuration(int seconds) { - if (seconds < 3600) return '${(seconds / 60).round()} мин'; +String formatNavDuration(num seconds, [AppStrings? s]) { + final minSign = s?.minutesSign ?? 'min'; + final hourSign = s?.hourSign ?? 'h'; + if (seconds < 3600) return '${(seconds / 60).round()} $minSign'; final h = seconds ~/ 3600; final m = (seconds % 3600) ~/ 60; - return m == 0 ? '${h}ч' : '${h}ч ${m}мин'; + return m == 0 ? '$h$hourSign' : '$h$hourSign $m$minSign'; } diff --git a/lib/data/api/auth_api.dart b/lib/data/api/auth_api.dart index 73eff74..52e6644 100644 --- a/lib/data/api/auth_api.dart +++ b/lib/data/api/auth_api.dart @@ -25,4 +25,9 @@ class AuthApi { Future requestPasswordReset(String email) => _dio.post('/auth/password-reset/request', data: {'email': email}); + + Future updateProfile(Map data) async { + final response = await _dio.put('/users/me', data: data); + return UserDto.fromJson(response.data as Map); + } } diff --git a/lib/data/api/routing_api.dart b/lib/data/api/routing_api.dart index 20b2de6..fef121e 100644 --- a/lib/data/api/routing_api.dart +++ b/lib/data/api/routing_api.dart @@ -19,7 +19,7 @@ class RoutingApi { {int? selectedZoneId}) async { final data = { ...request.toJson()..removeWhere((_, v) => v == null), - if (selectedZoneId != null) 'selected_zone_id': selectedZoneId, + 'selected_zone_id': ?selectedZoneId, }; final response = await _dio.post('/routing/new', data: data); return RouteDto.fromJson(response.data as Map); @@ -36,8 +36,8 @@ class RoutingApi { int? selectedZoneId, }) async { final response = await _dio.put('/routing/$routeId', data: { - if (status != null) 'status': status, - if (selectedZoneId != null) 'selected_zone_id': selectedZoneId, + 'status': ?status, + 'selected_zone_id': ?selectedZoneId, }); return RouteDto.fromJson(response.data as Map); } diff --git a/lib/data/api/zones_api.dart b/lib/data/api/zones_api.dart index 3cec3bd..efaf04a 100644 --- a/lib/data/api/zones_api.dart +++ b/lib/data/api/zones_api.dart @@ -19,12 +19,12 @@ class ZonesApi { final response = await _dio.get('/zones', queryParameters: { 'bbox': bbox, 'view': view, - if (isActive != null) 'is_active': isActive, - if (minFreeCount != null) 'min_free_count': minFreeCount, - if (minConfidence != null) 'min_confidence': minConfidence, - if (maxPay != null) 'max_pay': maxPay, - if (includePrivate != null) 'include_private': includePrivate, - if (hideLocationTypes != null) 'hide_location_types': hideLocationTypes, + 'is_active': ?isActive, + 'min_free_count': ?minFreeCount, + 'min_confidence': ?minConfidence, + 'max_pay': ?maxPay, + 'include_private': ?includePrivate, + 'hide_location_types': ?hideLocationTypes, }); return (response.data as List) .map((e) => ZoneMapItemDto.fromJson(e as Map)) diff --git a/lib/data/repositories/auth_repository.dart b/lib/data/repositories/auth_repository.dart index fce1426..0cd32e9 100644 --- a/lib/data/repositories/auth_repository.dart +++ b/lib/data/repositories/auth_repository.dart @@ -15,13 +15,14 @@ class AuthRepository { try { final response = await _api.login(LoginRequestDto(login: login, password: password)); await _tokenStorage.saveAccessToken(response.accessToken); + await _tokenStorage.saveCredentials(login, password); return _mapUser(response.user); } on DioException catch (e) { throw ApiException.fromStatusCode(e.response?.statusCode ?? 0); } on ApiException { rethrow; } catch (e) { - throw ApiException('Ошибка: $e'); + throw ApiException(e.toString()); } } @@ -31,13 +32,14 @@ class AuthRepository { RegisterRequestDto(email: email, password: password, fullName: fullName), ); await _tokenStorage.saveAccessToken(response.accessToken); + await _tokenStorage.saveCredentials(email, password); return _mapUser(response.user); } on DioException catch (e) { throw ApiException.fromStatusCode(e.response?.statusCode ?? 0); } on ApiException { rethrow; } catch (e) { - throw ApiException('Ошибка: $e'); + throw ApiException(e.toString()); } } @@ -45,7 +47,7 @@ class AuthRepository { try { await _api.logout(); } finally { - await _tokenStorage.clearTokens(); + await _tokenStorage.clearAll(); } } @@ -65,7 +67,21 @@ class AuthRepository { } on DioException catch (e) { throw ApiException.fromStatusCode(e.response?.statusCode ?? 0); } catch (e) { - throw ApiException('Ошибка: $e'); + throw ApiException(e.toString()); + } + } + + Future updateProfile({String? fullName}) async { + try { + final data = {}; + if (fullName != null) data['full_name'] = fullName; + + final dto = await _api.updateProfile(data); + return _mapUser(dto); + } on DioException catch (e) { + throw ApiException.fromStatusCode(e.response?.statusCode ?? 0); + } catch (e) { + throw ApiException(e.toString()); } } diff --git a/lib/main.dart b/lib/main.dart index aabd5a0..f5a6577 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,6 +3,7 @@ import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'core/router/app_router.dart'; import 'core/theme/app_theme.dart'; +import 'presentation/providers/settings_provider.dart'; void main() { WidgetsFlutterBinding.ensureInitialized(); @@ -16,12 +17,16 @@ class ParkTrackApp extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final router = ref.watch(routerProvider); + final settings = ref.watch(settingsProvider); + return MaterialApp.router( title: 'ParkTrack', theme: AppTheme.light, + darkTheme: AppTheme.dark, + themeMode: settings.themeMode, routerConfig: router, debugShowCheckedModeBanner: false, - locale: const Locale('ru', 'RU'), + locale: settings.locale, localizationsDelegates: const [ GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, diff --git a/lib/presentation/providers/auth_provider.dart b/lib/presentation/providers/auth_provider.dart index 0cdaeca..acbbdce 100644 --- a/lib/presentation/providers/auth_provider.dart +++ b/lib/presentation/providers/auth_provider.dart @@ -79,6 +79,11 @@ class AuthNotifier extends StateNotifier { Future requestPasswordReset(String email) => _ref.read(authRepositoryProvider).requestPasswordReset(email); + + Future updateProfile({String? fullName}) async { + final user = await _ref.read(authRepositoryProvider).updateProfile(fullName: fullName); + state = AuthState.authenticated(user); + } } final authStateProvider = StateNotifierProvider( diff --git a/lib/presentation/providers/navigation_provider.dart b/lib/presentation/providers/navigation_provider.dart index 11a96eb..3fbdd79 100644 --- a/lib/presentation/providers/navigation_provider.dart +++ b/lib/presentation/providers/navigation_provider.dart @@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:geolocator/geolocator.dart'; import 'package:yandex_mapkit/yandex_mapkit.dart'; import '../../core/utils/nav_math.dart'; +import '../../core/localization/app_localizations.dart'; class NavigationData { final int zoneId; @@ -61,6 +62,7 @@ class NavigationNotifier extends Notifier { required double totalMeters, required double destLat, required double destLon, + required AppStrings s, }) async { _cleanupTimers(); await _sub?.cancel(); @@ -77,7 +79,7 @@ class NavigationNotifier extends Notifier { _smoothHeading = null; _sub = Geolocator.getPositionStream( - locationSettings: _buildLocationSettings(), + locationSettings: _buildLocationSettings(s), ).listen(_onPosition); } @@ -90,14 +92,14 @@ class NavigationNotifier extends Notifier { state = null; } - LocationSettings _buildLocationSettings() { + LocationSettings _buildLocationSettings(AppStrings s) { if (Platform.isAndroid) { return AndroidSettings( accuracy: LocationAccuracy.high, distanceFilter: 5, - foregroundNotificationConfig: const ForegroundNotificationConfig( - notificationTitle: 'ParkTrack', - notificationText: 'Навигация активна', + foregroundNotificationConfig: ForegroundNotificationConfig( + notificationTitle: s.appTitle, + notificationText: s.navigationActive, enableWakeLock: true, ), ); diff --git a/lib/presentation/providers/settings_provider.dart b/lib/presentation/providers/settings_provider.dart new file mode 100644 index 0000000..14de20d --- /dev/null +++ b/lib/presentation/providers/settings_provider.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../core/storage/settings_storage.dart'; + +class SettingsState { + final ThemeMode themeMode; + final Locale? locale; + + SettingsState({ + required this.themeMode, + this.locale, + }); + + SettingsState copyWith({ + ThemeMode? themeMode, + Locale? locale, + bool clearLocale = false, + }) { + return SettingsState( + themeMode: themeMode ?? this.themeMode, + locale: clearLocale ? null : (locale ?? this.locale), + ); + } +} + +class SettingsNotifier extends StateNotifier { + final SettingsStorage _storage; + static const _channel = MethodChannel('com.parktrack.mobile/mapkit'); + + SettingsNotifier(this._storage) + : super(SettingsState(themeMode: ThemeMode.system)) { + _loadSettings(); + } + + Future _loadSettings() async { + final theme = await _storage.getThemeMode(); + final lang = await _storage.getLanguage(); + if (lang != null) { + _setMapKitLocale(lang); + } + state = state.copyWith( + themeMode: theme, + locale: lang != null ? Locale(lang) : null, + ); + } + + Future setThemeMode(ThemeMode mode) async { + await _storage.saveThemeMode(mode); + state = state.copyWith(themeMode: mode); + } + + Future setLanguage(String? languageCode) async { + await _storage.saveLanguage(languageCode); + if (languageCode != null) { + _setMapKitLocale(languageCode); + } + state = state.copyWith( + locale: languageCode != null ? Locale(languageCode) : null, + clearLocale: languageCode == null, + ); + } + + Future _setMapKitLocale(String lang) async { + try { + // MapKit uses language codes like "ru_RU" or "en_US" + final mapLocale = lang == 'ru' ? 'ru_RU' : 'en_US'; + await _channel.invokeMethod('setLocale', {'locale': mapLocale}); + } catch (_) {} + } +} + +final settingsStorageProvider = Provider((ref) => SettingsStorage()); + +final settingsProvider = StateNotifierProvider((ref) { + return SettingsNotifier(ref.watch(settingsStorageProvider)); +}); diff --git a/lib/presentation/screens/auth/login_screen.dart b/lib/presentation/screens/auth/login_screen.dart index 1be1a40..1c643d4 100644 --- a/lib/presentation/screens/auth/login_screen.dart +++ b/lib/presentation/screens/auth/login_screen.dart @@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../providers/auth_provider.dart'; import '../../../core/network/api_exception.dart'; +import '../../../core/localization/app_localizations.dart'; class LoginScreen extends ConsumerStatefulWidget { const LoginScreen({super.key}); @@ -34,8 +35,9 @@ class _LoginScreenState extends ConsumerState { .login(_loginCtrl.text.trim(), _passwordCtrl.text); } on ApiException catch (e) { if (mounted) { + final s = ref.read(l10nProvider); ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(e.message), backgroundColor: Colors.red), + SnackBar(content: Text(e.getLocalizedMessage(s)), backgroundColor: Colors.red), ); } } finally { @@ -45,6 +47,7 @@ class _LoginScreenState extends ConsumerState { @override Widget build(BuildContext context) { + final s = ref.watch(l10nProvider); return Scaffold( body: SafeArea( child: Center( @@ -58,7 +61,7 @@ class _LoginScreenState extends ConsumerState { const Icon(Icons.local_parking, size: 64, color: Color(0xFF2E7D32)), const SizedBox(height: 8), Text( - 'ParkTrack', + s.appTitle, textAlign: TextAlign.center, style: Theme.of(context).textTheme.headlineMedium?.copyWith( fontWeight: FontWeight.bold, @@ -67,20 +70,20 @@ class _LoginScreenState extends ConsumerState { const SizedBox(height: 40), TextFormField( controller: _loginCtrl, - decoration: const InputDecoration( - labelText: 'Email', - prefixIcon: Icon(Icons.email_outlined), - border: OutlineInputBorder(), + decoration: InputDecoration( + labelText: s.login, + prefixIcon: const Icon(Icons.email_outlined), + border: const OutlineInputBorder(), ), keyboardType: TextInputType.emailAddress, validator: (v) => - v == null || v.isEmpty ? 'Введите email' : null, + v == null || v.isEmpty ? s.emailRequired : null, ), const SizedBox(height: 16), TextFormField( controller: _passwordCtrl, decoration: InputDecoration( - labelText: 'Пароль', + labelText: s.password, prefixIcon: const Icon(Icons.lock_outlined), border: const OutlineInputBorder(), suffixIcon: IconButton( @@ -92,7 +95,7 @@ class _LoginScreenState extends ConsumerState { ), obscureText: _obscure, validator: (v) => - v == null || v.length < 6 ? 'Минимум 6 символов' : null, + v == null || v.length < 6 ? s.passwordTooShort : null, ), const SizedBox(height: 24), FilledButton( @@ -109,16 +112,16 @@ class _LoginScreenState extends ConsumerState { color: Colors.white, ), ) - : const Text('Войти'), + : Text(s.signIn), ), const SizedBox(height: 16), TextButton( onPressed: () => context.push('/register'), - child: const Text('Нет аккаунта? Зарегистрироваться'), + child: Text(s.noAccount), ), TextButton( onPressed: () => context.push('/password-reset'), - child: const Text('Забыли пароль?'), + child: Text(s.forgotPassword), ), ], ), diff --git a/lib/presentation/screens/auth/password_reset_screen.dart b/lib/presentation/screens/auth/password_reset_screen.dart index b30af2b..2f1ae50 100644 --- a/lib/presentation/screens/auth/password_reset_screen.dart +++ b/lib/presentation/screens/auth/password_reset_screen.dart @@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../providers/auth_provider.dart'; import '../../../core/network/api_exception.dart'; +import '../../../core/localization/app_localizations.dart'; class PasswordResetScreen extends ConsumerStatefulWidget { const PasswordResetScreen({super.key}); @@ -30,14 +31,15 @@ class _PasswordResetScreenState extends ConsumerState { await ref.read(authStateProvider.notifier).requestPasswordReset(email); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Ссылка для сброса отправлена на $email')), + SnackBar(content: Text('Instructions sent to $email')), ); context.pop(); } } on ApiException catch (e) { if (mounted) { + final s = ref.read(l10nProvider); ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(e.message), backgroundColor: Colors.red), + SnackBar(content: Text(e.getLocalizedMessage(s)), backgroundColor: Colors.red), ); } } finally { @@ -47,8 +49,9 @@ class _PasswordResetScreenState extends ConsumerState { @override Widget build(BuildContext context) { + final s = ref.watch(l10nProvider); return Scaffold( - appBar: AppBar(title: const Text('Сброс пароля')), + appBar: AppBar(title: Text(s.resetPassword)), body: SafeArea( child: Center( child: SingleChildScrollView( @@ -61,21 +64,21 @@ class _PasswordResetScreenState extends ConsumerState { const Icon(Icons.lock_reset_outlined, size: 64, color: Color(0xFF2E7D32)), const SizedBox(height: 16), Text( - 'Введите email, указанный при регистрации. Мы отправим ссылку для сброса пароля.', + 'Enter your email to receive password reset instructions.', textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodyMedium, ), const SizedBox(height: 32), TextFormField( controller: _emailCtrl, - decoration: const InputDecoration( - labelText: 'Email', - prefixIcon: Icon(Icons.email_outlined), - border: OutlineInputBorder(), + decoration: InputDecoration( + labelText: s.login, + prefixIcon: const Icon(Icons.email_outlined), + border: const OutlineInputBorder(), ), keyboardType: TextInputType.emailAddress, validator: (v) => - v == null || v.isEmpty ? 'Введите email' : null, + v == null || v.isEmpty ? s.emailRequired : null, ), const SizedBox(height: 24), FilledButton( @@ -92,7 +95,7 @@ class _PasswordResetScreenState extends ConsumerState { color: Colors.white, ), ) - : const Text('Отправить'), + : Text(s.sendInstructions), ), ], ), diff --git a/lib/presentation/screens/auth/register_screen.dart b/lib/presentation/screens/auth/register_screen.dart index 9407248..6c53285 100644 --- a/lib/presentation/screens/auth/register_screen.dart +++ b/lib/presentation/screens/auth/register_screen.dart @@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../providers/auth_provider.dart'; import '../../../core/network/api_exception.dart'; +import '../../../core/localization/app_localizations.dart'; class RegisterScreen extends ConsumerStatefulWidget { const RegisterScreen({super.key}); @@ -38,8 +39,9 @@ class _RegisterScreenState extends ConsumerState { ); } on ApiException catch (e) { if (mounted) { + final s = ref.read(l10nProvider); ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(e.message), backgroundColor: Colors.red), + SnackBar(content: Text(e.getLocalizedMessage(s)), backgroundColor: Colors.red), ); } } finally { @@ -49,8 +51,9 @@ class _RegisterScreenState extends ConsumerState { @override Widget build(BuildContext context) { + final s = ref.watch(l10nProvider); return Scaffold( - appBar: AppBar(title: const Text('Регистрация')), + appBar: AppBar(title: Text(s.registration)), body: SafeArea( child: SingleChildScrollView( padding: const EdgeInsets.all(24), @@ -61,29 +64,29 @@ class _RegisterScreenState extends ConsumerState { children: [ TextFormField( controller: _nameCtrl, - decoration: const InputDecoration( - labelText: 'Имя', - prefixIcon: Icon(Icons.person_outlined), - border: OutlineInputBorder(), + decoration: InputDecoration( + labelText: s.name, + prefixIcon: const Icon(Icons.person_outlined), + border: const OutlineInputBorder(), ), - validator: (v) => v == null || v.trim().isEmpty ? 'Введите имя' : null, + validator: (v) => v == null || v.trim().isEmpty ? s.nameRequired : null, ), const SizedBox(height: 16), TextFormField( controller: _emailCtrl, - decoration: const InputDecoration( - labelText: 'Email', - prefixIcon: Icon(Icons.email_outlined), - border: OutlineInputBorder(), + decoration: InputDecoration( + labelText: s.login, + prefixIcon: const Icon(Icons.email_outlined), + border: const OutlineInputBorder(), ), keyboardType: TextInputType.emailAddress, - validator: (v) => v == null || !v.contains('@') ? 'Неверный email' : null, + validator: (v) => v == null || !v.contains('@') ? s.invalidEmail : null, ), const SizedBox(height: 16), TextFormField( controller: _passwordCtrl, decoration: InputDecoration( - labelText: 'Пароль', + labelText: s.password, prefixIcon: const Icon(Icons.lock_outlined), border: const OutlineInputBorder(), suffixIcon: IconButton( @@ -95,7 +98,7 @@ class _RegisterScreenState extends ConsumerState { ), obscureText: _obscure, validator: (v) => - v == null || v.length < 6 ? 'Минимум 6 символов' : null, + v == null || v.length < 6 ? s.passwordTooShort : null, ), const SizedBox(height: 24), FilledButton( @@ -110,12 +113,12 @@ class _RegisterScreenState extends ConsumerState { child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white), ) - : const Text('Создать аккаунт'), + : Text(s.createAccount), ), const SizedBox(height: 16), TextButton( onPressed: () => context.pop(), - child: const Text('Уже есть аккаунт? Войти'), + child: Text(s.alreadyHaveAccount), ), ], ), diff --git a/lib/presentation/screens/map/map_screen.dart b/lib/presentation/screens/map/map_screen.dart index 56dcc63..5697134 100644 --- a/lib/presentation/screens/map/map_screen.dart +++ b/lib/presentation/screens/map/map_screen.dart @@ -2,10 +2,13 @@ import 'dart:async'; import 'dart:math' as math; import 'dart:typed_data'; import 'dart:ui' as ui; +import 'package:flutter/foundation.dart' + show TargetPlatform, defaultTargetPlatform, kIsWeb; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:geolocator/geolocator.dart'; import 'package:go_router/go_router.dart'; +import 'package:url_launcher/url_launcher.dart'; import 'package:yandex_mapkit/yandex_mapkit.dart'; import '../../providers/zones_provider.dart'; import '../../../domain/models/zone.dart'; @@ -17,8 +20,10 @@ import '../../../core/theme/app_colors.dart'; import '../../../core/utils/nav_math.dart'; import '../../../core/utils/navigation_deeplink.dart'; import '../../../core/utils/error_snackbar.dart'; +import '../../../core/localization/app_localizations.dart'; import 'widgets/candidates_sheet.dart'; -import 'widgets/navigation_overlay.dart' show NavigationTurnCard, NavigationBottomBar; +import 'widgets/navigation_overlay.dart' + show NavigationTurnCard, NavigationBottomBar; import 'widgets/parking_zone_layer.dart'; import 'widgets/time_selector_widget.dart'; import 'widgets/parking_card_sheet.dart'; @@ -101,7 +106,8 @@ class _MapScreenState extends ConsumerState { } Future _updateZoneBitmaps(List zones) async { - final same = zones.length == _zonesById.length && + final same = + zones.length == _zonesById.length && zones.every((z) { final prev = _zonesById[z.zoneId]; return prev != null && @@ -138,7 +144,11 @@ class _MapScreenState extends ConsumerState { final recorder = ui.PictureRecorder(); final canvas = Canvas(recorder); canvas.drawCircle(center, size / 2 - 1, Paint()..color = Colors.white); - canvas.drawCircle(center, size / 2 - 5, Paint()..color = const Color(0xFF007AFF)); + canvas.drawCircle( + center, + size / 2 - 5, + Paint()..color = const Color(0xFF007AFF), + ); final picture = recorder.endRecording(); final image = await picture.toImage(size.toInt(), size.toInt()); final byteData = await image.toByteData(format: ui.ImageByteFormat.png); @@ -188,7 +198,7 @@ class _MapScreenState extends ConsumerState { return byteData!.buffer.asUint8List(); } - void _onCameraPositionChanged(CameraPosition position, _, __) { + void _onCameraPositionChanged(CameraPosition position, _, _) { _lastCameraTarget = position.target; _debounce?.cancel(); _debounce = Timer(const Duration(milliseconds: 400), _fetchZones); @@ -268,12 +278,7 @@ class _MapScreenState extends ConsumerState { final pos = await _mapController!.getCameraPosition(); await _mapController!.moveCamera( CameraUpdate.newCameraPosition( - CameraPosition( - target: pos.target, - zoom: pos.zoom, - azimuth: 0, - tilt: 0, - ), + CameraPosition(target: pos.target, zoom: pos.zoom, azimuth: 0, tilt: 0), ), animation: const MapAnimation(duration: 0.4), ); @@ -312,24 +317,23 @@ class _MapScreenState extends ConsumerState { } void _showLocationDeniedDialog() { + final s = ref.read(l10nProvider); showDialog( context: context, builder: (_) => AlertDialog( - title: const Text('Нет доступа к геолокации'), - content: const Text( - 'Для поиска парковок рядом с вами необходим доступ к местоположению.', - ), + title: Text(s.locationPermissionDenied), + content: Text(s.locationPermissionReason), actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: const Text('Отмена'), + child: Text(s.cancel), ), FilledButton( onPressed: () { Navigator.pop(context); Geolocator.openAppSettings(); }, - child: const Text('Настройки'), + child: Text(s.settings), ), ], ), @@ -343,11 +347,11 @@ class _MapScreenState extends ConsumerState { longitude: _userPosition!.longitude, ) : _lastCameraTarget != null - ? SearchBias( - latitude: _lastCameraTarget!.latitude, - longitude: _lastCameraTarget!.longitude, - ) - : null; + ? SearchBias( + latitude: _lastCameraTarget!.latitude, + longitude: _lastCameraTarget!.longitude, + ) + : null; ref.read(searchBiasProvider.notifier).state = bias; await context.push('/search'); } @@ -355,17 +359,18 @@ class _MapScreenState extends ConsumerState { Future _findParking() async { final pos = await _getCurrentPosition(); if (pos == null) return; - await ref.read(routingProvider.notifier).searchParking( - originLat: pos.latitude, - originLon: pos.longitude, - ); + await ref + .read(routingProvider.notifier) + .searchParking(originLat: pos.latitude, originLon: pos.longitude); } Future _buildRouteForZone(int zoneId) async { final pos = await _getCurrentPosition(); if (pos == null) return; setState(() => _isSelectingOnMap = false); - await ref.read(routingProvider.notifier).buildRoute( + await ref + .read(routingProvider.notifier) + .buildRoute( originLat: pos.latitude, originLon: pos.longitude, selectedZoneId: zoneId, @@ -384,11 +389,13 @@ class _MapScreenState extends ConsumerState { if (pos == null) return; await _mapController?.moveCamera( - CameraUpdate.newCameraPosition(CameraPosition( - target: Point(latitude: pos.latitude, longitude: pos.longitude), - zoom: 17, - tilt: 40, - )), + CameraUpdate.newCameraPosition( + CameraPosition( + target: Point(latitude: pos.latitude, longitude: pos.longitude), + zoom: 17, + tilt: 40, + ), + ), animation: const MapAnimation(duration: 0.8), ); @@ -432,20 +439,29 @@ class _MapScreenState extends ConsumerState { totalSeconds = totalMeters / 10; } - await ref.read(navigationProvider.notifier).startNavigation( + await ref + .read(navigationProvider.notifier) + .startNavigation( zoneId: zoneId, route: route, totalSeconds: totalSeconds, totalMeters: totalMeters, destLat: toLat, destLon: toLon, + s: ref.read(l10nProvider), ); await _mapController?.toggleUserLayer(visible: false); await _mapController?.toggleTrafficLayer(visible: true); } catch (e, st) { if (mounted) { - showErrorSnackBar(context, 'Не удалось построить маршрут', error: e, stackTrace: st); + showErrorSnackBar( + context, + ref.read(l10nProvider).errorCreatingRoute, + error: e, + stackTrace: st, + s: ref.read(l10nProvider), + ); } } finally { if (mounted) setState(() => _navBuilding = false); @@ -460,9 +476,61 @@ class _MapScreenState extends ConsumerState { ); } + Widget _buildMacOsFallback(BuildContext context, AppStrings s) { + return Scaffold( + appBar: AppBar( + title: Text(s.appTitle), + actions: [ + IconButton( + tooltip: s.settings, + onPressed: () => context.push('/profile'), + icon: const Icon(Icons.settings_outlined), + ), + ], + ), + body: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 520), + child: Padding( + padding: const EdgeInsets.all(32), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.desktop_mac_outlined, + size: 72, + color: Theme.of(context).colorScheme.primary, + ), + const SizedBox(height: 24), + Text( + s.desktopMapUnavailable, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 24), + FilledButton.icon( + onPressed: () => launchUrl( + Uri.parse('https://m.parktrack.live'), + mode: LaunchMode.externalApplication, + ), + icon: const Icon(Icons.open_in_browser), + label: Text(s.openWebVersion), + ), + ], + ), + ), + ), + ), + ); + } + @override Widget build(BuildContext context) { const bottomInset = 0.0; + final s = ref.watch(l10nProvider); + if (!kIsWeb && defaultTargetPlatform == TargetPlatform.macOS) { + return _buildMacOsFallback(context, s); + } final zones = ref.watch(filteredZonesProvider); final zonesAsync = ref.watch(rawZonesProvider); final routingState = ref.watch(routingProvider); @@ -474,7 +542,7 @@ class _MapScreenState extends ConsumerState { orElse: () => false, ); - ref.listen(timeSelectorProvider, (_, __) { + ref.listen(timeSelectorProvider, (_, _) { ref.read(rawZonesProvider.notifier).clearZones(); _zoneLabelCache.clear(); _zonesById.clear(); @@ -492,12 +560,14 @@ class _MapScreenState extends ConsumerState { setState(() => _routePolyline = nav.route); } _mapController?.moveCamera( - CameraUpdate.newCameraPosition(CameraPosition( - target: nav.currentPosition, - zoom: 17, - azimuth: nav.heading, - tilt: 40, - )), + CameraUpdate.newCameraPosition( + CameraPosition( + target: nav.currentPosition, + zoom: 17, + azimuth: nav.heading, + tilt: 40, + ), + ), animation: const MapAnimation(duration: 0.6), ); }); @@ -505,7 +575,13 @@ class _MapScreenState extends ConsumerState { ref.listen(rawZonesProvider, (_, next) { next.whenOrNull( error: (e, st) { - showErrorSnackBar(context, 'Не удалось загрузить парковки', error: e, stackTrace: st); + showErrorSnackBar( + context, + ref.read(l10nProvider).errorLoadingZones, + error: e, + stackTrace: st, + s: ref.read(l10nProvider), + ); }, ); }); @@ -536,7 +612,12 @@ class _MapScreenState extends ConsumerState { }, searching: () async {}, error: (message) async { - showErrorSnackBar(context, 'Ошибка при построении маршрута', error: message); + showErrorSnackBar( + context, + ref.read(l10nProvider).errorCreatingRoute, + error: message, + s: ref.read(l10nProvider), + ); }, candidates: (candidates) async { if (_isCandidatesSheetOpen || candidates.isEmpty) return; @@ -554,9 +635,7 @@ class _MapScreenState extends ConsumerState { Navigator.pop(context); setState(() => _isSelectingOnMap = true); ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Выберите парковочную зону на карте'), - ), + SnackBar(content: Text(ref.read(l10nProvider).pickZoneOnMap)), ); }, ), @@ -598,10 +677,10 @@ class _MapScreenState extends ConsumerState { zoneLon: zoneLon, onNavigateInApp: (zoneLat != null && zoneLon != null) ? () => _startInAppNavigation( - zoneId: route.selectedZoneId, - toLat: zoneLat!, - toLon: zoneLon!, - ) + zoneId: route.selectedZoneId, + toLat: zoneLat!, + toLon: zoneLon!, + ) : null, ), ); @@ -677,16 +756,20 @@ class _MapScreenState extends ConsumerState { final latPad = (latMax - latMin) * 0.6 + 0.003; final lonPad = (lonMax - lonMin) * 0.6 + 0.003; _mapController?.moveCamera( - CameraUpdate.newGeometry(Geometry.fromBoundingBox(BoundingBox( - southWest: Point( - latitude: latMin - latPad, - longitude: lonMin - lonPad, - ), - northEast: Point( - latitude: latMax + latPad, - longitude: lonMax + lonPad, + CameraUpdate.newGeometry( + Geometry.fromBoundingBox( + BoundingBox( + southWest: Point( + latitude: latMin - latPad, + longitude: lonMin - lonPad, + ), + northEast: Point( + latitude: latMax + latPad, + longitude: lonMax + lonPad, + ), + ), ), - ))), + ), animation: const MapAnimation(duration: 0.5), ); }, @@ -698,20 +781,24 @@ class _MapScreenState extends ConsumerState { latitude: _userPosition!.latitude, longitude: _userPosition!.longitude, ), - icon: PlacemarkIcon.single(PlacemarkIconStyle( - image: BitmapDescriptor.fromBytes(_userLocationBytes!), - scale: 1.0, - )), + icon: PlacemarkIcon.single( + PlacemarkIconStyle( + image: BitmapDescriptor.fromBytes(_userLocationBytes!), + scale: 1.0, + ), + ), ), if (isNavigating && _navArrowBytes != null) PlacemarkMapObject( mapId: const MapObjectId('nav_arrow'), - point: navState!.currentPosition, + point: navState.currentPosition, opacity: 1.0, - icon: PlacemarkIcon.single(PlacemarkIconStyle( - image: BitmapDescriptor.fromBytes(_navArrowBytes!), - scale: 1.0, - )), + icon: PlacemarkIcon.single( + PlacemarkIconStyle( + image: BitmapDescriptor.fromBytes(_navArrowBytes!), + scale: 1.0, + ), + ), ), if (destination != null && _destinationPinBytes != null) PlacemarkMapObject( @@ -720,273 +807,302 @@ class _MapScreenState extends ConsumerState { latitude: destination.latitude, longitude: destination.longitude, ), - icon: PlacemarkIcon.single(PlacemarkIconStyle( - image: BitmapDescriptor.fromBytes(_destinationPinBytes!), - scale: 1.0, - )), + icon: PlacemarkIcon.single( + PlacemarkIconStyle( + image: BitmapDescriptor.fromBytes(_destinationPinBytes!), + scale: 1.0, + ), + ), ), ]; + final isDark = Theme.of(context).brightness == Brightness.dark; + return Scaffold( - body: SafeArea(child: Stack( - children: [ - YandexMap( - mapObjects: mapObjects, - onMapCreated: (controller) async { - _mapController = controller; - const fallback = Point(latitude: 61.789114, longitude: 34.359757); - _lastCameraTarget = fallback; - await controller.moveCamera( - CameraUpdate.newCameraPosition( - const CameraPosition(target: fallback, zoom: 14), - ), - ); - _fetchZones(); - final pos = await _getCurrentPosition(); - if (pos != null && mounted) { - final target = Point(latitude: 61.789114, longitude: 34.359757); - _lastCameraTarget = target; + body: SafeArea( + child: Stack( + children: [ + YandexMap( + mapObjects: mapObjects, + nightModeEnabled: isDark, + onMapCreated: (controller) async { + _mapController = controller; + const fallback = Point( + latitude: 61.789114, + longitude: 34.359757, + ); + _lastCameraTarget = fallback; await controller.moveCamera( CameraUpdate.newCameraPosition( - CameraPosition(target: target, zoom: 15), + const CameraPosition(target: fallback, zoom: 14), ), - animation: const MapAnimation(duration: 0.8), ); _fetchZones(); - } - }, - onCameraPositionChanged: _onCameraPositionChanged, - ), - // ─── Top bar ─────────────────────────────────────────────── - if (!isNavigating) - SafeArea( - child: Padding( - padding: const EdgeInsets.all(12), - child: Row( - children: [ - Expanded( - child: GestureDetector( - onTap: _openSearch, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.1), - blurRadius: 8, + final pos = await _getCurrentPosition(); + if (pos != null && mounted) { + final target = Point( + latitude: 61.789114, + longitude: 34.359757, + ); + _lastCameraTarget = target; + await controller.moveCamera( + CameraUpdate.newCameraPosition( + CameraPosition(target: target, zoom: 15), + ), + animation: const MapAnimation(duration: 0.8), + ); + _fetchZones(); + } + }, + onCameraPositionChanged: _onCameraPositionChanged, + ), + // ─── Top bar ─────────────────────────────────────────────── + if (!isNavigating) + SafeArea( + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + Expanded( + child: GestureDetector( + onTap: _openSearch, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, ), - ], - ), - child: Row( - children: [ - const Icon(Icons.search, - color: AppColors.textSecondary), - const SizedBox(width: 8), - Expanded( - child: Text( - destination?.name ?? 'Найти место назначения', - style: const TextStyle( - color: AppColors.textSecondary), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.1), + blurRadius: 8, + ), + ], ), - ], + child: Row( + children: [ + Icon( + Icons.search, + color: Theme.of(context).hintColor, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + destination?.name ?? s.searchPlaceholder, + style: TextStyle( + color: Theme.of(context).hintColor, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), ), ), - ), + const SizedBox(width: 8), + _MapButton( + icon: Icons.tune, + onTap: () => _showFilters(context), + ), + const SizedBox(width: 4), + _MapButton( + icon: Icons.person_outlined, + onTap: () => context.push('/profile'), + ), + ], ), - const SizedBox(width: 8), - _MapButton( - icon: Icons.tune, - onTap: () => _showFilters(context)), - const SizedBox(width: 4), - _MapButton( - icon: Icons.person_outlined, - onTap: () => context.push('/profile')), - ], + ), ), - ), - ), -Positioned( - right: 12, - top: 0, - bottom: 0, - child: Align( - alignment: Alignment.center, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - _MapButton( - onTap: _resetNorth, - child: Transform.rotate( - angle: -_currentAzimuth * math.pi / 180, - child: const Icon(Icons.explore, - size: 22, color: Color(0xFF424242)), + Positioned( + right: 12, + top: 0, + bottom: 0, + child: Align( + alignment: Alignment.center, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _MapButton( + onTap: _resetNorth, + child: Transform.rotate( + angle: -_currentAzimuth * math.pi / 180, + child: Icon( + Icons.explore, + size: 22, + color: Theme.of(context).colorScheme.onSurface, + ), + ), ), - ), - const SizedBox(height: 4), - _MapButton(icon: Icons.add, onTap: _zoomIn), - const SizedBox(height: 4), - _MapButton(icon: Icons.remove, onTap: _zoomOut), - const SizedBox(height: 4), - _MapButton(icon: Icons.my_location, onTap: _goToMyLocation), - ], + const SizedBox(height: 4), + _MapButton(icon: Icons.add, onTap: _zoomIn), + const SizedBox(height: 4), + _MapButton(icon: Icons.remove, onTap: _zoomOut), + const SizedBox(height: 4), + _MapButton(icon: Icons.my_location, onTap: _goToMyLocation), + ], + ), ), ), - ), - // ─── Селектор времени: всегда левый край ────────────── - if (destination == null && !isNavigating) - Positioned( - bottom: bottomInset + 20, - left: 16, - child: const TimeSelectorWidget(), - ), - // ─── FAB: всегда правый край ─────────────────────────── - if (destination == null && !isNavigating) - Positioned( - bottom: bottomInset + 20, - right: 16, - child: FloatingActionButton.extended( - heroTag: 'find_parking', - onPressed: isRoutingLoading ? null : _findParking, - backgroundColor: AppColors.primary, - icon: const Icon(Icons.local_parking, color: Colors.white), - label: Text( - isRoutingLoading ? 'Ищем...' : 'Припарковаться', - style: const TextStyle(color: Colors.white), + // ─── Селектор времени: всегда левый край ────────────── + if (destination == null && !isNavigating) + Positioned( + bottom: bottomInset + 20, + left: 16, + child: const TimeSelectorWidget(), + ), + // ─── FAB: всегда правый край ─────────────────────────── + if (destination == null && !isNavigating) + Positioned( + bottom: bottomInset + 20, + right: 16, + child: FloatingActionButton.extended( + heroTag: 'find_parking', + onPressed: isRoutingLoading ? null : _findParking, + backgroundColor: AppColors.primary, + icon: const Icon(Icons.local_parking, color: Colors.white), + label: Text( + isRoutingLoading ? s.searching : s.findParking, + style: const TextStyle(color: Colors.white), + ), ), ), - ), - // ─── Карточка назначения ──────────────────────────────────── - if (destination != null && !isNavigating) - Positioned( - bottom: bottomInset + 76, - left: 12, - right: 12, - child: _DestinationCard( - destination: destination, - onFindParking: isRoutingLoading ? null : _findParking, - onNavigate: () => openYandexNavigator( - destination.latitude, destination.longitude), - onNavigateInApp: () => _startInAppNavigation( + // ─── Карточка назначения ──────────────────────────────────── + if (destination != null && !isNavigating) + Positioned( + bottom: bottomInset + 76, + left: 12, + right: 12, + child: _DestinationCard( + destination: destination, + onFindParking: isRoutingLoading ? null : _findParking, + onNavigate: () => openYandexNavigator( + destination.latitude, + destination.longitude, + ), + onNavigateInApp: () => _startInAppNavigation( zoneId: 0, toLat: destination.latitude, - toLon: destination.longitude), - onClear: () { - ref.read(destinationProvider.notifier).state = null; - ref.read(routingProvider.notifier).reset(); - }, + toLon: destination.longitude, + ), + onClear: () { + ref.read(destinationProvider.notifier).state = null; + ref.read(routingProvider.notifier).reset(); + }, + ), ), - ), - // ─── Loading indicators ───────────────────────────────────── - if (zonesAsync is AsyncLoading) - const Positioned( - top: 100, - left: 0, - right: 0, - child: Center( - child: SizedBox( - width: 24, - height: 24, - child: CircularProgressIndicator(strokeWidth: 2), + // ─── Loading indicators ───────────────────────────────────── + if (zonesAsync is AsyncLoading) + const Positioned( + top: 100, + left: 0, + right: 0, + child: Center( + child: SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator(strokeWidth: 2), + ), ), ), - ), - if (isNavigating) - const Positioned( - top: 0, - left: 0, - right: 0, - child: NavigationTurnCard(), - ), - if (isNavigating) - Positioned( - bottom: 0, - left: 0, - right: 0, - child: NavigationBottomBar( - onFinish: () { - ref.read(navigationProvider.notifier).stop(); - ref.read(routingProvider.notifier).reset(); - }, + if (isNavigating) + const Positioned( + top: 0, + left: 0, + right: 0, + child: NavigationTurnCard(), ), - ), - if (_navBuilding) - const Positioned.fill( - child: ColoredBox( - color: Color(0x55000000), + if (isNavigating) + Positioned( + bottom: 0, + left: 0, + right: 0, + child: NavigationBottomBar( + onFinish: () { + ref.read(navigationProvider.notifier).stop(); + ref.read(routingProvider.notifier).reset(); + }, + ), + ), + if (_navBuilding) + Positioned.fill( + child: ColoredBox( + color: Colors.black.withValues(alpha: 0.3), + child: Center( + child: Card( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 14, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator(strokeWidth: 2), + ), + const SizedBox(width: 12), + Text(s.searching), + ], + ), + ), + ), + ), + ), + ), + if (isRoutingLoading) + Positioned( + top: 148, + left: 0, + right: 0, child: Center( - child: Card( + child: DecoratedBox( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.1), + blurRadius: 8, + ), + ], + ), child: Padding( - padding: EdgeInsets.symmetric(horizontal: 20, vertical: 14), + padding: const EdgeInsets.symmetric( + horizontal: 14, + vertical: 8, + ), child: Row( mainAxisSize: MainAxisSize.min, children: [ - SizedBox( - width: 18, - height: 18, + const SizedBox( + width: 16, + height: 16, child: CircularProgressIndicator(strokeWidth: 2), ), - SizedBox(width: 12), - Text('Строим маршрут...'), + const SizedBox(width: 8), + Text(s.searching), ], ), ), ), ), ), - ), - if (isRoutingLoading) - Positioned( - top: 148, - left: 0, - right: 0, - child: Center( - child: DecoratedBox( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(20), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.1), - blurRadius: 8, - ), - ], - ), - child: const Padding( - padding: - EdgeInsets.symmetric(horizontal: 14, vertical: 8), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator(strokeWidth: 2), - ), - SizedBox(width: 8), - Text('Поиск маршрута...'), - ], - ), - ), - ), - ), - ), - ], - )), + ], + ), + ), ); } } // ─────────────────────────────────────────────────────────────────────────── -class _DestinationCard extends StatelessWidget { +class _DestinationCard extends ConsumerWidget { const _DestinationCard({ required this.destination, required this.onFindParking, @@ -1002,11 +1118,12 @@ class _DestinationCard extends StatelessWidget { final VoidCallback onClear; @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final s = ref.watch(l10nProvider); return Container( padding: const EdgeInsets.fromLTRB(16, 12, 8, 12), decoration: BoxDecoration( - color: Colors.white, + color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( @@ -1026,19 +1143,24 @@ class _DestinationCard extends StatelessWidget { const SizedBox(width: 8), Expanded( child: Text( - destination.name ?? 'Выбранное место', + destination.name ?? s.selectedPlace, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( - fontSize: 14, fontWeight: FontWeight.w600), + fontSize: 14, + fontWeight: FontWeight.w600, + ), ), ), GestureDetector( onTap: onClear, - child: const Padding( - padding: EdgeInsets.all(8), - child: Icon(Icons.close, - size: 18, color: AppColors.textSecondary), + child: Padding( + padding: const EdgeInsets.all(8), + child: Icon( + Icons.close, + size: 18, + color: Theme.of(context).hintColor, + ), ), ), ], @@ -1047,7 +1169,7 @@ class _DestinationCard extends StatelessWidget { FilledButton.icon( onPressed: onFindParking, icon: const Icon(Icons.local_parking, size: 16), - label: const Text('Искать парковку рядом'), + label: Text(s.searchParkingNear), style: FilledButton.styleFrom( textStyle: const TextStyle(fontSize: 13), padding: const EdgeInsets.symmetric(vertical: 10), @@ -1060,7 +1182,7 @@ class _DestinationCard extends StatelessWidget { child: FilledButton.icon( onPressed: onNavigateInApp, icon: const Icon(Icons.map_outlined, size: 16), - label: const Text('В приложении'), + label: Text(s.inAppRoute), style: FilledButton.styleFrom( textStyle: const TextStyle(fontSize: 13), padding: const EdgeInsets.symmetric(vertical: 10), @@ -1075,7 +1197,7 @@ class _DestinationCard extends StatelessWidget { textStyle: const TextStyle(fontSize: 13), padding: const EdgeInsets.symmetric(vertical: 10), ), - child: const Text('Яндекс'), + child: Text(s.yandexNavigator), ), ), ], @@ -1102,7 +1224,7 @@ class _MapButton extends StatelessWidget { child: Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( - color: Colors.white, + color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( @@ -1111,8 +1233,13 @@ class _MapButton extends StatelessWidget { ), ], ), - child: child ?? - Icon(icon!, size: 22, color: const Color(0xFF424242)), + child: + child ?? + Icon( + icon!, + size: 22, + color: Theme.of(context).colorScheme.onSurface, + ), ), ); } @@ -1125,6 +1252,7 @@ class _FiltersSheet extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final s = ref.watch(l10nProvider); final filters = ref.watch(filtersProvider); final notifier = ref.read(filtersProvider.notifier); final hasPayLimit = filters.maxPayPerHour != null; @@ -1135,14 +1263,18 @@ class _FiltersSheet extends ConsumerWidget { initialChildSize: 0.75, maxChildSize: 0.95, builder: (_, controller) => Container( - decoration: const BoxDecoration( - color: Color(0xFFE8F5E9), - borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), ), child: ListView( controller: controller, padding: EdgeInsets.fromLTRB( - 20, 12, 20, MediaQuery.of(context).padding.bottom + 24), + 20, + 12, + 20, + MediaQuery.of(context).padding.bottom + 24, + ), children: [ Center( child: Container( @@ -1157,48 +1289,48 @@ class _FiltersSheet extends ConsumerWidget { const SizedBox(height: 16), Row( children: [ - const Expanded( + Expanded( child: Text( - 'Фильтры', - style: - TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + s.filters, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), ), ), - TextButton( - onPressed: notifier.reset, - child: const Text('Сбросить')), + TextButton(onPressed: notifier.reset, child: Text(s.reset)), ], ), SwitchListTile( - title: const Text('Скрыть занятые'), + title: Text(s.hideFull), value: filters.hideNoFreeSpots, onChanged: (_) => notifier.toggleHideNoFreeSpots(), ), SwitchListTile( - title: const Text('Скрыть частные'), + title: Text(s.hidePrivate), value: filters.hidePrivate, onChanged: (_) => notifier.toggleHidePrivate(), ), SwitchListTile( - title: const Text('Скрыть места для инвалидов'), + title: Text(s.hideInaccessible), value: filters.hideInaccessible, onChanged: (_) => notifier.toggleHideInaccessible(), ), SwitchListTile( - title: const Text('Скрыть закрытые'), + title: Text(s.hideInactive), value: filters.hideInactive, onChanged: (_) => notifier.toggleHideInactive(), ), const Divider(), - const Text( - 'Минимальная уверенность', - style: TextStyle(fontWeight: FontWeight.w600), + Text( + s.minConfidence, + style: const TextStyle(fontWeight: FontWeight.w600), ), const SizedBox(height: 4), Text( '${(filters.minConfidence * 100).round()}%', - style: const TextStyle( - color: AppColors.textSecondary, + style: TextStyle( + color: Theme.of(context).hintColor, fontWeight: FontWeight.w500, ), ), @@ -1212,7 +1344,7 @@ class _FiltersSheet extends ConsumerWidget { ), const Divider(), SwitchListTile( - title: const Text('Ограничить стоимость'), + title: Text(s.limitPrice), value: hasPayLimit, onChanged: (value) { notifier.setMaxPay(value ? 200 : null); @@ -1223,9 +1355,9 @@ class _FiltersSheet extends ConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - '$payValue ₽/ч', - style: const TextStyle( - color: AppColors.textSecondary, + '$payValue ₽/${s.hourSign}', + style: TextStyle( + color: Theme.of(context).hintColor, fontWeight: FontWeight.w500, ), ), @@ -1234,22 +1366,21 @@ class _FiltersSheet extends ConsumerWidget { min: 0, max: 500, divisions: 20, - label: '$payValue ₽/ч', - onChanged: (value) => - notifier.setMaxPay(value.round()), + label: '$payValue ₽/${s.hourSign}', + onChanged: (value) => notifier.setMaxPay(value.round()), ), ], ), const Divider(), - const Text( - 'Минимум свободных мест', - style: TextStyle(fontWeight: FontWeight.w600), + Text( + s.minFreeSpots, + style: const TextStyle(fontWeight: FontWeight.w600), ), const SizedBox(height: 4), Text( - filters.minFreeCount == 0 ? 'Любое' : '${filters.minFreeCount}+', - style: const TextStyle( - color: AppColors.textSecondary, + filters.minFreeCount == 0 ? s.any : '${filters.minFreeCount}+', + style: TextStyle( + color: Theme.of(context).hintColor, fontWeight: FontWeight.w500, ), ), @@ -1258,34 +1389,34 @@ class _FiltersSheet extends ConsumerWidget { min: 0, max: 20, divisions: 20, - label: filters.minFreeCount == 0 ? 'Любое' : '${filters.minFreeCount}+', + label: filters.minFreeCount == 0 + ? s.any + : '${filters.minFreeCount}+', onChanged: (v) => notifier.setMinFreeCount(v.round()), ), const Divider(), - const Text( - 'Тип парковки', - style: TextStyle(fontWeight: FontWeight.w600), + Text( + s.locationType, + style: const TextStyle(fontWeight: FontWeight.w600), ), const SizedBox(height: 8), for (final entry in { - 'street': 'Уличная', - 'yard': 'Дворовая', - 'open_lot': 'Открытая стоянка', - 'underground': 'Подземная', - 'multilevel': 'Многоуровневая', + 'street': s.street, + 'yard': s.yard, + 'open_lot': s.openLot, + 'underground': s.underground, + 'multilevel': s.multilevel, }.entries) CheckboxListTile( title: Text(entry.value), value: !filters.hiddenLocationTypes.contains(entry.key), - onChanged: (_) => - notifier.toggleLocationType(entry.key), + onChanged: (_) => notifier.toggleLocationType(entry.key), dense: true, ), const SizedBox(height: 16), FilledButton( - style: FilledButton.styleFrom(backgroundColor: AppColors.primary), onPressed: () => Navigator.pop(context), - child: const Text('Применить', style: TextStyle(color: Colors.white)), + child: Text(s.apply), ), ], ), @@ -1293,4 +1424,3 @@ class _FiltersSheet extends ConsumerWidget { ); } } - diff --git a/lib/presentation/screens/map/widgets/candidates_sheet.dart b/lib/presentation/screens/map/widgets/candidates_sheet.dart index a5d29cd..5f618e1 100644 --- a/lib/presentation/screens/map/widgets/candidates_sheet.dart +++ b/lib/presentation/screens/map/widgets/candidates_sheet.dart @@ -1,36 +1,35 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../../domain/models/route_result.dart'; import '../../../../core/theme/app_colors.dart'; +import '../../../../core/localization/app_localizations.dart'; +import '../../../../core/utils/nav_math.dart'; -String _formatDuration(int seconds) { - final mins = (seconds / 60).round(); - if (mins < 60) return '~$mins мин'; - final h = mins ~/ 60; - final m = mins % 60; - return m == 0 ? '~${h}ч' : '~${h}ч ${m}мин'; -} - -class _CandidateSubtitle extends StatelessWidget { +class _CandidateSubtitle extends ConsumerWidget { final RouteCandidate candidate; const _CandidateSubtitle({required this.candidate}); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final s = ref.watch(l10nProvider); final parts = [ - 'Свободно: ${candidate.freeCount}', - candidate.pay == 0 ? 'бесплатно' : '${candidate.pay} ₽/ч', + '${s.free}: ${candidate.freeCount}', + candidate.pay == 0 ? s.free.toLowerCase() : '${candidate.pay} ₽/${s.hourSign}', ]; - if (candidate.distanceToDestinationMeters != null) - parts.add('${(candidate.distanceToDestinationMeters! / 1000).toStringAsFixed(1)} км'); - if (candidate.durationFromOriginSeconds != null) - parts.add(_formatDuration(candidate.durationFromOriginSeconds!)); - if (candidate.predictedFreeCount != null) - parts.add('прогноз: ${candidate.predictedFreeCount}'); + if (candidate.distanceToDestinationMeters != null) { + parts.add(formatNavDistance(candidate.distanceToDestinationMeters!, s)); + } + if (candidate.durationFromOriginSeconds != null) { + parts.add(formatNavDuration(candidate.durationFromOriginSeconds!, s)); + } + if (candidate.predictedFreeCount != null) { + parts.add('${s.forecast.toLowerCase()}: ${candidate.predictedFreeCount}'); + } return Text(parts.join(' • '), overflow: TextOverflow.ellipsis, maxLines: 2); } } -class CandidatesSheet extends StatefulWidget { +class CandidatesSheet extends ConsumerStatefulWidget { const CandidatesSheet({ super.key, required this.candidates, @@ -43,23 +42,24 @@ class CandidatesSheet extends StatefulWidget { final VoidCallback onSelectOnMap; @override - State createState() => _CandidatesSheetState(); + ConsumerState createState() => _CandidatesSheetState(); } -class _CandidatesSheetState extends State { +class _CandidatesSheetState extends ConsumerState { bool _listMode = true; @override Widget build(BuildContext context) { + final s = ref.watch(l10nProvider); return DraggableScrollableSheet( expand: false, initialChildSize: 0.55, minChildSize: 0.4, maxChildSize: 0.85, builder: (_, controller) => Container( - decoration: const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), ), child: ListView( controller: controller, @@ -76,15 +76,15 @@ class _CandidatesSheetState extends State { ), ), const SizedBox(height: 16), - const Text( - 'Выберите парковку', - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + Text( + s.selectParking, + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), const SizedBox(height: 10), SegmentedButton( - segments: const [ - ButtonSegment(value: true, label: Text('Список')), - ButtonSegment(value: false, label: Text('Карта')), + segments: [ + ButtonSegment(value: true, label: Text(s.list)), + ButtonSegment(value: false, label: Text(s.map)), ], selected: {_listMode}, onSelectionChanged: (value) { @@ -97,22 +97,22 @@ class _CandidatesSheetState extends State { Card( margin: const EdgeInsets.symmetric(vertical: 6), child: ListTile( - title: Text('Зона #${candidate.zoneId}'), + title: Text('${s.parkingZone} #${candidate.zoneId}'), subtitle: _CandidateSubtitle(candidate: candidate), trailing: const Icon(Icons.chevron_right), onTap: () => widget.onSelectByList(candidate.zoneId), ), ), ] else ...[ - const Text( - 'Нажмите «Выбирать на карте», затем тапните подходящую зону на карте.', - style: TextStyle(color: AppColors.textSecondary), + Text( + s.pickOnMapInstruction, + style: const TextStyle(color: AppColors.textSecondary), ), const SizedBox(height: 12), FilledButton.icon( onPressed: widget.onSelectOnMap, icon: const Icon(Icons.map_outlined), - label: const Text('Выбирать на карте'), + label: Text(s.pickOnMapAction), ), ], ], diff --git a/lib/presentation/screens/map/widgets/navigation_overlay.dart b/lib/presentation/screens/map/widgets/navigation_overlay.dart index 15ad5cd..aa35398 100644 --- a/lib/presentation/screens/map/widgets/navigation_overlay.dart +++ b/lib/presentation/screens/map/widgets/navigation_overlay.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../../core/theme/app_colors.dart'; import '../../../../core/utils/nav_math.dart'; +import '../../../../core/localization/app_localizations.dart'; import '../../../providers/navigation_provider.dart'; class NavigationTurnCard extends ConsumerWidget { @@ -34,7 +35,7 @@ class NavigationBottomBar extends ConsumerWidget { // ─── Turn instruction card ───────────────────────────────────────────────── -class _TurnCard extends StatelessWidget { +class _TurnCard extends ConsumerWidget { const _TurnCard({required this.turn, required this.isOffRoute, required this.hasArrived}); final NavTurn? turn; @@ -42,7 +43,8 @@ class _TurnCard extends StatelessWidget { final bool hasArrived; @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final s = ref.watch(l10nProvider); final Color bg; final IconData icon; final String primary; @@ -51,23 +53,23 @@ class _TurnCard extends StatelessWidget { if (hasArrived) { bg = AppColors.primary; icon = Icons.local_parking; - primary = 'Вы прибыли'; + primary = s.arrival; secondary = null; } else if (isOffRoute) { bg = Colors.orange.shade800; icon = Icons.warning_rounded; - primary = 'Пересчёт маршрута...'; + primary = s.recalculating; secondary = null; } else if (turn != null) { final t = turn!; bg = AppColors.primary; icon = _turnIcon(t.direction); - primary = _turnLabel(t.direction); - secondary = 'через ${formatNavDistance(t.distanceMeters)}'; + primary = _turnLabel(t.direction, s); + secondary = '${s.inWord} ${formatNavDistance(t.distanceMeters, s)}'; } else { bg = AppColors.primary; icon = Icons.arrow_upward_rounded; - primary = 'Движение прямо'; + primary = s.straightAhead; secondary = null; } @@ -135,42 +137,42 @@ class _TurnCard extends StatelessWidget { TurnDirection.straight => Icons.arrow_upward_rounded, }; - String _turnLabel(TurnDirection d) => switch (d) { - TurnDirection.left => 'Поверните налево', - TurnDirection.slightLeft => 'Держитесь левее', - TurnDirection.right => 'Поверните направо', - TurnDirection.slightRight => 'Держитесь правее', - TurnDirection.uTurn => 'Выполните разворот', - TurnDirection.arrive => 'Вы прибыли', - TurnDirection.straight => 'Движение прямо', + String _turnLabel(TurnDirection d, AppStrings s) => switch (d) { + TurnDirection.left => s.turnLeft, + TurnDirection.slightLeft => s.keepLeft, + TurnDirection.right => s.turnRight, + TurnDirection.slightRight => s.keepRight, + TurnDirection.uTurn => s.uTurn, + TurnDirection.arrive => s.arrival, + TurnDirection.straight => s.straightAhead, }; } // ─── Bottom stats bar ────────────────────────────────────────────────────── -class _NavBottomBar extends StatelessWidget { +class _NavBottomBar extends ConsumerWidget { const _NavBottomBar({required this.nav, required this.onFinish}); final NavigationData nav; final VoidCallback onFinish; - void _confirmFinish(BuildContext context) { + void _confirmFinish(BuildContext context, AppStrings s) { showDialog( context: context, builder: (_) => AlertDialog( - title: const Text('Завершить маршрут?'), - content: const Text('Навигация будет остановлена.'), + title: Text(s.finishConfirmTitle), + content: Text(s.finishConfirmContent), actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: const Text('Нет'), + child: Text(s.cancel), ), FilledButton( onPressed: () { Navigator.pop(context); onFinish(); }, - child: const Text('Завершить'), + child: Text(s.finish), ), ], ), @@ -178,10 +180,11 @@ class _NavBottomBar extends StatelessWidget { } @override - Widget build(BuildContext context) { - final speedColor = nav.speedKmh > 90 ? Colors.red.shade600 : AppColors.onSurface; + Widget build(BuildContext context, WidgetRef ref) { + final s = ref.watch(l10nProvider); + final speedColor = nav.speedKmh > 90 ? Colors.red.shade600 : Theme.of(context).colorScheme.onSurface; return Container( - color: Colors.white, + color: Theme.of(context).colorScheme.surface, padding: EdgeInsets.fromLTRB( 12, 10, 4, MediaQuery.of(context).padding.bottom + 10, ), @@ -189,31 +192,31 @@ class _NavBottomBar extends StatelessWidget { children: [ Expanded( child: _StatCell( - value: formatNavDuration(nav.remainingSeconds), - label: 'времени', + value: formatNavDuration(nav.remainingSeconds, s), + label: s.timeLabel, ), ), const SizedBox(width: 12), Expanded( child: _StatCell( - value: formatNavDistance(nav.remainingMeters), - label: 'до цели', + value: formatNavDistance(nav.remainingMeters, s), + label: s.distanceLabel, ), ), const SizedBox(width: 12), Expanded( child: _StatCell( value: '${nav.speedKmh.round()}', - label: 'км/ч', + label: s.speedLabel, valueColor: speedColor, ), ), IconButton( - onPressed: () => _confirmFinish(context), + onPressed: () => _confirmFinish(context, s), icon: const Icon(Icons.stop_circle_outlined), - color: AppColors.textSecondary, + color: Theme.of(context).hintColor, iconSize: 28, - tooltip: 'Завершить', + tooltip: s.finish, ), ], ), @@ -242,15 +245,15 @@ class _StatCell extends StatelessWidget { style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, - color: valueColor ?? AppColors.onSurface, + color: valueColor ?? Theme.of(context).colorScheme.onSurface, height: 1, ), ), Text( label, - style: const TextStyle( + style: TextStyle( fontSize: 12, - color: AppColors.textSecondary, + color: Theme.of(context).hintColor, height: 1.2, ), ), diff --git a/lib/presentation/screens/map/widgets/parking_card_sheet.dart b/lib/presentation/screens/map/widgets/parking_card_sheet.dart index cba9e5c..d3f3189 100644 --- a/lib/presentation/screens/map/widgets/parking_card_sheet.dart +++ b/lib/presentation/screens/map/widgets/parking_card_sheet.dart @@ -4,6 +4,7 @@ import '../../../../domain/models/zone.dart'; import '../../../../core/theme/app_colors.dart'; import '../../../../presentation/providers/time_selector_provider.dart'; import '../../../../presentation/providers/zones_provider.dart'; +import '../../../../core/localization/app_localizations.dart'; Future showParkingCard( BuildContext context, @@ -25,6 +26,7 @@ class _ParkingCardSheet extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final s = ref.watch(l10nProvider); final timeMode = ref.watch(timeSelectorProvider); final isFuture = timeMode.maybeWhen(future: (_) => true, orElse: () => false); final isPast = timeMode.maybeWhen(past: (_) => true, orElse: () => false); @@ -44,9 +46,9 @@ class _ParkingCardSheet extends ConsumerWidget { ), child: SingleChildScrollView( child: Container( - decoration: const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), ), padding: const EdgeInsets.fromLTRB(20, 12, 20, 24), child: Column( @@ -67,7 +69,7 @@ class _ParkingCardSheet extends ConsumerWidget { children: [ Expanded( child: Text( - 'Парковка #${currentZone.zoneId}', + '${s.parkingZone} #${currentZone.zoneId}', style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, @@ -75,29 +77,29 @@ class _ParkingCardSheet extends ConsumerWidget { ), ), if (currentZone.isPrivate == true) - _Badge('Частная', Colors.orange), + _Badge(s.private, Colors.orange), if (currentZone.isAccessible == true) _Badge('♿', AppColors.primary), ], ), const SizedBox(height: 12), if (isLoading) - const Padding( - padding: EdgeInsets.symmetric(vertical: 24), + Padding( + padding: const EdgeInsets.symmetric(vertical: 24), child: Center( child: Text( - 'Данные загружаются...', - style: TextStyle(color: AppColors.textSecondary), + s.loadingData, + style: const TextStyle(color: AppColors.textSecondary), ), ), ) else ...[ _InfoRow( icon: Icons.local_parking, - label: 'Мест', + label: s.capacity, value: currentZone.hasForecast - ? '${currentZone.freeCount} / ${currentZone.capacity} свободно' - : 'Нет прогноза', + ? '${currentZone.freeCount} / ${currentZone.capacity} ${s.free.toLowerCase()}' + : s.noForecast, valueColor: currentZone.hasForecast ? null : AppColors.textSecondary, ), @@ -106,50 +108,50 @@ class _ParkingCardSheet extends ConsumerWidget { currentZone.forecastFor != null) _InfoRow( icon: Icons.schedule, - label: 'Прогноз на', - value: _formatDateTime(currentZone.forecastFor!), + label: '${s.forecast} ${s.forWord}', + value: _formatDateTime(currentZone.forecastFor!, s), ), if (isFuture && currentZone.hasForecast && currentZone.forecastGeneratedAt != null) _InfoRow( icon: Icons.build_circle_outlined, - label: 'Создан', - value: _formatDateTime(currentZone.forecastGeneratedAt!), + label: s.generated, + value: _formatDateTime(currentZone.forecastGeneratedAt!, s), ), _InfoRow( icon: Icons.payments_outlined, - label: 'Стоимость', + label: s.pay, value: currentZone.pay == 0 - ? 'Бесплатно' - : '${currentZone.pay} ₽/ч', + ? s.free + : '${currentZone.pay} ₽/${s.hourSign}', valueColor: currentZone.pay == 0 ? AppColors.primary : null, ), if (!isPast) _InfoRow( icon: Icons.verified_outlined, - label: 'Уверенность', + label: s.confidence, value: _confidenceLabel(currentZone), ), if (currentZone.locationType != null) _InfoRow( icon: Icons.place_outlined, - label: 'Тип', - value: _locationTypeLabel(currentZone.locationType!), + label: s.type, + value: _locationTypeLabel(currentZone.locationType!, s), ), _InfoRow( icon: Icons.directions_car_outlined, - label: 'Постановка', + label: s.parkingType, value: currentZone.zoneType == ZoneType.parallel - ? 'Параллельная' - : 'Обычная', + ? s.parallel + : s.regular, ), - if (!isFuture && currentZone.occupancyUpdatedAt != null) + if (!isPast && currentZone.occupancyUpdatedAt != null) _InfoRow( icon: Icons.update, - label: 'Обновлено', - value: _formatDateTime(currentZone.occupancyUpdatedAt!), + label: s.updatedAt, + value: _formatDateTime(currentZone.occupancyUpdatedAt!, s), ), if (isFuture && currentZone.forecastFor != null && @@ -157,6 +159,7 @@ class _ParkingCardSheet extends ConsumerWidget { _StaleForecastBanner( forecastFor: currentZone.forecastFor!, userSelectedAt: userSelectedAt, + s: s, onSnap: () { ref .read(timeSelectorProvider.notifier) @@ -166,13 +169,16 @@ class _ParkingCardSheet extends ConsumerWidget { ), ], const SizedBox(height: 20), - FilledButton.icon( - onPressed: () { - Navigator.pop(context); - onBuildRoute?.call(); - }, - icon: const Icon(Icons.directions), - label: const Text('Построить маршрут'), + SizedBox( + width: double.infinity, + child: FilledButton.icon( + onPressed: () { + Navigator.pop(context); + onBuildRoute?.call(); + }, + icon: const Icon(Icons.directions), + label: Text(s.buildRoute), + ), ), ], ), @@ -183,12 +189,12 @@ class _ParkingCardSheet extends ConsumerWidget { String _confidenceLabel(Zone z) => '${(z.confidence * 100).round()}%'; - String _locationTypeLabel(LocationType type) => switch (type) { - LocationType.street => 'Уличная', - LocationType.yard => 'Дворовая', - LocationType.openLot => 'Открытая стоянка', - LocationType.underground => 'Подземная', - LocationType.multilevel => 'Многоуровневая', + String _locationTypeLabel(LocationType type, AppStrings s) => switch (type) { + LocationType.street => s.street, + LocationType.yard => s.yard, + LocationType.openLot => s.openLot, + LocationType.underground => s.underground, + LocationType.multilevel => s.multilevel, }; static String _formatTime(DateTime dt) { @@ -196,42 +202,28 @@ class _ParkingCardSheet extends ConsumerWidget { return '${l.hour.toString().padLeft(2, '0')}:${l.minute.toString().padLeft(2, '0')}'; } - static String _formatDateTime(DateTime dt) { + static String _formatDateTime(DateTime dt, AppStrings s) { final l = dt.toLocal(); final now = DateTime.now(); final timeStr = _formatTime(l); if (l.year == now.year && l.month == now.month && l.day == now.day) { - return 'Сегодня $timeStr'; + return '${s.today} $timeStr'; } - return '${l.day} ${_monthName(l.month)} $timeStr'; + return '${l.day} ${s.monthNames[l.month]} $timeStr'; } - - static String _monthName(int m) => const [ - '', - 'янв', - 'фев', - 'мар', - 'апр', - 'май', - 'июн', - 'июл', - 'авг', - 'сен', - 'окт', - 'ноя', - 'дек' - ][m]; } class _StaleForecastBanner extends StatelessWidget { final DateTime forecastFor; final DateTime userSelectedAt; final VoidCallback onSnap; + final AppStrings s; const _StaleForecastBanner({ required this.forecastFor, required this.userSelectedAt, required this.onSnap, + required this.s, }); @override @@ -242,28 +234,28 @@ class _StaleForecastBanner extends StatelessWidget { margin: const EdgeInsets.only(top: 12), padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: Colors.orange.shade50, + color: Colors.orange.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(10), - border: Border.all(color: Colors.orange.shade300), + border: Border.all(color: Colors.orange.withValues(alpha: 0.3)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Прогноз на ${_ParkingCardSheet._formatTime(forecastFor)}, ' - 'вы выбрали ${_ParkingCardSheet._formatTime(userSelectedAt)}', - style: TextStyle(color: Colors.orange.shade900, fontSize: 13), + '${s.forecastFor} ${_ParkingCardSheet._formatTime(forecastFor)}, ' + '${s.youPicked} ${_ParkingCardSheet._formatTime(userSelectedAt)}', + style: const TextStyle(color: Colors.orange, fontSize: 13), ), const SizedBox(height: 8), SizedBox( width: double.infinity, child: OutlinedButton( style: OutlinedButton.styleFrom( - foregroundColor: Colors.orange.shade800, - side: BorderSide(color: Colors.orange.shade400), + foregroundColor: Colors.orange, + side: const BorderSide(color: Colors.orange), ), onPressed: onSnap, - child: const Text('Открыть ближайшее доступное'), + child: Text(s.jumpToClosest), ), ), ], @@ -291,18 +283,18 @@ class _InfoRow extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: 6), child: Row( children: [ - Icon(icon, size: 20, color: AppColors.textSecondary), + Icon(icon, size: 20, color: Theme.of(context).hintColor), const SizedBox(width: 12), Text(label, style: - const TextStyle(color: AppColors.textSecondary, fontSize: 14)), + TextStyle(color: Theme.of(context).hintColor, fontSize: 14)), const Spacer(), Text( value, style: TextStyle( fontWeight: FontWeight.w600, fontSize: 14, - color: valueColor ?? AppColors.onSurface, + color: valueColor ?? Theme.of(context).colorScheme.onSurface, ), ), ], diff --git a/lib/presentation/screens/map/widgets/parking_zone_layer.dart b/lib/presentation/screens/map/widgets/parking_zone_layer.dart index 2e65b7c..77ce5fd 100644 --- a/lib/presentation/screens/map/widgets/parking_zone_layer.dart +++ b/lib/presentation/screens/map/widgets/parking_zone_layer.dart @@ -88,7 +88,7 @@ MapObject buildZoneLabels({ image: BitmapDescriptor.fromBytes(bitmapCache[zone.zoneId]!), scale: 1.0, )), - onTap: onZoneTap != null ? (_, __) => onZoneTap(zone) : null, + onTap: onZoneTap != null ? (_, _) => onZoneTap(zone) : null, )) .toList(); @@ -227,7 +227,7 @@ MapObject _buildPolygon(Zone zone, Color color, void Function(Zone) onTap, fillColor: color.withValues(alpha: 0.5), strokeColor: highlighted ? Colors.white : color, strokeWidth: highlighted ? 4 : 2, - onTap: (_, __) => onTap(zone), + onTap: (_, _) => onTap(zone), ); } @@ -255,7 +255,7 @@ MapObject _buildParallelLine(Zone zone, Color color, void Function(Zone) onTap, polyline: Polyline(points: [mid1, mid2]), strokeColor: highlighted ? Colors.white : color, strokeWidth: highlighted ? 9 : 6, - onTap: (_, __) => onTap(zone), + onTap: (_, _) => onTap(zone), ); } diff --git a/lib/presentation/screens/map/widgets/route_preview_sheet.dart b/lib/presentation/screens/map/widgets/route_preview_sheet.dart index d3d344a..8c4ad6d 100644 --- a/lib/presentation/screens/map/widgets/route_preview_sheet.dart +++ b/lib/presentation/screens/map/widgets/route_preview_sheet.dart @@ -3,6 +3,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../../core/theme/app_colors.dart'; import '../../../../core/utils/navigation_deeplink.dart'; import '../../../../domain/models/route_result.dart'; +import '../../../../core/localization/app_localizations.dart'; +import '../../../../core/utils/nav_math.dart'; import '../../../providers/routing_provider.dart'; class RoutePreviewSheet extends ConsumerStatefulWidget { @@ -26,24 +28,16 @@ class RoutePreviewSheet extends ConsumerStatefulWidget { class _RoutePreviewSheetState extends ConsumerState { bool _launching = false; - String _formatArrival(String iso) { + String _formatArrival(String iso, AppStrings s) { final dt = DateTime.tryParse(iso)?.toLocal(); if (dt == null) return iso; final mins = dt.difference(DateTime.now()).inMinutes; - if (mins > 0 && mins <= 90) return 'через $mins мин'; + if (mins > 0 && mins <= 90) return '${s.inWord} $mins ${s.minutesSign}'; final hh = dt.hour.toString().padLeft(2, '0'); final mm = dt.minute.toString().padLeft(2, '0'); return '$hh:$mm'; } - String _formatDuration(int seconds) { - final mins = (seconds / 60).round(); - if (mins < 60) return '~$mins мин'; - final h = mins ~/ 60; - final m = mins % 60; - return m == 0 ? '~${h}ч' : '~${h}ч ${m}мин'; - } - Future _launchNavigator() async { setState(() => _launching = true); try { @@ -56,8 +50,9 @@ class _RoutePreviewSheetState extends ConsumerState { await openYandexNavigator(lat, lon); } else { if (mounted) { + final s = ref.read(l10nProvider); ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Нет данных для навигации')), + SnackBar(content: Text(s.pointLookupError)), ); } return; @@ -70,6 +65,7 @@ class _RoutePreviewSheetState extends ConsumerState { @override Widget build(BuildContext context) { + final s = ref.watch(l10nProvider); final candidate = widget.route.candidates.firstOrNull; return DraggableScrollableSheet( expand: false, @@ -77,9 +73,9 @@ class _RoutePreviewSheetState extends ConsumerState { minChildSize: 0.3, maxChildSize: 0.7, builder: (_, controller) => Container( - decoration: const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), ), child: ListView( controller: controller, @@ -96,24 +92,26 @@ class _RoutePreviewSheetState extends ConsumerState { ), ), const SizedBox(height: 16), - const Text( - 'Маршрут готов', - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + Text( + s.routeReady, + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), const SizedBox(height: 10), - Text('Зона: #${widget.route.selectedZoneId}'), + Text('${s.parkingZone}: #${widget.route.selectedZoneId}'), if (widget.route.arrivalTime != null) - Text('Прибытие: ${_formatArrival(widget.route.arrivalTime!)}'), + Text('${s.arrival}: ${_formatArrival(widget.route.arrivalTime!, s)}'), if (candidate != null) ...[ Builder(builder: (_) { final parts = []; - if (candidate.distanceToDestinationMeters != null) - parts.add('${(candidate.distanceToDestinationMeters! / 1000).toStringAsFixed(1)} км'); - if (candidate.durationFromOriginSeconds != null) - parts.add(_formatDuration(candidate.durationFromOriginSeconds!)); + if (candidate.distanceToDestinationMeters != null) { + parts.add(formatNavDistance(candidate.distanceToDestinationMeters!, s)); + } + if (candidate.durationFromOriginSeconds != null) { + parts.add(formatNavDuration(candidate.durationFromOriginSeconds!, s)); + } if (parts.isEmpty) return const SizedBox.shrink(); return Text(parts.join(' • '), - style: const TextStyle(color: Color(0xFF666666))); + style: TextStyle(color: Theme.of(context).hintColor)); }), ], const SizedBox(height: 18), @@ -124,13 +122,13 @@ class _RoutePreviewSheetState extends ConsumerState { widget.onNavigateInApp!(); }, icon: const Icon(Icons.map_outlined), - label: const Text('Маршрут в приложении'), + label: Text(s.inAppRoute), ), const SizedBox(height: 8), FilledButton.icon( style: FilledButton.styleFrom( backgroundColor: widget.onNavigateInApp != null - ? Colors.white + ? Theme.of(context).colorScheme.surface : null, foregroundColor: widget.onNavigateInApp != null ? AppColors.primary @@ -141,7 +139,7 @@ class _RoutePreviewSheetState extends ConsumerState { ), onPressed: _launching ? null : _launchNavigator, icon: const Icon(Icons.navigation_outlined), - label: Text(_launching ? 'Открываем...' : 'Яндекс Навигатор'), + label: Text(_launching ? s.searching : s.yandexNavigator), ), const SizedBox(height: 8), TextButton( @@ -149,9 +147,9 @@ class _RoutePreviewSheetState extends ConsumerState { ref.read(routingProvider.notifier).reset(); Navigator.pop(context); }, - child: const Text( - 'Сбросить', - style: TextStyle(color: AppColors.textSecondary), + child: Text( + s.reset, + style: TextStyle(color: Theme.of(context).hintColor), ), ), ], diff --git a/lib/presentation/screens/map/widgets/time_selector_widget.dart b/lib/presentation/screens/map/widgets/time_selector_widget.dart index a4355c0..f33948f 100644 --- a/lib/presentation/screens/map/widgets/time_selector_widget.dart +++ b/lib/presentation/screens/map/widgets/time_selector_widget.dart @@ -3,12 +3,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../../presentation/providers/time_selector_provider.dart'; import '../../../../core/theme/app_colors.dart'; +import '../../../../core/localization/app_localizations.dart'; class TimeSelectorWidget extends ConsumerWidget { const TimeSelectorWidget({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { + final s = ref.watch(l10nProvider); final timeMode = ref.watch(timeSelectorProvider); final isNow = timeMode.maybeWhen(now: () => true, orElse: () => false); final selectedDt = timeMode.maybeWhen( @@ -35,7 +37,7 @@ class TimeSelectorWidget extends ConsumerWidget { children: [ if (!isNow) ...[ _Chip( - label: 'Сейчас', + label: s.now, icon: Icons.restore, active: false, onTap: () => ref.read(timeSelectorProvider.notifier).setNow(), @@ -43,7 +45,7 @@ class TimeSelectorWidget extends ConsumerWidget { const SizedBox(height: 6), ], _Chip( - label: isNow ? 'Время' : _selectionLabel(selectedDt!), + label: isNow ? s.time : _selectionLabel(selectedDt!, s), icon: Icons.access_time_outlined, active: !isNow, onTap: openPicker, @@ -68,7 +70,7 @@ class TimeSelectorWidget extends ConsumerWidget { } } - String _selectionLabel(DateTime dt) { + String _selectionLabel(DateTime dt, AppStrings s) { final now = DateTime.now(); final tomorrow = DateTime(now.year, now.month, now.day + 1); final yesterday = DateTime(now.year, now.month, now.day - 1); @@ -76,30 +78,30 @@ class TimeSelectorWidget extends ConsumerWidget { final h = dt.hour.toString().padLeft(2, '0'); final m = dt.minute.toString().padLeft(2, '0'); final time = '$h:$m'; - if (dtDay == DateTime(now.year, now.month, now.day)) return 'Сегодня $time'; - if (dtDay == DateTime(tomorrow.year, tomorrow.month, tomorrow.day)) return 'Завтра $time'; - if (dtDay == DateTime(yesterday.year, yesterday.month, yesterday.day)) return 'Вчера $time'; - const months = ['', 'янв', 'фев', 'мар', 'апр', 'май', 'июн', 'июл', 'авг', 'сен', 'окт', 'ноя', 'дек']; - return '${dt.day} ${months[dt.month]} $time'; + if (dtDay == DateTime(now.year, now.month, now.day)) return '${s.today} $time'; + if (dtDay == DateTime(tomorrow.year, tomorrow.month, tomorrow.day)) return '${s.tomorrow} $time'; + if (dtDay == DateTime(yesterday.year, yesterday.month, yesterday.day)) return '${s.yesterday} $time'; + + return '${dt.day} ${s.monthNames[dt.month]} $time'; } } // ─── Bottom sheet ─────────────────────────────────────────────────────────── -class _TimePickerSheet extends StatefulWidget { +class _TimePickerSheet extends ConsumerStatefulWidget { final DateTime? initial; final void Function(DateTime? dt) onApply; const _TimePickerSheet({required this.initial, required this.onApply}); @override - State<_TimePickerSheet> createState() => _TimePickerSheetState(); + ConsumerState<_TimePickerSheet> createState() => _TimePickerSheetState(); } -class _TimePickerSheetState extends State<_TimePickerSheet> { +class _TimePickerSheetState extends ConsumerState<_TimePickerSheet> { static const _step = 30; - late int _dayOffset; // -1 вчера, 0 сегодня, 1 завтра + late int _dayOffset; // -1 yesterday, 0 today, 1 tomorrow late DateTime _time; @override @@ -127,16 +129,17 @@ class _TimePickerSheetState extends State<_TimePickerSheet> { @override Widget build(BuildContext context) { + final s = ref.watch(l10nProvider); final days = [ - (-1, 'Вчера'), - (0, 'Сегодня'), - (1, 'Завтра'), + (-1, s.yesterday), + (0, s.today), + (1, s.tomorrow), ]; return Container( - decoration: const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), ), padding: EdgeInsets.fromLTRB( 20, 16, 20, MediaQuery.of(context).padding.bottom + 20, @@ -152,12 +155,12 @@ class _TimePickerSheetState extends State<_TimePickerSheet> { ), ), const SizedBox(height: 16), - const Text( - 'Выбрать время', - style: TextStyle(fontSize: 17, fontWeight: FontWeight.w600), + Text( + s.time, + style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w600), ), const SizedBox(height: 16), - // День + // Day Row( children: days.map((pair) { final (offset, label) = pair; @@ -170,7 +173,7 @@ class _TimePickerSheetState extends State<_TimePickerSheet> { margin: const EdgeInsets.symmetric(horizontal: 4), padding: const EdgeInsets.symmetric(vertical: 10), decoration: BoxDecoration( - color: sel ? AppColors.primary : Colors.grey.shade100, + color: sel ? AppColors.primary : Theme.of(context).colorScheme.surfaceVariant, borderRadius: BorderRadius.circular(10), ), child: Center( @@ -179,7 +182,7 @@ class _TimePickerSheetState extends State<_TimePickerSheet> { style: TextStyle( fontWeight: FontWeight.w600, fontSize: 14, - color: sel ? Colors.white : AppColors.onSurface, + color: sel ? Colors.white : Theme.of(context).colorScheme.onSurface, ), ), ), @@ -189,20 +192,25 @@ class _TimePickerSheetState extends State<_TimePickerSheet> { }).toList(), ), const SizedBox(height: 8), - // Барабан времени + // Time picker SizedBox( height: 150, - child: CupertinoDatePicker( - mode: CupertinoDatePickerMode.time, - use24hFormat: true, - minuteInterval: _step, - initialDateTime: _time, - onDateTimeChanged: (dt) { - setState(() => _time = DateTime( - _time.year, _time.month, _time.day, - dt.hour, dt.minute, - )); - }, + child: CupertinoTheme( + data: CupertinoThemeData( + brightness: Theme.of(context).brightness, + ), + child: CupertinoDatePicker( + mode: CupertinoDatePickerMode.time, + use24hFormat: true, + minuteInterval: _step, + initialDateTime: _time, + onDateTimeChanged: (dt) { + setState(() => _time = DateTime( + _time.year, _time.month, _time.day, + dt.hour, dt.minute, + )); + }, + ), ), ), const SizedBox(height: 12), @@ -214,7 +222,7 @@ class _TimePickerSheetState extends State<_TimePickerSheet> { Navigator.pop(context); widget.onApply(null); }, - child: const Text('Сейчас'), + child: Text(s.now), ), ), const SizedBox(width: 12), @@ -225,7 +233,7 @@ class _TimePickerSheetState extends State<_TimePickerSheet> { Navigator.pop(context); widget.onApply(_result); }, - child: const Text('Применить'), + child: Text(s.apply), ), ), ], @@ -253,23 +261,23 @@ class _Chip extends StatelessWidget { @override Widget build(BuildContext context) { - final borderColor = active ? AppColors.primary : Colors.grey.shade400; - final textColor = active ? AppColors.primary : AppColors.onSurface; - final iconColor = active ? AppColors.primary : AppColors.textSecondary; + final borderColor = active ? AppColors.primary : Theme.of(context).dividerColor; + final textColor = active ? AppColors.primary : Theme.of(context).colorScheme.onSurface; + final iconColor = active ? AppColors.primary : Theme.of(context).hintColor; return GestureDetector( onTap: onTap, child: Container( padding: const EdgeInsets.symmetric(horizontal: 13, vertical: 10), decoration: BoxDecoration( - color: Colors.white, + color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(20), border: Border.all(color: borderColor, width: active ? 1.5 : 1.0), - boxShadow: const [ + boxShadow: [ BoxShadow( - color: Color(0x33000000), + color: Colors.black.withValues(alpha: 0.1), blurRadius: 8, - offset: Offset(0, 3), + offset: const Offset(0, 3), ), ], ), diff --git a/lib/presentation/screens/profile/edit_profile_screen.dart b/lib/presentation/screens/profile/edit_profile_screen.dart new file mode 100644 index 0000000..da4f575 --- /dev/null +++ b/lib/presentation/screens/profile/edit_profile_screen.dart @@ -0,0 +1,125 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import '../../providers/auth_provider.dart'; +import '../../../core/localization/app_localizations.dart'; +import '../../../core/network/api_exception.dart'; + +class EditProfileScreen extends ConsumerStatefulWidget { + const EditProfileScreen({super.key}); + + @override + ConsumerState createState() => _EditProfileScreenState(); +} + +class _EditProfileScreenState extends ConsumerState { + final _formKey = GlobalKey(); + late TextEditingController _nameCtrl; + bool _loading = false; + + @override + void initState() { + super.initState(); + final user = ref.read(authStateProvider).maybeWhen( + authenticated: (u) => u, + orElse: () => null, + ); + _nameCtrl = TextEditingController(text: user?.fullName ?? ''); + } + + @override + void dispose() { + _nameCtrl.dispose(); + super.dispose(); + } + + Future _save() async { + if (!_formKey.currentState!.validate()) return; + setState(() => _loading = true); + final s = ref.read(l10nProvider); + try { + await ref.read(authStateProvider.notifier).updateProfile( + fullName: _nameCtrl.text.trim(), + ); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(s.profileUpdated)), + ); + context.pop(); + } + } on ApiException catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(e.getLocalizedMessage(s)), + backgroundColor: Colors.red, + ), + ); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(e.toString()), backgroundColor: Colors.red), + ); + } + } finally { + if (mounted) setState(() => _loading = false); + } + } + + @override + Widget build(BuildContext context) { + final s = ref.watch(l10nProvider); + return Scaffold( + appBar: AppBar( + title: Text(s.editProfile), + actions: [ + if (!_loading) + TextButton( + onPressed: _save, + child: Text(s.save, style: const TextStyle(color: Colors.white)), + ) + else + const Padding( + padding: EdgeInsets.symmetric(horizontal: 16), + child: Center( + child: SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white), + ), + ), + ), + ], + ), + body: Form( + key: _formKey, + child: ListView( + padding: const EdgeInsets.all(24), + children: [ + TextFormField( + controller: _nameCtrl, + decoration: InputDecoration( + labelText: s.name, + prefixIcon: const Icon(Icons.person_outlined), + border: const OutlineInputBorder(), + ), + validator: (v) => v == null || v.trim().isEmpty ? s.nameRequired : null, + ), + const SizedBox(height: 32), + FilledButton( + onPressed: _loading ? null : _save, + child: _loading + ? const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white), + ) + : Text(s.save), + ), + ], + ), + ), + ); + } +} diff --git a/lib/presentation/screens/profile/profile_screen.dart b/lib/presentation/screens/profile/profile_screen.dart index 26ab473..05a68e3 100644 --- a/lib/presentation/screens/profile/profile_screen.dart +++ b/lib/presentation/screens/profile/profile_screen.dart @@ -1,13 +1,26 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:url_launcher/url_launcher.dart'; import '../../providers/auth_provider.dart'; +import '../../providers/settings_provider.dart'; +import '../../../core/localization/app_localizations.dart'; class ProfileScreen extends ConsumerWidget { const ProfileScreen({super.key}); + Future _launchUrl(String url) async { + final uri = Uri.parse(url); + if (!await launchUrl(uri, mode: LaunchMode.externalApplication)) { + throw Exception('Could not launch $url'); + } + } + @override Widget build(BuildContext context, WidgetRef ref) { + final s = ref.watch(l10nProvider); final authState = ref.watch(authStateProvider); + final settings = ref.watch(settingsProvider); final user = authState.maybeWhen( authenticated: (u) => u, @@ -15,38 +28,207 @@ class ProfileScreen extends ConsumerWidget { ); return Scaffold( - appBar: AppBar(title: const Text('Профиль')), + appBar: AppBar(title: Text(s.settings)), body: ListView( - padding: const EdgeInsets.all(20), + padding: const EdgeInsets.symmetric(vertical: 16), children: [ - const CircleAvatar( - radius: 40, - child: Icon(Icons.person, size: 40), + // --- Account Section --- + _buildSectionHeader(s.account), + if (user != null) + ListTile( + leading: const CircleAvatar(child: Icon(Icons.person)), + title: Text(user.fullName ?? user.email), + subtitle: Text(user.email), + trailing: const Icon(Icons.chevron_right), + onTap: () => GoRouter.of(context).push('/profile/edit'), + ) + else + ListTile( + leading: const CircleAvatar(child: Icon(Icons.person_outline)), + title: Text(s.notAuthenticated), + ), + + const Divider(), + + // --- Appearance Section --- + _buildSectionHeader(s.appearance), + ListTile( + leading: const Icon(Icons.palette_outlined), + title: Text(s.theme), + subtitle: Text(_themeModeName(settings.themeMode, s)), + onTap: () => _showThemePicker(context, ref, s), + ), + + const Divider(), + + // --- Language Section --- + _buildSectionHeader(s.language), + ListTile( + leading: const Icon(Icons.language_outlined), + title: Text(s.language), + subtitle: Text(_languageName(settings.locale?.languageCode, s)), + onTap: () => _showLanguagePicker(context, ref, s), + ), + + const Divider(), + + // --- Information Section --- + _buildSectionHeader(s.info), + ListTile( + leading: const Icon(Icons.privacy_tip_outlined), + title: Text(s.privacyPolicy), + onTap: () => _launchUrl('https://parktrack.live/privacy'), + ), + ListTile( + leading: const Icon(Icons.delete_outline), + title: Text(s.dataErasure), + onTap: () => _launchUrl('https://parktrack.live/data-erasure'), ), - const SizedBox(height: 16), - if (user != null) ...[ - Text( - user.fullName ?? user.email, - textAlign: TextAlign.center, - style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ListTile( + leading: const Icon(Icons.code_outlined), + title: Text(s.github), + onTap: () => _launchUrl('https://github.com/ParkTrack-Project'), + ), + + const SizedBox(height: 32), + + if (user != null) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: OutlinedButton.icon( + onPressed: () => _showLogoutDialog(context, ref, s), + icon: const Icon(Icons.logout, color: Colors.red), + label: Text(s.logout, style: const TextStyle(color: Colors.red)), + style: OutlinedButton.styleFrom( + side: const BorderSide(color: Colors.red), + padding: const EdgeInsets.symmetric(vertical: 12), + ), + ), + ), + ], + ), + ); + } + + Widget _buildSectionHeader(String title) { + return Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 4), + child: Text( + title.toUpperCase(), + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: Colors.grey, + letterSpacing: 1.2, + ), + ), + ); + } + + String _themeModeName(ThemeMode mode, AppStrings s) { + switch (mode) { + case ThemeMode.system: + return s.themeSystem; + case ThemeMode.light: + return s.themeLight; + case ThemeMode.dark: + return s.themeDark; + } + } + + String _languageName(String? code, AppStrings s) { + if (code == null) return s.langSystem; + if (code == 'ru') return s.langRussian; + if (code == 'en') return s.langEnglish; + return code; + } + + void _showThemePicker(BuildContext context, WidgetRef ref, AppStrings s) { + final currentMode = ref.read(settingsProvider).themeMode; + showModalBottomSheet( + context: context, + builder: (_) => SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: Text(s.selectTheme, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), ), - Text( - user.email, - textAlign: TextAlign.center, - style: const TextStyle(color: Color(0xFF757575)), + _buildPickerItem(context, ref, s.themeSystem, ThemeMode.system, currentMode == ThemeMode.system), + _buildPickerItem(context, ref, s.themeLight, ThemeMode.light, currentMode == ThemeMode.light), + _buildPickerItem(context, ref, s.themeDark, ThemeMode.dark, currentMode == ThemeMode.dark), + const SizedBox(height: 8), + ], + ), + ), + ); + } + + Widget _buildPickerItem(BuildContext context, WidgetRef ref, String title, ThemeMode mode, bool selected) { + return ListTile( + title: Text(title), + trailing: selected ? const Icon(Icons.check, color: Colors.green) : null, + onTap: () { + ref.read(settingsProvider.notifier).setThemeMode(mode); + Navigator.pop(context); + }, + ); + } + + void _showLanguagePicker(BuildContext context, WidgetRef ref, AppStrings s) { + final currentLang = ref.read(settingsProvider).locale?.languageCode; + showModalBottomSheet( + context: context, + builder: (_) => SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: Text(s.selectLanguage, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), ), + _buildLangItem(context, ref, s.langSystem, null, currentLang == null, s), + _buildLangItem(context, ref, s.langRussian, 'ru', currentLang == 'ru', s), + _buildLangItem(context, ref, s.langEnglish, 'en', currentLang == 'en', s), + const SizedBox(height: 8), ], - const SizedBox(height: 40), - OutlinedButton.icon( - onPressed: () async { - await ref.read(authStateProvider.notifier).signOut(); + ), + ), + ); + } + + Widget _buildLangItem(BuildContext context, WidgetRef ref, String title, String? code, bool selected, AppStrings s) { + return ListTile( + title: Text(title), + trailing: selected ? const Icon(Icons.check, color: Colors.green) : null, + onTap: () { + ref.read(settingsProvider.notifier).setLanguage(code); + Navigator.pop(context); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(s.restartRequiredForMap), + behavior: SnackBarBehavior.floating, + ), + ); + }, + ); + } + + void _showLogoutDialog(BuildContext context, WidgetRef ref, AppStrings s) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(s.logout), + content: Text(s.logoutConfirm), + actions: [ + TextButton(onPressed: () => Navigator.pop(context), child: Text(s.cancel)), + TextButton( + onPressed: () { + Navigator.pop(context); + ref.read(authStateProvider.notifier).signOut(); }, - icon: const Icon(Icons.logout, color: Colors.red), - label: const Text('Выйти', style: TextStyle(color: Colors.red)), - style: OutlinedButton.styleFrom( - side: const BorderSide(color: Colors.red), - padding: const EdgeInsets.symmetric(vertical: 14), - ), + child: Text(s.logout, style: const TextStyle(color: Colors.red)), ), ], ), diff --git a/lib/presentation/screens/search/search_screen.dart b/lib/presentation/screens/search/search_screen.dart index ab1d97e..e3fe3d4 100644 --- a/lib/presentation/screens/search/search_screen.dart +++ b/lib/presentation/screens/search/search_screen.dart @@ -5,6 +5,7 @@ import 'package:go_router/go_router.dart'; import 'package:yandex_mapkit/yandex_mapkit.dart'; import '../../providers/routing_provider.dart'; import '../../../core/theme/app_colors.dart'; +import '../../../core/localization/app_localizations.dart'; class SearchScreen extends ConsumerStatefulWidget { const SearchScreen({super.key}); @@ -14,11 +15,11 @@ class SearchScreen extends ConsumerStatefulWidget { } class _SearchScreenState extends ConsumerState { - // Широкая bbox вокруг точки биаса (покрывает ~1000 км в каждую сторону). - // Центр bbox = текущая позиция камеры, что правильно биасирует Yandex Suggest. + // Wide bbox around bias point (covers ~1000 km in each direction). + // bbox center = current camera position, which correctly biases Yandex Suggest. static const double _biasLatDelta = 10.0; static const double _biasLonDelta = 15.0; - // Запасной вариант если геолокация/позиция камеры неизвестны — центр России. + // Fallback if location/camera position unknown — center of Russia. static const BoundingBox _russiaBoundingBox = BoundingBox( southWest: Point(latitude: 41.0, longitude: 19.0), northEast: Point(latitude: 82.0, longitude: 169.0), @@ -124,6 +125,7 @@ class _SearchScreenState extends ConsumerState { if (!mounted) return; final seq = ++_suggestSeq; setState(() => _loading = true); + final s = ref.read(l10nProvider); try { final searchBias = ref.read(searchBiasProvider); final bbox = searchBias == null @@ -139,13 +141,13 @@ class _SearchScreenState extends ConsumerState { if (!mounted || seq != _suggestSeq) return; setState(() => _loading = false); ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Поиск занял слишком много времени')), + SnackBar(content: Text(s.searchTimeout)), ); } catch (_) { if (!mounted || seq != _suggestSeq) return; setState(() => _loading = false); ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Не удалось выполнить поиск')), + SnackBar(content: Text(s.searchError)), ); } } @@ -170,6 +172,7 @@ class _SearchScreenState extends ConsumerState { _loading = true; _suggestions = []; }); + final s = ref.read(l10nProvider); try { final searchBias = ref.read(searchBiasProvider); final bbox = searchBias == null @@ -180,7 +183,7 @@ class _SearchScreenState extends ConsumerState { if (point == null) { setState(() => _loading = false); ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Не удалось определить координаты места')), + SnackBar(content: Text(s.pointLookupError)), ); return; } @@ -193,23 +196,24 @@ class _SearchScreenState extends ConsumerState { if (!mounted) return; setState(() => _loading = false); ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Определение точки заняло слишком много времени')), + SnackBar(content: Text(s.pointLookupTimeout)), ); } catch (_) { if (!mounted) return; setState(() => _loading = false); ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Не удалось определить координаты места')), + SnackBar(content: Text(s.searchError)), ); } } @override Widget build(BuildContext context) { + final s = ref.watch(l10nProvider); + final theme = Theme.of(context); return Scaffold( - backgroundColor: Colors.white, appBar: AppBar( - backgroundColor: Colors.white, + backgroundColor: theme.colorScheme.surface, elevation: 0, leading: IconButton( icon: const Icon(Icons.arrow_back), @@ -224,9 +228,9 @@ class _SearchScreenState extends ConsumerState { if (text.trim().isNotEmpty) _suggest(text.trim()); }, textInputAction: TextInputAction.search, - decoration: const InputDecoration( - hintText: 'Введите адрес или место', - hintStyle: TextStyle(color: AppColors.textSecondary), + decoration: InputDecoration( + hintText: s.searchHint, + hintStyle: TextStyle(color: theme.hintColor), border: InputBorder.none, ), ), @@ -241,9 +245,9 @@ class _SearchScreenState extends ConsumerState { }, ), ], - bottom: const PreferredSize( - preferredSize: Size.fromHeight(1), - child: Divider(height: 1), + bottom: PreferredSize( + preferredSize: const Size.fromHeight(1), + child: Divider(height: 1, color: theme.dividerColor), ), ), body: _loading @@ -255,16 +259,16 @@ class _SearchScreenState extends ConsumerState { itemBuilder: (_, i) { final item = _suggestions[i]; return ListTile( - leading: const Icon( + leading: Icon( Icons.place_outlined, - color: AppColors.textSecondary, + color: theme.hintColor, ), title: Text(item.title), subtitle: item.subtitle != null ? Text( item.subtitle!, - style: const TextStyle( - color: AppColors.textSecondary, + style: TextStyle( + color: theme.hintColor, fontSize: 13, ), ) @@ -277,13 +281,14 @@ class _SearchScreenState extends ConsumerState { } } -class _EmptyState extends StatelessWidget { +class _EmptyState extends ConsumerWidget { const _EmptyState({required this.hasQuery}); final bool hasQuery; @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final s = ref.watch(l10nProvider); return Center( child: Column( mainAxisSize: MainAxisSize.min, @@ -291,13 +296,13 @@ class _EmptyState extends StatelessWidget { Icon( hasQuery ? Icons.search_off : Icons.search, size: 56, - color: Colors.grey[300], + color: Theme.of(context).dividerColor, ), const SizedBox(height: 16), Text( - hasQuery ? 'Ничего не найдено' : 'Введите адрес или место', - style: const TextStyle( - color: AppColors.textSecondary, + hasQuery ? s.searchNoResults : s.searchHint, + style: TextStyle( + color: Theme.of(context).hintColor, fontSize: 15, ), ), diff --git a/web/favicon.png b/web/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..3c0b81fab2218300de2c5f2959012fc52e289652 GIT binary patch literal 18900 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

      0GvI9M4$*tFo8)Y4~>Eq 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 literal 0 HcmV?d00001 From 895ef22fc713c78265d44af4690eaed6ba72119b Mon Sep 17 00:00:00 2001 From: Nikita Aksenov Date: Thu, 16 Jul 2026 20:28:04 +0300 Subject: [PATCH 053/145] feat: add web app and macOS support --- macos/.gitignore | 7 + macos/Flutter/Flutter-Debug.xcconfig | 1 + macos/Flutter/Flutter-Release.xcconfig | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 16 + macos/Runner.xcodeproj/project.pbxproj | 705 ++++++++++++++++++ .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/xcschemes/Runner.xcscheme | 99 +++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + macos/Runner/AppDelegate.swift | 13 + .../AppIcon.appiconset/Contents.json | 68 ++ .../AppIcon.appiconset/app_icon_1024.png | Bin 0 -> 102994 bytes .../AppIcon.appiconset/app_icon_128.png | Bin 0 -> 5680 bytes .../AppIcon.appiconset/app_icon_16.png | Bin 0 -> 520 bytes .../AppIcon.appiconset/app_icon_256.png | Bin 0 -> 14142 bytes .../AppIcon.appiconset/app_icon_32.png | Bin 0 -> 1066 bytes .../AppIcon.appiconset/app_icon_512.png | Bin 0 -> 36406 bytes .../AppIcon.appiconset/app_icon_64.png | Bin 0 -> 2218 bytes macos/Runner/Base.lproj/MainMenu.xib | 343 +++++++++ macos/Runner/Configs/AppInfo.xcconfig | 14 + macos/Runner/Configs/Debug.xcconfig | 2 + macos/Runner/Configs/Release.xcconfig | 2 + macos/Runner/Configs/Warnings.xcconfig | 13 + macos/Runner/DebugProfile.entitlements | 16 + macos/Runner/Info.plist | 34 + macos/Runner/MainFlutterWindow.swift | 15 + macos/Runner/Release.entitlements | 12 + macos/RunnerTests/RunnerTests.swift | 12 + web/index.html | 46 ++ web/manifest.json | 18 + 30 files changed, 1460 insertions(+) create mode 100644 macos/.gitignore create mode 100644 macos/Flutter/Flutter-Debug.xcconfig create mode 100644 macos/Flutter/Flutter-Release.xcconfig create mode 100644 macos/Flutter/GeneratedPluginRegistrant.swift create mode 100644 macos/Runner.xcodeproj/project.pbxproj create mode 100644 macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 macos/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 macos/Runner/AppDelegate.swift create mode 100644 macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png create mode 100644 macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png create mode 100644 macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png create mode 100644 macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png create mode 100644 macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png create mode 100644 macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png create mode 100644 macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png create mode 100644 macos/Runner/Base.lproj/MainMenu.xib create mode 100644 macos/Runner/Configs/AppInfo.xcconfig create mode 100644 macos/Runner/Configs/Debug.xcconfig create mode 100644 macos/Runner/Configs/Release.xcconfig create mode 100644 macos/Runner/Configs/Warnings.xcconfig create mode 100644 macos/Runner/DebugProfile.entitlements create mode 100644 macos/Runner/Info.plist create mode 100644 macos/Runner/MainFlutterWindow.swift create mode 100644 macos/Runner/Release.entitlements create mode 100644 macos/RunnerTests/RunnerTests.swift create mode 100644 web/index.html create mode 100644 web/manifest.json diff --git a/macos/.gitignore b/macos/.gitignore new file mode 100644 index 0000000..746adbb --- /dev/null +++ b/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/macos/Flutter/Flutter-Debug.xcconfig b/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 0000000..c2efd0b --- /dev/null +++ b/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/Flutter-Release.xcconfig b/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 0000000..c2efd0b --- /dev/null +++ b/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 0000000..7f03e25 --- /dev/null +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,16 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import flutter_secure_storage_macos +import geolocator_apple +import url_launcher_macos + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) + GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) +} diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..5c8cc21 --- /dev/null +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,705 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* mobile.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "mobile.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* mobile.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* mobile.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.parktrack.mobile.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/mobile.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/mobile"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.parktrack.mobile.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/mobile.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/mobile"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.parktrack.mobile.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/mobile.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/mobile"; + }; + name = Profile; + }; + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..09d4458 --- /dev/null +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macos/Runner.xcworkspace/contents.xcworkspacedata b/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/macos/Runner/AppDelegate.swift b/macos/Runner/AppDelegate.swift new file mode 100644 index 0000000..b3c1761 --- /dev/null +++ b/macos/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Cocoa +import FlutterMacOS + +@main +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } +} diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..a2ec33f --- /dev/null +++ b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000000000000000000000000000000000000..82b6f9d9a33e198f5747104729e1fcef999772a5 GIT binary patch literal 102994 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ⅅ8MglG&ER{i5lN315cO?EdHNLrg? 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 z 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 Pe7HEdAEg|=c=LY&YVNkY literal 0 HcmV?d00001 diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000000000000000000000000000000000000..13b35eba55c6dabc3aac36f33d859266c18fa0d0 GIT binary patch literal 5680 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 literal 0 HcmV?d00001 diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000000000000000000000000000000000000..0a3f5fa40fb3d1e0710331a48de5d256da3f275d GIT binary patch literal 520 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 literal 0 HcmV?d00001 diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000000000000000000000000000000000000..2f1632cfddf3d9dade342351e627a0a75609fb46 GIT binary patch literal 2218 zcmV;b2vzrqP)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91K%fHv1ONa40RR91KmY&$07g+lumAuE6iGxuRCodHTWf3-RTMruyW6Fu zQYeUM04eX6D5c0FCjKKPrco1(K`<0SL=crI{PC3-^hZU0kQie$gh-5!7z6SH6Q0J% zqot*`H1q{R5fHFYS}dje@;kG=v$L0(yY0?wY2%*c?A&{2?!D*x?m71{of2gv!$5|C z3>qG_BW}7K_yUcT3A5C6QD<+{aq?x;MAUyAiJn#Jv8_zZtQ{P zTRzbL3U9!qVuZzS$xKU10KiW~Bgdcv1-!uAhQxf3a7q+dU6lj?yoO4Lq4TUN4}h{N z*fIM=SS8|C2$(T>w$`t@3Tka!(r!7W`x z-isCVgQD^mG-MJ;XtJuK3V{Vy72GQ83KRWsHU?e*wrhKk=ApIYeDqLi;JI1e zuvv}5^Dc=k7F7?nm3nIw$NVmU-+R>> zyqOR$-2SDpJ}Pt;^RkJytDVXNTsu|mI1`~G7yw`EJR?VkGfNdqK9^^8P`JdtTV&tX4CNcV4 z&N06nZa??Fw1AgQOUSE2AmPE@WO(Fvo`%m`cDgiv(fAeRA%3AGXUbsGw{7Q`cY;1BI#ac3iN$$Hw z0LT0;xc%=q)me?Y*$xI@GRAw?+}>=9D+KTk??-HJ4=A>`V&vKFS75@MKdSF1JTq{S zc1!^8?YA|t+uKigaq!sT;Z!&0F2=k7F0PIU;F$leJLaw2UI6FL^w}OG&!;+b%ya1c z1n+6-inU<0VM-Y_s5iTElq)ThyF?StVcebpGI znw#+zLx2@ah{$_2jn+@}(zJZ{+}_N9BM;z)0yr|gF-4=Iyu@hI*Lk=-A8f#bAzc9f z`Kd6K--x@t04swJVC3JK1cHY-Hq+=|PN-VO;?^_C#;coU6TDP7Bt`;{JTG;!+jj(` zw5cLQ-(Cz-Tlb`A^w7|R56Ce;Wmr0)$KWOUZ6ai0PhzPeHwdl0H(etP zUV`va_i0s-4#DkNM8lUlqI7>YQLf)(lz9Q3Uw`)nc(z3{m5ZE77Ul$V%m)E}3&8L0 z-XaU|eB~Is08eORPk;=<>!1w)Kf}FOVS2l&9~A+@R#koFJ$Czd%Y(ENTV&A~U(IPI z;UY+gf+&6ioZ=roly<0Yst8ck>(M=S?B-ys3mLdM&)ex!hbt+ol|T6CTS+Sc0jv(& z7ijdvFwBq;0a{%3GGwkDKTeG`b+lyj0jjS1OMkYnepCdoosNY`*zmBIo*981BU%%U z@~$z0V`OVtIbEx5pa|Tct|Lg#ZQf5OYMUMRD>Wdxm5SAqV2}3!ceE-M2 z@O~lQ0OiKQp}o9I;?uxCgYVV?FH|?Riri*U$Zi_`V2eiA>l zdSm6;SEm6#T+SpcE8Ro_f2AwxzI z44hfe^WE3!h@W3RDyA_H440cpmYkv*)6m1XazTqw%=E5Xv7^@^^T7Q2wxr+Z2kVYr + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macos/Runner/Configs/AppInfo.xcconfig b/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 0000000..543b706 --- /dev/null +++ b/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = ParkTrack + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.parktrack.mobile + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2026 ParkTrack. All rights reserved. diff --git a/macos/Runner/Configs/Debug.xcconfig b/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 0000000..36b0fd9 --- /dev/null +++ b/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/macos/Runner/Configs/Release.xcconfig b/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 0000000..dff4f49 --- /dev/null +++ b/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/macos/Runner/Configs/Warnings.xcconfig b/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 0000000..42bcbf4 --- /dev/null +++ b/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/macos/Runner/DebugProfile.entitlements b/macos/Runner/DebugProfile.entitlements new file mode 100644 index 0000000..0b192be --- /dev/null +++ b/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,16 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + com.apple.security.network.client + + com.apple.security.personal-information.location + + + diff --git a/macos/Runner/Info.plist b/macos/Runner/Info.plist new file mode 100644 index 0000000..f79e8bb --- /dev/null +++ b/macos/Runner/Info.plist @@ -0,0 +1,34 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSLocationUsageDescription + ParkTrack использует геопозицию, чтобы показать парковки рядом с вами и построить маршрут. + NSPrincipalClass + NSApplication + + diff --git a/macos/Runner/MainFlutterWindow.swift b/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 0000000..3cc05eb --- /dev/null +++ b/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/macos/Runner/Release.entitlements b/macos/Runner/Release.entitlements new file mode 100644 index 0000000..fc07546 --- /dev/null +++ b/macos/Runner/Release.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.network.client + + com.apple.security.personal-information.location + + + diff --git a/macos/RunnerTests/RunnerTests.swift b/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..61f3bd1 --- /dev/null +++ b/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Cocoa +import FlutterMacOS +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..9ffe9d1 --- /dev/null +++ b/web/index.html @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + ParkTrack + + + + + + + diff --git a/web/manifest.json b/web/manifest.json new file mode 100644 index 0000000..177216c --- /dev/null +++ b/web/manifest.json @@ -0,0 +1,18 @@ +{ + "name": "ParkTrack", + "short_name": "ParkTrack", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "Available parking map.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "favicon.png", + "sizes": "192x192", + "type": "image/png" + } + ] +} From c97a4349e6722193af797990e3765e0b2f0a918a Mon Sep 17 00:00:00 2001 From: Nikita Aksenov Date: Thu, 16 Jul 2026 21:01:11 +0300 Subject: [PATCH 054/145] ci: build bix --- .github/workflows/deploy-web.yml | 5 ++++- .github/workflows/publish-google-play.yml | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy-web.yml b/.github/workflows/deploy-web.yml index 57fd450..b38a793 100644 --- a/.github/workflows/deploy-web.yml +++ b/.github/workflows/deploy-web.yml @@ -36,8 +36,11 @@ jobs: - name: Install dependencies run: flutter pub get + - name: Generate source files + run: dart run build_runner build --delete-conflicting-outputs + - name: Build web - run: flutter build web --release --base-href / + run: flutter build web --release --base-href / --no-wasm-dry-run - name: Configure GitHub Pages uses: actions/configure-pages@v5 diff --git a/.github/workflows/publish-google-play.yml b/.github/workflows/publish-google-play.yml index af8becc..9013f18 100644 --- a/.github/workflows/publish-google-play.yml +++ b/.github/workflows/publish-google-play.yml @@ -48,6 +48,9 @@ jobs: - name: Install dependencies run: flutter pub get + - name: Generate source files + run: dart run build_runner build --delete-conflicting-outputs + - name: Validate release version env: RELEASE_TAG: ${{ github.event.release.tag_name }} @@ -197,4 +200,4 @@ jobs: tracks: alpha status: completed releaseName: ${{ github.event.release.tag_name }} - whatsNewDirectory: build/google-play/what \ No newline at end of file + whatsNewDirectory: build/google-play/what From 83f71ddb158b15cbd7544a8bada2b1246005bba0 Mon Sep 17 00:00:00 2001 From: Nikita Aksenov Date: Thu, 16 Jul 2026 21:20:01 +0300 Subject: [PATCH 055/145] fix: web app theme and background color --- web/manifest.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/manifest.json b/web/manifest.json index 177216c..4ae2511 100644 --- a/web/manifest.json +++ b/web/manifest.json @@ -3,8 +3,8 @@ "short_name": "ParkTrack", "start_url": ".", "display": "standalone", - "background_color": "#0175C2", - "theme_color": "#0175C2", + "background_color": "#121212FF", + "theme_color": "#2E7D32FF", "description": "Available parking map.", "orientation": "portrait-primary", "prefer_related_applications": false, From 85c7e1957a39cbbcfc5be55bae6e4ba472fae691 Mon Sep 17 00:00:00 2001 From: Nikita Aksenov Date: Thu, 16 Jul 2026 21:28:04 +0300 Subject: [PATCH 056/145] refactor: optimize networking, maps, and platform support --- .gitignore | 2 + ios/Flutter/Debug.xcconfig | 1 + ios/Flutter/Release.xcconfig | 1 + ios/Podfile | 43 ++++ .../AppIcon.appiconset/Icon-App-50x50@1x.png | Bin 0 -> 3355 bytes .../AppIcon.appiconset/Icon-App-50x50@2x.png | Bin 0 -> 9021 bytes .../AppIcon.appiconset/Icon-App-57x57@1x.png | Bin 0 -> 3744 bytes .../AppIcon.appiconset/Icon-App-57x57@2x.png | Bin 0 -> 11210 bytes .../AppIcon.appiconset/Icon-App-72x72@1x.png | Bin 0 -> 5167 bytes .../AppIcon.appiconset/Icon-App-72x72@2x.png | Bin 0 -> 13124 bytes lib/core/network/auth_interceptor.dart | 143 ++++++----- lib/core/network/dio_client.dart | 33 ++- lib/core/storage/token_storage.dart | 43 +++- lib/core/utils/nav_math.dart | 240 ++++++++++++------ lib/data/api/forecasts_api.dart | 17 +- lib/data/api/occupancy_api.dart | 17 +- lib/data/api/routing_api.dart | 35 ++- lib/data/api/zones_api.dart | 25 +- lib/data/repositories/routing_repository.dart | 61 +++-- lib/data/repositories/zones_repository.dart | 203 +++++++++------ .../providers/navigation_provider.dart | 70 +++-- .../providers/routing_provider.dart | 69 ++++- .../providers/zones_provider.dart | 67 +++-- lib/presentation/screens/map/map_screen.dart | 218 +++++++++------- .../map/widgets/parking_zone_layer.dart | 118 ++++++--- .../screens/search/search_screen.dart | 1 - macos/Flutter/Flutter-Debug.xcconfig | 1 + macos/Flutter/Flutter-Release.xcconfig | 1 + macos/Podfile | 42 +++ macos/Podfile.lock | 35 +++ macos/Runner.xcodeproj/project.pbxproj | 98 ++++++- .../contents.xcworkspacedata | 3 + test/core/utils/nav_math_test.dart | 60 +++++ 33 files changed, 1189 insertions(+), 458 deletions(-) create mode 100644 ios/Podfile create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png create mode 100644 macos/Podfile create mode 100644 macos/Podfile.lock create mode 100644 test/core/utils/nav_math_test.dart diff --git a/.gitignore b/.gitignore index 812fc47..85ff2f7 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,7 @@ android/app/debug/ android/app/profile/ android/app/release/ android/.gradle/ +android/build/ android/captures/ # iOS @@ -57,6 +58,7 @@ app.*.symbols app.*.map.json # Misc +.artifacts/ migrate_working_dir/ .history .svn/ diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig index 592ceee..ec97fc6 100644 --- a/ios/Flutter/Debug.xcconfig +++ b/ios/Flutter/Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig index 592ceee..c4855bf 100644 --- a/ios/Flutter/Release.xcconfig +++ b/ios/Flutter/Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 0000000..620e46e --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,43 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '13.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..252ff4905816e512e9533ce475f4d60722d6db44 GIT binary patch literal 3355 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 literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..778107f17955d34bf0750b98149be4b14018f2e6 GIT binary patch literal 9021 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 KMNUMnLSTYW&rp~E literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..adc8fbcfe6ed2b64bda747efdda00d4597042aab GIT binary patch literal 11210 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 literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..980bbab4598d4e534d7e5edc3de84268b6e1de48 GIT binary patch literal 13124 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 e9q~<*!2bu!6^oW^(@*RG0000 _deferredHandlers = []; + final Dio _dio; + Future? _refreshFuture; + bool _sessionExpired = false; - AuthInterceptor(this._tokenStorage); + AuthInterceptor(this._tokenStorage, this._dio); @override Future onRequest( @@ -23,73 +26,93 @@ class AuthInterceptor extends Interceptor { } @override - Future onError(DioException err, ErrorInterceptorHandler handler) async { - if (err.response?.statusCode == 401) { - final login = await _tokenStorage.getLogin(); - final password = await _tokenStorage.getPassword(); + Future onError( + DioException err, + ErrorInterceptorHandler handler, + ) async { + if (err.response?.statusCode != 401 || + err.requestOptions.extra[_retryKey] == true) { + handler.next(err); + return; + } - if (login != null && password != null) { - if (_isRefreshing) { - _deferredHandlers.add(handler); - return; - } + try { + final latestToken = await _tokenStorage.getAccessToken(); + final failedToken = _bearerToken(err.requestOptions); + final token = latestToken != null && latestToken != failedToken + ? latestToken + : await _refreshAccessToken(); + final response = await _retry(err.requestOptions, token); + handler.resolve(response); + } catch (_) { + await _expireSession(); + handler.next(err); + } + } - _isRefreshing = true; + String? _bearerToken(RequestOptions options) { + final value = options.headers['Authorization']?.toString(); + return value?.startsWith('Bearer ') == true ? value!.substring(7) : null; + } - try { - // Use a fresh Dio instance to avoid interceptor recursion - final refreshDio = _createRefreshDio(); - final response = await refreshDio.post('/auth/login', data: { - 'login': login, - 'password': password, - }); + Future _refreshAccessToken() { + final activeRefresh = _refreshFuture; + if (activeRefresh != null) return activeRefresh; - final newToken = response.data['access_token'] as String; - await _tokenStorage.saveAccessToken(newToken); + final refresh = _loginAgain(); + _refreshFuture = refresh; + return refresh.whenComplete(() { + if (identical(_refreshFuture, refresh)) _refreshFuture = null; + }); + } - // Retry the original request - final retryResponse = await _retry(err.requestOptions, newToken); - handler.resolve(retryResponse); + Future _loginAgain() async { + final login = await _tokenStorage.getLogin(); + final password = await _tokenStorage.getPassword(); + if (login == null || password == null) { + throw StateError('No credentials available for token refresh'); + } - // For simplicity, we let deferred handlers fail, - // but a robust implementation would retry them as well. - for (final h in _deferredHandlers) { - h.next(err); - } - _deferredHandlers.clear(); - return; - } catch (e) { - await _tokenStorage.clearAll(); - sessionExpiredNotifier.value = true; - for (final h in _deferredHandlers) { - h.next(err); - } - _deferredHandlers.clear(); - } finally { - _isRefreshing = false; - } - } else { - await _tokenStorage.clearAll(); - sessionExpiredNotifier.value = true; - } + final refreshDio = Dio( + BaseOptions( + baseUrl: kBaseUrl, + connectTimeout: _dio.options.connectTimeout, + receiveTimeout: _dio.options.receiveTimeout, + sendTimeout: _dio.options.sendTimeout, + headers: {'Content-Type': 'application/json'}, + ), + ); + final response = await refreshDio.post>( + '/auth/login', + data: {'login': login, 'password': password}, + ); + final token = response.data?['access_token'] as String?; + if (token == null || token.isEmpty) { + throw const FormatException('Login response does not contain a token'); } - handler.next(err); + await _tokenStorage.saveAccessToken(token); + _sessionExpired = false; + return token; } - Dio _createRefreshDio() { - return Dio(BaseOptions(baseUrl: kBaseUrl)); + Future> _retry( + RequestOptions requestOptions, + String token, + ) { + return _dio.fetch( + requestOptions.copyWith( + headers: Map.from(requestOptions.headers) + ..['Authorization'] = 'Bearer $token', + extra: Map.from(requestOptions.extra) + ..[_retryKey] = true, + ), + ); } - Future> _retry(RequestOptions requestOptions, String token) { - final options = Options( - method: requestOptions.method, - headers: Map.from(requestOptions.headers)..['Authorization'] = 'Bearer $token', - ); - return _createRefreshDio().request( - requestOptions.path, - data: requestOptions.data, - queryParameters: requestOptions.queryParameters, - options: options, - ); + Future _expireSession() async { + if (_sessionExpired) return; + _sessionExpired = true; + await _tokenStorage.clearAll(); + sessionExpiredNotifier.value = true; } } diff --git a/lib/core/network/dio_client.dart b/lib/core/network/dio_client.dart index 78d29da..69d7f84 100644 --- a/lib/core/network/dio_client.dart +++ b/lib/core/network/dio_client.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:dio/dio.dart'; import '../constants.dart'; import '../storage/token_storage.dart'; @@ -5,25 +6,29 @@ import 'auth_interceptor.dart'; import 'mock_interceptor.dart'; Dio createDio(TokenStorage tokenStorage) { - final dio = Dio(BaseOptions( - baseUrl: kBaseUrl, - connectTimeout: const Duration(seconds: 15), - receiveTimeout: const Duration(seconds: 20), - headers: {'Content-Type': 'application/json'}, - )); + final dio = Dio( + BaseOptions( + baseUrl: kBaseUrl, + connectTimeout: const Duration(seconds: 15), + receiveTimeout: const Duration(seconds: 20), + headers: {'Content-Type': 'application/json'}, + ), + ); final interceptors = []; if (kUseMocks) { interceptors.add(MockInterceptor()); } - interceptors.add(AuthInterceptor(tokenStorage)); - interceptors.add( - LogInterceptor( - requestBody: false, - responseBody: false, - requestHeader: false, - ), - ); + interceptors.add(AuthInterceptor(tokenStorage, dio)); + if (kDebugMode) { + interceptors.add( + LogInterceptor( + requestBody: false, + responseBody: false, + requestHeader: false, + ), + ); + } dio.interceptors.addAll(interceptors); return dio; diff --git a/lib/core/storage/token_storage.dart b/lib/core/storage/token_storage.dart index 0fe3bd8..2d8f363 100644 --- a/lib/core/storage/token_storage.dart +++ b/lib/core/storage/token_storage.dart @@ -6,29 +6,62 @@ class TokenStorage { static const _keyPassword = 'user_password'; static const _storage = FlutterSecureStorage( aOptions: AndroidOptions(encryptedSharedPreferences: true), + mOptions: MacOsOptions(useDataProtectionKeyChain: false), ); - Future saveAccessToken(String token) => - _storage.write(key: _keyAccess, value: token); + String? _cachedAccessToken; + bool _accessTokenLoaded = false; + Future? _accessTokenLoad; + int _accessTokenRevision = 0; - Future getAccessToken() => _storage.read(key: _keyAccess); + Future saveAccessToken(String token) async { + _accessTokenRevision++; + _cachedAccessToken = token; + _accessTokenLoaded = true; + await _storage.write(key: _keyAccess, value: token); + } + + Future getAccessToken() { + if (_accessTokenLoaded) return Future.value(_cachedAccessToken); + return _accessTokenLoad ??= _loadAccessToken(_accessTokenRevision); + } + + Future _loadAccessToken(int revision) async { + try { + final storedToken = await _storage.read(key: _keyAccess); + if (revision == _accessTokenRevision) { + _cachedAccessToken = storedToken; + _accessTokenLoaded = true; + } + return _cachedAccessToken; + } finally { + _accessTokenLoad = null; + } + } Future saveCredentials(String login, String password) async { await _storage.write(key: _keyLogin, value: login); await _storage.write(key: _keyPassword, value: password); - print("Credentials saved."); } Future getLogin() => _storage.read(key: _keyLogin); Future getPassword() => _storage.read(key: _keyPassword); Future clearAll() async { + _accessTokenRevision++; + _cachedAccessToken = null; + _accessTokenLoaded = true; await _storage.delete(key: _keyAccess); await _storage.delete(key: _keyLogin); await _storage.delete(key: _keyPassword); } - Future clearTokens() => _storage.delete(key: _keyAccess); + Future clearTokens() async { + _accessTokenRevision++; + _cachedAccessToken = null; + _accessTokenLoaded = true; + await _storage.delete(key: _keyAccess); + } Future hasToken() async => (await getAccessToken()) != null; } diff --git a/lib/core/utils/nav_math.dart b/lib/core/utils/nav_math.dart index 9cb5497..7194f12 100644 --- a/lib/core/utils/nav_math.dart +++ b/lib/core/utils/nav_math.dart @@ -4,7 +4,15 @@ import '../localization/app_localizations.dart'; const _kEarthRadius = 6371000.0; -enum TurnDirection { left, slightLeft, straight, slightRight, right, uTurn, arrive } +enum TurnDirection { + left, + slightLeft, + straight, + slightRight, + right, + uTurn, + arrive, +} class NavTurn { final TurnDirection direction; @@ -12,12 +20,167 @@ class NavTurn { const NavTurn(this.direction, this.distanceMeters); } +class RouteMatch { + const RouteMatch({ + required this.segment, + required this.t, + required this.projectedPoint, + required this.distanceFromRoute, + }); + + final int segment; + final double t; + final Point projectedPoint; + final double distanceFromRoute; +} + +class _PreparedTurn { + const _PreparedTurn(this.pointIndex, this.direction); + + final int pointIndex; + final TurnDirection direction; +} + +/// Immutable route index used by the navigation hot path. +/// +/// Segment lengths, remaining distances and turns are calculated once when a +/// route is received. Position updates then inspect only a small window around +/// the last matched segment instead of repeatedly walking the whole polyline. +class PreparedRoute { + PreparedRoute(List points) + : points = List.unmodifiable(points), + _segmentLengths = _buildSegmentLengths(points), + _remainingAtPoint = _buildRemainingDistances(points), + _turns = _buildTurns(points); + + final List points; + final List _segmentLengths; + final List _remainingAtPoint; + final List<_PreparedTurn> _turns; + + bool get isValid => points.length >= 2; + double get totalDistance => + _remainingAtPoint.isEmpty ? 0 : _remainingAtPoint.first; + + RouteMatch match(Point point, {int hintSegment = 0}) { + if (!isValid) { + return RouteMatch( + segment: 0, + t: 0, + projectedPoint: point, + distanceFromRoute: 0, + ); + } + + final lastSegment = points.length - 2; + final start = max(0, hintSegment - 5); + final end = min(lastSegment, hintSegment + 40); + var best = _matchRange(point, start, end); + + // A large jump, GPS recovery or app resume may put the user outside the + // local search window. The expensive full scan is reserved for that case. + if (best.distanceFromRoute > 200 && (start > 0 || end < lastSegment)) { + best = _matchRange(point, 0, lastSegment); + } + return best; + } + + double remainingDistance(RouteMatch match) { + if (!isValid || match.segment >= _segmentLengths.length) return 0; + final segmentRemainder = + _segmentLengths[match.segment] * (1 - match.t.clamp(0.0, 1.0)); + return segmentRemainder + _remainingAtPoint[match.segment + 1]; + } + + NavTurn? nextTurn(RouteMatch match) { + if (_turns.isEmpty) return null; + final remaining = remainingDistance(match); + for (final turn in _turns) { + if (turn.pointIndex < match.segment + 1) continue; + final distance = remaining - _remainingAtPoint[turn.pointIndex]; + if (distance > 3000) return null; + return NavTurn(turn.direction, max(0, distance)); + } + return null; + } + + RouteMatch _matchRange(Point point, int start, int end) { + var bestSegment = start; + var bestT = 0.0; + var bestDistance = double.infinity; + var bestPoint = points[start]; + + for (var i = start; i <= end; i++) { + final a = points[i]; + final b = points[i + 1]; + final dx = b.longitude - a.longitude; + final dy = b.latitude - a.latitude; + final lengthSquared = dx * dx + dy * dy; + var t = 0.0; + if (lengthSquared > 1e-18) { + t = + ((point.longitude - a.longitude) * dx + + (point.latitude - a.latitude) * dy) / + lengthSquared; + t = t.clamp(0.0, 1.0); + } + final projected = Point( + latitude: a.latitude + t * dy, + longitude: a.longitude + t * dx, + ); + final distance = navDistanceM(point, projected); + if (distance < bestDistance) { + bestSegment = i; + bestT = t; + bestDistance = distance; + bestPoint = projected; + } + } + return RouteMatch( + segment: bestSegment, + t: bestT, + projectedPoint: bestPoint, + distanceFromRoute: bestDistance, + ); + } + + static List _buildSegmentLengths(List points) { + return [ + for (var i = 0; i < points.length - 1; i++) + navDistanceM(points[i], points[i + 1]), + ]; + } + + static List _buildRemainingDistances(List points) { + if (points.isEmpty) return const []; + final result = List.filled(points.length, 0); + for (var i = points.length - 2; i >= 0; i--) { + result[i] = result[i + 1] + navDistanceM(points[i], points[i + 1]); + } + return result; + } + + static List<_PreparedTurn> _buildTurns(List points) { + final result = <_PreparedTurn>[]; + for (var i = 1; i < points.length - 1; i++) { + final before = navBearingDeg(points[i - 1], points[i]); + final after = navBearingDeg(points[i], points[i + 1]); + final difference = _bearingDiff(after, before); + if (difference.abs() >= 18) { + result.add(_PreparedTurn(i, _classifyTurn(difference))); + } + } + return List.unmodifiable(result); + } +} + double navDistanceM(Point a, Point b) { final lat1 = a.latitude * pi / 180; final lat2 = b.latitude * pi / 180; final dlat = (b.latitude - a.latitude) * pi / 180; final dlon = (b.longitude - a.longitude) * pi / 180; - final h = sin(dlat / 2) * sin(dlat / 2) + + final h = + sin(dlat / 2) * sin(dlat / 2) + cos(lat1) * cos(lat2) * sin(dlon / 2) * sin(dlon / 2); return 2 * _kEarthRadius * asin(sqrt(h.clamp(0.0, 1.0))); } @@ -33,79 +196,6 @@ double navBearingDeg(Point a, Point b) { double _bearingDiff(double to, double from) => (to - from + 540) % 360 - 180; -({int segment, double t}) closestOnRoute(Point p, List pts) { - int bestSeg = 0; - double bestT = 0; - double bestDist = double.infinity; - for (int i = 0; i < pts.length - 1; i++) { - final a = pts[i]; - final b = pts[i + 1]; - final dx = b.longitude - a.longitude; - final dy = b.latitude - a.latitude; - final len2 = dx * dx + dy * dy; - double t = 0; - if (len2 > 1e-18) { - t = ((p.longitude - a.longitude) * dx + (p.latitude - a.latitude) * dy) / len2; - t = t.clamp(0.0, 1.0); - } - final proj = Point( - latitude: a.latitude + t * dy, - longitude: a.longitude + t * dx, - ); - final d = navDistanceM(p, proj); - if (d < bestDist) { - bestDist = d; - bestSeg = i; - bestT = t; - } - } - return (segment: bestSeg, t: bestT); -} - -double remainingRouteDistance(List pts, int seg, double t) { - if (pts.length < 2 || seg >= pts.length - 1) return 0; - final a = pts[seg]; - final b = pts[seg + 1]; - final proj = Point( - latitude: a.latitude + t * (b.latitude - a.latitude), - longitude: a.longitude + t * (b.longitude - a.longitude), - ); - double dist = navDistanceM(proj, b); - for (int i = seg + 1; i < pts.length - 1; i++) { - dist += navDistanceM(pts[i], pts[i + 1]); - } - return dist; -} - -double perpDistToRoute(Point p, List pts, int seg, double t) { - if (pts.isEmpty || seg >= pts.length - 1) return 0; - final a = pts[seg]; - final b = pts[seg + 1]; - final proj = Point( - latitude: a.latitude + t * (b.latitude - a.latitude), - longitude: a.longitude + t * (b.longitude - a.longitude), - ); - return navDistanceM(p, proj); -} - -NavTurn? findNextTurn(List pts, int fromSeg, Point currentPos) { - if (pts.length < 3) return null; - double accDist = fromSeg < pts.length - 1 - ? navDistanceM(currentPos, pts[fromSeg + 1]) - : 0; - for (int i = max(fromSeg + 1, 1); i < pts.length - 1; i++) { - final b1 = navBearingDeg(pts[i - 1], pts[i]); - final b2 = navBearingDeg(pts[i], pts[i + 1]); - final diff = _bearingDiff(b2, b1); - if (diff.abs() >= 18) { - return NavTurn(_classifyTurn(diff), accDist); - } - accDist += navDistanceM(pts[i], pts[i + 1]); - if (accDist > 3000) break; - } - return null; -} - TurnDirection _classifyTurn(double diff) { final a = diff.abs(); if (a > 150) return TurnDirection.uTurn; diff --git a/lib/data/api/forecasts_api.dart b/lib/data/api/forecasts_api.dart index 54924f5..bf956f8 100644 --- a/lib/data/api/forecasts_api.dart +++ b/lib/data/api/forecasts_api.dart @@ -8,13 +8,18 @@ class ForecastsApi { Future>> getForecastsMap({ required String bbox, required String at, + CancelToken? cancelToken, }) async { - final response = await _dio.get('/forecasts', queryParameters: { - 'bbox': bbox, - 'at': at, - 'view': 'map', - 'latest_model_only': true, - }); + final response = await _dio.get( + '/forecasts', + queryParameters: { + 'bbox': bbox, + 'at': at, + 'view': 'map', + 'latest_model_only': true, + }, + cancelToken: cancelToken, + ); return (response.data as List).cast>(); } } diff --git a/lib/data/api/occupancy_api.dart b/lib/data/api/occupancy_api.dart index 50207fc..2ccd6e9 100644 --- a/lib/data/api/occupancy_api.dart +++ b/lib/data/api/occupancy_api.dart @@ -8,13 +8,18 @@ class OccupancyApi { Future>> getOccupancyMap({ required String bbox, required String at, + CancelToken? cancelToken, }) async { - final response = await _dio.get('/occupancy', queryParameters: { - 'bbox': bbox, - 'at': at, - 'view': 'map', - 'latest_only': true, - }); + final response = await _dio.get( + '/occupancy', + queryParameters: { + 'bbox': bbox, + 'at': at, + 'view': 'map', + 'latest_only': true, + }, + cancelToken: cancelToken, + ); return (response.data as List).cast>(); } } diff --git a/lib/data/api/routing_api.dart b/lib/data/api/routing_api.dart index fef121e..d758094 100644 --- a/lib/data/api/routing_api.dart +++ b/lib/data/api/routing_api.dart @@ -7,21 +7,34 @@ class RoutingApi { RoutingApi(this._dio); Future searchParking( - RoutingSearchRequestDto request, - ) async { + RoutingSearchRequestDto request, { + CancelToken? cancelToken, + }) async { final data = request.toJson()..removeWhere((_, v) => v == null); - final response = await _dio.post('/routing/search', data: data); + final response = await _dio.post( + '/routing/search', + data: data, + cancelToken: cancelToken, + ); return RoutingSearchResponseDto.fromJson( - response.data as Map); + response.data as Map, + ); } - Future createRoute(RoutingSearchRequestDto request, - {int? selectedZoneId}) async { + Future createRoute( + RoutingSearchRequestDto request, { + int? selectedZoneId, + CancelToken? cancelToken, + }) async { final data = { ...request.toJson()..removeWhere((_, v) => v == null), 'selected_zone_id': ?selectedZoneId, }; - final response = await _dio.post('/routing/new', data: data); + final response = await _dio.post( + '/routing/new', + data: data, + cancelToken: cancelToken, + ); return RouteDto.fromJson(response.data as Map); } @@ -35,10 +48,10 @@ class RoutingApi { String? status, int? selectedZoneId, }) async { - final response = await _dio.put('/routing/$routeId', data: { - 'status': ?status, - 'selected_zone_id': ?selectedZoneId, - }); + final response = await _dio.put( + '/routing/$routeId', + data: {'status': ?status, 'selected_zone_id': ?selectedZoneId}, + ); return RouteDto.fromJson(response.data as Map); } } diff --git a/lib/data/api/zones_api.dart b/lib/data/api/zones_api.dart index efaf04a..ab59ec0 100644 --- a/lib/data/api/zones_api.dart +++ b/lib/data/api/zones_api.dart @@ -15,17 +15,22 @@ class ZonesApi { int? maxPay, bool? includePrivate, String? hideLocationTypes, + CancelToken? cancelToken, }) async { - final response = await _dio.get('/zones', queryParameters: { - 'bbox': bbox, - 'view': view, - 'is_active': ?isActive, - 'min_free_count': ?minFreeCount, - 'min_confidence': ?minConfidence, - 'max_pay': ?maxPay, - 'include_private': ?includePrivate, - 'hide_location_types': ?hideLocationTypes, - }); + final response = await _dio.get( + '/zones', + queryParameters: { + 'bbox': bbox, + 'view': view, + 'is_active': ?isActive, + 'min_free_count': ?minFreeCount, + 'min_confidence': ?minConfidence, + 'max_pay': ?maxPay, + 'include_private': ?includePrivate, + 'hide_location_types': ?hideLocationTypes, + }, + cancelToken: cancelToken, + ); return (response.data as List) .map((e) => ZoneMapItemDto.fromJson(e as Map)) .toList(); diff --git a/lib/data/repositories/routing_repository.dart b/lib/data/repositories/routing_repository.dart index b4ab528..c7c36d3 100644 --- a/lib/data/repositories/routing_repository.dart +++ b/lib/data/repositories/routing_repository.dart @@ -1,3 +1,4 @@ +import 'package:dio/dio.dart'; import 'package:yandex_mapkit/yandex_mapkit.dart'; import '../api/routing_api.dart'; import '../models/routing_models.dart'; @@ -17,6 +18,7 @@ class RoutingRepository { int? minFreeCount, double? minConfidence, bool? useForecast, + CancelToken? cancelToken, }) async { final request = RoutingSearchRequestDto( mode: destinationLat != null ? 'route_to_destination' : 'find_parking', @@ -29,7 +31,10 @@ class RoutingRepository { minConfidence: minConfidence, useForecast: useForecast, ); - final response = await _api.searchParking(request); + final response = await _api.searchParking( + request, + cancelToken: cancelToken, + ); return response.candidates.map(_mapCandidate).toList(); } @@ -40,6 +45,7 @@ class RoutingRepository { double? destinationLon, required int selectedZoneId, bool? useForecast, + CancelToken? cancelToken, }) async { final request = RoutingSearchRequestDto( mode: destinationLat != null ? 'route_to_destination' : 'find_parking', @@ -49,34 +55,38 @@ class RoutingRepository { : null, useForecast: useForecast, ); - final dto = await _api.createRoute(request, selectedZoneId: selectedZoneId); + final dto = await _api.createRoute( + request, + selectedZoneId: selectedZoneId, + cancelToken: cancelToken, + ); return _mapRoute(dto); } RouteCandidate _mapCandidate(RouteCandidateDto dto) => RouteCandidate( - zoneId: dto.zoneId, - rank: dto.rank, - freeCount: dto.currentFreeCount, - confidence: dto.currentConfidence, - pay: dto.pay, - distanceToDestinationMeters: dto.distanceToDestinationMeters, - durationFromOriginSeconds: dto.durationFromOriginSeconds, - predictedFreeCount: dto.predictedFreeCount, - eta: dto.eta, - routePolyline: _parsePolyline(dto.routeGeometry), - ); + zoneId: dto.zoneId, + rank: dto.rank, + freeCount: dto.currentFreeCount, + confidence: dto.currentConfidence, + pay: dto.pay, + distanceToDestinationMeters: dto.distanceToDestinationMeters, + durationFromOriginSeconds: dto.durationFromOriginSeconds, + predictedFreeCount: dto.predictedFreeCount, + eta: dto.eta, + routePolyline: _parsePolyline(dto.routeGeometry), + ); ActiveRoute _mapRoute(RouteDto dto) => ActiveRoute( - routeId: dto.routeId, - status: dto.status, - selectedZoneId: dto.selectedZoneId ?? 0, - arrivalTime: dto.arrivalTime, - deeplinkUrl: dto.deeplinkUrl, - routePolyline: null, - candidates: dto.selectedCandidate != null - ? [_mapCandidate(dto.selectedCandidate!)] - : [], - ); + routeId: dto.routeId, + status: dto.status, + selectedZoneId: dto.selectedZoneId ?? 0, + arrivalTime: dto.arrivalTime, + deeplinkUrl: dto.deeplinkUrl, + routePolyline: null, + candidates: dto.selectedCandidate != null + ? [_mapCandidate(dto.selectedCandidate!)] + : [], + ); List? _parsePolyline(Map? geometry) { if (geometry == null) return null; @@ -84,7 +94,10 @@ class RoutingRepository { if (coordinates == null) return null; return coordinates.map((c) { final coord = c as List; - return Point(latitude: (coord[1] as num).toDouble(), longitude: (coord[0] as num).toDouble()); + return Point( + latitude: (coord[1] as num).toDouble(), + longitude: (coord[0] as num).toDouble(), + ); }).toList(); } } diff --git a/lib/data/repositories/zones_repository.dart b/lib/data/repositories/zones_repository.dart index 7c94a08..b70df50 100644 --- a/lib/data/repositories/zones_repository.dart +++ b/lib/data/repositories/zones_repository.dart @@ -1,3 +1,4 @@ +import 'package:dio/dio.dart'; import 'package:yandex_mapkit/yandex_mapkit.dart'; import '../api/zones_api.dart'; import '../api/occupancy_api.dart'; @@ -12,17 +13,39 @@ class ZonesRepository { ZonesRepository(this._zonesApi, this._occupancyApi, this._forecastsApi); - Future> getZonesNow(String bbox) async { - final dtos = await _zonesApi.getZones(bbox: bbox, view: 'map'); + Future> getZonesNow( + String bbox, { + CancelToken? cancelToken, + }) async { + final dtos = await _zonesApi.getZones( + bbox: bbox, + view: 'map', + cancelToken: cancelToken, + ); return dtos.map(_mapZone).toList(); } - Future> getZonesPast(String bbox, DateTime at) async { + Future> getZonesPast( + String bbox, + DateTime at, { + CancelToken? cancelToken, + }) async { final atStr = at.toUtc().toIso8601String(); - final zonesFuture = _zonesApi.getZones(bbox: bbox, view: 'map'); + final zonesFuture = _zonesApi.getZones( + bbox: bbox, + view: 'map', + cancelToken: cancelToken, + ); List> items; try { - items = await _occupancyApi.getOccupancyMap(bbox: bbox, at: atStr); + items = await _occupancyApi.getOccupancyMap( + bbox: bbox, + at: atStr, + cancelToken: cancelToken, + ); + } on DioException catch (error) { + if (CancelToken.isCancel(error)) rethrow; + items = []; } catch (_) { items = []; } @@ -44,11 +67,14 @@ class ZonesRepository { capacity: occ['capacity'] as int? ?? dto.capacity, freeCount: occ['free_count'] as int? ?? dto.freeCount, confidence: (occ['confidence'] as num?)?.toDouble() ?? dto.confidence, - confidenceLevel: occ['confidence_level'] as String? ?? dto.confidenceLevel, + confidenceLevel: + occ['confidence_level'] as String? ?? dto.confidenceLevel, pay: dto.pay, geometry: _parseGeometry(dto.geometry), isActive: occ['is_active'] as bool? ?? dto.isActive, - locationType: _parseLocationType(occ['location_type'] as String? ?? dto.locationType), + locationType: _parseLocationType( + occ['location_type'] as String? ?? dto.locationType, + ), isPrivate: dto.isPrivate, isAccessible: occ['is_accessible'] as bool? ?? dto.isAccessible, occupancyUpdatedAt: occ['observed_at'] != null @@ -58,15 +84,30 @@ class ZonesRepository { }).toList(); } - Future> getZonesFuture(String bbox, DateTime at) async { + Future> getZonesFuture( + String bbox, + DateTime at, { + CancelToken? cancelToken, + }) async { final atStr = at.toUtc().toIso8601String(); - final zonesFuture = _zonesApi.getZones(bbox: bbox, view: 'map'); - final forecastsFuture = _forecastsApi.getForecastsMap(bbox: bbox, at: atStr); + final zonesFuture = _zonesApi.getZones( + bbox: bbox, + view: 'map', + cancelToken: cancelToken, + ); + final forecastsFuture = _forecastsApi.getForecastsMap( + bbox: bbox, + at: atStr, + cancelToken: cancelToken, + ); final zoneDtos = await zonesFuture; List> forecasts; try { forecasts = await forecastsFuture; + } on DioException catch (error) { + if (CancelToken.isCancel(error)) rethrow; + forecasts = []; } catch (_) { forecasts = []; } @@ -92,78 +133,85 @@ class ZonesRepository { } Zone _mapZone(ZoneMapItemDto dto) => Zone( - zoneId: dto.zoneId, - zoneType: _parseZoneType(dto.zoneType), - capacity: dto.capacity, - freeCount: dto.freeCount, - confidence: dto.confidence, - pay: dto.pay, - geometry: _parseGeometry(dto.geometry), - isActive: dto.isActive, - locationType: _parseLocationType(dto.locationType), - isPrivate: dto.isPrivate, - isAccessible: dto.isAccessible, - confidenceLevel: dto.confidenceLevel, - occupancyUpdatedAt: dto.occupancyUpdatedAt != null - ? DateTime.tryParse(dto.occupancyUpdatedAt!) - : null, - ); + zoneId: dto.zoneId, + zoneType: _parseZoneType(dto.zoneType), + capacity: dto.capacity, + freeCount: dto.freeCount, + confidence: dto.confidence, + pay: dto.pay, + geometry: _parseGeometry(dto.geometry), + isActive: dto.isActive, + locationType: _parseLocationType(dto.locationType), + isPrivate: dto.isPrivate, + isAccessible: dto.isAccessible, + confidenceLevel: dto.confidenceLevel, + occupancyUpdatedAt: dto.occupancyUpdatedAt != null + ? DateTime.tryParse(dto.occupancyUpdatedAt!) + : null, + ); Zone _mapFromFull(ZoneDto dto) => Zone( - zoneId: dto.zoneId, - zoneType: _parseZoneType(dto.zoneType), - capacity: dto.capacity, - freeCount: dto.freeCount, - confidence: dto.confidence, - pay: dto.pay, - geometry: _parseGeometry(dto.geometry), - isActive: dto.isActive, - locationType: _parseLocationType(dto.locationType), - isPrivate: dto.isPrivate, - isAccessible: dto.isAccessible, - confidenceLevel: dto.confidenceLevel, - occupancyUpdatedAt: dto.occupancyUpdatedAt != null - ? DateTime.tryParse(dto.occupancyUpdatedAt!) - : null, - ); - - Zone _mapZoneWithForecast(ZoneMapItemDto dto, Map forecast) => Zone( - zoneId: dto.zoneId, - zoneType: _parseZoneType(dto.zoneType), - capacity: (forecast['capacity'] as num?)?.toInt() ?? dto.capacity, - freeCount: (forecast['predicted_free_count'] as num?)?.toInt() ?? dto.freeCount, - confidence: (forecast['confidence'] as num?)?.toDouble() ?? dto.confidence, - pay: (forecast['pay'] as num?)?.toInt() ?? dto.pay, - geometry: forecast['geometry'] != null - ? _parseGeometry(forecast['geometry'] as Map) - : _parseGeometry(dto.geometry), - isActive: forecast['is_active'] as bool? ?? dto.isActive, - locationType: _parseLocationType(forecast['location_type'] as String? ?? dto.locationType), - isPrivate: dto.isPrivate, - isAccessible: forecast['is_accessible'] as bool? ?? dto.isAccessible, - confidenceLevel: forecast['confidence_level'] as String? ?? dto.confidenceLevel, - occupancyUpdatedAt: dto.occupancyUpdatedAt != null - ? DateTime.tryParse(dto.occupancyUpdatedAt!) - : null, - forecastFor: forecast['predicted_for'] != null - ? DateTime.tryParse(forecast['predicted_for'] as String) - : null, - forecastGeneratedAt: forecast['generated_at'] != null - ? DateTime.tryParse(forecast['generated_at'] as String) - : null, - ); + zoneId: dto.zoneId, + zoneType: _parseZoneType(dto.zoneType), + capacity: dto.capacity, + freeCount: dto.freeCount, + confidence: dto.confidence, + pay: dto.pay, + geometry: _parseGeometry(dto.geometry), + isActive: dto.isActive, + locationType: _parseLocationType(dto.locationType), + isPrivate: dto.isPrivate, + isAccessible: dto.isAccessible, + confidenceLevel: dto.confidenceLevel, + occupancyUpdatedAt: dto.occupancyUpdatedAt != null + ? DateTime.tryParse(dto.occupancyUpdatedAt!) + : null, + ); + + Zone _mapZoneWithForecast( + ZoneMapItemDto dto, + Map forecast, + ) => Zone( + zoneId: dto.zoneId, + zoneType: _parseZoneType(dto.zoneType), + capacity: (forecast['capacity'] as num?)?.toInt() ?? dto.capacity, + freeCount: + (forecast['predicted_free_count'] as num?)?.toInt() ?? dto.freeCount, + confidence: (forecast['confidence'] as num?)?.toDouble() ?? dto.confidence, + pay: (forecast['pay'] as num?)?.toInt() ?? dto.pay, + geometry: forecast['geometry'] != null + ? _parseGeometry(forecast['geometry'] as Map) + : _parseGeometry(dto.geometry), + isActive: forecast['is_active'] as bool? ?? dto.isActive, + locationType: _parseLocationType( + forecast['location_type'] as String? ?? dto.locationType, + ), + isPrivate: dto.isPrivate, + isAccessible: forecast['is_accessible'] as bool? ?? dto.isAccessible, + confidenceLevel: + forecast['confidence_level'] as String? ?? dto.confidenceLevel, + occupancyUpdatedAt: dto.occupancyUpdatedAt != null + ? DateTime.tryParse(dto.occupancyUpdatedAt!) + : null, + forecastFor: forecast['predicted_for'] != null + ? DateTime.tryParse(forecast['predicted_for'] as String) + : null, + forecastGeneratedAt: forecast['generated_at'] != null + ? DateTime.tryParse(forecast['generated_at'] as String) + : null, + ); ZoneType _parseZoneType(String type) => type == 'parallel' ? ZoneType.parallel : ZoneType.standard; LocationType? _parseLocationType(String? type) => switch (type) { - 'street' => LocationType.street, - 'yard' => LocationType.yard, - 'open_lot' => LocationType.openLot, - 'underground' => LocationType.underground, - 'multilevel' => LocationType.multilevel, - _ => null, - }; + 'street' => LocationType.street, + 'yard' => LocationType.yard, + 'open_lot' => LocationType.openLot, + 'underground' => LocationType.underground, + 'multilevel' => LocationType.multilevel, + _ => null, + }; List _parseGeometry(Map geometry) { final coordinates = geometry['coordinates']; @@ -172,7 +220,10 @@ class ZonesRepository { return ring.map((c) { final coord = c as List; // GeoJSON: [longitude, latitude] → Yandex: Point(latitude, longitude) - return Point(latitude: (coord[1] as num).toDouble(), longitude: (coord[0] as num).toDouble()); + return Point( + latitude: (coord[1] as num).toDouble(), + longitude: (coord[0] as num).toDouble(), + ); }).toList(); } } diff --git a/lib/presentation/providers/navigation_provider.dart b/lib/presentation/providers/navigation_provider.dart index 3fbdd79..c44fcab 100644 --- a/lib/presentation/providers/navigation_provider.dart +++ b/lib/presentation/providers/navigation_provider.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:io'; +import 'dart:math' show max; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:geolocator/geolocator.dart'; import 'package:yandex_mapkit/yandex_mapkit.dart'; @@ -44,16 +45,22 @@ class NavigationNotifier extends Notifier { double _totalSeconds = 0; double _totalMeters = 0; List _route = []; + PreparedRoute? _preparedRoute; double _destLat = 0; double _destLon = 0; int _zoneId = 0; + int _navigationGeneration = 0; + int _offRouteSamples = 0; double? _smoothLat; double? _smoothLon; double? _smoothHeading; @override - NavigationData? build() => null; + NavigationData? build() { + ref.onDispose(_disposeResources); + return null; + } Future startNavigation({ required int zoneId, @@ -64,6 +71,7 @@ class NavigationNotifier extends Notifier { required double destLon, required AppStrings s, }) async { + _navigationGeneration++; _cleanupTimers(); await _sub?.cancel(); @@ -71,12 +79,20 @@ class NavigationNotifier extends Notifier { _totalSeconds = totalSeconds; _totalMeters = totalMeters; _route = route; + _preparedRoute = PreparedRoute(route); + if (_totalMeters <= 0) { + _totalMeters = _preparedRoute!.totalDistance; + } + if (_totalSeconds <= 0 && _totalMeters > 0) { + _totalSeconds = _totalMeters / 10; + } _destLat = destLat; _destLon = destLon; _zoneId = zoneId; _smoothLat = null; _smoothLon = null; _smoothHeading = null; + _offRouteSamples = 0; _sub = Geolocator.getPositionStream( locationSettings: _buildLocationSettings(s), @@ -84,12 +100,18 @@ class NavigationNotifier extends Notifier { } void stop() { + _navigationGeneration++; + _disposeResources(); + state = null; + } + + void _disposeResources() { _cleanupTimers(); _sub?.cancel(); _sub = null; _drivingSession?.close(); _drivingSession = null; - state = null; + _preparedRoute = null; } LocationSettings _buildLocationSettings(AppStrings s) { @@ -135,19 +157,27 @@ class NavigationNotifier extends Notifier { _smoothHeading = _emaCircular(rawHeading, _smoothHeading); final current = Point(latitude: _smoothLat!, longitude: _smoothLon!); - final closest = closestOnRoute(current, _route); + final preparedRoute = _preparedRoute; + if (preparedRoute == null || !preparedRoute.isValid) return; + final closest = preparedRoute.match(current, hintSegment: _segmentIndex); if (closest.segment > _segmentIndex) { _segmentIndex = closest.segment; } - final remaining = remainingRouteDistance(_route, _segmentIndex, closest.t); - final perp = perpDistToRoute(current, _route, _segmentIndex, closest.t); + final remaining = preparedRoute.remainingDistance(closest); + final perp = closest.distanceFromRoute; final remainingSecs = _totalMeters > 0 ? (_totalSeconds * remaining / _totalMeters).round() : 0; - final nextTurn = findNextTurn(_route, _segmentIndex, current); - final isOff = perp > 60; + final nextTurn = preparedRoute.nextTurn(closest); + final offRouteThreshold = max(60.0, pos.accuracy * 1.5); + if (pos.accuracy <= 50 && perp > offRouteThreshold) { + _offRouteSamples++; + } else { + _offRouteSamples = 0; + } + final isOff = _offRouteSamples >= 3; state = NavigationData( zoneId: _zoneId, @@ -175,7 +205,10 @@ class NavigationNotifier extends Notifier { // Debounced off-route recalculation if (isOff) { - _recalcDebounce ??= Timer(const Duration(seconds: 5), () => _recalculate(current)); + _recalcDebounce ??= Timer( + const Duration(seconds: 5), + () => _recalculate(current), + ); } else { _recalcDebounce?.cancel(); _recalcDebounce = null; @@ -185,6 +218,7 @@ class NavigationNotifier extends Notifier { Future _recalculate(Point from) async { _recalcDebounce = null; + final generation = _navigationGeneration; final current = state; if (current == null) return; @@ -204,7 +238,10 @@ class NavigationNotifier extends Notifier { await _drivingSession?.close(); final rws = YandexDriving.requestRoutes( points: [ - RequestPoint(point: from, requestPointType: RequestPointType.wayPoint), + RequestPoint( + point: from, + requestPointType: RequestPointType.wayPoint, + ), RequestPoint( point: Point(latitude: _destLat, longitude: _destLon), requestPointType: RequestPointType.wayPoint, @@ -218,17 +255,18 @@ class NavigationNotifier extends Notifier { _drivingSession = null; final dr = result.routes?.firstOrNull; - if (dr == null || state == null) return; + if (dr == null || state == null || generation != _navigationGeneration) { + return; + } _route = dr.geometry; + _preparedRoute = PreparedRoute(_route); _segmentIndex = 0; _totalSeconds = dr.metadata.weight.timeWithTraffic.value ?? _totalSeconds; _totalMeters = dr.metadata.weight.distance.value ?? _totalMeters; - if (_totalMeters == 0 && _route.length >= 2) { - for (int i = 0; i < _route.length - 1; i++) { - _totalMeters += navDistanceM(_route[i], _route[i + 1]); - } + if (_totalMeters == 0 && _preparedRoute!.isValid) { + _totalMeters = _preparedRoute!.totalDistance; _totalSeconds = _totalMeters / 10; } @@ -264,4 +302,6 @@ class NavigationNotifier extends Notifier { } final navigationProvider = - NotifierProvider(NavigationNotifier.new); + NotifierProvider( + NavigationNotifier.new, + ); diff --git a/lib/presentation/providers/routing_provider.dart b/lib/presentation/providers/routing_provider.dart index 2e32554..2086948 100644 --- a/lib/presentation/providers/routing_provider.dart +++ b/lib/presentation/providers/routing_provider.dart @@ -12,7 +12,8 @@ part 'routing_provider.freezed.dart'; class RoutingState with _$RoutingState { const factory RoutingState.idle() = _Idle; const factory RoutingState.searching() = _Searching; - const factory RoutingState.candidates(List candidates) = _Candidates; + const factory RoutingState.candidates(List candidates) = + _Candidates; const factory RoutingState.routePreview(ActiveRoute route) = _RoutePreview; const factory RoutingState.error(String message) = _Error; } @@ -30,8 +31,9 @@ final destinationProvider = StateProvider((ref) => null); enum DestinationMode { routeToAddress, nearestParking } -final destinationModeProvider = - StateProvider((ref) => DestinationMode.nearestParking); +final destinationModeProvider = StateProvider( + (ref) => DestinationMode.nearestParking, +); class SearchBias { const SearchBias({required this.latitude, required this.longitude}); @@ -48,30 +50,46 @@ class RoutingNotifier extends StateNotifier { RoutingNotifier(this._ref) : super(const RoutingState.idle()); final Ref _ref; + CancelToken? _cancelToken; + int _requestGeneration = 0; Future searchParking({ required double originLat, required double originLon, }) async { + final generation = ++_requestGeneration; + _cancelToken?.cancel('Superseded by a newer routing request'); + final cancelToken = CancelToken(); + _cancelToken = cancelToken; state = const RoutingState.searching(); try { final destination = _ref.read(destinationProvider); final filters = _ref.read(filtersProvider); final useForecast = true; - final candidates = await _ref.read(routingRepositoryProvider).searchParking( + final candidates = await _ref + .read(routingRepositoryProvider) + .searchParking( originLat: originLat, originLon: originLon, destinationLat: destination?.latitude, destinationLon: destination?.longitude, maxPay: filters.maxPayPerHour, - minConfidence: filters.minConfidence > 0 ? filters.minConfidence : null, + minConfidence: filters.minConfidence > 0 + ? filters.minConfidence + : null, minFreeCount: filters.hideNoFreeSpots ? 1 : null, useForecast: useForecast, + cancelToken: cancelToken, ); + if (generation != _requestGeneration || cancelToken.isCancelled) return; state = RoutingState.candidates(candidates); } catch (e) { + if (e is DioException && CancelToken.isCancel(e)) return; + if (generation != _requestGeneration) return; if (e is DioException && e.response != null) { - state = RoutingState.error('${e.response!.statusCode}: ${e.response!.data}'); + state = RoutingState.error( + '${e.response!.statusCode}: ${e.response!.data}', + ); } else { state = RoutingState.error(e.toString()); } @@ -83,31 +101,58 @@ class RoutingNotifier extends StateNotifier { required double originLon, required int selectedZoneId, }) async { + final generation = ++_requestGeneration; + _cancelToken?.cancel('Superseded by a newer routing request'); + final cancelToken = CancelToken(); + _cancelToken = cancelToken; state = const RoutingState.searching(); try { final destination = _ref.read(destinationProvider); final timeMode = _ref.read(timeSelectorProvider); - final useForecast = timeMode.maybeWhen(future: (_) => true, orElse: () => null); - final route = await _ref.read(routingRepositoryProvider).createRoute( + final useForecast = timeMode.maybeWhen( + future: (_) => true, + orElse: () => null, + ); + final route = await _ref + .read(routingRepositoryProvider) + .createRoute( originLat: originLat, originLon: originLon, destinationLat: destination?.latitude, destinationLon: destination?.longitude, selectedZoneId: selectedZoneId, useForecast: useForecast, + cancelToken: cancelToken, ); + if (generation != _requestGeneration || cancelToken.isCancelled) return; state = RoutingState.routePreview(route); } catch (e) { + if (e is DioException && CancelToken.isCancel(e)) return; + if (generation != _requestGeneration) return; if (e is DioException && e.response != null) { - state = RoutingState.error('${e.response!.statusCode}: ${e.response!.data}'); + state = RoutingState.error( + '${e.response!.statusCode}: ${e.response!.data}', + ); } else { state = RoutingState.error(e.toString()); } } } - void reset() => state = const RoutingState.idle(); + void reset() { + _requestGeneration++; + _cancelToken?.cancel('Routing reset'); + _cancelToken = null; + state = const RoutingState.idle(); + } + + @override + void dispose() { + _cancelToken?.cancel('Routing provider disposed'); + super.dispose(); + } } -final routingProvider = - StateNotifierProvider((ref) => RoutingNotifier(ref)); +final routingProvider = StateNotifierProvider( + (ref) => RoutingNotifier(ref), +); diff --git a/lib/presentation/providers/zones_provider.dart b/lib/presentation/providers/zones_provider.dart index 42bd6f5..4384d99 100644 --- a/lib/presentation/providers/zones_provider.dart +++ b/lib/presentation/providers/zones_provider.dart @@ -1,3 +1,4 @@ +import 'package:dio/dio.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../domain/models/zone.dart'; import 'app_providers.dart'; @@ -6,43 +7,71 @@ import 'filters_provider.dart'; final rawZonesProvider = StateNotifierProvider>>( - (ref) => ZonesNotifier(ref), -); + (ref) => ZonesNotifier(ref), + ); class ZonesNotifier extends StateNotifier>> { ZonesNotifier(this._ref) : super(const AsyncValue.data([])); final Ref _ref; String? _lastBbox; + String? _lastRequestKey; + CancelToken? _cancelToken; + int _requestGeneration = 0; + + Future fetchZones(String bbox, {bool force = false}) async { + final timeMode = _ref.read(timeSelectorProvider); + final requestKey = '$bbox|$timeMode'; + if (!force && requestKey == _lastRequestKey && state.hasValue) return; - Future fetchZones(String bbox) async { _lastBbox = bbox; + _lastRequestKey = requestKey; + final generation = ++_requestGeneration; + _cancelToken?.cancel('Superseded by a newer viewport request'); + final cancelToken = CancelToken(); + _cancelToken = cancelToken; state = const AsyncValue>.loading().copyWithPrevious(state); try { final repo = _ref.read(zonesRepositoryProvider); - final timeMode = _ref.read(timeSelectorProvider); final zones = await timeMode.when( - now: () => repo.getZonesNow(bbox), - past: (at) => repo.getZonesPast(bbox, at), - future: (at) => repo.getZonesFuture(bbox, at), + now: () => repo.getZonesNow(bbox, cancelToken: cancelToken), + past: (at) => repo.getZonesPast(bbox, at, cancelToken: cancelToken), + future: (at) => repo.getZonesFuture(bbox, at, cancelToken: cancelToken), ); + if (generation != _requestGeneration || cancelToken.isCancelled) return; state = AsyncValue.data(zones); + } on DioException catch (e) { + if (CancelToken.isCancel(e) || generation != _requestGeneration) return; + state = AsyncValue.error(e, e.stackTrace); } catch (e, st) { + if (generation != _requestGeneration) return; state = AsyncValue.error(e, st); + } finally { + if (identical(_cancelToken, cancelToken)) _cancelToken = null; } } Future refresh() async { - if (_lastBbox != null) await fetchZones(_lastBbox!); + if (_lastBbox != null) await fetchZones(_lastBbox!, force: true); } void clearZones() { + _requestGeneration++; + _cancelToken?.cancel('Zone state cleared'); + _cancelToken = null; + _lastRequestKey = null; state = const AsyncValue.loading(); } void setErrorState(Object error, StackTrace stackTrace) { state = AsyncValue.error(error, stackTrace); } + + @override + void dispose() { + _cancelToken?.cancel('Zones provider disposed'); + super.dispose(); + } } final filteredZonesProvider = Provider>((ref) { @@ -53,11 +82,15 @@ final filteredZonesProvider = Provider>((ref) { return zones.where((z) { if (filters.hideInactive && !z.isActive) return false; if (filters.hideNoFreeSpots && z.freeCount == 0) return false; - if (filters.minFreeCount > 0 && z.freeCount < filters.minFreeCount) return false; + if (filters.minFreeCount > 0 && z.freeCount < filters.minFreeCount) { + return false; + } if (z.confidence < filters.minConfidence) return false; - if (filters.maxPayPerHour != null && z.pay > filters.maxPayPerHour!) return false; + if (filters.maxPayPerHour != null && z.pay > filters.maxPayPerHour!) { + return false; + } if (filters.hidePrivate && (z.isPrivate ?? false)) return false; - if (filters.hideInaccessible && (z.isAccessible == true)) return false; + if (filters.hideInaccessible && (z.isAccessible == false)) return false; if (z.locationType != null) { final typeKey = _locationTypeKey(z.locationType!); if (filters.hiddenLocationTypes.contains(typeKey)) return false; @@ -67,9 +100,9 @@ final filteredZonesProvider = Provider>((ref) { }); String _locationTypeKey(LocationType type) => switch (type) { - LocationType.street => 'street', - LocationType.yard => 'yard', - LocationType.openLot => 'open_lot', - LocationType.underground => 'underground', - LocationType.multilevel => 'multilevel', - }; + LocationType.street => 'street', + LocationType.yard => 'yard', + LocationType.openLot => 'open_lot', + LocationType.underground => 'underground', + LocationType.multilevel => 'multilevel', +}; diff --git a/lib/presentation/screens/map/map_screen.dart b/lib/presentation/screens/map/map_screen.dart index 5697134..1a746e9 100644 --- a/lib/presentation/screens/map/map_screen.dart +++ b/lib/presentation/screens/map/map_screen.dart @@ -3,7 +3,7 @@ import 'dart:math' as math; import 'dart:typed_data'; import 'dart:ui' as ui; import 'package:flutter/foundation.dart' - show TargetPlatform, defaultTargetPlatform, kIsWeb; + show TargetPlatform, defaultTargetPlatform, kIsWeb, setEquals; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:geolocator/geolocator.dart'; @@ -17,7 +17,6 @@ import '../../providers/routing_provider.dart'; import '../../providers/navigation_provider.dart'; import '../../providers/time_selector_provider.dart'; import '../../../core/theme/app_colors.dart'; -import '../../../core/utils/nav_math.dart'; import '../../../core/utils/navigation_deeplink.dart'; import '../../../core/utils/error_snackbar.dart'; import '../../../core/localization/app_localizations.dart'; @@ -54,12 +53,22 @@ class _MapScreenState extends ConsumerState { Map _zoneLabelCache = {}; Map _zonesById = {}; + final Map<({int? count, int color}), Uint8List> _zoneBitmapCache = {}; + Map _zoneStylesById = {}; int _bitmapGeneration = 0; List? _routePolyline; + double _routeDurationSeconds = 0; int? _activeRouteZoneId; DrivingSession? _drivingSession; bool _navBuilding = false; + List? _cachedMapZones; + Set _cachedCandidateIds = const {}; + List _cachedZoneObjects = const []; + Map? _cachedLabelBitmaps; + MapObject? _cachedZoneLabels; + Set _candidateIds = const {}; + @override void initState() { super.initState(); @@ -106,27 +115,36 @@ class _MapScreenState extends ConsumerState { } Future _updateZoneBitmaps(List zones) async { + final styles = {}; + for (final zone in zones) { + final color = zoneColor(zone); + styles[zone.zoneId] = ( + count: color == AppColors.parkingUnknown ? null : zone.freeCount, + color: color.toARGB32(), + ); + } final same = - zones.length == _zonesById.length && - zones.every((z) { - final prev = _zonesById[z.zoneId]; - return prev != null && - prev.freeCount == z.freeCount && - prev.hasForecast == z.hasForecast && - prev.isActive == z.isActive; - }); + styles.length == _zoneStylesById.length && + styles.entries.every( + (entry) => _zoneStylesById[entry.key] == entry.value, + ); if (same) return; final generation = ++_bitmapGeneration; final newCache = {}; final newById = {}; + final usedStyles = <({int? count, int color})>{}; for (final zone in zones) { if (_bitmapGeneration != generation) return; if (zone.geometry.length < 3) continue; - final color = zoneColor(zone); - newCache[zone.zoneId] = await buildCountBitmap( - color == AppColors.parkingUnknown ? null : zone.freeCount, - color, - ); + final style = styles[zone.zoneId]!; + usedStyles.add(style); + var bitmap = _zoneBitmapCache[style]; + if (bitmap == null) { + bitmap = await buildCountBitmap(style.count, Color(style.color)); + if (_bitmapGeneration != generation) return; + _zoneBitmapCache[style] = bitmap; + } + newCache[zone.zoneId] = bitmap; newById[zone.zoneId] = zone; } if (_bitmapGeneration != generation) return; @@ -134,6 +152,8 @@ class _MapScreenState extends ConsumerState { setState(() { _zoneLabelCache = newCache; _zonesById = newById; + _zoneStylesById = styles; + _zoneBitmapCache.removeWhere((key, _) => !usedStyles.contains(key)); }); } } @@ -202,11 +222,88 @@ class _MapScreenState extends ConsumerState { _lastCameraTarget = position.target; _debounce?.cancel(); _debounce = Timer(const Duration(milliseconds: 400), _fetchZones); - if ((position.azimuth - _currentAzimuth).abs() > 0.5) { + if ((position.azimuth - _currentAzimuth).abs() > 2) { setState(() => _currentAzimuth = position.azimuth); } } + void _onZoneTap(Zone zone) { + if (_isSelectingOnMap && _candidateIds.contains(zone.zoneId)) { + _buildRouteForZone(zone.zoneId); + return; + } + if (_isParkingCardOpen) return; + setState(() => _isParkingCardOpen = true); + showParkingCard( + context, + zone, + onBuildRoute: () => _buildRouteForZone(zone.zoneId), + ).then((_) { + if (mounted) setState(() => _isParkingCardOpen = false); + }); + } + + void _onClusterTap(_, Cluster cluster) { + if (cluster.placemarks.isEmpty) return; + final lats = cluster.placemarks.map((p) => p.point.latitude); + final lons = cluster.placemarks.map((p) => p.point.longitude); + final latMin = lats.reduce(math.min); + final latMax = lats.reduce(math.max); + final lonMin = lons.reduce(math.min); + final lonMax = lons.reduce(math.max); + final latPad = (latMax - latMin) * 0.6 + 0.003; + final lonPad = (lonMax - lonMin) * 0.6 + 0.003; + _mapController?.moveCamera( + CameraUpdate.newGeometry( + Geometry.fromBoundingBox( + BoundingBox( + southWest: Point( + latitude: latMin - latPad, + longitude: lonMin - lonPad, + ), + northEast: Point( + latitude: latMax + latPad, + longitude: lonMax + lonPad, + ), + ), + ), + ), + animation: const MapAnimation(duration: 0.5), + ); + } + + List _zoneObjectsFor(List zones, Set candidateIds) { + if (!identical(_cachedMapZones, zones) || + !setEquals(_cachedCandidateIds, candidateIds)) { + _cachedMapZones = zones; + _cachedCandidateIds = Set.unmodifiable(candidateIds); + _cachedZoneObjects = buildZoneMapObjects( + zones: zones, + highlightedIds: candidateIds, + onTap: _onZoneTap, + ); + _cachedZoneLabels = null; + } + return _cachedZoneObjects; + } + + MapObject? _zoneLabelsFor(List zones) { + if (_zoneLabelCache.isEmpty) return null; + if (_cachedZoneLabels == null || + !identical(_cachedMapZones, zones) || + !identical(_cachedLabelBitmaps, _zoneLabelCache)) { + _cachedLabelBitmaps = _zoneLabelCache; + _cachedZoneLabels = buildZoneLabels( + zones: zones, + bitmapCache: _zoneLabelCache, + zonesById: _zonesById, + onZoneTap: _onZoneTap, + onClusterTap: _onClusterTap, + ); + } + return _cachedZoneLabels; + } + Future _fetchZones({bool clearCache = false}) async { if (_mapController == null) return; if (clearCache) { @@ -400,7 +497,7 @@ class _MapScreenState extends ConsumerState { ); List route; - double totalSeconds = 0; + double totalSeconds = _routeDurationSeconds; double totalMeters = 0; if (_routePolyline != null && _routePolyline!.length >= 2) { @@ -432,13 +529,6 @@ class _MapScreenState extends ConsumerState { setState(() => _routePolyline = route); } - if (totalMeters == 0 && route.length >= 2) { - for (int i = 0; i < route.length - 1; i++) { - totalMeters += navDistanceM(route[i], route[i + 1]); - } - totalSeconds = totalMeters / 10; - } - await ref .read(navigationProvider.notifier) .startNavigation( @@ -568,7 +658,6 @@ class _MapScreenState extends ConsumerState { tilt: 40, ), ), - animation: const MapAnimation(duration: 0.6), ); }); @@ -607,6 +696,7 @@ class _MapScreenState extends ConsumerState { await _mapController?.toggleTrafficLayer(visible: false); setState(() { _routePolyline = null; + _routeDurationSeconds = 0; _activeRouteZoneId = null; }); }, @@ -664,10 +754,14 @@ class _MapScreenState extends ConsumerState { } setState(() { - _routePolyline = route.candidates.firstOrNull?.routePolyline; + final candidate = route.candidates.firstOrNull; + _routePolyline = candidate?.routePolyline; + _routeDurationSeconds = + candidate?.durationFromOriginSeconds?.toDouble() ?? 0; _activeRouteZoneId = route.selectedZoneId; }); + if (!context.mounted) return; await showModalBottomSheet( context: context, isScrollControlled: true, @@ -693,26 +787,10 @@ class _MapScreenState extends ConsumerState { candidates: (c) => c.map((e) => e.zoneId).toSet(), orElse: () => {}, ); + _candidateIds = candidateIds; - final zoneObjects = buildZoneMapObjects( - zones: zones, - highlightedIds: candidateIds, - onTap: (zone) { - if (_isSelectingOnMap && candidateIds.contains(zone.zoneId)) { - _buildRouteForZone(zone.zoneId); - return; - } - if (_isParkingCardOpen) return; - setState(() => _isParkingCardOpen = true); - showParkingCard( - context, - zone, - onBuildRoute: () => _buildRouteForZone(zone.zoneId), - ).then((_) { - if (mounted) setState(() => _isParkingCardOpen = false); - }); - }, - ); + final zoneObjects = _zoneObjectsFor(zones, candidateIds); + final zoneLabels = _zoneLabelsFor(zones); final mapObjects = [ ...zoneObjects, @@ -725,55 +803,7 @@ class _MapScreenState extends ConsumerState { ), if (_activeRouteZoneId != null) ...buildHighlightZone(zones, _activeRouteZoneId!), - if (_zoneLabelCache.isNotEmpty) - buildZoneLabels( - zones: zones, - bitmapCache: _zoneLabelCache, - zonesById: _zonesById, - onZoneTap: (zone) { - if (_isSelectingOnMap && candidateIds.contains(zone.zoneId)) { - _buildRouteForZone(zone.zoneId); - return; - } - if (_isParkingCardOpen) return; - setState(() => _isParkingCardOpen = true); - showParkingCard( - context, - zone, - onBuildRoute: () => _buildRouteForZone(zone.zoneId), - ).then((_) { - if (mounted) setState(() => _isParkingCardOpen = false); - }); - }, - onClusterTap: (_, cluster) { - if (cluster.placemarks.isEmpty) return; - final lats = cluster.placemarks.map((p) => p.point.latitude); - final lons = cluster.placemarks.map((p) => p.point.longitude); - final latMin = lats.reduce(math.min); - final latMax = lats.reduce(math.max); - final lonMin = lons.reduce(math.min); - final lonMax = lons.reduce(math.max); - final latPad = (latMax - latMin) * 0.6 + 0.003; - final lonPad = (lonMax - lonMin) * 0.6 + 0.003; - _mapController?.moveCamera( - CameraUpdate.newGeometry( - Geometry.fromBoundingBox( - BoundingBox( - southWest: Point( - latitude: latMin - latPad, - longitude: lonMin - lonPad, - ), - northEast: Point( - latitude: latMax + latPad, - longitude: lonMax + lonPad, - ), - ), - ), - ), - animation: const MapAnimation(duration: 0.5), - ); - }, - ), + ?zoneLabels, if (_userPosition != null && _userLocationBytes != null && !isNavigating) PlacemarkMapObject( mapId: const MapObjectId('user_location'), diff --git a/lib/presentation/screens/map/widgets/parking_zone_layer.dart b/lib/presentation/screens/map/widgets/parking_zone_layer.dart index 77ce5fd..7b7f77d 100644 --- a/lib/presentation/screens/map/widgets/parking_zone_layer.dart +++ b/lib/presentation/screens/map/widgets/parking_zone_layer.dart @@ -7,6 +7,26 @@ import '../../../../domain/models/zone.dart'; import '../../../../core/constants.dart'; import '../../../../core/theme/app_colors.dart'; +final Map<({int? totalFree, int clusterSize, int color}), Future> +_clusterBitmapCache = {}; + +Future _cachedClusterBitmap( + int? totalFree, + int clusterSize, + Color color, +) { + final key = ( + totalFree: totalFree, + clusterSize: clusterSize, + color: color.toARGB32(), + ); + if (_clusterBitmapCache.length > 128) _clusterBitmapCache.clear(); + return _clusterBitmapCache.putIfAbsent( + key, + () => buildClusterBitmap(totalFree, clusterSize, color), + ); +} + List buildZoneMapObjects({ required List zones, required void Function(Zone) onTap, @@ -18,7 +38,9 @@ List buildZoneMapObjects({ final color = zoneColor(zone); final highlighted = highlightedIds.contains(zone.zoneId); if (zone.zoneType == ZoneType.parallel) { - result.add(_buildParallelLine(zone, color, onTap, highlighted: highlighted)); + result.add( + _buildParallelLine(zone, color, onTap, highlighted: highlighted), + ); } else { result.add(_buildPolygon(zone, color, onTap, highlighted: highlighted)); } @@ -27,7 +49,9 @@ List buildZoneMapObjects({ } List buildHighlightZone(List zones, int zoneId) { - final matches = zones.where((z) => z.zoneId == zoneId && !_isDegenerate(z.geometry)); + final matches = zones.where( + (z) => z.zoneId == zoneId && !_isDegenerate(z.geometry), + ); if (matches.isEmpty) return []; final zone = matches.first; final color = zoneColor(zone); @@ -62,7 +86,10 @@ List buildHighlightZone(List zones, int zoneId) { return [ PolygonMapObject( mapId: MapObjectId('zone_highlight_${zone.zoneId}'), - polygon: Polygon(outerRing: LinearRing(points: zone.geometry), innerRings: []), + polygon: Polygon( + outerRing: LinearRing(points: zone.geometry), + innerRings: [], + ), fillColor: color.withValues(alpha: 0.6), strokeColor: Colors.white, strokeWidth: 4, @@ -79,17 +106,23 @@ MapObject buildZoneLabels({ void Function(Zone)? onZoneTap, }) { final placemarks = zones - .where((z) => !_isDegenerate(z.geometry) && bitmapCache.containsKey(z.zoneId)) - .map((zone) => PlacemarkMapObject( - mapId: MapObjectId('zone_label_${zone.zoneId}'), - point: centroid(zone.geometry), - opacity: 1.0, - icon: PlacemarkIcon.single(PlacemarkIconStyle( + .where( + (z) => !_isDegenerate(z.geometry) && bitmapCache.containsKey(z.zoneId), + ) + .map( + (zone) => PlacemarkMapObject( + mapId: MapObjectId('zone_label_${zone.zoneId}'), + point: centroid(zone.geometry), + opacity: 1.0, + icon: PlacemarkIcon.single( + PlacemarkIconStyle( image: BitmapDescriptor.fromBytes(bitmapCache[zone.zoneId]!), scale: 1.0, - )), - onTap: onZoneTap != null ? (_, _) => onZoneTap(zone) : null, - )) + ), + ), + onTap: onZoneTap != null ? (_, _) => onZoneTap(zone) : null, + ), + ) .toList(); return ClusterizedPlacemarkCollection( @@ -100,15 +133,19 @@ MapObject buildZoneLabels({ onClusterTap: onClusterTap, onClusterAdded: (collection, cluster) async { final zoneIds = cluster.placemarks - .map((p) => int.tryParse(p.mapId.value.replaceFirst('zone_label_', ''))) + .map( + (p) => int.tryParse(p.mapId.value.replaceFirst('zone_label_', '')), + ) .whereType() .toList(); - final hasForecast = zoneIds.any((id) => zonesById[id]?.hasForecast == true); + final hasForecast = zoneIds.any( + (id) => zonesById[id]?.hasForecast == true, + ); final int? totalFree = hasForecast ? zoneIds - .where((id) => zonesById[id]?.hasForecast == true) - .map((id) => zonesById[id]?.freeCount ?? 0) - .fold(0, (a, b) => a + b) + .where((id) => zonesById[id]?.hasForecast == true) + .map((id) => zonesById[id]?.freeCount ?? 0) + .fold(0, (a, b) => a + b) : null; final Color clusterColor; if (totalFree == null) { @@ -118,14 +155,20 @@ MapObject buildZoneLabels({ } else { clusterColor = AppColors.primary; } - final bytes = await buildClusterBitmap(totalFree, cluster.placemarks.length, clusterColor); + final bytes = await _cachedClusterBitmap( + totalFree, + cluster.placemarks.length, + clusterColor, + ); return cluster.copyWith( appearance: cluster.appearance.copyWith( opacity: 1.0, - icon: PlacemarkIcon.single(PlacemarkIconStyle( - image: BitmapDescriptor.fromBytes(bytes), - scale: 1.0, - )), + icon: PlacemarkIcon.single( + PlacemarkIconStyle( + image: BitmapDescriptor.fromBytes(bytes), + scale: 1.0, + ), + ), ), ); }, @@ -162,7 +205,11 @@ Future buildCountBitmap(int? count, Color color) async { return data!.buffer.asUint8List(); } -Future buildClusterBitmap(int? totalFree, int clusterSize, Color color) async { +Future buildClusterBitmap( + int? totalFree, + int clusterSize, + Color color, +) async { const size = 84.0; final center = Offset(size / 2, size / 2); final recorder = ui.PictureRecorder(); @@ -216,8 +263,12 @@ Color zoneColor(Zone zone) { return AppColors.parkingFewLow; } -MapObject _buildPolygon(Zone zone, Color color, void Function(Zone) onTap, - {bool highlighted = false}) { +MapObject _buildPolygon( + Zone zone, + Color color, + void Function(Zone) onTap, { + bool highlighted = false, +}) { return PolygonMapObject( mapId: MapObjectId('zone_polygon_${zone.zoneId}'), polygon: Polygon( @@ -231,8 +282,12 @@ MapObject _buildPolygon(Zone zone, Color color, void Function(Zone) onTap, ); } -MapObject _buildParallelLine(Zone zone, Color color, void Function(Zone) onTap, - {bool highlighted = false}) { +MapObject _buildParallelLine( + Zone zone, + Color color, + void Function(Zone) onTap, { + bool highlighted = false, +}) { final points = zone.geometry; if (points.length < 4) return _buildPolygon(zone, color, onTap); @@ -266,12 +321,13 @@ double _distance(Point a, Point b) { } Point _midpoint(Point a, Point b) => Point( - latitude: (a.latitude + b.latitude) / 2, - longitude: (a.longitude + b.longitude) / 2, - ); + latitude: (a.latitude + b.latitude) / 2, + longitude: (a.longitude + b.longitude) / 2, +); Point centroid(List points) { - final pts = points.length > 1 && + final pts = + points.length > 1 && points.first.latitude == points.last.latitude && points.first.longitude == points.last.longitude ? points.sublist(0, points.length - 1) diff --git a/lib/presentation/screens/search/search_screen.dart b/lib/presentation/screens/search/search_screen.dart index e3fe3d4..2399218 100644 --- a/lib/presentation/screens/search/search_screen.dart +++ b/lib/presentation/screens/search/search_screen.dart @@ -4,7 +4,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:yandex_mapkit/yandex_mapkit.dart'; import '../../providers/routing_provider.dart'; -import '../../../core/theme/app_colors.dart'; import '../../../core/localization/app_localizations.dart'; class SearchScreen extends ConsumerStatefulWidget { diff --git a/macos/Flutter/Flutter-Debug.xcconfig b/macos/Flutter/Flutter-Debug.xcconfig index c2efd0b..4b81f9b 100644 --- a/macos/Flutter/Flutter-Debug.xcconfig +++ b/macos/Flutter/Flutter-Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/Flutter-Release.xcconfig b/macos/Flutter/Flutter-Release.xcconfig index c2efd0b..5caa9d1 100644 --- a/macos/Flutter/Flutter-Release.xcconfig +++ b/macos/Flutter/Flutter-Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Podfile b/macos/Podfile new file mode 100644 index 0000000..ff5ddb3 --- /dev/null +++ b/macos/Podfile @@ -0,0 +1,42 @@ +platform :osx, '10.15' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/macos/Podfile.lock b/macos/Podfile.lock new file mode 100644 index 0000000..58f39fb --- /dev/null +++ b/macos/Podfile.lock @@ -0,0 +1,35 @@ +PODS: + - flutter_secure_storage_macos (6.1.3): + - FlutterMacOS + - FlutterMacOS (1.0.0) + - geolocator_apple (1.2.0): + - Flutter + - FlutterMacOS + - url_launcher_macos (0.0.1): + - FlutterMacOS + +DEPENDENCIES: + - flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`) + - FlutterMacOS (from `Flutter/ephemeral`) + - geolocator_apple (from `Flutter/ephemeral/.symlinks/plugins/geolocator_apple/darwin`) + - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) + +EXTERNAL SOURCES: + flutter_secure_storage_macos: + :path: Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos + FlutterMacOS: + :path: Flutter/ephemeral + geolocator_apple: + :path: Flutter/ephemeral/.symlinks/plugins/geolocator_apple/darwin + url_launcher_macos: + :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos + +SPEC CHECKSUMS: + flutter_secure_storage_macos: 7f45e30f838cf2659862a4e4e3ee1c347c2b3b54 + FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 + geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e + url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd + +PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009 + +COCOAPODS: 1.17.0 diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 5c8cc21..46d5975 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -27,6 +27,8 @@ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 3D62B0016C620836C6C35E2C /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2442744519EC1EF8EE1EFF98 /* Pods_Runner.framework */; }; + B01792C27E4D48C9A53D15C4 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5594F6A2767694551B3D5C11 /* Pods_RunnerTests.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -60,11 +62,14 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 03D865B8A42E9E854365E150 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 20DBD8F291B0A489DE6F30A6 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 2442744519EC1EF8EE1EFF98 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* mobile.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "mobile.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10ED2044A3C60003C045 /* mobile.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = mobile.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; @@ -76,8 +81,13 @@ 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 5594F6A2767694551B3D5C11 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 6395593667471E2853E4E0A1 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 6FCEA48C7297F3086F0BCBAC /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + AB9C206508EA4DA6EF155D97 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + AF02B0530EC35ACC93142B98 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -85,6 +95,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + B01792C27E4D48C9A53D15C4 /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -92,6 +103,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 3D62B0016C620836C6C35E2C /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -125,6 +137,7 @@ 331C80D6294CF71000263BE5 /* RunnerTests */, 33CC10EE2044A3C60003C045 /* Products */, D73912EC22F37F3D000D13A0 /* Frameworks */, + 6897AF598BB9F1B8016DBA0E /* Pods */, ); sourceTree = ""; }; @@ -172,9 +185,25 @@ path = Runner; sourceTree = ""; }; + 6897AF598BB9F1B8016DBA0E /* Pods */ = { + isa = PBXGroup; + children = ( + 03D865B8A42E9E854365E150 /* Pods-Runner.debug.xcconfig */, + AF02B0530EC35ACC93142B98 /* Pods-Runner.release.xcconfig */, + 6FCEA48C7297F3086F0BCBAC /* Pods-Runner.profile.xcconfig */, + 6395593667471E2853E4E0A1 /* Pods-RunnerTests.debug.xcconfig */, + 20DBD8F291B0A489DE6F30A6 /* Pods-RunnerTests.release.xcconfig */, + AB9C206508EA4DA6EF155D97 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; D73912EC22F37F3D000D13A0 /* Frameworks */ = { isa = PBXGroup; children = ( + 2442744519EC1EF8EE1EFF98 /* Pods_Runner.framework */, + 5594F6A2767694551B3D5C11 /* Pods_RunnerTests.framework */, ); name = Frameworks; sourceTree = ""; @@ -186,6 +215,7 @@ isa = PBXNativeTarget; buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( + 36FA47B4D35D8323D6E895F9 /* [CP] Check Pods Manifest.lock */, 331C80D1294CF70F00263BE5 /* Sources */, 331C80D2294CF70F00263BE5 /* Frameworks */, 331C80D3294CF70F00263BE5 /* Resources */, @@ -204,11 +234,13 @@ isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + B1361D45F043E7ECF02E575B /* [CP] Check Pods Manifest.lock */, 33CC10E92044A3C60003C045 /* Sources */, 33CC10EA2044A3C60003C045 /* Frameworks */, 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, + 421F0D8C7F99BC195FB4DC83 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -329,6 +361,67 @@ shellPath = /bin/sh; shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; }; + 36FA47B4D35D8323D6E895F9 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 421F0D8C7F99BC195FB4DC83 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + B1361D45F043E7ECF02E575B /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -380,6 +473,7 @@ /* Begin XCBuildConfiguration section */ 331C80DB294CF71000263BE5 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 6395593667471E2853E4E0A1 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; @@ -394,6 +488,7 @@ }; 331C80DC294CF71000263BE5 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 20DBD8F291B0A489DE6F30A6 /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; @@ -408,6 +503,7 @@ }; 331C80DD294CF71000263BE5 /* Profile */ = { isa = XCBuildConfiguration; + baseConfigurationReference = AB9C206508EA4DA6EF155D97 /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; diff --git a/macos/Runner.xcworkspace/contents.xcworkspacedata b/macos/Runner.xcworkspace/contents.xcworkspacedata index 1d526a1..21a3cc1 100644 --- a/macos/Runner.xcworkspace/contents.xcworkspacedata +++ b/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + diff --git a/test/core/utils/nav_math_test.dart b/test/core/utils/nav_math_test.dart new file mode 100644 index 0000000..9f92c4f --- /dev/null +++ b/test/core/utils/nav_math_test.dart @@ -0,0 +1,60 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mobile/core/utils/nav_math.dart'; +import 'package:yandex_mapkit/yandex_mapkit.dart'; + +void main() { + group('PreparedRoute', () { + test('precomputes distance and returns decreasing remaining distance', () { + final route = PreparedRoute(const [ + Point(latitude: 0, longitude: 0), + Point(latitude: 0, longitude: 0.001), + Point(latitude: 0, longitude: 0.002), + ]); + + final first = route.match(const Point(latitude: 0, longitude: 0.00025)); + final second = route.match( + const Point(latitude: 0, longitude: 0.0015), + hintSegment: first.segment, + ); + + expect(route.totalDistance, closeTo(222.4, 1)); + expect(first.segment, 0); + expect(second.segment, 1); + expect( + route.remainingDistance(second), + lessThan(route.remainingDistance(first)), + ); + }); + + test('precomputes the next turn', () { + final route = PreparedRoute(const [ + Point(latitude: 0, longitude: 0), + Point(latitude: 0.001, longitude: 0), + Point(latitude: 0.001, longitude: 0.001), + ]); + + final match = route.match(const Point(latitude: 0.0002, longitude: 0)); + final turn = route.nextTurn(match); + + expect(turn, isNotNull); + expect(turn!.direction, TurnDirection.right); + expect(turn.distanceMeters, closeTo(89, 2)); + }); + + test('falls back to a full scan after a large position jump', () { + final points = List.generate( + 101, + (index) => Point(latitude: 0, longitude: index * 0.001), + ); + final route = PreparedRoute(points); + + final match = route.match( + const Point(latitude: 0, longitude: 0.0905), + hintSegment: 0, + ); + + expect(match.segment, 90); + expect(match.distanceFromRoute, lessThan(1)); + }); + }); +} From 816c7cc02cb48abd32aa7b4abaef216e2ccc6580 Mon Sep 17 00:00:00 2001 From: Nikita Aksenov Date: Thu, 16 Jul 2026 22:10:52 +0300 Subject: [PATCH 057/145] fix: remove macOS support as Yandex Maps are not supported --- .metadata | 3 - lib/core/localization/app_localizations.dart | 10 - lib/core/storage/token_storage.dart | 1 - lib/presentation/screens/map/map_screen.dart | 55 +- macos/.gitignore | 7 - macos/Flutter/Flutter-Debug.xcconfig | 2 - macos/Flutter/Flutter-Release.xcconfig | 2 - macos/Flutter/GeneratedPluginRegistrant.swift | 16 - macos/Podfile | 42 - macos/Podfile.lock | 35 - macos/Runner.xcodeproj/project.pbxproj | 801 ------------------ .../xcshareddata/IDEWorkspaceChecks.plist | 8 - .../xcshareddata/xcschemes/Runner.xcscheme | 99 --- .../contents.xcworkspacedata | 10 - .../xcshareddata/IDEWorkspaceChecks.plist | 8 - macos/Runner/AppDelegate.swift | 13 - .../AppIcon.appiconset/Contents.json | 68 -- .../AppIcon.appiconset/app_icon_1024.png | Bin 102994 -> 0 bytes .../AppIcon.appiconset/app_icon_128.png | Bin 5680 -> 0 bytes .../AppIcon.appiconset/app_icon_16.png | Bin 520 -> 0 bytes .../AppIcon.appiconset/app_icon_256.png | Bin 14142 -> 0 bytes .../AppIcon.appiconset/app_icon_32.png | Bin 1066 -> 0 bytes .../AppIcon.appiconset/app_icon_512.png | Bin 36406 -> 0 bytes .../AppIcon.appiconset/app_icon_64.png | Bin 2218 -> 0 bytes macos/Runner/Base.lproj/MainMenu.xib | 343 -------- macos/Runner/Configs/AppInfo.xcconfig | 14 - macos/Runner/Configs/Debug.xcconfig | 2 - macos/Runner/Configs/Release.xcconfig | 2 - macos/Runner/Configs/Warnings.xcconfig | 13 - macos/Runner/DebugProfile.entitlements | 16 - macos/Runner/Info.plist | 34 - macos/Runner/MainFlutterWindow.swift | 15 - macos/Runner/Release.entitlements | 12 - macos/RunnerTests/RunnerTests.swift | 12 - 34 files changed, 1 insertion(+), 1642 deletions(-) delete mode 100644 macos/.gitignore delete mode 100644 macos/Flutter/Flutter-Debug.xcconfig delete mode 100644 macos/Flutter/Flutter-Release.xcconfig delete mode 100644 macos/Flutter/GeneratedPluginRegistrant.swift delete mode 100644 macos/Podfile delete mode 100644 macos/Podfile.lock delete mode 100644 macos/Runner.xcodeproj/project.pbxproj delete mode 100644 macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist delete mode 100644 macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme delete mode 100644 macos/Runner.xcworkspace/contents.xcworkspacedata delete mode 100644 macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist delete mode 100644 macos/Runner/AppDelegate.swift delete mode 100644 macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json delete mode 100644 macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png delete mode 100644 macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png delete mode 100644 macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png delete mode 100644 macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png delete mode 100644 macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png delete mode 100644 macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png delete mode 100644 macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png delete mode 100644 macos/Runner/Base.lproj/MainMenu.xib delete mode 100644 macos/Runner/Configs/AppInfo.xcconfig delete mode 100644 macos/Runner/Configs/Debug.xcconfig delete mode 100644 macos/Runner/Configs/Release.xcconfig delete mode 100644 macos/Runner/Configs/Warnings.xcconfig delete mode 100644 macos/Runner/DebugProfile.entitlements delete mode 100644 macos/Runner/Info.plist delete mode 100644 macos/Runner/MainFlutterWindow.swift delete mode 100644 macos/Runner/Release.entitlements delete mode 100644 macos/RunnerTests/RunnerTests.swift diff --git a/.metadata b/.metadata index 5feb463..77aa5a4 100644 --- a/.metadata +++ b/.metadata @@ -21,9 +21,6 @@ migration: - platform: ios create_revision: cc0734ac716fbb8b90f3f9db8020958b1553afa7 base_revision: cc0734ac716fbb8b90f3f9db8020958b1553afa7 - - platform: macos - create_revision: cc0734ac716fbb8b90f3f9db8020958b1553afa7 - base_revision: cc0734ac716fbb8b90f3f9db8020958b1553afa7 - platform: web create_revision: cc0734ac716fbb8b90f3f9db8020958b1553afa7 base_revision: cc0734ac716fbb8b90f3f9db8020958b1553afa7 diff --git a/lib/core/localization/app_localizations.dart b/lib/core/localization/app_localizations.dart index 297d825..7eecd91 100644 --- a/lib/core/localization/app_localizations.dart +++ b/lib/core/localization/app_localizations.dart @@ -145,8 +145,6 @@ class AppStrings { final String editProfile; final String save; final String profileUpdated; - final String desktopMapUnavailable; - final String openWebVersion; AppStrings({ required this.appTitle, @@ -291,8 +289,6 @@ class AppStrings { required this.editProfile, required this.save, required this.profileUpdated, - required this.desktopMapUnavailable, - required this.openWebVersion, }); static AppStrings ru = AppStrings( @@ -455,9 +451,6 @@ class AppStrings { editProfile: 'Редактировать профиль', save: 'Сохранить', profileUpdated: 'Профиль обновлен', - desktopMapUnavailable: - 'Яндекс Карты пока не поддерживают нативные приложения Flutter для macOS.', - openWebVersion: 'Открыть веб-версию', ); static AppStrings en = AppStrings( @@ -619,9 +612,6 @@ class AppStrings { editProfile: 'Edit Profile', save: 'Save', profileUpdated: 'Profile updated', - desktopMapUnavailable: - 'Yandex Maps does not currently support native Flutter apps for macOS.', - openWebVersion: 'Open web version', ); } diff --git a/lib/core/storage/token_storage.dart b/lib/core/storage/token_storage.dart index 2d8f363..5f592e9 100644 --- a/lib/core/storage/token_storage.dart +++ b/lib/core/storage/token_storage.dart @@ -6,7 +6,6 @@ class TokenStorage { static const _keyPassword = 'user_password'; static const _storage = FlutterSecureStorage( aOptions: AndroidOptions(encryptedSharedPreferences: true), - mOptions: MacOsOptions(useDataProtectionKeyChain: false), ); String? _cachedAccessToken; diff --git a/lib/presentation/screens/map/map_screen.dart b/lib/presentation/screens/map/map_screen.dart index 1a746e9..6971c3a 100644 --- a/lib/presentation/screens/map/map_screen.dart +++ b/lib/presentation/screens/map/map_screen.dart @@ -2,13 +2,11 @@ import 'dart:async'; import 'dart:math' as math; import 'dart:typed_data'; import 'dart:ui' as ui; -import 'package:flutter/foundation.dart' - show TargetPlatform, defaultTargetPlatform, kIsWeb, setEquals; +import 'package:flutter/foundation.dart' show setEquals; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:geolocator/geolocator.dart'; import 'package:go_router/go_router.dart'; -import 'package:url_launcher/url_launcher.dart'; import 'package:yandex_mapkit/yandex_mapkit.dart'; import '../../providers/zones_provider.dart'; import '../../../domain/models/zone.dart'; @@ -566,61 +564,10 @@ class _MapScreenState extends ConsumerState { ); } - Widget _buildMacOsFallback(BuildContext context, AppStrings s) { - return Scaffold( - appBar: AppBar( - title: Text(s.appTitle), - actions: [ - IconButton( - tooltip: s.settings, - onPressed: () => context.push('/profile'), - icon: const Icon(Icons.settings_outlined), - ), - ], - ), - body: Center( - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 520), - child: Padding( - padding: const EdgeInsets.all(32), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.desktop_mac_outlined, - size: 72, - color: Theme.of(context).colorScheme.primary, - ), - const SizedBox(height: 24), - Text( - s.desktopMapUnavailable, - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.titleLarge, - ), - const SizedBox(height: 24), - FilledButton.icon( - onPressed: () => launchUrl( - Uri.parse('https://m.parktrack.live'), - mode: LaunchMode.externalApplication, - ), - icon: const Icon(Icons.open_in_browser), - label: Text(s.openWebVersion), - ), - ], - ), - ), - ), - ), - ); - } - @override Widget build(BuildContext context) { const bottomInset = 0.0; final s = ref.watch(l10nProvider); - if (!kIsWeb && defaultTargetPlatform == TargetPlatform.macOS) { - return _buildMacOsFallback(context, s); - } final zones = ref.watch(filteredZonesProvider); final zonesAsync = ref.watch(rawZonesProvider); final routingState = ref.watch(routingProvider); diff --git a/macos/.gitignore b/macos/.gitignore deleted file mode 100644 index 746adbb..0000000 --- a/macos/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -# Flutter-related -**/Flutter/ephemeral/ -**/Pods/ - -# Xcode-related -**/dgph -**/xcuserdata/ diff --git a/macos/Flutter/Flutter-Debug.xcconfig b/macos/Flutter/Flutter-Debug.xcconfig deleted file mode 100644 index 4b81f9b..0000000 --- a/macos/Flutter/Flutter-Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" -#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/Flutter-Release.xcconfig b/macos/Flutter/Flutter-Release.xcconfig deleted file mode 100644 index 5caa9d1..0000000 --- a/macos/Flutter/Flutter-Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" -#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift deleted file mode 100644 index 7f03e25..0000000 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// Generated file. Do not edit. -// - -import FlutterMacOS -import Foundation - -import flutter_secure_storage_macos -import geolocator_apple -import url_launcher_macos - -func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { - FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) - GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) - UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) -} diff --git a/macos/Podfile b/macos/Podfile deleted file mode 100644 index ff5ddb3..0000000 --- a/macos/Podfile +++ /dev/null @@ -1,42 +0,0 @@ -platform :osx, '10.15' - -# CocoaPods analytics sends network stats synchronously affecting flutter build latency. -ENV['COCOAPODS_DISABLE_STATS'] = 'true' - -project 'Runner', { - 'Debug' => :debug, - 'Profile' => :release, - 'Release' => :release, -} - -def flutter_root - generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) - unless File.exist?(generated_xcode_build_settings_path) - raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" - end - - File.foreach(generated_xcode_build_settings_path) do |line| - matches = line.match(/FLUTTER_ROOT\=(.*)/) - return matches[1].strip if matches - end - raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" -end - -require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) - -flutter_macos_podfile_setup - -target 'Runner' do - use_frameworks! - - flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) - target 'RunnerTests' do - inherit! :search_paths - end -end - -post_install do |installer| - installer.pods_project.targets.each do |target| - flutter_additional_macos_build_settings(target) - end -end diff --git a/macos/Podfile.lock b/macos/Podfile.lock deleted file mode 100644 index 58f39fb..0000000 --- a/macos/Podfile.lock +++ /dev/null @@ -1,35 +0,0 @@ -PODS: - - flutter_secure_storage_macos (6.1.3): - - FlutterMacOS - - FlutterMacOS (1.0.0) - - geolocator_apple (1.2.0): - - Flutter - - FlutterMacOS - - url_launcher_macos (0.0.1): - - FlutterMacOS - -DEPENDENCIES: - - flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`) - - FlutterMacOS (from `Flutter/ephemeral`) - - geolocator_apple (from `Flutter/ephemeral/.symlinks/plugins/geolocator_apple/darwin`) - - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) - -EXTERNAL SOURCES: - flutter_secure_storage_macos: - :path: Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos - FlutterMacOS: - :path: Flutter/ephemeral - geolocator_apple: - :path: Flutter/ephemeral/.symlinks/plugins/geolocator_apple/darwin - url_launcher_macos: - :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos - -SPEC CHECKSUMS: - flutter_secure_storage_macos: 7f45e30f838cf2659862a4e4e3ee1c347c2b3b54 - FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 - geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e - url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd - -PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009 - -COCOAPODS: 1.17.0 diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index 46d5975..0000000 --- a/macos/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,801 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 54; - objects = { - -/* Begin PBXAggregateTarget section */ - 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { - isa = PBXAggregateTarget; - buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; - buildPhases = ( - 33CC111E2044C6BF0003C045 /* ShellScript */, - ); - dependencies = ( - ); - name = "Flutter Assemble"; - productName = FLX; - }; -/* End PBXAggregateTarget section */ - -/* Begin PBXBuildFile section */ - 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; - 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; - 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; - 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; - 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; - 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; - 3D62B0016C620836C6C35E2C /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2442744519EC1EF8EE1EFF98 /* Pods_Runner.framework */; }; - B01792C27E4D48C9A53D15C4 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5594F6A2767694551B3D5C11 /* Pods_RunnerTests.framework */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 33CC10E52044A3C60003C045 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 33CC10EC2044A3C60003C045; - remoteInfo = Runner; - }; - 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 33CC10E52044A3C60003C045 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 33CC111A2044C6BA0003C045; - remoteInfo = FLX; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 33CC110E2044A8840003C045 /* Bundle Framework */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - ); - name = "Bundle Framework"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 03D865B8A42E9E854365E150 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; - 20DBD8F291B0A489DE6F30A6 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; - 2442744519EC1EF8EE1EFF98 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; - 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; - 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* mobile.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = mobile.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; - 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; - 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; - 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; - 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; - 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; - 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; - 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; - 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; - 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; - 5594F6A2767694551B3D5C11 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 6395593667471E2853E4E0A1 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; - 6FCEA48C7297F3086F0BCBAC /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; - AB9C206508EA4DA6EF155D97 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; - AF02B0530EC35ACC93142B98 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 331C80D2294CF70F00263BE5 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - B01792C27E4D48C9A53D15C4 /* Pods_RunnerTests.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 33CC10EA2044A3C60003C045 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 3D62B0016C620836C6C35E2C /* Pods_Runner.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 331C80D6294CF71000263BE5 /* RunnerTests */ = { - isa = PBXGroup; - children = ( - 331C80D7294CF71000263BE5 /* RunnerTests.swift */, - ); - path = RunnerTests; - sourceTree = ""; - }; - 33BA886A226E78AF003329D5 /* Configs */ = { - isa = PBXGroup; - children = ( - 33E5194F232828860026EE4D /* AppInfo.xcconfig */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, - ); - path = Configs; - sourceTree = ""; - }; - 33CC10E42044A3C60003C045 = { - isa = PBXGroup; - children = ( - 33FAB671232836740065AC1E /* Runner */, - 33CEB47122A05771004F2AC0 /* Flutter */, - 331C80D6294CF71000263BE5 /* RunnerTests */, - 33CC10EE2044A3C60003C045 /* Products */, - D73912EC22F37F3D000D13A0 /* Frameworks */, - 6897AF598BB9F1B8016DBA0E /* Pods */, - ); - sourceTree = ""; - }; - 33CC10EE2044A3C60003C045 /* Products */ = { - isa = PBXGroup; - children = ( - 33CC10ED2044A3C60003C045 /* mobile.app */, - 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, - ); - name = Products; - sourceTree = ""; - }; - 33CC11242044D66E0003C045 /* Resources */ = { - isa = PBXGroup; - children = ( - 33CC10F22044A3C60003C045 /* Assets.xcassets */, - 33CC10F42044A3C60003C045 /* MainMenu.xib */, - 33CC10F72044A3C60003C045 /* Info.plist */, - ); - name = Resources; - path = ..; - sourceTree = ""; - }; - 33CEB47122A05771004F2AC0 /* Flutter */ = { - isa = PBXGroup; - children = ( - 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, - 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, - 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, - 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, - ); - path = Flutter; - sourceTree = ""; - }; - 33FAB671232836740065AC1E /* Runner */ = { - isa = PBXGroup; - children = ( - 33CC10F02044A3C60003C045 /* AppDelegate.swift */, - 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, - 33E51913231747F40026EE4D /* DebugProfile.entitlements */, - 33E51914231749380026EE4D /* Release.entitlements */, - 33CC11242044D66E0003C045 /* Resources */, - 33BA886A226E78AF003329D5 /* Configs */, - ); - path = Runner; - sourceTree = ""; - }; - 6897AF598BB9F1B8016DBA0E /* Pods */ = { - isa = PBXGroup; - children = ( - 03D865B8A42E9E854365E150 /* Pods-Runner.debug.xcconfig */, - AF02B0530EC35ACC93142B98 /* Pods-Runner.release.xcconfig */, - 6FCEA48C7297F3086F0BCBAC /* Pods-Runner.profile.xcconfig */, - 6395593667471E2853E4E0A1 /* Pods-RunnerTests.debug.xcconfig */, - 20DBD8F291B0A489DE6F30A6 /* Pods-RunnerTests.release.xcconfig */, - AB9C206508EA4DA6EF155D97 /* Pods-RunnerTests.profile.xcconfig */, - ); - name = Pods; - path = Pods; - sourceTree = ""; - }; - D73912EC22F37F3D000D13A0 /* Frameworks */ = { - isa = PBXGroup; - children = ( - 2442744519EC1EF8EE1EFF98 /* Pods_Runner.framework */, - 5594F6A2767694551B3D5C11 /* Pods_RunnerTests.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 331C80D4294CF70F00263BE5 /* RunnerTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; - buildPhases = ( - 36FA47B4D35D8323D6E895F9 /* [CP] Check Pods Manifest.lock */, - 331C80D1294CF70F00263BE5 /* Sources */, - 331C80D2294CF70F00263BE5 /* Frameworks */, - 331C80D3294CF70F00263BE5 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 331C80DA294CF71000263BE5 /* PBXTargetDependency */, - ); - name = RunnerTests; - productName = RunnerTests; - productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - 33CC10EC2044A3C60003C045 /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - B1361D45F043E7ECF02E575B /* [CP] Check Pods Manifest.lock */, - 33CC10E92044A3C60003C045 /* Sources */, - 33CC10EA2044A3C60003C045 /* Frameworks */, - 33CC10EB2044A3C60003C045 /* Resources */, - 33CC110E2044A8840003C045 /* Bundle Framework */, - 3399D490228B24CF009A79C7 /* ShellScript */, - 421F0D8C7F99BC195FB4DC83 /* [CP] Embed Pods Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - 33CC11202044C79F0003C045 /* PBXTargetDependency */, - ); - name = Runner; - productName = Runner; - productReference = 33CC10ED2044A3C60003C045 /* mobile.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 33CC10E52044A3C60003C045 /* Project object */ = { - isa = PBXProject; - attributes = { - BuildIndependentTargetsInParallel = YES; - LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1510; - ORGANIZATIONNAME = ""; - TargetAttributes = { - 331C80D4294CF70F00263BE5 = { - CreatedOnToolsVersion = 14.0; - TestTargetID = 33CC10EC2044A3C60003C045; - }; - 33CC10EC2044A3C60003C045 = { - CreatedOnToolsVersion = 9.2; - LastSwiftMigration = 1100; - ProvisioningStyle = Automatic; - SystemCapabilities = { - com.apple.Sandbox = { - enabled = 1; - }; - }; - }; - 33CC111A2044C6BA0003C045 = { - CreatedOnToolsVersion = 9.2; - ProvisioningStyle = Manual; - }; - }; - }; - buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 9.3"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 33CC10E42044A3C60003C045; - productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 33CC10EC2044A3C60003C045 /* Runner */, - 331C80D4294CF70F00263BE5 /* RunnerTests */, - 33CC111A2044C6BA0003C045 /* Flutter Assemble */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 331C80D3294CF70F00263BE5 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 33CC10EB2044A3C60003C045 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, - 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3399D490228B24CF009A79C7 /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; - }; - 33CC111E2044C6BF0003C045 /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - Flutter/ephemeral/FlutterInputs.xcfilelist, - ); - inputPaths = ( - Flutter/ephemeral/tripwire, - ); - outputFileListPaths = ( - Flutter/ephemeral/FlutterOutputs.xcfilelist, - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; - }; - 36FA47B4D35D8323D6E895F9 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - 421F0D8C7F99BC195FB4DC83 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - B1361D45F043E7ECF02E575B /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 331C80D1294CF70F00263BE5 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 33CC10E92044A3C60003C045 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, - 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, - 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 33CC10EC2044A3C60003C045 /* Runner */; - targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; - }; - 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; - targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin PBXVariantGroup section */ - 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { - isa = PBXVariantGroup; - children = ( - 33CC10F52044A3C60003C045 /* Base */, - ); - name = MainMenu.xib; - path = Runner; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 331C80DB294CF71000263BE5 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 6395593667471E2853E4E0A1 /* Pods-RunnerTests.debug.xcconfig */; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.parktrack.mobile.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/mobile.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/mobile"; - }; - name = Debug; - }; - 331C80DC294CF71000263BE5 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 20DBD8F291B0A489DE6F30A6 /* Pods-RunnerTests.release.xcconfig */; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.parktrack.mobile.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/mobile.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/mobile"; - }; - name = Release; - }; - 331C80DD294CF71000263BE5 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = AB9C206508EA4DA6EF155D97 /* Pods-RunnerTests.profile.xcconfig */; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.parktrack.mobile.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/mobile.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/mobile"; - }; - name = Profile; - }; - 338D0CE9231458BD00FA5F75 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - DEAD_CODE_STRIPPING = YES; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.15; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = macosx; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - }; - name = Profile; - }; - 338D0CEA231458BD00FA5F75 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_VERSION = 5.0; - }; - name = Profile; - }; - 338D0CEB231458BD00FA5F75 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Manual; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Profile; - }; - 33CC10F92044A3C60003C045 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - DEAD_CODE_STRIPPING = YES; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.15; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = macosx; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - }; - name = Debug; - }; - 33CC10FA2044A3C60003C045 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - DEAD_CODE_STRIPPING = YES; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.15; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = macosx; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - }; - name = Release; - }; - 33CC10FC2044A3C60003C045 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - }; - name = Debug; - }; - 33CC10FD2044A3C60003C045 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_VERSION = 5.0; - }; - name = Release; - }; - 33CC111C2044C6BA0003C045 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Manual; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Debug; - }; - 33CC111D2044C6BA0003C045 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 331C80DB294CF71000263BE5 /* Debug */, - 331C80DC294CF71000263BE5 /* Release */, - 331C80DD294CF71000263BE5 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 33CC10F92044A3C60003C045 /* Debug */, - 33CC10FA2044A3C60003C045 /* Release */, - 338D0CE9231458BD00FA5F75 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 33CC10FC2044A3C60003C045 /* Debug */, - 33CC10FD2044A3C60003C045 /* Release */, - 338D0CEA231458BD00FA5F75 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 33CC111C2044C6BA0003C045 /* Debug */, - 33CC111D2044C6BA0003C045 /* Release */, - 338D0CEB231458BD00FA5F75 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 33CC10E52044A3C60003C045 /* Project object */; -} diff --git a/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d9810..0000000 --- a/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index 09d4458..0000000 --- a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,99 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/macos/Runner.xcworkspace/contents.xcworkspacedata b/macos/Runner.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 21a3cc1..0000000 --- a/macos/Runner.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d9810..0000000 --- a/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/macos/Runner/AppDelegate.swift b/macos/Runner/AppDelegate.swift deleted file mode 100644 index b3c1761..0000000 --- a/macos/Runner/AppDelegate.swift +++ /dev/null @@ -1,13 +0,0 @@ -import Cocoa -import FlutterMacOS - -@main -class AppDelegate: FlutterAppDelegate { - override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { - return true - } - - override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { - return true - } -} diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index a2ec33f..0000000 --- a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "images" : [ - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_16.png", - "scale" : "1x" - }, - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "2x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "1x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_64.png", - "scale" : "2x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_128.png", - "scale" : "1x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "2x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "1x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "2x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "1x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_1024.png", - "scale" : "2x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png deleted file mode 100644 index 82b6f9d9a33e198f5747104729e1fcef999772a5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 102994 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ⅅ8MglG&ER{i5lN315cO?EdHNLrg? zgxkP+ytd)OMWe7QvTf8yj4;V=?m172!BEt@6*TPUT4m3)yir}esnIodFGatGnsSfJ z**;;yw=1VCb2J|A7cBz-F5QFOQh2JDQFLarE>;4ZMzQ$s^)fOscIVv2-o{?ct3~Zv zy{0zU>3`+-PluS|ADraI9n~=3#Tvfx{pDr^5i$^-h5tL*CV@AeQFLxv4Y<$xI{9y< zZ}li*WIQ+XS!IK;?IVD0)C?pNBA(DMxqozMy1L#j+ba1Cd+2w&{^d-OEWSSHmNH>9 z%1Ldo(}5*>a8rjQF&@%Ka`-M|HM+m<^E#bJtVg&YM}uMb7UVJ|OVQI-zt-*BqQ zG&mq`Bn7EY;;+b%Obs9i{gC^%>kUz`{Qnc=ps7ra_UxEP$!?f&|5fHnU(rr?7?)D z$3m9e{&;Zu6yfa1ixTr;80IP7KLgkKCbgv1%f_weZK6b7tY+AS%fyjf6dR(wQa9TD zYG9`#!N4DqpMim|{uViKVf0B+Vmsr7p)Y+;*T~-2HFr!IOedrpiXXz+BDppd5BTf3 ztsg4U?0wR?9@~`iV*nwGmtYFGnq`X< zf?G%=o!t50?gk^qN#J(~!sxi=_yeg?Vio04*w<2iBT+NYX>V#CFuQGLsX^u8dPIkP zPraQK?ro`rqA4t7yUbGYk;pw6Z})Bv=!l-a5^R5Ra^TjoXI?=Qdup)rtyhwo<(c9_ zF>6P%-6Aqxb8gf?wY1z!4*hagIch)&A4treifFk=E9v@kRXyMm?V*~^LEu%Y%0u(| z52VvVF?P^D<|fG)_au(!iqo~1<5eF$Sc5?)*$4P3MAlSircZ|F+9T66-$)0VUD6>e zl2zlSl_QQ?>ULUA~H?QbWazYeh61%B!!u;c(cs`;J|l z=7?q+vo^T#kzddr>C;VZ5h*;De8^F2y{iA#9|(|5@zYh4^FZ-3r)xej=GghMN3K2Y z=(xE`TM%V8UHc4`6Cdhz4%i0OY^%DSguLUXQ?Y3LP+5x3jyN)-UDVhEC}AI5wImt; zHY|*=UW}^bS3va-@L$-fJz2P2LbCl)XybkY)p%2MjPJd-FzkdyWW~NBC@NlPJkz{v z+6k6#nif`E>>KCGaP34oY*c#nBFm#G8a0^px1S6mm6Cs+d}E8{J;DX=NEHb|{fZm0 z@Ors@ebTgbf^Jg&DzVS|h&Or)56$+;%&sh0)`&6VkS@QxQ=#6WxF5g+FWSr7Lp9uF zV#rc`yLe?f*u6oZoi3WpOkKFf^>lHb2GC6t!)dyGaQbK7&BNZ7oyP)hUX1Y(LdW-I z6LI2$i%+g!zsjT(5l}5ROLb)8`9kkldbklcq6tfLSrAyh#s(C1U2Sz9`h3#T9eX#Hryi1AU^!uv*&6I~qdM_B7-@`~8#O^jN&t7+S zTKI6;T$1@`Kky-;;$rU1*TdY;cUyg$JXalGc&3-Rh zJ&7kx=}~4lEx*%NUJA??g8eIeavDIDC7hTvojgRIT$=MlpU}ff0BTTTvjsZ0=wR)8 z?{xmc((XLburb0!&SA&fc%%46KU0e&QkA%_?9ZrZU%9Wt{*5DCUbqIBR%T#Ksp?)3 z%qL(XlnM!>F!=q@jE>x_P?EU=J!{G!BQq3k#mvFR%lJO2EU2M8egD?0r!2s*lL2Y} zdrmy`XvEarM&qTUz4c@>Zn}39Xi2h?n#)r3C4wosel_RUiL8$t;FSuga{9}-%FuOU z!R9L$Q!njtyY!^070-)|#E8My)w*~4k#hi%Y77)c5zfs6o(0zaj~nla0Vt&7bUqfD zrZmH~A50GOvk73qiyfXX6R9x3Qh)K=>#g^^D65<$5wbZjtrtWxfG4w1f<2CzsKj@e zvdsQ$$f6N=-%GJk~N7G(+-29R)Cbz8SIn_u|(VYVSAnlWZhPp8z6qm5=hvS$Y zULkbE?8HQ}vkwD!V*wW7BDBOGc|75qLVkyIWo~3<#nAT6?H_YSsvS+%l_X$}aUj7o z>A9&3f2i-`__#MiM#|ORNbK!HZ|N&jKNL<-pFkqAwuMJi=(jlv5zAN6EW`ex#;d^Z z<;gldpFcVD&mpfJ1d7><79BnCn~z8U*4qo0-{i@1$CCaw+<$T{29l1S2A|8n9ccx0!1Pyf;)aGWQ15lwEEyU35_Y zQS8y~9j9ZiByE-#BV7eknm>ba75<_d1^*% zB_xp#q`bpV1f9o6C(vbhN((A-K+f#~3EJtjWVhRm+g$1$f2scX!eZkfa%EIZd2ZVG z6sbBo@~`iwZQC4rH9w84rlHjd!|fHc9~12Il&?-FldyN50A`jzt~?_4`OWmc$qkgI zD_@7^L@cwg4WdL(sWrBYmkH;OjZGE^0*^iWZM3HBfYNw(hxh5>k@MH>AerLNqUg*Og9LiYmTgPw zX9IiqU)s?_obULF(#f~YeK#6P>;21x+cJ$KTL}|$xeG?i`zO;dAk0{Uj6GhT-p-=f zP2NJUcRJ{fZy=bbsN1Jk3q}(!&|Fkt_~GYdcBd7^JIt)Q!!7L8`3@so@|GM9b(D$+ zlD&69JhPnT>;xlr(W#x`JJvf*DPX(4^OQ%1{t@)Lkw5nc5zLVmRt|s+v zn(25v*1Z(c8RP@=3l_c6j{{=M$=*aO^ zPMUbbEKO7m2Q$4Xn>GIdwm#P_P4`or_w0+J+joK&qIP#uEiCo&RdOaP_7Z;PvfMh@ zsXUTn>ppdoEINmmq5T1BO&57*?QNLolW-8iz-jv7VAIgoV&o<<-vbD)--SD%FFOLd z>T$u+V>)4Dl6?A24xd1vgm}MovrQjf-@YH7cIk6tP^eq-xYFymnoSxcw}{lsbCP1g zE_sX|c_nq(+INR3iq+Oj^TwkjhbdOo}FmpPS2*#NGxNgl98|H0M*lu)Cu0TrA|*t=i`KIqoUl(Q7jN zb6!H-rO*!&_>-t)vG5jG>WR6z#O9O&IvA-4ho9g;as~hSnt!oF5 z6w(4pxz|WpO?HO<>sC_OB4MW)l`-E9DZJ$!=ytzO}fWXwnP>`8yWm5tYw`b1KDdg zp@oD;g===H+sj+^v6DCpEu7R?fh7>@pz>f74V5&#PvBN+95?28`mIdGR@f*L@j2%% z%;Rz5R>l#1U zYCS_5_)zUjgq#0SdO#)xEfYJ)JrHLXfe8^GK3F*CA(Y)jsSPJ{j&Ae!SeWN%Ev727 zxdd3Y0n^OBOtBSKdglEBL)i5=NdKfqK=1n~6LX`ja;#Tr!II$AAH{Z#sp%`rwNGT5 zvHT%(LJB+kD{5N}7c_Rk6}@tikIeq%@MqxX%$P!(238YD(H<_d;xxo*oMiv^1io>g zt5z&6`}cjci90q2r0hutQXr!UA~|4e*u=k81D(Cp7n{4LVCa+u0%-8Uha+sqI#Om~ z!&)KN(#Zone^~&@Ja{|l?X64Dxk)q>tLRv{=0|t$`Kdaj z#{AJr>{_BtpS|XEgTVJ4WMvBRk-(mk@ZYGdY1VwI z81;z(MBGV|2j*Cj%dvl8?b2{{B#e0B7&7wfv+>g`R2^Ai5C_WUx|CnTrHm+RFGXrt zs<~zBtk@?Niu%|o6IEL+y60Q>zJlv``ePCa07C%*O~lj?74|}&A0!uA)3V7ST8b_- z6CBP1;x+S@xTzgOY2#s%@=bhZ@i@BwmS)neQG&=9KUtRf^K=MvjC5JnqLqykCE_P0 zjf#V4SdH2#%2EuDb!>FLHK7j;nd6VLW|$3gJuegpEl3DZ`BpJU$<}}A(rW?<6OB@9 zKP9G3An?T5BztrLdlximA;{>Tr7GAeSU=^<*y;%RHj+7;v+tonyh(8d;Izn}2{oz& zW)fsZ9gHYpI?B|uekS3zHUue3mI zb7?0+&Zm>Kq(F>~%VYEn)0b32I3~O^?Wx-HI|Zu?1-OA2yfyJ;gWygLOeU;)vRm3u z5J4vDIQYztnEm=QauX2(WJO{yzI0HUFl+oO&isMf!Yh2pu@p}65)|0EdWRbg(@J6qo5_Els>#|_2a1p0&y&UP z8x#Z69q=d663NPPi>DHx3|QhJl5Ka$Cfqbvl*oRLYYXiH>g8*vriy!0XgmT~&jh3l z+!|~l=oCj<*PD>1EY*#+^a{rVk3T(66rJ^DxGt|~XTNnJf$vix1v1qdYu+d@Jn~bh z!7`a`y+IEcS#O*fSzA;I`e_T~XYzpW7alC%&?1nr);tSkNwO&J`JnX+7X1Q8fRh_d zx%)Xh_YjI3hwTCmGUeq_Z@H#ovkk_b(`osa$`aNmt`9A#t&<^jvuf z1E1DrW(%7PpAOQGwURz@luEW9-)L!`Jy*aC*4mcD?Si~mb=3Kn#M#1il9%`C0wkZ` zbpJ-qEPaOE5Y5iv_z%Wr{y4jh#U+o^KtP{pPCq-Qf&!=Uu)cEE(Iu9`uT#oHwHj+w z_R=kr7vmr~{^5sxXkj|WzNhAlXkW^oB4V)BZ{({~4ylOcM#O>DR)ZhD;RWwmf|(}y zDn)>%iwCE=*82>zP0db>I4jN#uxcYWod+<;#RtdMGPDpQW;riE;3cu``1toL|FaWa zK)MVA%ogXt3q55(Q&q+sjOG`?h=UJE9P;8i#gI*#f}@JbV(DuGEkee;La*9{p&Z?;~lE!&-kUFCtoDHY*MS zzj+S$L9+aTs(F^4ufZe6>SBg;m@>0&+kEZMFmD*~p~sx?rx=!>Ge;KYw<33y#*&77 zFZI`YE(Iz?+tH;Fq;y=MaSqT{Ayh*HFv0(z{_?Q+7@nE%p?S8%X6c!+y;!0NLXwJV8Co_}R3*7>n+oMsQpv8}8ZS-P@(Rg|gmxZHzf=nMOUAAY}AZGfWVzZjE@4$=7xkIrs8BE%606aVU%kxz_04ipig51k& z(>c9rJL2q%xvU%Zj#GR9C9)HLCR;#zQBB@x;e_9$ayn(JmSg_*0G?+wOF?&iu@}S{ zt$;TPf*Lj$3=d<}Q3o!Hq@3~lFxoiCyeEt}o3fihIn{x2s1)e2@3##&GYDq~YO|!q zUs0P-zy)+ohl-VQ`bhvUpC{-d$lkpML_M%Kl6@#_@A}w{jWCDsPa#cSbWA#C4Sf|*C*&Z{ zz?hOU7Cc`?>H$WGqITA2P~fYudnQHxB8^;0ZFKC;19F#~n_2P@{cE{Czq-#K5L_8| zc3aOEwq4%zL5>YU_mc9fc-p~{fBTWUkxTiZvxt9FOqC{s#TBp(#dWc+{Ee{dZ#B!g zHnaOJ8;KO1G;QU2ciodE+#Z$Wuz*Hc6NRO!AUMi|gov=>=cwcZeL&`>Jfn!35hV1J z;B2@0!bIR853w%T*m6)gQ?DPnQ)o6EtKaN3L;o?*q<83d&lG&U=A|6hcT?f0)4h6{ zGIZ0|!}-?*n{zr}-}cC}qWxEN%g60+{my)o^57{QEn(tSrmD7o)|r0+HVpQPopFu; z0<S}pW8W2vXzSxEqGD+qePj^x?R$e2LO&*ewsLo{+_Z)Wl|Z1K47j zsKoNRlX)h2z^ls_>IZ0!2X5t&irUs%RAO$Dr>0o$-D+$!Kb9puSgpoWza1jnX6(eG zTg-U z6|kf1atI!_>#@|=d01Ro@Rg)BD?mY3XBsG7U9%lmq>4;Gf&2k3_oyEOdEN&X6Hl5K zCz^hyt67G;IE&@w1n~%ji_{sob_ssP#Ke|qd!Xx?J&+|2K=^`WfwZ-zt|sklFouxC zXZeDgluD2a?Zd3e{MtE$gQfAY9eO@KLX;@8N`(?1-m`?AWp!a8bA%UN>QTntIcJX zvbY+C-GD&F?>E?jo$xhyKa@ps9$Dnwq>&)GB=W~2V3m)k;GNR$JoPRk%#f3#hgVdZ zhW3?cSQ*((Fog26jiEeNvum-6ID-fbfJ?q1ZU#)dgnJ^FCm`+sdP?g;d4VD$3XKx{ zs|Y4ePJp|93fpu)RL+#lIN9Ormd;<_5|oN!k5CENnpO>{60X;DN>vgHCX$QZYtgrj z*1{bEA1LKi8#U%oa!4W-4G+458~`5O4S1&tuyv>%H9DjLip7cC~RRS@HvdJ<|c z$TxEL=)r)XTfTgVxaG!gtZhLL`$#=gz1X=j|I@n~eHDUCW39r=o_ml@B z0cDx$5;3OA2l)&41kiKY^z7sO_U%1=)Ka4gV(P#(<^ z_zhThw=}tRG|2|1m4EP|p{Swfq#eNzDdi&QcVWwP+7920UQB*DpO0(tZHvLVMIGJl zdZ5;2J%a!N1lzxFwAkq05DPUg2*6SxcLRsSNI6dLiK0&JRuYAqwL}Z!YVJ$?mdnDF z82)J_t=jbY&le6Hq$Qs}@AOZGpB1}$Ah#i;&SzD1QQNwi6&1ddUf7UG0*@kX?E zDCbHypPZ9+H~KnDwBeOXZ-W-Y80wpoGB*A) z_;26Z`#s0tKrf~QBi2rl2=>;CS1w)rcD3-sB!8NI*1iQo59PJ>OLnqeV4iK7`RBi^ zFW{*6;nlD&cSunmU3v4JKj|K4xeN(q>H%;SsY8yDdw5BJ75q8>Ov)&D5OPZ`XiRHl z;)mAA0Woy6f!xCK(9H2rq?qzp83liZAIpBPl-dQ&$2=&H?Im~%g;vnIw1I+8q|kr! z36&^9}CMmR(U2rf|j12oG=vb%Ypsq8u9Kq}U*ANX*)9uK}fAi8;V_7Z;0_4*iydDxN-? zv?qJ=T*{MzL~-xUv{_Kh_q9#F{8gPV!yPUUS8pEq*=}2-#1d=sC_|U-rX~F0 zBLawgCWy#?#ax{~DAnDvh^`}wyUO`ioMK~jgh%L7^}#h?beSyvQ_g>+`2`}`-1h7# zg*?qJdm=53hwN8~B=^|LPmYtOVrQ(W{sNm4uofq=4P@dUA%$onWbw_m-KWia&n9iv zi)!9#OJ#^}eg8tE{wSb9(c0D^PS1 z9EBS5*ypSiVRS_G0v?$hyoZOS7hFWlp4qbYkf9Y&{%OzhsIdHskLptn96@k6@^K@U zszd8POehITDK+AyW#JKpnWY;ju#MC$JjB1Y*~(E6N%{p#kO+bVxG3X<34n3fW=k{A zCZt|KP%x^GQ9%mU)KE0{LA=vaZvRQbxSlK~eAkwWo2Z<{j5eS5NVTMe`m%re8%~7K zZLtU&b~YDN%~uA9wPf>x2=PI=MA6_oVe>Ek$s5&&Z=8vvF5EODP4Av(b|dlNgF1O8 zy83W0WRdzjz2iNA~t1piEqlyU&`$yZtqR`6X_PmuP>W+D|8iH;FQ zN{JuU#Tz9mV=4R_IewROL1|mK^`lLat#LcIBfggzM(iO$pQT*-c_ z94^LUWw#5B9~sp2W1p`c)Y(xfR<{O^9n4E6vDDw{#-R4UMBKo{>Hqlqn*a9rl_>+0 zS5MwJC~nCC`1X%VCyWFsiDX;bfAJQAUkU#105f_s5U-8rqO}n8fA1{b>Fr6Q|Ea(V z5B11Lo^ooWF?`^{-U#?iatokWI-e$632frzY?Yzzx(xJc@LFM4A~-eg!u|tl{)8Nx ztZLXsSC*68g%9TFu(f&J9nmc^9hgyy#uUOMJFCaifSaDcyQ&6=8e9=t zIFEAQ{EK{|73{($!a4=!wj4ABcQrUQp#+gGM?wEUp(w@+Fzi{!lt}|3`PM%&d-seeR zB$}BrFGD3R10CE>Hsb>;PrP}pd` zaY4}6+Wu(`#uAV+E5SV7VIT7ES#b(U0%%DgN1}USJH>)mm;CHPv>}B18&0F~Kj@1= z&^Jyo+z-E)GRT4U*7$8wJO1OibWg0Jw>C$%Ge|=YwV@Y1(4fR>cV#6aGtRoF@I`*w_V4;)V231NzNqb6g@jdpjmjv*<2j02yU$F8ZS$fTvCC`%|Yn#x< zXUnP&b!GLpOY-TY3d?<-Hhxom_LM9`JC9LEX2{t1P-Nj%nG+0Vq)vQwvO^}coPH-> zAo8w#s>Je^Yy*#PlK=XDxpVS~pFe-j#jN-(As&LRewOf(kN-aKF(H+s*{*!0xrlZw zchJu@XAvQWX7DI1E8?F}Wc8m46eT+C<0eXVB+Z^(g=Kl@FG-cn@u$suj)1V2(KNg_ zh29ws6&6(q~+sOAoHY^o86A<#n*?Pg2)cK$+y;cY$hJLq4)4V84=j+3ShSr##Tk5kgmxB zkW+8A1GtceEx~^Ebhwm36U?oA)h)!mt=eg0QE$D1QsLNZ_T3NH?=B&0j~#298!6iv zhc0|-{46*3`Rx&nKSXnf1&w-Rs>#PGAGuY@cBTU-j|Fxbn3z49S#6KBaP^Lx*AOXxIibr z!1ysMi(&kr!1wwQB5w`BDH2~>T4bI`T1}A2RM0zd7ikC&kuBRsB`Z2@J!Udm{AmSN zrr0k6_qCZL**=)xRW`MFu(OY=OT;3G8eF~ z2mmkXZ9X(sjuKmq+_<=LSjphB$~R1o^Yb=rO!j!(4ErIox^x55o{pXSE9X$!76^*$ zoKhlAX6y%n^U=C~@!vIlEgXQGD@>oOU=_(aXF-Sjas*$AKESfRzxQ8#3yOj|y0OCU z>6Z-0%LCcjla&7I+CXm&caKp@@jQ!5M`(_{CL=@4#JJ}cHeZw>^b6fpv269LSV?gV5Q{kk?4;;y9RIsy5vk%DIRiL(9xe1aA@4!VX zDh2}xgUd5X?6nji%&7-%QuyKSYA-Z{PwJijUQ}In+EJl|x@dF1P<5bPa5W3&&?^h$ zZCo8LepKo0a(Fsln*cHL;D(gu9MMkoiM0*n31u)jHqX5x^F95tnI&^}^yKx3YwEm@ zo8?EZ710ykx@19{=yz5IXb8w4yjdveWb{IVL6Z(Cs>!a_0X^1E27o!4e&b43+J*u2Gb(59k2uK0goLwhO{ujLS ziI9LA9`&x~Y$6JNX!aEXR``}LUI}Gr#=<^wBHmg%v<)zRWDVtq)kT$-P7iU1R)2XZ zi~bYhV@EZ`@prgK(cs{>2jn$pxg$<|KjJ7%26Km>%KcXh^bU@y@V_Lf@=j1x%R4{v zOcQn{I}!2W<~08FOVnoV>zOTH=+>v9!jFo|q)ucqIe!N4{U5_G`>>*sVD{8I~4FqyU8imZ**-Gy`~Xd z4w35GMf%7^i65HdX{Iz|f2Kg193#KhPIeR)-=eYx3Z!%RM=JjwLrdk^B#6rg!ym2w zPbFqYyO4>W_Z6PonAwiu7?!h=x%sR-T+_*xZOGh2wWhWr%}%2^$$ zQvACIB~pi=m|`hXIMvoq`TOCx=J_D2>pi6$NPy3&8#vy|oX)=kM0Z}$BR$r0G}MzOk-OqG+VmZtOZoj6x4(tLh|5h) zBv64Y{DPHsy&_H(5_l(&Y}FhVvr9m_*_Q~Zy-}V9+VmGnvndEjYW4qt4K~N&Y&6g| zfpz*V=A#^mVmuOAz)(KVI<%v5NY0%Goy!{9&o41upsPWk(yFuRP|A4q6NMnX%V~MT zi_Rb-Bno2kI+j0Cw`@ydy{e%ARS#Z%b6I%_yfo_ZKXr4BLVoHzBKJ^ZG z-2>2IzU)55@9C|?_P$ew^-7zEiAKG1XAi{!3h%1m#9s%^pGy6S9wKFYY4<$djeoJP z{GI}Vd%idY$4_fh(7NXm7#;cC!DS&-{tGr!Qze{^%bUx2jgG@-kMta^q-EwrKB}d8 z{%FT>rFk_bzW<{lc%eYlrsiYTZXGgzD1&lmRyp+c1O=0=zAX=KV62bx-a~JP{cPF4 zU$-XT#(9&T>l@bMu3nSr{)%-5lV+0t&bxip4DVJ~vlL$J2P6X~ zd{FS8vm{Lhrieul*7&(AgPuXhjpGila%6_?-+k#b)cdk#M1jB*nE>G6NGOr+Ek{`= z9b%S1`$`=g0CC$>0$Db;l_szReLYVmce*(()9%Zz1`*fNXhI*oRlerWHarD(v^W^c zuc1Vuw6Gbp7ZsoRH>QGt#&lv;5G~Ovt$%7VFd*-rN2>UjbOWBFGNGO`bru7CFB4tn zL`^?69Lj_g_TA&`9`dSI8s|)K|QM0 zybvV7!>xDY|6c6y;Q}qs`){1+WQu_5Dgd8Qe|q}}bxjH+joQQtqs1IVZn6{e7T{ia zF|=^xa%eWO%(x<7j*QZbcU_;aVaVP!arexOLOtoSNt*hvsRL%}%)jPetSich(`b-^ zMZ$PM9%s@%*jPVz0Z^W*cK_>G4f}+eEVX`HOaHg#!B`<4v;x}zDLMR*M27`kNfp!! zOfdt(>k-g>7jf^{Se@3$8<+;R*cYtw+wD_Z8Pl~!JDCUEPq{Ea*!J9`%ihyNJZ30i zmfve}S5<$Uso}_?SuI$ks|{-ddGLu9WR9`^9)Kdi@Vs;x#SY-xp}wHPU0|vEA7234 z@BN1z7OF=OOQtPF$4twn3!HTVlUVD_)ubMM7PEPoiC6lQgL2q9PK4~e8v-OuH%lie z?NgBLkIdPMG$QBq(>r^AOHB`|*1#*!2Z? zuU8H|FD`OBRu^(R?Z-Vhr0j;FLpS~a34KREnd}B=EYHS*>Hm+f%tgJt!4J8Q`qn^4 z9F=tO#JRJ}tzA`vx$nZ)O%wC?Uiv0+_nz}5Lj4ki*&=K&*#U`=rv z`Q@Q{+IhAj@6lrNK2B=8Yln!O2%zomfRehFT~;!O@(@Xy|1Jlw*uOB-M$#6K^)QBm z_7%#QVUDPwnW{iOV-grMQQU|3{=BQMh}c5(yMGdoQf*)k9-B zMQ(^GdJh+y)>qJprknS!%WxqM>HlHOP#7UVdy>%PW$!l72J`n-p7j(DBKoGxXWh(Y z>BFDZl|7knU_jg_SSbvFk8)39%2)Hu5W0}HKlh>EaqvFoXI&56Yy)3) zQkE4X^P0QnPn?iUUVHJZXzPp`s5uv?pG{K9IgGoHvcmlBxubi|iF7n{)mhenIcxGs zgr0OpQy#Y#u=5lOyiECfE_Sn?Fj1LyoRKcbTgX{p<T*v!CGkPc)pcA2D=4Ekp0Gb*wpy7S88C%Ywsbr?MI(3UdsCM?XJ1X%*hNjB)XqZ*W(qDdtSb z<3XN74ARXL3=c^bfW~F%NM^5*Zx92>Wq`&M625p~j$8mYwLbk%Kf)jbn#<2z$%vP5 zy#b>-tF-S2_AB4;R^K&^-1LJrUmi@9rB^FLF)-k&YHK8P+k@RCJ1qSTZ@=kHxA3l$ zmK_ZG)l6(nmCR1a8|;QF-B5e_ELnjJ1$m-;4UXX?WytF_wz7#&AjwZYTMVieLbq@R z3t-q|G4^BB#EpNu4uyfDebB+-uu_$9>y-dzB30Y9F=R zrW-Heqnj*InPTWHgR9v^R7~hokldh&h8=HDhMW(EFfim1*{)5Lc1-+eBVkK-2!u=N zuZKABgJs3I--NbjE;>Undg6uK`^U>AQ6V zhc!RhYgvrmeGNsftr+(C<_MtuV$`5RZTf#5r=DR?gWG->#})#=(td%C3`oO+2B7im zUqY}&a_QNTn?s+?=mNXiREN%x_=(H)L|DtYPY>SR3pQfBOel7G_jR_{!9`dSj8Up-`JgcB;=Oor)U=_EVjF3C5{Sqh8cq=~bRjoBpoc$kJCgtTyZGSpQ4= zYi$6b$-dGmuTDF&@amhV?cU05g(AZV&v2$4m&j_~GZk;&keSO(@LRESRZ&p`dV*6w z2$em~p*8yM6j;SYorw`M5K2mluJq7P5Yn$VtZj8DEs2Zk=O@4T&Q}>~f31Z{uk}`E z{Dp{KObh1kk~~MfLUod72{Pk6G@T$_0_N??lOrdR=Z;VV#m0l)&@hz{Z?)@sgImi-&i1@95g53rON83v!yVPDHRU*Mzc4yZ(-Fr z{8{WXmIJf7jeswk$;6s~Qac6QyM3W&`}m#gRt=rr95A+Ad&wSAgvXZ|F))rBJVJ5W1CsjN`QaOzct2ocq#0!v zmj#075)C!3oS>&N;aHS@<+c>RHL)8j^p)k(8#7$LEx!1g_1^02!4_qA=;uhKW=+ix zGX%+vBMiRiF^^jm{mdO(?GdWJ#unO#_F^7mhT8)s(z_WlwFyJ#Xh)k5+RG2f;LC*K**1dr`#}~6A=0B=I&V;%zDA1)d@G!X#Rng)7G*2k8Kg447r0ox> z5NK`d(H-afBwo9feDOUi>;BbPsu!2|=@g=3j*PY}@YrOb+SX6?#Yb2xaaK!?>SX1J z_!VsB`2n1=wwSftkydm!39|-1?c%Epx?TO<(#GO~I&{f4+)XwRk<7RQ1~5>QcKH|D z?!}j1ueO0Lk;FZ{k4FA_(S`Ot0w~tl&m0duID*f6RY#bkw||o;kZ# zISYNTb|{~|X$m$Q-Jv#uxyw)eM0gIv`V#wOAp&Vv@>X4_tSZ&L#juM@$S9 zx_X_tLh<_^-F;LAQ09s@sPb%PMTrcw*HUV0P=RYSlM&AXEOI&&R&YCm_S<7DRBx^L zA^R^iwW+LMk(r*$Pq-fKU5X@=mQ=`ErO30H@@&qqnI7zJcrbSh+H<V ze&7Uli0xj@WrW#&-9%*FP~kPYF_YYM_hs5~|ExMynQ%qvq`leRB6W0yhC@pCb8>_P zlf=F~WMv_u*-DV=UaVu#2rlzK{q8D95VwZrfV?gj@rSNWXFvktUq)V5+YrlxwX302ae(;aG4e>L-M@3J+-f3IT{b9l!kg*2M zC1+ND9}6m^()LE87Mt+^Q|)!y#suc&v26C=0W88%a{?)E8Yvo@kM&KNMaOst#|-_CbUTm}WS@-c>nRb;&z^ zYr)+IE$1=jov(CZ%3uR+`~NI>1&Gs6W(jaamjcN$a`2!*nO}l|b%?)Q%%UWzw>A`C zR@px(P*7j$TK?jbv*%x)e^|jcLsv}aF(Z0=7(%Oa7+1wY>{B>d+i&ZA$}k(qgZPZY z;VkW~8eWnU&HPIAbco?&tc2O1$6=7n{u|^Y*nXoac{o1W-6aXfy~KlNbJfLoq~6;+ zDYmnv--Fhqrl+UV#k@_(1=gWNtqhyVKN=9CZ-{Ohi>e=~bm4IKbhM%%W zW8oXE!rGpV7Wt(_^4nndH1_imheaWzDi|I})9ZVZ9>pN+P%dVc5wG`Ze*4`@rjn1^ z`ln(;vPBHQUb}y8S>=8q__r7g+=z$>!pReVB0@XKchAvyGjLQs-u>+w%`frV4FeIG zj=7n~hGrwx*&5aHy(7X$bDZ7YhcP%(*>G^lAYMK;qG~V8Jz@b7oNg;IA1z$9@TbzW z;@I51@Ekef#qbxnG$Y8Z%bm~ibZ=4#%yKr%#b)CDrfKN`ujIY?tA4h9)i~dZ4E;ZM znvb$n2)zn$Wx&zlW%mJZDh28ox$@%`w3i7YFepXUChw}$UXKI=-TM51`M#FH=tdr*mQ!c=aB1296Lu>iTTKZWss0f z5~ihdImPN$aTle_AdbYC^31}_^EK|9R&l#%3hbx;8vJ+Gp^tm{9JDILu*1PW!rh^Dn9p<)h#Sl4kKM%nm<+!ESSk* zC;lLNT$fgr-!+{aBsSx$41b}yy6o>r3F#1&iv3cfY2N<+`0qJ+>=&Qxs}JOEkD?^l-F5i`t5+zNuvJf z3Fh4$mNqiFXL-aq4U4K@Ae$fq-TDT`rvrx;gqx96w^*@s=mcthCaIyPe(w)6kI{EqV10tcShHU9eeAPs)s?6#vrq}>y3FeTJu$Udha+z zs7}rmA@yR(L&>35sNjQqrw}o^)UitMU!5g6nnG)(tgst!^`FKJEzI1(d@j_w@;^hr zgYxlIRYjho4U$bhczfq&YySCqCE(5_d>l(4tk1v9!V7PB%Vx{QO=G2NC@c1%3rEzw zN<6i?h;CJX>h)kn49Sr)g#Em6km6ESP`1qc5C3ZHizN>r>V-fSS=X1nT{+Thh@kC! z(H=PlqDt7V6gOYezXUK-dretz!1?IUD6&eL2b!4=9h+HUO&DYZKMM>|YhlEEg?q?S z^XT4$2Fd|zT=x3U#L1|F;-#`to-Y6hiYkWdO=rRC)meY72pIfl`3zEGDU8($iWR^K zI$nq80aSJII<;#W5Pj>^_T&013BJ*O89Uoq z5>;Paa^E}xar^r=!pexg&OTM8wluk4R~Ru=)Hgk`Y#i_$jk{jc8hx}?(dW*X!l4vs z6_%$s#duJJFmaFc-5#>v6Yea=I~)s_pXGS>Tkz?s+WS}>Qp<9MappMLXpkXpSM~SmH6u)`Z5>o02kJs;w@KhdiZ3}29y*xr|6tMo zBHzGic+b+dTd!xOJ;p{Rguh^corJ;K?R6daayQKm+0rf7|AXg0qs!R9eS7t4{G=fs z1$=?kK1Ih=gEkI>@jgXDWHZt*C7FUEWs|u^pE3Z``^K|1KEC^sbN*4nQUfRc_AyE0 zn)?RrGjgPkzfE~_s!rDB!fDsV+*|kEX4+DyS#8%!cshn;s8svwBXSsDGX2ZRa0={* z=`p1F{zD17*Rk>Uk_cw3t5j=9-d6$}MoM~z{v{t^M!g75-+o8_XkP@CZWUQ2z!^26 zCNOu~hgrrK)y>bgqb{`Q_1^zrG4;cGarP!nb4E~(ZKWc`LVeEq;IewVneLp^ZU2+% z95PgN*M5v7Q;ZlGvM#`&u2NdHm%&gZ{bZM5wBCp&?HeZhwU87wyT_z!n4z+1?=RvXZ^72d*%+R1s1$KbAFtR|= zw;MEq=O7pMIKpFwKH6$OOszJAf<_Z<1)36cB>D>|Z6$gJL~jH`n3MMou$#Si%rDAu z4pSkJspG|^CJ86vg6kkfXsA_`8@8iOryOe!Qhn8SV6}mPlof3=WJRVqAr_b;e->`Z zMR(p|K|$L0^6;u~USxg#B6-ZNc%E1dv*^P=|2k*^NOBni#G%9Y?##{=)8KZwh85OL zSBG9|gb|hdmY^gn(ziY&O5#@I?W)W;361Yb^VQNpz0A7&^(7HRAsUvw#)fvhocvja zLxV65J0_$>&cVRctJFsn^qLos^tG`+B0_gQ{NeOwKt-!C^gGFufdtPT*Vi>l#X1|V z2XxsAcixN)Ekq=a##_^=k_^BFH5_zpvPDRP>u6+3$}i&b zy0@FdzAHw?i9OqnlTts_w5D@Nd#eM)KKEuN#m{|AJyscxa}(eA?z4&4yvXo{OBS65 z-?gW;<+;+ntM}U_yTmHm6*2zj0Imj<&ZgE9Wj|gfsXhrVH-c0p$7HXnR8bxDYOi z=_r3FA~u`L&2;Vir8}P3)k|@c?sK1U@&iWo{HEXcoy>6wQSuJ+b4l%aTBuigs&k@Y<2c=S3Ef?p zH>ki4yDuXdo_eu>X1{E$g(Q-u#zVXN^&%70guoizo7x(kQ0OZ}H$O9UB}(FaX8Ct1 zFpx~}EbHf2r6V;x=@8GH$C2|6*?K~?LrtMYd^bw*WYXhA z_))@RMH;nZedW3+qfWbv<|_#BYOxX^rhbN+!za)|!|8K*LRs(R$O*2SDM{g9k7e{u zN4VIdi}e#0&h?sBxu$>Yy%)j(k1V2fuhp8r!}gfF@b;F?U`6}YnnMh1&sSU&lR^?# zu!61+lGsuFEfDraX3+$QZibCbKzc{75G^T7@WZSQ)j5898G1AOXB*H*TSd`f<`IK# zm1%&t?i|2Z-a&r!pJehzg@!awNp)R)aa?q_SqGrxE5u+T#f?K2;GAHV?O&>!W@Q*k)7=g2vDW+7K zbyY9i{|nOF*SbMYoRQSAbSH2y$bE5(@d6xKxcF#@TE~X#3o=;`0sc!RupdRmQsML? z&>SCwS{FOpSr+@6Uuz3m`hj}(^g`Jz|6?({!%WVJn$H|ugxW+x-GEA?J&U^ugj3Nb z;65~)W<}iH2PJ@st8LtLfSOLXYgj=9<;?ih7rq$bXW9J#!B8!Wu6#U`A$wlcoC*&` z_9Js~7%m79#+edeT&P`@_Ng@e&5J+pqpx%31tAF71)pcz~-yJ>P5yX(nuM4;bUHDa8E(~~l{j~JeCGkX>nHJDpgSf&bTHEf)qw8{Q~CBPEVen|MW2P3vmf`8X9-g|>>ddp zcgfjbl~(?3Wa*NzQH>4nsM$3}Ul>pX1xC0oF3TZXe7=V!9!n?WgvH|R zpbruczmB%z=zkZ>=1R|gXwGThLELqD5KCUhtiRGT*JwKIvzbzV%ZU!e!VcNHSSX3> zObH|oohc8nvQZ2}q??C}@>!fe3gH+HF@4(qWqi>;ag~md#D;cl8&gQb^?2a@5cikT z=7r78@&5gV3Ggc9f=<<8v~yz`NcEGvbX1V_`IL(&+Z>LB zM~$ok2qXzod@1$TEl*U~H$V5g$er{Uj^($sWb7Nr{gsIbE(`$LRGECTOraXiU%=uq z0zvpi1S%)RxTjzoVcR4#10)fs()4Mtsa@e?9j)Bk!LsYyXIZga2q7d%`vQE!V@<1Y zmkpH3LeXJNO9f7l>F84g;huc=4nk(UnU}RLZmYk2TtB#lv34K(?8~gyx-mN%g=U44 zOPdr_!j-;IEbe|l9-buuKEy^Q9MLjSKG$S6dz)!U_32{1)N}L)3+COmlg=nY1@od$ zJ<0z-B%sisAR1yh>z-RfQQb6M4i-d#vxvb~f69M{JLPZv1JSCh1$gQ*LxOF-tH9!k zbQ0ZW)S7)qCSF|=2`q_A3}OHBNBueZwTTz^ar~gz#2KA74&&D)KHt~m4F_nK<^*7_ z!!pN@xiGkq%>1N(rNxw$zu-=1t*IpAy$ z4~dD0w%9;E?(greVWZ3(o9ux`elM>Rek#0 zO=#-(4p5B+wFzlEU7^k{3EdL6sIp|K*>xrriI`}E8ze|z-$YpN`^_teL_7P`%e>IN z7tNiH619P+0Q1hBR|W#POOta)1|LkIRtgz zMJ9VOxXN#o)mlXS=u%`Q>~PBuKEmOWsIuQRp{y%!ty{fEyL0gV)$LQeL#pqX3L@SR zJ2Gb^E9+KVd?;joVOXlGie3?z6>(>u(i!(qGz(W( ze~^xj&IRF<98ypEis{Y_FoHn%C0bW(XeF#Lj=2WUEBqKNPPFppEH?_a3}-h906X}C zSYKcZFU`Om5YlWhh@ogzCn3NvuM~F9jOX|xe-X*!YL+#ceh_tJoHXz`aTnvSrOAZ| zOtdGz?QdT!oAJr3(XL2G(p%2X4{xEohU&vd_zQ(U%ihHOlKPWnb$&YYhx48?|R++>`5?sxvM?!;ru|9 zZ#nwuTK^S%ce<+ggdJBE&fRrXN7O!{nu`%q`M{2Ef_+IRad2cf01P9pST9AOK>y75c!9}~)Et^6$`&Nm{wzWcm4c0j9DF!xJTpGrMp3esI4D_iiDe`sswXSu{dQZE_`^A11 z?Z@Hw=65mVu^%X`>;$mciK}XiZ{xw7I_!t)S00^JuxdCXhIRO~S*lPS(S^je`DH4E zxbKNs8RL`N?gCQ@YSOU=>0FE#Ku#DRO7JA&fu-X8b;3!^#{=7`WsDXUxfUsE(FKSQ z&=N`A7IwLq%+vt(F;z+T=uZNl=@K4|E%p{p^o5(BGjsE|WOR`%8+XgGW8xJTFJc4L zVY#L`OdnSM{HyS$fX1)3_JuNNH1aDsDqi>CzCT5=kY5zV<~29bX)c^I8R5n&ymHkx zj(QC4t#mDK;2xi8O%V;C{HqDQeM64=b4@sa*N_K0a&ro4+8LY6cFHz< ze|!g}zF|tDrP=`+U7KwKl20gdW1%!iN>1=uxA|NZJ2peruBOj?RBPb~8G;s6xIi6- z?_odhafsxoxiBf zwZZ)c*)FLc0#wE~bXw0TPBYl+h9hs|DYr_B4LR_YL@S1hQs=p zNEh%_fUvWZCbJtaF#kP5=(O#{8|g&Kmz1&8{@Lufw^DhtvKx955~aqxi2C=)Z-!Kd z+m-u+#^U4(HYn6a1w652kO0bYBt&goyx(n?MR^kI+{Q?0Y{G~W2) z0dS3fuJ?SU(6ZDp=kUley%PK}K_;YQyK|U|?7t9SHiyIfpT4a_kUVIhH4PSaj@3mo z`z}|mHhx1Pq?@(3vTBb5HTXuFAzFZEt0D-fw_kd=XvwIUh3VXTm{wbDA~cESd5cI1 zd>6=&AvG3yu+)`9oxmfrDQ(1fzv(_0l?bp{a364dXLRRBI8kBv!KsL;brY)#E3`o{ z3TlWUsS0{Voci?6MejccG9x_KiqN>So*1{25r6BSl9jUyR}1TgXBLL7Pr6Wv~Nu47;fbiU7TbL}>qmtl36YSZ() zVf@nqW(As~#`@bIC+AxSw!O5Pocf&rYaCFm?Jd?XR)p#@{!|5^Ws@wd855)mI^8y{ zws+VvGXW6%xoj@JkGb=~%oJ~7m6+uhOv?bH+jJJ~eFgp+}~*^C+3>R-MY!IZQoabCh( zN(T+z@Oyc^C)WqQESmh{d!!T8zS(!wX=R#hEKxMXy(eg zZ+Cwm1a%?;RH$h2_ws|nRjn8ZY!>3gn+6Ep4xT|AeFox7!rac2Lw?jsz}JqPE?5JG zok0}q1P;cuzs%Yrze|&d$oTr<`Lx{fbq2OV=!3v-ODq(n?|WxuhtmwJBIoW^^FB+D z-?Ok9HBKc5@)L(W&vmI{prL?4^OE9TR)bELS=<>*w%&aKjzi*@;5#P3moG@dm{Eke zhE#Is;&=o|{2GWai}7LYEI+gmc^Kj4K7w7n)+9godg?yB2?xs}pF1<*!Sv?D~Uvbkgs9xx9s#6zBv9l@ox>d#H6eqw^KZO;Vg}h!q zI33^$4}yF*q+q{DsJsa(SsV!YQ#zi^IF9MQV6i{SiN4dWWCi%YQ+hNc1r!^+<(YnB zG62-D`M3w3Q2;@X{S`n`{QO>migDpz0FK`->sYDOESs6u>-~<}_XN_6><2g7U#XC{ z$#Ig;n{_yEMnlvx-lP*;ts#DHV0r8j518>~33?Ak#jocW>uk>6V||p7{4rov#RS9c zdPD6r`qF1om9r!zS4Jk1>7fn#GCnmD=JIt1Na`X)=*LP7R!3XATgk`;&U*P<(0d z9p<0T&eYqQ9jot39FxpfuPSPYlfQ$s-*;+c1KL+cHIVcG5`H~^Ryu1Hk7%Nf$TCwR!SzG31@NHpm`mcp8v!wyWM49TjTxASJ-8JP*MTHLC}hF==PUOh8kaaXeGFGd<|e29vSDaS ztPeu&zv0^wN}Hahi`$pcDs~FVt2F;K!q}q*Y@{7i#stWfU`u2La4aerBKhV`^zG~j zJWvtZpcHIP7x*tfLSQcng6D(`HVp4=LWp_0Xt=2wEHjK)!DSz_Z?5J@>awRyk?azj zU-kdSs~cp))*pfJ_q7u`IsCq8F|OShB~D56S(Mwwlt?{yURE7#eI&WcpVq(@9Fd~g zeUiD!a4w51Nj(YzLnau+O3MDub|?loF0=<#jLztAM>PruE7yNDD0L}y=Ayuc?^?Ni zf~%GK=iEhn2}xKp7GonJx!JpDmDsco$|$XtRdUDwbM9$9s7x9-of2nKNj~?b@UOKz z9{`=Irz^ba-c&1vSQxSh;I2`cKc8-4)aCy%#bam;3_8vSJ-jw`_}lyukEC~z00EbC zI*dU3F21A)dSZr{qA5QF+{a%D`h#?8o%M?)*hWxuqnQD(TpcmfNq&UN$BmB)0!r8) zxno@Q?$_D&*4(rW6b+?-Y^5|*P`DHmJ%pI<6*yP)o}2^?>d7P#bd2j=vvx2mfLW@R zQLD`%buR*}nzNYNf%68w-D$7%v|=bXg1mYrdZy~}(@RRZ-U+Gx=nmCjVxr5Ag# zLw3R29-MHJl|`mRxj#sv@EfyR#-q>BE-XFEENbV$#dWM?!VjU8~kKZsd@G=HPrI{HiqN&j<92*-3$^M*;n@rG*i! zvi#?j;lc5w>@+r!6*CVUrN9as=S3?(ZBT979$5R#ZpPm?2VjIyQcEFp9orGR>f;G? zK<~FiYY6ow-&}|v7k?+03TC++so$)2~rN``u z>N%j$AbNQLX_!evzG8abf=15260vIXdz7K^a$YS)iw{@x5<|Rr#ii|ov=LJ{eu>dZYe_ip$ZuzvRu1dpjQK1BvP zH~m#t=2_wy>9+YkdNF-z` zQ*#7=^r%R*pIi2AI`>n9>(QJVE1k8?Ilav<)NUjW^O$}^yZZ{_Uwn!4Fq1`aslX;Y zj`XDIm`E1sz|wShA=?a@ZGKDSMU#Z3$E!1nZ)g^Eg3ZDoSN6@RXrGVCHvMIauS7d> zuJltXf9)LdTWdF!n%-iA9b#2$W#i??K)zYho^((ZqluvhAr@{H{diy0%@-~VW zKYC|2Ma)2^=skdLT@ZVqJfiCDqS@~qIGexL(BKy6Aw9ch0hoHN&E+m3*uka9+AIh3gTWdSe~W({-&^oFw`!j7$DcsF$7`pO?kRMK<9h=SV?cmyJIe`$4|zoI(6u9#qY9zM?#zNe^!Dl2>Z^dH`>`wSY# ztU;V*+g0R0DH6EnJA$U{QL&T~&s{`smeC2I-5mzv=v$l@iF;yN0hMibU=CG^e>J;+9k`Si9PzLaj$>}QKI6lWmO_o+_( zmhxA*0|-Na`+*J1qEMIXZf9rb#;pcOw>EDeDjb!|GumQ2!1ac;YqU|X;F@l1_lemzTN0J|U zFJF(kO21aHg)*KfuKT=BA{VDkOvlx(b{f|A9D69_BHUm#S$F>~`Mt@GesjLp3;reY zP~q>6Tt;`XkjqV?i7lqPbWGh`y<7dq<}pDHl-dDA4QG6`QDq)+vq_&HfW!}P6Cp4d zt>Qnli5ri*I1ILEOGD~3Y!@2^Jmcy1xDXmKolC?at}_6;neEfca0rLHT}NLpoUYh` zDbCtfZnYN&>}m-(F{5d1=)bBuZ?OcP`GmsQV@kn%JMJUIep`Avon#8=ATpEo-@hg& z12f-)R=HCD%pUjvbWa|P!}u)=wInpZG*LHKrZDMeC>Qils^IyY)x;kDRs4c3!DDOG zAptSsf#1X>kSli|Qka@S)6O4un-2aKL?bcV;$*>KSxHovjrfZ^-+c#>;(42yj71K| zzRyFiLrwv$rPcNA{mtv=o(*JDA0kS93>OE0D{KMJzLk$cc_5dCLWnJcFJd6_>BpE< z?aW9;^!;arQcIjloW&YL+~MkNO&a>N=pmhg>{SM<@`a&VeUA`ay*P@R$_+WS2%r?_ zs&Z%c`>ie+%!I=Lz>$9$7a`-`hoc&*dl60^whsaQ;~9~@JYn1Oc_bmgVVyAzUOYgZ z#j{`#D_YZ)(wa5;qzR#zo4a|-ANJjBB90r4Iun3*BkMxw_Ti>SjhktsmR|BPCLt>9 zZ_3eQjweI*-8+HNt)$9^s|+10w@sU!PY{`#BnF!ULS=#{k0Zr5`yOS?p8PfWbKT`6 z@T+PeRJ4`fj5t8bMs)0>o9|C>mBTlfQ*nFG#Rri-Q7}E}+eaz`LmO!`Y_pHkoAruu z`&!5VNnA3IG$}Pz)V&pt&AF!$E{J-;or3vWv3&Sl&9KzG+ae73Zf}=aP*SCI1{?0T z9SAC)W(?DSKOkcmW$(K5Bl?c@(5#>J#j@eq#ctX~$TIjkl>Wrfv%Ey+bl1Z-v?NxJ zwZ9!ae-MsHPUx&_W22?9$mCE%&~lzVG?hDXM%~gXGk+Q!Jf0BspkMWxy;^!n<6JIrSYjv z6F%~$8)0^qbUho9Sdf97b_n({$;|XH9-RHrohHuPcro@03KEPFejN&q?&nJFoIQY; zSI#uL6>2^^yOR!51OLO65xGas55dPG;3=uQ35ZYW04#+~byXQf^7Vq`G z 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 Pe7HEdAEg|=c=LY&YVNkY diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png deleted file mode 100644 index 13b35eba55c6dabc3aac36f33d859266c18fa0d0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5680 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 diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png deleted file mode 100644 index 0a3f5fa40fb3d1e0710331a48de5d256da3f275d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 520 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 diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png deleted file mode 100644 index 2f1632cfddf3d9dade342351e627a0a75609fb46..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2218 zcmV;b2vzrqP)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91K%fHv1ONa40RR91KmY&$07g+lumAuE6iGxuRCodHTWf3-RTMruyW6Fu zQYeUM04eX6D5c0FCjKKPrco1(K`<0SL=crI{PC3-^hZU0kQie$gh-5!7z6SH6Q0J% zqot*`H1q{R5fHFYS}dje@;kG=v$L0(yY0?wY2%*c?A&{2?!D*x?m71{of2gv!$5|C z3>qG_BW}7K_yUcT3A5C6QD<+{aq?x;MAUyAiJn#Jv8_zZtQ{P zTRzbL3U9!qVuZzS$xKU10KiW~Bgdcv1-!uAhQxf3a7q+dU6lj?yoO4Lq4TUN4}h{N z*fIM=SS8|C2$(T>w$`t@3Tka!(r!7W`x z-isCVgQD^mG-MJ;XtJuK3V{Vy72GQ83KRWsHU?e*wrhKk=ApIYeDqLi;JI1e zuvv}5^Dc=k7F7?nm3nIw$NVmU-+R>> zyqOR$-2SDpJ}Pt;^RkJytDVXNTsu|mI1`~G7yw`EJR?VkGfNdqK9^^8P`JdtTV&tX4CNcV4 z&N06nZa??Fw1AgQOUSE2AmPE@WO(Fvo`%m`cDgiv(fAeRA%3AGXUbsGw{7Q`cY;1BI#ac3iN$$Hw z0LT0;xc%=q)me?Y*$xI@GRAw?+}>=9D+KTk??-HJ4=A>`V&vKFS75@MKdSF1JTq{S zc1!^8?YA|t+uKigaq!sT;Z!&0F2=k7F0PIU;F$leJLaw2UI6FL^w}OG&!;+b%ya1c z1n+6-inU<0VM-Y_s5iTElq)ThyF?StVcebpGI znw#+zLx2@ah{$_2jn+@}(zJZ{+}_N9BM;z)0yr|gF-4=Iyu@hI*Lk=-A8f#bAzc9f z`Kd6K--x@t04swJVC3JK1cHY-Hq+=|PN-VO;?^_C#;coU6TDP7Bt`;{JTG;!+jj(` zw5cLQ-(Cz-Tlb`A^w7|R56Ce;Wmr0)$KWOUZ6ai0PhzPeHwdl0H(etP zUV`va_i0s-4#DkNM8lUlqI7>YQLf)(lz9Q3Uw`)nc(z3{m5ZE77Ul$V%m)E}3&8L0 z-XaU|eB~Is08eORPk;=<>!1w)Kf}FOVS2l&9~A+@R#koFJ$Czd%Y(ENTV&A~U(IPI z;UY+gf+&6ioZ=roly<0Yst8ck>(M=S?B-ys3mLdM&)ex!hbt+ol|T6CTS+Sc0jv(& z7ijdvFwBq;0a{%3GGwkDKTeG`b+lyj0jjS1OMkYnepCdoosNY`*zmBIo*981BU%%U z@~$z0V`OVtIbEx5pa|Tct|Lg#ZQf5OYMUMRD>Wdxm5SAqV2}3!ceE-M2 z@O~lQ0OiKQp}o9I;?uxCgYVV?FH|?Riri*U$Zi_`V2eiA>l zdSm6;SEm6#T+SpcE8Ro_f2AwxzI z44hfe^WE3!h@W3RDyA_H440cpmYkv*)6m1XazTqw%=E5Xv7^@^^T7Q2wxr+Z2kVYr - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/macos/Runner/Configs/AppInfo.xcconfig b/macos/Runner/Configs/AppInfo.xcconfig deleted file mode 100644 index 543b706..0000000 --- a/macos/Runner/Configs/AppInfo.xcconfig +++ /dev/null @@ -1,14 +0,0 @@ -// Application-level settings for the Runner target. -// -// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the -// future. If not, the values below would default to using the project name when this becomes a -// 'flutter create' template. - -// The application's name. By default this is also the title of the Flutter window. -PRODUCT_NAME = ParkTrack - -// The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = com.parktrack.mobile - -// The copyright displayed in application information -PRODUCT_COPYRIGHT = Copyright © 2026 ParkTrack. All rights reserved. diff --git a/macos/Runner/Configs/Debug.xcconfig b/macos/Runner/Configs/Debug.xcconfig deleted file mode 100644 index 36b0fd9..0000000 --- a/macos/Runner/Configs/Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "../../Flutter/Flutter-Debug.xcconfig" -#include "Warnings.xcconfig" diff --git a/macos/Runner/Configs/Release.xcconfig b/macos/Runner/Configs/Release.xcconfig deleted file mode 100644 index dff4f49..0000000 --- a/macos/Runner/Configs/Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "../../Flutter/Flutter-Release.xcconfig" -#include "Warnings.xcconfig" diff --git a/macos/Runner/Configs/Warnings.xcconfig b/macos/Runner/Configs/Warnings.xcconfig deleted file mode 100644 index 42bcbf4..0000000 --- a/macos/Runner/Configs/Warnings.xcconfig +++ /dev/null @@ -1,13 +0,0 @@ -WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings -GCC_WARN_UNDECLARED_SELECTOR = YES -CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES -CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE -CLANG_WARN__DUPLICATE_METHOD_MATCH = YES -CLANG_WARN_PRAGMA_PACK = YES -CLANG_WARN_STRICT_PROTOTYPES = YES -CLANG_WARN_COMMA = YES -GCC_WARN_STRICT_SELECTOR_MATCH = YES -CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES -CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES -GCC_WARN_SHADOW = YES -CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/macos/Runner/DebugProfile.entitlements b/macos/Runner/DebugProfile.entitlements deleted file mode 100644 index 0b192be..0000000 --- a/macos/Runner/DebugProfile.entitlements +++ /dev/null @@ -1,16 +0,0 @@ - - - - - com.apple.security.app-sandbox - - com.apple.security.cs.allow-jit - - com.apple.security.network.server - - com.apple.security.network.client - - com.apple.security.personal-information.location - - - diff --git a/macos/Runner/Info.plist b/macos/Runner/Info.plist deleted file mode 100644 index f79e8bb..0000000 --- a/macos/Runner/Info.plist +++ /dev/null @@ -1,34 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIconFile - - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSMinimumSystemVersion - $(MACOSX_DEPLOYMENT_TARGET) - NSHumanReadableCopyright - $(PRODUCT_COPYRIGHT) - NSMainNibFile - MainMenu - NSLocationUsageDescription - ParkTrack использует геопозицию, чтобы показать парковки рядом с вами и построить маршрут. - NSPrincipalClass - NSApplication - - diff --git a/macos/Runner/MainFlutterWindow.swift b/macos/Runner/MainFlutterWindow.swift deleted file mode 100644 index 3cc05eb..0000000 --- a/macos/Runner/MainFlutterWindow.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Cocoa -import FlutterMacOS - -class MainFlutterWindow: NSWindow { - override func awakeFromNib() { - let flutterViewController = FlutterViewController() - let windowFrame = self.frame - self.contentViewController = flutterViewController - self.setFrame(windowFrame, display: true) - - RegisterGeneratedPlugins(registry: flutterViewController) - - super.awakeFromNib() - } -} diff --git a/macos/Runner/Release.entitlements b/macos/Runner/Release.entitlements deleted file mode 100644 index fc07546..0000000 --- a/macos/Runner/Release.entitlements +++ /dev/null @@ -1,12 +0,0 @@ - - - - - com.apple.security.app-sandbox - - com.apple.security.network.client - - com.apple.security.personal-information.location - - - diff --git a/macos/RunnerTests/RunnerTests.swift b/macos/RunnerTests/RunnerTests.swift deleted file mode 100644 index 61f3bd1..0000000 --- a/macos/RunnerTests/RunnerTests.swift +++ /dev/null @@ -1,12 +0,0 @@ -import Cocoa -import FlutterMacOS -import XCTest - -class RunnerTests: XCTestCase { - - func testExample() { - // If you add code to the Runner application, consider adding tests here. - // See https://developer.apple.com/documentation/xctest for more information about using XCTest. - } - -} From 94cbf9b21eb0cfce8af767c9cf7c68ee88fa999b Mon Sep 17 00:00:00 2001 From: Nikita Aksenov Date: Thu, 16 Jul 2026 23:06:27 +0300 Subject: [PATCH 058/145] chore: prepare Android 1.2.0 release --- .github/workflows/publish-google-play.yml | 2 +- analysis_options.yaml | 5 ++ .../releases/1.2.0/github-release-notes.md | 18 +++++ distribution/releases/1.2.0/whatsnew-en-US | 4 ++ distribution/releases/1.2.0/whatsnew-ru-RU | 4 ++ .../map/widgets/time_selector_widget.dart | 67 +++++++++++++------ pubspec.yaml | 2 +- 7 files changed, 80 insertions(+), 22 deletions(-) create mode 100644 distribution/releases/1.2.0/github-release-notes.md create mode 100644 distribution/releases/1.2.0/whatsnew-en-US create mode 100644 distribution/releases/1.2.0/whatsnew-ru-RU diff --git a/.github/workflows/publish-google-play.yml b/.github/workflows/publish-google-play.yml index 9013f18..c990acf 100644 --- a/.github/workflows/publish-google-play.yml +++ b/.github/workflows/publish-google-play.yml @@ -200,4 +200,4 @@ jobs: tracks: alpha status: completed releaseName: ${{ github.event.release.tag_name }} - whatsNewDirectory: build/google-play/what + whatsNewDirectory: build/google-play/whatsnew diff --git a/analysis_options.yaml b/analysis_options.yaml index 0d29021..97e5f06 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -9,6 +9,11 @@ # packages, and plugins designed to encourage good coding practices. include: package:flutter_lints/flutter.yaml +analyzer: + exclude: + - "**/*.freezed.dart" + - "**/*.g.dart" + linter: # The lint rules applied to this project can be customized in the # section below to disable rules from the `package:flutter_lints/flutter.yaml` diff --git a/distribution/releases/1.2.0/github-release-notes.md b/distribution/releases/1.2.0/github-release-notes.md new file mode 100644 index 0000000..8ffd29c --- /dev/null +++ b/distribution/releases/1.2.0/github-release-notes.md @@ -0,0 +1,18 @@ +# ParkTrack 1.2.0 + +This release makes the ParkTrack Android app faster and more reliable. + +## What's new + +- Added app settings, profile editing, dark theme, and full English localization. +- Improved parking availability and route loading, especially when moving quickly around the map. +- Improved in-app navigation performance, route matching, remaining-distance calculations, and ETA accuracy. +- Made authentication and expired-session recovery more reliable. + +## Fixes and improvements + +- Fixed accessible parking filtering. +- Prevented stale parking and routing responses from replacing newer results. +- Reduced unnecessary map redraws and cached parking markers, polygons, and clusters. +- Reduced false route recalculations caused by inaccurate GPS samples. +- Added an automated Google Play release workflow with localized release notes. diff --git a/distribution/releases/1.2.0/whatsnew-en-US b/distribution/releases/1.2.0/whatsnew-en-US new file mode 100644 index 0000000..a08b3a6 --- /dev/null +++ b/distribution/releases/1.2.0/whatsnew-en-US @@ -0,0 +1,4 @@ +Added app settings, profile editing, dark theme, and English language support. +Improved parking and route loading, map performance, and in-app navigation. +Made authentication and session recovery more reliable. +Fixed accessible parking filtering, travel time calculations, and data refresh stability. diff --git a/distribution/releases/1.2.0/whatsnew-ru-RU b/distribution/releases/1.2.0/whatsnew-ru-RU new file mode 100644 index 0000000..acab009 --- /dev/null +++ b/distribution/releases/1.2.0/whatsnew-ru-RU @@ -0,0 +1,4 @@ +Добавлены настройки приложения, редактирование профиля, тёмная тема и английский язык. +Ускорены загрузка парковок и маршрутов, работа карты и встроенной навигации. +Улучшены авторизация и восстановление сессии. +Исправлены фильтр доступности парковок, расчёт времени в пути и стабильность обновления данных. diff --git a/lib/presentation/screens/map/widgets/time_selector_widget.dart b/lib/presentation/screens/map/widgets/time_selector_widget.dart index f33948f..5812ef2 100644 --- a/lib/presentation/screens/map/widgets/time_selector_widget.dart +++ b/lib/presentation/screens/map/widgets/time_selector_widget.dart @@ -78,10 +78,16 @@ class TimeSelectorWidget extends ConsumerWidget { final h = dt.hour.toString().padLeft(2, '0'); final m = dt.minute.toString().padLeft(2, '0'); final time = '$h:$m'; - if (dtDay == DateTime(now.year, now.month, now.day)) return '${s.today} $time'; - if (dtDay == DateTime(tomorrow.year, tomorrow.month, tomorrow.day)) return '${s.tomorrow} $time'; - if (dtDay == DateTime(yesterday.year, yesterday.month, yesterday.day)) return '${s.yesterday} $time'; - + if (dtDay == DateTime(now.year, now.month, now.day)) { + return '${s.today} $time'; + } + if (dtDay == DateTime(tomorrow.year, tomorrow.month, tomorrow.day)) { + return '${s.tomorrow} $time'; + } + if (dtDay == DateTime(yesterday.year, yesterday.month, yesterday.day)) { + return '${s.yesterday} $time'; + } + return '${dt.day} ${s.monthNames[dt.month]} $time'; } } @@ -119,7 +125,13 @@ class _TimePickerSheetState extends ConsumerState<_TimePickerSheet> { static DateTime _snapTo30(DateTime dt) { final snapped = ((dt.minute / _step).round() * _step) % 60; final hourAdd = ((dt.minute / _step).round() * _step) ~/ 60; - return DateTime(dt.year, dt.month, dt.day, (dt.hour + hourAdd) % 24, snapped); + return DateTime( + dt.year, + dt.month, + dt.day, + (dt.hour + hourAdd) % 24, + snapped, + ); } DateTime get _result { @@ -130,11 +142,7 @@ class _TimePickerSheetState extends ConsumerState<_TimePickerSheet> { @override Widget build(BuildContext context) { final s = ref.watch(l10nProvider); - final days = [ - (-1, s.yesterday), - (0, s.today), - (1, s.tomorrow), - ]; + final days = [(-1, s.yesterday), (0, s.today), (1, s.tomorrow)]; return Container( decoration: BoxDecoration( @@ -142,13 +150,17 @@ class _TimePickerSheetState extends ConsumerState<_TimePickerSheet> { borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), ), padding: EdgeInsets.fromLTRB( - 20, 16, 20, MediaQuery.of(context).padding.bottom + 20, + 20, + 16, + 20, + MediaQuery.of(context).padding.bottom + 20, ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( - width: 40, height: 4, + width: 40, + height: 4, decoration: BoxDecoration( color: Colors.grey[300], borderRadius: BorderRadius.circular(2), @@ -173,7 +185,11 @@ class _TimePickerSheetState extends ConsumerState<_TimePickerSheet> { margin: const EdgeInsets.symmetric(horizontal: 4), padding: const EdgeInsets.symmetric(vertical: 10), decoration: BoxDecoration( - color: sel ? AppColors.primary : Theme.of(context).colorScheme.surfaceVariant, + color: sel + ? AppColors.primary + : Theme.of( + context, + ).colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(10), ), child: Center( @@ -182,7 +198,9 @@ class _TimePickerSheetState extends ConsumerState<_TimePickerSheet> { style: TextStyle( fontWeight: FontWeight.w600, fontSize: 14, - color: sel ? Colors.white : Theme.of(context).colorScheme.onSurface, + color: sel + ? Colors.white + : Theme.of(context).colorScheme.onSurface, ), ), ), @@ -205,10 +223,15 @@ class _TimePickerSheetState extends ConsumerState<_TimePickerSheet> { minuteInterval: _step, initialDateTime: _time, onDateTimeChanged: (dt) { - setState(() => _time = DateTime( - _time.year, _time.month, _time.day, - dt.hour, dt.minute, - )); + setState( + () => _time = DateTime( + _time.year, + _time.month, + _time.day, + dt.hour, + dt.minute, + ), + ); }, ), ), @@ -261,8 +284,12 @@ class _Chip extends StatelessWidget { @override Widget build(BuildContext context) { - final borderColor = active ? AppColors.primary : Theme.of(context).dividerColor; - final textColor = active ? AppColors.primary : Theme.of(context).colorScheme.onSurface; + final borderColor = active + ? AppColors.primary + : Theme.of(context).dividerColor; + final textColor = active + ? AppColors.primary + : Theme.of(context).colorScheme.onSurface; final iconColor = active ? AppColors.primary : Theme.of(context).hintColor; return GestureDetector( diff --git a/pubspec.yaml b/pubspec.yaml index 3e3be73..b87cb5d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: mobile description: "ParkTrack — мобильное приложение для поиска парковок" publish_to: 'none' -version: 1.0.0+1 +version: 1.2.0+2 environment: sdk: ^3.11.5 From 5d89ee1d8d405c486647fb328825509f88c57e36 Mon Sep 17 00:00:00 2001 From: Nikita Aksenov Date: Thu, 16 Jul 2026 23:11:10 +0300 Subject: [PATCH 059/145] fix: edit translations --- lib/core/localization/app_localizations.dart | 8 ++++++-- lib/presentation/screens/auth/password_reset_screen.dart | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/core/localization/app_localizations.dart b/lib/core/localization/app_localizations.dart index 7eecd91..44054c0 100644 --- a/lib/core/localization/app_localizations.dart +++ b/lib/core/localization/app_localizations.dart @@ -73,6 +73,7 @@ class AppStrings { final String alreadyHaveAccount; final String registration; final String resetPassword; + final String passwordResetInstructions; final String sendInstructions; final String hideFull; final String hidePrivate; @@ -166,6 +167,7 @@ class AppStrings { required this.filters, required this.apply, required this.reset, + required this.passwordResetInstructions, required this.logout, required this.settings, required this.appearance, @@ -311,6 +313,7 @@ class AppStrings { filters: 'Фильтры', apply: 'Применить', reset: 'Сбросить', + passwordResetInstructions: 'Введите свой email чтобы получить инструкции по сбросу пароля.', logout: 'Выйти', settings: 'Настройки', appearance: 'Внешний вид', @@ -326,7 +329,7 @@ class AppStrings { langEnglish: 'English', langSystem: 'Системный', account: 'Аккаунт', - notAuthenticated: 'Не авторизован', + notAuthenticated: 'Неверные данные для входа', logoutConfirm: 'Вы уверены, что хотите выйти из аккаунта?', cancel: 'Отмена', inAppRoute: 'В приложении', @@ -473,6 +476,7 @@ class AppStrings { filters: 'Filters', apply: 'Apply', reset: 'Reset', + passwordResetInstructions: 'Enter your email to receive password reset instructions.', logout: 'Logout', settings: 'Settings', appearance: 'Appearance', @@ -488,7 +492,7 @@ class AppStrings { langEnglish: 'English', langSystem: 'System', account: 'Account', - notAuthenticated: 'Not authenticated', + notAuthenticated: 'Invalid login credentials', logoutConfirm: 'Are you sure you want to sign out?', cancel: 'Cancel', inAppRoute: 'In-app', diff --git a/lib/presentation/screens/auth/password_reset_screen.dart b/lib/presentation/screens/auth/password_reset_screen.dart index 2f1ae50..a149feb 100644 --- a/lib/presentation/screens/auth/password_reset_screen.dart +++ b/lib/presentation/screens/auth/password_reset_screen.dart @@ -64,7 +64,7 @@ class _PasswordResetScreenState extends ConsumerState { const Icon(Icons.lock_reset_outlined, size: 64, color: Color(0xFF2E7D32)), const SizedBox(height: 16), Text( - 'Enter your email to receive password reset instructions.', + s.passwordResetInstructions, textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodyMedium, ), From ac09c87895a3617255ead4d571aa018a6b1213fe Mon Sep 17 00:00:00 2001 From: Nikita Aksenov Date: Thu, 16 Jul 2026 23:18:54 +0300 Subject: [PATCH 060/145] ci: attach signed APK to releases --- .github/workflows/publish-google-play.yml | 103 +++++++++++++++++++++- 1 file changed, 101 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish-google-play.yml b/.github/workflows/publish-google-play.yml index c990acf..8a99ba4 100644 --- a/.github/workflows/publish-google-play.yml +++ b/.github/workflows/publish-google-play.yml @@ -6,7 +6,7 @@ on: - published permissions: - contents: read + contents: write concurrency: group: google-play-publish @@ -136,10 +136,109 @@ jobs: - name: Build signed Android App Bundle run: flutter build appbundle --release - - name: Check generated bundle + - name: Build signed Android APK + run: flutter build apk --release + + - name: Check generated Android artifacts run: | ls -lh build/app/outputs/bundle/release/ + ls -lh build/app/outputs/flutter-apk/ test -f build/app/outputs/bundle/release/app-release.aab + test -f build/app/outputs/flutter-apk/app-release.apk + + - name: Prepare and sign GitHub release APK + env: + RELEASE_TAG: ${{ github.event.release.tag_name }} + KEYSTORE_PATH: android/app/upload-keystore.jks + STORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} + KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} + KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} + run: | + set -euo pipefail + + RELEASE_DIR="build/github-release" + APK_PATH="$RELEASE_DIR/ParkTrack-${RELEASE_TAG}.apk" + SIGNATURE_PATH="$APK_PATH.sig" + + mkdir -p "$RELEASE_DIR" + cp build/app/outputs/flutter-apk/app-release.apk "$APK_PATH" + + cat > /tmp/SignReleaseArtifact.java <<'JAVA' + import java.io.File; + import java.io.InputStream; + import java.nio.file.Files; + import java.nio.file.Path; + import java.security.KeyStore; + import java.security.PrivateKey; + import java.security.Signature; + import java.security.cert.Certificate; + import java.util.Base64; + + public final class SignReleaseArtifact { + private static void update(Signature signature, Path path) throws Exception { + try (InputStream input = Files.newInputStream(path)) { + byte[] buffer = new byte[64 * 1024]; + int read; + while ((read = input.read(buffer)) != -1) { + signature.update(buffer, 0, read); + } + } + } + + public static void main(String[] args) throws Exception { + Path artifact = Path.of(args[0]); + Path output = Path.of(args[1]); + char[] storePassword = System.getenv("STORE_PASSWORD").toCharArray(); + char[] keyPassword = System.getenv("KEY_PASSWORD").toCharArray(); + String alias = System.getenv("KEY_ALIAS"); + + KeyStore keyStore = KeyStore.getInstance( + new File(System.getenv("KEYSTORE_PATH")), + storePassword + ); + PrivateKey privateKey = (PrivateKey) keyStore.getKey(alias, keyPassword); + Certificate certificate = keyStore.getCertificate(alias); + + String algorithm = switch (privateKey.getAlgorithm()) { + case "RSA" -> "SHA256withRSA"; + case "EC" -> "SHA256withECDSA"; + case "DSA" -> "SHA256withDSA"; + default -> throw new IllegalArgumentException( + "Unsupported signing key algorithm: " + privateKey.getAlgorithm() + ); + }; + + Signature signer = Signature.getInstance(algorithm); + signer.initSign(privateKey); + update(signer, artifact); + byte[] signatureBytes = signer.sign(); + Files.write(output, Base64.getEncoder().encode(signatureBytes)); + + Signature verifier = Signature.getInstance(algorithm); + verifier.initVerify(certificate); + update(verifier, artifact); + if (!verifier.verify(Base64.getDecoder().decode(Files.readAllBytes(output)))) { + throw new IllegalStateException("Generated APK signature is invalid"); + } + + System.out.println("Created and verified " + algorithm + " signature"); + } + } + JAVA + + java /tmp/SignReleaseArtifact.java "$APK_PATH" "$SIGNATURE_PATH" + + - name: Attach APK and signature to GitHub release + env: + GH_TOKEN: ${{ github.token }} + RELEASE_TAG: ${{ github.event.release.tag_name }} + run: | + set -euo pipefail + + gh release upload "$RELEASE_TAG" \ + "build/github-release/ParkTrack-${RELEASE_TAG}.apk" \ + "build/github-release/ParkTrack-${RELEASE_TAG}.apk.sig" \ + --clobber - name: Prepare Google Play release notes env: From 0feb2b8bb989bf1f2022c647812fd166f7785eef Mon Sep 17 00:00:00 2001 From: Nikita Aksenov Date: Thu, 16 Jul 2026 23:31:58 +0300 Subject: [PATCH 061/145] fix: install Android NDK in release workflow --- .github/workflows/publish-google-play.yml | 25 ++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/.github/workflows/publish-google-play.yml b/.github/workflows/publish-google-play.yml index 8a99ba4..83048fe 100644 --- a/.github/workflows/publish-google-play.yml +++ b/.github/workflows/publish-google-play.yml @@ -4,6 +4,12 @@ on: release: types: - published + workflow_dispatch: + inputs: + release_tag: + description: Existing GitHub release tag to build and publish + required: true + type: string permissions: contents: write @@ -14,6 +20,8 @@ concurrency: env: PACKAGE_NAME: com.parktrack.mobile + NDK_VERSION: 30.0.14904198 + RELEASE_TAG: ${{ github.event.release.tag_name || inputs.release_tag }} jobs: publish: @@ -40,10 +48,19 @@ jobs: channel: stable cache: true + - name: Accept Android SDK licenses and install NDK + run: | + set -euo pipefail + + SDKMANAGER="$(command -v sdkmanager)" + "$SDKMANAGER" --licenses < <(yes) + "$SDKMANAGER" "ndk;${NDK_VERSION}" + - name: Show versions run: | flutter --version java --version + test -d "${ANDROID_SDK_ROOT}/ndk/${NDK_VERSION}" - name: Install dependencies run: flutter pub get @@ -52,8 +69,6 @@ jobs: run: dart run build_runner build --delete-conflicting-outputs - name: Validate release version - env: - RELEASE_TAG: ${{ github.event.release.tag_name }} run: | set -euo pipefail @@ -148,7 +163,6 @@ jobs: - name: Prepare and sign GitHub release APK env: - RELEASE_TAG: ${{ github.event.release.tag_name }} KEYSTORE_PATH: android/app/upload-keystore.jks STORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} @@ -231,7 +245,6 @@ jobs: - name: Attach APK and signature to GitHub release env: GH_TOKEN: ${{ github.token }} - RELEASE_TAG: ${{ github.event.release.tag_name }} run: | set -euo pipefail @@ -241,8 +254,6 @@ jobs: --clobber - name: Prepare Google Play release notes - env: - RELEASE_TAG: ${{ github.event.release.tag_name }} run: | set -euo pipefail @@ -298,5 +309,5 @@ jobs: releaseFiles: build/app/outputs/bundle/release/app-release.aab tracks: alpha status: completed - releaseName: ${{ github.event.release.tag_name }} + releaseName: ${{ env.RELEASE_TAG }} whatsNewDirectory: build/google-play/whatsnew From 087042f183bf6198bfd085460f765796b7f5bd53 Mon Sep 17 00:00:00 2001 From: Nikita Aksenov Date: Thu, 16 Jul 2026 23:36:30 +0300 Subject: [PATCH 062/145] fix: locate sdkmanager in Android release workflow --- .github/workflows/publish-google-play.yml | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish-google-play.yml b/.github/workflows/publish-google-play.yml index 83048fe..f0640d0 100644 --- a/.github/workflows/publish-google-play.yml +++ b/.github/workflows/publish-google-play.yml @@ -52,15 +52,34 @@ jobs: run: | set -euo pipefail - SDKMANAGER="$(command -v sdkmanager)" + ANDROID_SDK_DIR="${ANDROID_SDK_ROOT:-${ANDROID_HOME:-/usr/local/lib/android/sdk}}" + SDKMANAGER="$ANDROID_SDK_DIR/cmdline-tools/latest/bin/sdkmanager" + + if [ ! -x "$SDKMANAGER" ]; then + SDKMANAGER="$( + find "$ANDROID_SDK_DIR/cmdline-tools" \ + -type f \ + -name sdkmanager \ + -print \ + -quit \ + 2>/dev/null || true + )" + fi + + if [ -z "$SDKMANAGER" ] || [ ! -x "$SDKMANAGER" ]; then + echo "sdkmanager was not found in $ANDROID_SDK_DIR" + exit 1 + fi + + echo "Using sdkmanager: $SDKMANAGER" "$SDKMANAGER" --licenses < <(yes) "$SDKMANAGER" "ndk;${NDK_VERSION}" + test -d "$ANDROID_SDK_DIR/ndk/${NDK_VERSION}" - name: Show versions run: | flutter --version java --version - test -d "${ANDROID_SDK_ROOT}/ndk/${NDK_VERSION}" - name: Install dependencies run: flutter pub get From 54085ca2f82db6e6bbb3b2cb2bb683bb30970ac8 Mon Sep 17 00:00:00 2001 From: Nikita Aksenov Date: Fri, 17 Jul 2026 00:14:28 +0300 Subject: [PATCH 063/145] feat: add Yandex Maps support for web --- lib/core/services/yandex_web_route.dart | 2 + .../services/yandex_web_route_models.dart | 13 + lib/core/services/yandex_web_route_stub.dart | 8 + lib/core/services/yandex_web_route_web.dart | 45 ++ lib/core/utils/error_snackbar.dart | 101 +-- .../providers/navigation_provider.dart | 92 ++- lib/presentation/screens/map/map_screen.dart | 739 +++++++++++------- .../map/widgets/navigation_overlay.dart | 84 +- .../map/widgets/parking_card_sheet.dart | 63 +- .../map/widgets/time_selector_widget.dart | 59 +- .../screens/map/widgets/web_map_types.dart | 41 + .../screens/map/widgets/web_map_view.dart | 2 + .../map/widgets/web_map_view_stub.dart | 45 ++ .../screens/map/widgets/web_map_view_web.dart | 225 ++++++ .../screens/profile/profile_screen.dart | 235 ++++-- .../screens/search/place_search_models.dart | 36 + .../screens/search/place_search_service.dart | 2 + .../search/place_search_service_native.dart | 72 ++ .../search/place_search_service_web.dart | 72 ++ .../screens/search/search_screen.dart | 278 ++++--- pubspec.lock | 34 +- pubspec.yaml | 2 + .../time_selector_widget_test.dart | 28 + web/index.html | 9 + web/manifest.json | 2 +- web/yandex_maps.js | 385 +++++++++ 26 files changed, 2022 insertions(+), 652 deletions(-) create mode 100644 lib/core/services/yandex_web_route.dart create mode 100644 lib/core/services/yandex_web_route_models.dart create mode 100644 lib/core/services/yandex_web_route_stub.dart create mode 100644 lib/core/services/yandex_web_route_web.dart create mode 100644 lib/presentation/screens/map/widgets/web_map_types.dart create mode 100644 lib/presentation/screens/map/widgets/web_map_view.dart create mode 100644 lib/presentation/screens/map/widgets/web_map_view_stub.dart create mode 100644 lib/presentation/screens/map/widgets/web_map_view_web.dart create mode 100644 lib/presentation/screens/search/place_search_models.dart create mode 100644 lib/presentation/screens/search/place_search_service.dart create mode 100644 lib/presentation/screens/search/place_search_service_native.dart create mode 100644 lib/presentation/screens/search/place_search_service_web.dart create mode 100644 test/presentation/time_selector_widget_test.dart create mode 100644 web/yandex_maps.js diff --git a/lib/core/services/yandex_web_route.dart b/lib/core/services/yandex_web_route.dart new file mode 100644 index 0000000..6b12c2f --- /dev/null +++ b/lib/core/services/yandex_web_route.dart @@ -0,0 +1,2 @@ +export 'yandex_web_route_stub.dart' + if (dart.library.js_interop) 'yandex_web_route_web.dart'; diff --git a/lib/core/services/yandex_web_route_models.dart b/lib/core/services/yandex_web_route_models.dart new file mode 100644 index 0000000..f737c33 --- /dev/null +++ b/lib/core/services/yandex_web_route_models.dart @@ -0,0 +1,13 @@ +import 'package:yandex_mapkit/yandex_mapkit.dart'; + +class YandexWebRoute { + const YandexWebRoute({ + required this.points, + required this.durationSeconds, + required this.distanceMeters, + }); + + final List points; + final double durationSeconds; + final double distanceMeters; +} diff --git a/lib/core/services/yandex_web_route_stub.dart b/lib/core/services/yandex_web_route_stub.dart new file mode 100644 index 0000000..f6c9929 --- /dev/null +++ b/lib/core/services/yandex_web_route_stub.dart @@ -0,0 +1,8 @@ +import 'yandex_web_route_models.dart'; + +Future requestYandexWebRoute({ + required double fromLatitude, + required double fromLongitude, + required double toLatitude, + required double toLongitude, +}) async => null; diff --git a/lib/core/services/yandex_web_route_web.dart b/lib/core/services/yandex_web_route_web.dart new file mode 100644 index 0000000..d93840c --- /dev/null +++ b/lib/core/services/yandex_web_route_web.dart @@ -0,0 +1,45 @@ +import 'dart:convert'; +import 'dart:js_interop'; + +import 'package:yandex_mapkit/yandex_mapkit.dart'; + +import 'yandex_web_route_models.dart'; + +@JS('parkTrackYandexMaps.route') +external JSPromise _requestRoute( + JSNumber fromLatitude, + JSNumber fromLongitude, + JSNumber toLatitude, + JSNumber toLongitude, +); + +Future requestYandexWebRoute({ + required double fromLatitude, + required double fromLongitude, + required double toLatitude, + required double toLongitude, +}) async { + final response = await _requestRoute( + fromLatitude.toJS, + fromLongitude.toJS, + toLatitude.toJS, + toLongitude.toJS, + ).toDart.timeout(const Duration(seconds: 10)); + final value = jsonDecode(response.toDart) as Map; + final rawPoints = value['points'] as List? ?? const []; + final points = rawPoints + .map((rawPoint) { + final point = rawPoint as List; + return Point( + latitude: (point[0] as num).toDouble(), + longitude: (point[1] as num).toDouble(), + ); + }) + .toList(growable: false); + if (points.length < 2) return null; + return YandexWebRoute( + points: points, + durationSeconds: (value['duration'] as num?)?.toDouble() ?? 0, + distanceMeters: (value['distance'] as num?)?.toDouble() ?? 0, + ); +} diff --git a/lib/core/utils/error_snackbar.dart b/lib/core/utils/error_snackbar.dart index 8b411e7..2e2a15b 100644 --- a/lib/core/utils/error_snackbar.dart +++ b/lib/core/utils/error_snackbar.dart @@ -1,5 +1,7 @@ +import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:pointer_interceptor/pointer_interceptor.dart'; import '../localization/app_localizations.dart'; void showErrorSnackBar( @@ -10,7 +12,7 @@ void showErrorSnackBar( AppStrings? s, }) { final details = _buildDetails(error, stackTrace); - + final label = s?.moreInfo ?? 'Details'; ScaffoldMessenger.of(context) @@ -27,7 +29,8 @@ void showErrorSnackBar( action: details != null ? SnackBarAction( label: label, - onPressed: () => _showDetailsDialog(context, message, details, s), + onPressed: () => + _showDetailsDialog(context, message, details, s), ) : null, behavior: SnackBarBehavior.floating, @@ -47,56 +50,64 @@ String? _buildDetails(Object? error, StackTrace? stackTrace) { return buffer.toString(); } -void _showDetailsDialog(BuildContext context, String message, String details, AppStrings? s) { +void _showDetailsDialog( + BuildContext context, + String message, + String details, + AppStrings? s, +) { showDialog( context: context, - builder: (ctx) => AlertDialog( - title: Text(s?.errorDetails ?? 'Error Details'), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(message, style: const TextStyle(fontWeight: FontWeight.w600)), - const SizedBox(height: 12), - Container( - constraints: const BoxConstraints(maxHeight: 320), - decoration: BoxDecoration( - color: Theme.of(context).brightness == Brightness.dark - ? Colors.grey.shade800 - : Colors.grey.shade100, - borderRadius: BorderRadius.circular(8), - ), - child: SingleChildScrollView( - padding: const EdgeInsets.all(12), - child: SelectableText( - details, - style: const TextStyle(fontSize: 12, fontFamily: 'monospace'), + builder: (ctx) => PointerInterceptor( + intercepting: kIsWeb, + child: AlertDialog( + title: Text(s?.errorDetails ?? 'Error Details'), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(message, style: const TextStyle(fontWeight: FontWeight.w600)), + const SizedBox(height: 12), + Container( + constraints: const BoxConstraints(maxHeight: 320), + decoration: BoxDecoration( + color: Theme.of(context).brightness == Brightness.dark + ? Colors.grey.shade800 + : Colors.grey.shade100, + borderRadius: BorderRadius.circular(8), + ), + child: SingleChildScrollView( + padding: const EdgeInsets.all(12), + child: SelectableText( + details, + style: const TextStyle(fontSize: 12, fontFamily: 'monospace'), + ), ), ), + ], + ), + actions: [ + TextButton.icon( + icon: const Icon(Icons.copy, size: 16), + label: Text(s?.copy ?? 'Copy'), + onPressed: () { + Clipboard.setData(ClipboardData(text: details)); + Navigator.pop(ctx); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(s?.copiedToClipboard ?? 'Copied to clipboard'), + duration: const Duration(seconds: 2), + behavior: SnackBarBehavior.floating, + ), + ); + }, + ), + FilledButton( + onPressed: () => Navigator.pop(ctx), + child: Text(s?.close ?? 'Close'), ), ], ), - actions: [ - TextButton.icon( - icon: const Icon(Icons.copy, size: 16), - label: Text(s?.copy ?? 'Copy'), - onPressed: () { - Clipboard.setData(ClipboardData(text: details)); - Navigator.pop(ctx); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(s?.copiedToClipboard ?? 'Copied to clipboard'), - duration: const Duration(seconds: 2), - behavior: SnackBarBehavior.floating, - ), - ); - }, - ), - FilledButton( - onPressed: () => Navigator.pop(ctx), - child: Text(s?.close ?? 'Close'), - ), - ], ), ); } diff --git a/lib/presentation/providers/navigation_provider.dart b/lib/presentation/providers/navigation_provider.dart index c44fcab..6d20432 100644 --- a/lib/presentation/providers/navigation_provider.dart +++ b/lib/presentation/providers/navigation_provider.dart @@ -1,11 +1,13 @@ import 'dart:async'; -import 'dart:io'; import 'dart:math' show max; +import 'package:flutter/foundation.dart' + show TargetPlatform, defaultTargetPlatform, kIsWeb; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:geolocator/geolocator.dart'; import 'package:yandex_mapkit/yandex_mapkit.dart'; import '../../core/utils/nav_math.dart'; import '../../core/localization/app_localizations.dart'; +import '../../core/services/yandex_web_route.dart'; class NavigationData { final int zoneId; @@ -94,9 +96,20 @@ class NavigationNotifier extends Notifier { _smoothHeading = null; _offRouteSamples = 0; + final initialPosition = route.first; + state = NavigationData( + zoneId: zoneId, + route: route, + remainingMeters: _totalMeters, + remainingSeconds: _totalSeconds.round(), + speedKmh: 0, + currentPosition: initialPosition, + heading: 0, + ); + _sub = Geolocator.getPositionStream( locationSettings: _buildLocationSettings(s), - ).listen(_onPosition); + ).listen(_onPosition, onError: (_) {}); } void stop() { @@ -115,7 +128,7 @@ class NavigationNotifier extends Notifier { } LocationSettings _buildLocationSettings(AppStrings s) { - if (Platform.isAndroid) { + if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) { return AndroidSettings( accuracy: LocationAccuracy.high, distanceFilter: 5, @@ -235,35 +248,56 @@ class NavigationNotifier extends Notifier { ); try { - await _drivingSession?.close(); - final rws = YandexDriving.requestRoutes( - points: [ - RequestPoint( - point: from, - requestPointType: RequestPointType.wayPoint, - ), - RequestPoint( - point: Point(latitude: _destLat, longitude: _destLon), - requestPointType: RequestPointType.wayPoint, - ), - ], - drivingOptions: const DrivingOptions(routesCount: 1), - ); - _drivingSession = rws.session; - final result = await rws.result; - await rws.session.close(); - _drivingSession = null; - - final dr = result.routes?.firstOrNull; - if (dr == null || state == null || generation != _navigationGeneration) { - return; + if (kIsWeb) { + final webRoute = await requestYandexWebRoute( + fromLatitude: from.latitude, + fromLongitude: from.longitude, + toLatitude: _destLat, + toLongitude: _destLon, + ); + if (webRoute == null) { + throw StateError('Yandex Maps did not return a route'); + } + if (state == null || generation != _navigationGeneration) { + return; + } + _route = webRoute.points; + _totalSeconds = webRoute.durationSeconds; + _totalMeters = webRoute.distanceMeters; + } else { + await _drivingSession?.close(); + final rws = YandexDriving.requestRoutes( + points: [ + RequestPoint( + point: from, + requestPointType: RequestPointType.wayPoint, + ), + RequestPoint( + point: Point(latitude: _destLat, longitude: _destLon), + requestPointType: RequestPointType.wayPoint, + ), + ], + drivingOptions: const DrivingOptions(routesCount: 1), + ); + _drivingSession = rws.session; + final result = await rws.result; + await rws.session.close(); + _drivingSession = null; + + final dr = result.routes?.firstOrNull; + if (dr == null) { + throw StateError('Yandex MapKit did not return a route'); + } + if (state == null || generation != _navigationGeneration) { + return; + } + _route = dr.geometry; + _totalSeconds = + dr.metadata.weight.timeWithTraffic.value ?? _totalSeconds; + _totalMeters = dr.metadata.weight.distance.value ?? _totalMeters; } - - _route = dr.geometry; _preparedRoute = PreparedRoute(_route); _segmentIndex = 0; - _totalSeconds = dr.metadata.weight.timeWithTraffic.value ?? _totalSeconds; - _totalMeters = dr.metadata.weight.distance.value ?? _totalMeters; if (_totalMeters == 0 && _preparedRoute!.isValid) { _totalMeters = _preparedRoute!.totalDistance; diff --git a/lib/presentation/screens/map/map_screen.dart b/lib/presentation/screens/map/map_screen.dart index 6971c3a..dc8b582 100644 --- a/lib/presentation/screens/map/map_screen.dart +++ b/lib/presentation/screens/map/map_screen.dart @@ -2,11 +2,12 @@ import 'dart:async'; import 'dart:math' as math; import 'dart:typed_data'; import 'dart:ui' as ui; -import 'package:flutter/foundation.dart' show setEquals; +import 'package:flutter/foundation.dart' show kIsWeb, setEquals; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:geolocator/geolocator.dart'; import 'package:go_router/go_router.dart'; +import 'package:pointer_interceptor/pointer_interceptor.dart'; import 'package:yandex_mapkit/yandex_mapkit.dart'; import '../../providers/zones_provider.dart'; import '../../../domain/models/zone.dart'; @@ -18,11 +19,14 @@ import '../../../core/theme/app_colors.dart'; import '../../../core/utils/navigation_deeplink.dart'; import '../../../core/utils/error_snackbar.dart'; import '../../../core/localization/app_localizations.dart'; +import '../../../core/services/yandex_web_route.dart'; import 'widgets/candidates_sheet.dart'; import 'widgets/navigation_overlay.dart' show NavigationTurnCard, NavigationBottomBar; import 'widgets/parking_zone_layer.dart'; import 'widgets/time_selector_widget.dart'; +import 'widgets/web_map_view.dart'; +import 'widgets/web_map_types.dart'; import 'widgets/parking_card_sheet.dart'; import 'widgets/route_preview_sheet.dart'; @@ -35,6 +39,8 @@ class MapScreen extends ConsumerStatefulWidget { class _MapScreenState extends ConsumerState { YandexMapController? _mapController; + final WebMapController _webMapController = WebMapController(); + bool _webMapReady = false; Timer? _debounce; Timer? _timeDebounce; Position? _userPosition; @@ -303,11 +309,17 @@ class _MapScreenState extends ConsumerState { } Future _fetchZones({bool clearCache = false}) async { - if (_mapController == null) return; if (clearCache) { _zoneLabelCache.clear(); _zonesById.clear(); } + if (kIsWeb) { + final camera = _webMapController.camera; + if (!_webMapReady || camera == null) return; + await _fetchWebZones(camera); + return; + } + if (_mapController == null) return; try { final visibleRegion = await _mapController!.getVisibleRegion(); final bottomLeft = visibleRegion.bottomLeft; @@ -323,21 +335,54 @@ class _MapScreenState extends ConsumerState { } } - Future _getCurrentPosition() async { - var permission = await Geolocator.checkPermission(); - if (permission == LocationPermission.denied) { - permission = await Geolocator.requestPermission(); + Future _fetchWebZones(WebMapCamera camera) async { + if ((camera.east - camera.west).abs() < 0.000001 || + (camera.north - camera.south).abs() < 0.000001) { + return; } - if (permission == LocationPermission.deniedForever || - permission == LocationPermission.denied) { - _showLocationDeniedDialog(); - return null; + final dLon = (camera.east - camera.west) * 0.5; + final dLat = (camera.north - camera.south) * 0.5; + final bbox = + '${camera.west - dLon},${camera.south - dLat},' + '${camera.east + dLon},${camera.north + dLat}'; + try { + await ref.read(rawZonesProvider.notifier).fetchZones(bbox); + } catch (e, st) { + ref.read(rawZonesProvider.notifier).setErrorState(e, st); } + } + + void _onWebCameraChanged(WebMapCamera camera) { + _lastCameraTarget = Point( + latitude: camera.latitude, + longitude: camera.longitude, + ); + _debounce?.cancel(); + _debounce = Timer( + const Duration(milliseconds: 400), + () => _fetchWebZones(camera), + ); + } + + Future _getCurrentPosition({bool showDeniedDialog = true}) async { try { - final cached = await Geolocator.getLastKnownPosition(); - if (cached != null) { - _userPosition = cached; - return cached; + var permission = await Geolocator.checkPermission(); + if (permission == LocationPermission.denied) { + permission = await Geolocator.requestPermission(); + } + if (permission == LocationPermission.deniedForever || + permission == LocationPermission.denied) { + if (showDeniedDialog) _showLocationDeniedDialog(); + return null; + } + try { + final cached = await Geolocator.getLastKnownPosition(); + if (cached != null) { + _userPosition = cached; + return cached; + } + } catch (_) { + // Browsers may not implement last-known position; request a fresh fix. } final pos = await Geolocator.getCurrentPosition( locationSettings: const LocationSettings( @@ -353,10 +398,24 @@ class _MapScreenState extends ConsumerState { } } + Future _routingOrigin() async { + final position = await _getCurrentPosition(showDeniedDialog: !kIsWeb); + if (position != null) { + return Point(latitude: position.latitude, longitude: position.longitude); + } + if (!kIsWeb) return null; + return _lastCameraTarget ?? + const Point(latitude: 61.789114, longitude: 34.359757); + } + Future _goToMyLocation() async { final pos = await _getCurrentPosition(); if (pos == null) return; if (mounted) setState(() {}); + if (kIsWeb) { + _webMapController.move(pos.latitude, pos.longitude, 16); + return; + } await _mapController?.moveCamera( CameraUpdate.newCameraPosition( CameraPosition( @@ -369,6 +428,7 @@ class _MapScreenState extends ConsumerState { } Future _resetNorth() async { + if (kIsWeb) return; if (_mapController == null) return; final pos = await _mapController!.getCameraPosition(); await _mapController!.moveCamera( @@ -380,6 +440,10 @@ class _MapScreenState extends ConsumerState { } Future _zoomIn() async { + if (kIsWeb) { + _webMapController.zoomBy(1); + return; + } if (_mapController == null) return; final pos = await _mapController!.getCameraPosition(); await _mapController!.moveCamera( @@ -396,6 +460,10 @@ class _MapScreenState extends ConsumerState { } Future _zoomOut() async { + if (kIsWeb) { + _webMapController.zoomBy(-1); + return; + } if (_mapController == null) return; final pos = await _mapController!.getCameraPosition(); await _mapController!.moveCamera( @@ -415,22 +483,26 @@ class _MapScreenState extends ConsumerState { final s = ref.read(l10nProvider); showDialog( context: context, - builder: (_) => AlertDialog( - title: Text(s.locationPermissionDenied), - content: Text(s.locationPermissionReason), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(s.cancel), - ), - FilledButton( - onPressed: () { - Navigator.pop(context); - Geolocator.openAppSettings(); - }, - child: Text(s.settings), - ), - ], + builder: (_) => PointerInterceptor( + intercepting: kIsWeb, + child: AlertDialog( + title: Text(s.locationPermissionDenied), + content: Text(s.locationPermissionReason), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(s.cancel), + ), + if (!kIsWeb) + FilledButton( + onPressed: () { + Navigator.pop(context); + Geolocator.openAppSettings(); + }, + child: Text(s.settings), + ), + ], + ), ), ); } @@ -452,22 +524,22 @@ class _MapScreenState extends ConsumerState { } Future _findParking() async { - final pos = await _getCurrentPosition(); - if (pos == null) return; + final origin = await _routingOrigin(); + if (origin == null) return; await ref .read(routingProvider.notifier) - .searchParking(originLat: pos.latitude, originLon: pos.longitude); + .searchParking(originLat: origin.latitude, originLon: origin.longitude); } Future _buildRouteForZone(int zoneId) async { - final pos = await _getCurrentPosition(); - if (pos == null) return; + final origin = await _routingOrigin(); + if (origin == null) return; setState(() => _isSelectingOnMap = false); await ref .read(routingProvider.notifier) .buildRoute( - originLat: pos.latitude, - originLon: pos.longitude, + originLat: origin.latitude, + originLon: origin.longitude, selectedZoneId: zoneId, ); } @@ -480,19 +552,19 @@ class _MapScreenState extends ConsumerState { if (!mounted) return; setState(() => _navBuilding = true); try { - final pos = await _getCurrentPosition(); - if (pos == null) return; + final origin = await _routingOrigin(); + if (origin == null) return; - await _mapController?.moveCamera( - CameraUpdate.newCameraPosition( - CameraPosition( - target: Point(latitude: pos.latitude, longitude: pos.longitude), - zoom: 17, - tilt: 40, + if (kIsWeb) { + _webMapController.move(origin.latitude, origin.longitude, 17); + } else { + await _mapController?.moveCamera( + CameraUpdate.newCameraPosition( + CameraPosition(target: origin, zoom: 17, tilt: 40), ), - ), - animation: const MapAnimation(duration: 0.8), - ); + animation: const MapAnimation(duration: 0.8), + ); + } List route; double totalSeconds = _routeDurationSeconds; @@ -501,29 +573,48 @@ class _MapScreenState extends ConsumerState { if (_routePolyline != null && _routePolyline!.length >= 2) { route = _routePolyline!; } else { - await _drivingSession?.close(); - final rws = YandexDriving.requestRoutes( - points: [ - RequestPoint( - point: Point(latitude: pos.latitude, longitude: pos.longitude), - requestPointType: RequestPointType.wayPoint, - ), - RequestPoint( - point: Point(latitude: toLat, longitude: toLon), - requestPointType: RequestPointType.wayPoint, - ), - ], - drivingOptions: const DrivingOptions(routesCount: 1), - ); - _drivingSession = rws.session; - final result = await rws.result; - await rws.session.close(); - _drivingSession = null; - final dr = result.routes?.firstOrNull; - if (dr == null || !mounted) return; - route = dr.geometry; - totalSeconds = dr.metadata.weight.timeWithTraffic.value ?? 0; - totalMeters = dr.metadata.weight.distance.value ?? 0; + if (kIsWeb) { + final webRoute = await requestYandexWebRoute( + fromLatitude: origin.latitude, + fromLongitude: origin.longitude, + toLatitude: toLat, + toLongitude: toLon, + ); + if (webRoute == null) { + throw StateError('Yandex Maps did not return a route'); + } + if (!mounted) return; + route = webRoute.points; + totalSeconds = webRoute.durationSeconds; + totalMeters = webRoute.distanceMeters; + } else { + await _drivingSession?.close(); + final rws = YandexDriving.requestRoutes( + points: [ + RequestPoint( + point: origin, + requestPointType: RequestPointType.wayPoint, + ), + RequestPoint( + point: Point(latitude: toLat, longitude: toLon), + requestPointType: RequestPointType.wayPoint, + ), + ], + drivingOptions: const DrivingOptions(routesCount: 1), + ); + _drivingSession = rws.session; + final result = await rws.result; + await rws.session.close(); + _drivingSession = null; + final dr = result.routes?.firstOrNull; + if (dr == null) { + throw StateError('Yandex MapKit did not return a route'); + } + if (!mounted) return; + route = dr.geometry; + totalSeconds = dr.metadata.weight.timeWithTraffic.value ?? 0; + totalMeters = dr.metadata.weight.distance.value ?? 0; + } setState(() => _routePolyline = route); } @@ -560,7 +651,10 @@ class _MapScreenState extends ConsumerState { showModalBottomSheet( context: context, isScrollControlled: true, - builder: (_) => const _FiltersSheet(), + builder: (_) => PointerInterceptor( + intercepting: kIsWeb, + child: const _FiltersSheet(), + ), ); } @@ -596,6 +690,14 @@ class _MapScreenState extends ConsumerState { if (prev?.route != nav.route) { setState(() => _routePolyline = nav.route); } + if (kIsWeb) { + _webMapController.move( + nav.currentPosition.latitude, + nav.currentPosition.longitude, + 17, + ); + return; + } _mapController?.moveCamera( CameraUpdate.newCameraPosition( CameraPosition( @@ -624,6 +726,10 @@ class _MapScreenState extends ConsumerState { ref.listen(destinationProvider, (_, dest) { if (dest == null) return; + if (kIsWeb) { + _webMapController.move(dest.latitude, dest.longitude, 15); + return; + } _mapController?.moveCamera( CameraUpdate.newCameraPosition( CameraPosition( @@ -662,19 +768,24 @@ class _MapScreenState extends ConsumerState { await showModalBottomSheet( context: context, isScrollControlled: true, - builder: (_) => CandidatesSheet( - candidates: candidates, - onSelectByList: (zoneId) { - Navigator.pop(context); - _buildRouteForZone(zoneId); - }, - onSelectOnMap: () { - Navigator.pop(context); - setState(() => _isSelectingOnMap = true); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(ref.read(l10nProvider).pickZoneOnMap)), - ); - }, + builder: (_) => PointerInterceptor( + intercepting: kIsWeb, + child: CandidatesSheet( + candidates: candidates, + onSelectByList: (zoneId) { + Navigator.pop(context); + _buildRouteForZone(zoneId); + }, + onSelectOnMap: () { + Navigator.pop(context); + setState(() => _isSelectingOnMap = true); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(ref.read(l10nProvider).pickZoneOnMap), + ), + ); + }, + ), ), ); _isCandidatesSheetOpen = false; @@ -691,18 +802,26 @@ class _MapScreenState extends ConsumerState { final c = centroid(zone.geometry); zoneLat = c.latitude; zoneLon = c.longitude; - await _mapController?.moveCamera( - CameraUpdate.newCameraPosition( - CameraPosition(target: c, zoom: 17), - ), - animation: const MapAnimation(duration: 0.8), - ); + if (kIsWeb) { + _webMapController.move(c.latitude, c.longitude, 17); + } else { + await _mapController?.moveCamera( + CameraUpdate.newCameraPosition( + CameraPosition(target: c, zoom: 17), + ), + animation: const MapAnimation(duration: 0.8), + ); + } } } setState(() { - final candidate = route.candidates.firstOrNull; - _routePolyline = candidate?.routePolyline; + final selectedCandidates = route.candidates.where( + (candidate) => candidate.zoneId == route.selectedZoneId, + ); + final candidate = + selectedCandidates.firstOrNull ?? route.candidates.firstOrNull; + _routePolyline = route.routePolyline ?? candidate?.routePolyline; _routeDurationSeconds = candidate?.durationFromOriginSeconds?.toDouble() ?? 0; _activeRouteZoneId = route.selectedZoneId; @@ -712,17 +831,20 @@ class _MapScreenState extends ConsumerState { await showModalBottomSheet( context: context, isScrollControlled: true, - builder: (_) => RoutePreviewSheet( - route: route, - zoneLat: zoneLat, - zoneLon: zoneLon, - onNavigateInApp: (zoneLat != null && zoneLon != null) - ? () => _startInAppNavigation( - zoneId: route.selectedZoneId, - toLat: zoneLat!, - toLon: zoneLon!, - ) - : null, + builder: (_) => PointerInterceptor( + intercepting: kIsWeb, + child: RoutePreviewSheet( + route: route, + zoneLat: zoneLat, + zoneLon: zoneLon, + onNavigateInApp: (zoneLat != null && zoneLon != null) + ? () => _startInAppNavigation( + zoneId: route.selectedZoneId, + toLat: zoneLat!, + toLon: zoneLon!, + ) + : null, + ), ), ); _isRouteSheetOpen = false; @@ -799,98 +921,134 @@ class _MapScreenState extends ConsumerState { body: SafeArea( child: Stack( children: [ - YandexMap( - mapObjects: mapObjects, - nightModeEnabled: isDark, - onMapCreated: (controller) async { - _mapController = controller; - const fallback = Point( - latitude: 61.789114, - longitude: 34.359757, - ); - _lastCameraTarget = fallback; - await controller.moveCamera( - CameraUpdate.newCameraPosition( - const CameraPosition(target: fallback, zoom: 14), - ), - ); - _fetchZones(); - final pos = await _getCurrentPosition(); - if (pos != null && mounted) { - final target = Point( + if (kIsWeb) + WebMapView( + controller: _webMapController, + zones: zones, + candidateIds: candidateIds, + onZoneTap: _onZoneTap, + route: _routePolyline, + activeRouteZoneId: _activeRouteZoneId, + userLatitude: isNavigating ? null : _userPosition?.latitude, + userLongitude: isNavigating ? null : _userPosition?.longitude, + navigationLatitude: isNavigating + ? navState.currentPosition.latitude + : null, + navigationLongitude: isNavigating + ? navState.currentPosition.longitude + : null, + navigationHeading: isNavigating ? navState.heading : null, + destinationLatitude: destination?.latitude, + destinationLongitude: destination?.longitude, + onCameraChanged: _onWebCameraChanged, + onMapReady: () { + _webMapReady = true; + final camera = _webMapController.camera; + if (camera != null) { + _lastCameraTarget = Point( + latitude: camera.latitude, + longitude: camera.longitude, + ); + _fetchWebZones(camera); + } + }, + ) + else + YandexMap( + mapObjects: mapObjects, + nightModeEnabled: isDark, + onMapCreated: (controller) async { + _mapController = controller; + const fallback = Point( latitude: 61.789114, longitude: 34.359757, ); - _lastCameraTarget = target; + _lastCameraTarget = fallback; await controller.moveCamera( CameraUpdate.newCameraPosition( - CameraPosition(target: target, zoom: 15), + const CameraPosition(target: fallback, zoom: 14), ), - animation: const MapAnimation(duration: 0.8), ); _fetchZones(); - } - }, - onCameraPositionChanged: _onCameraPositionChanged, - ), + final pos = await _getCurrentPosition(); + if (pos != null && mounted) { + final target = Point( + latitude: 61.789114, + longitude: 34.359757, + ); + _lastCameraTarget = target; + await controller.moveCamera( + CameraUpdate.newCameraPosition( + CameraPosition(target: target, zoom: 15), + ), + animation: const MapAnimation(duration: 0.8), + ); + _fetchZones(); + } + }, + onCameraPositionChanged: _onCameraPositionChanged, + ), // ─── Top bar ─────────────────────────────────────────────── if (!isNavigating) - SafeArea( - child: Padding( - padding: const EdgeInsets.all(12), - child: Row( - children: [ - Expanded( - child: GestureDetector( - onTap: _openSearch, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.1), - blurRadius: 8, - ), - ], - ), - child: Row( - children: [ - Icon( - Icons.search, - color: Theme.of(context).hintColor, - ), - const SizedBox(width: 8), - Expanded( - child: Text( - destination?.name ?? s.searchPlaceholder, - style: TextStyle( - color: Theme.of(context).hintColor, + PointerInterceptor( + intercepting: kIsWeb, + child: SafeArea( + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + Expanded( + child: GestureDetector( + onTap: _openSearch, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.1), + blurRadius: 8, + ), + ], + ), + child: Row( + children: [ + Icon( + Icons.search, + color: Theme.of(context).hintColor, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + destination?.name ?? s.searchPlaceholder, + style: TextStyle( + color: Theme.of(context).hintColor, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), - maxLines: 1, - overflow: TextOverflow.ellipsis, ), - ), - ], + ], + ), ), ), ), - ), - const SizedBox(width: 8), - _MapButton( - icon: Icons.tune, - onTap: () => _showFilters(context), - ), - const SizedBox(width: 4), - _MapButton( - icon: Icons.person_outlined, - onTap: () => context.push('/profile'), - ), - ], + const SizedBox(width: 8), + _MapButton( + icon: Icons.tune, + onTap: () => _showFilters(context), + ), + const SizedBox(width: 4), + _MapButton( + icon: Icons.person_outlined, + onTap: () => context.push('/profile'), + ), + ], + ), ), ), ), @@ -900,27 +1058,33 @@ class _MapScreenState extends ConsumerState { bottom: 0, child: Align( alignment: Alignment.center, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - _MapButton( - onTap: _resetNorth, - child: Transform.rotate( - angle: -_currentAzimuth * math.pi / 180, - child: Icon( - Icons.explore, - size: 22, - color: Theme.of(context).colorScheme.onSurface, + child: PointerInterceptor( + intercepting: kIsWeb, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _MapButton( + onTap: _resetNorth, + child: Transform.rotate( + angle: -_currentAzimuth * math.pi / 180, + child: Icon( + Icons.explore, + size: 22, + color: Theme.of(context).colorScheme.onSurface, + ), ), ), - ), - const SizedBox(height: 4), - _MapButton(icon: Icons.add, onTap: _zoomIn), - const SizedBox(height: 4), - _MapButton(icon: Icons.remove, onTap: _zoomOut), - const SizedBox(height: 4), - _MapButton(icon: Icons.my_location, onTap: _goToMyLocation), - ], + const SizedBox(height: 4), + _MapButton(icon: Icons.add, onTap: _zoomIn), + const SizedBox(height: 4), + _MapButton(icon: Icons.remove, onTap: _zoomOut), + const SizedBox(height: 4), + _MapButton( + icon: Icons.my_location, + onTap: _goToMyLocation, + ), + ], + ), ), ), ), @@ -929,21 +1093,27 @@ class _MapScreenState extends ConsumerState { Positioned( bottom: bottomInset + 20, left: 16, - child: const TimeSelectorWidget(), + child: PointerInterceptor( + intercepting: kIsWeb, + child: const TimeSelectorWidget(), + ), ), // ─── FAB: всегда правый край ─────────────────────────── if (destination == null && !isNavigating) Positioned( bottom: bottomInset + 20, right: 16, - child: FloatingActionButton.extended( - heroTag: 'find_parking', - onPressed: isRoutingLoading ? null : _findParking, - backgroundColor: AppColors.primary, - icon: const Icon(Icons.local_parking, color: Colors.white), - label: Text( - isRoutingLoading ? s.searching : s.findParking, - style: const TextStyle(color: Colors.white), + child: PointerInterceptor( + intercepting: kIsWeb, + child: FloatingActionButton.extended( + heroTag: 'find_parking', + onPressed: isRoutingLoading ? null : _findParking, + backgroundColor: AppColors.primary, + icon: const Icon(Icons.local_parking, color: Colors.white), + label: Text( + isRoutingLoading ? s.searching : s.findParking, + style: const TextStyle(color: Colors.white), + ), ), ), ), @@ -953,22 +1123,25 @@ class _MapScreenState extends ConsumerState { bottom: bottomInset + 76, left: 12, right: 12, - child: _DestinationCard( - destination: destination, - onFindParking: isRoutingLoading ? null : _findParking, - onNavigate: () => openYandexNavigator( - destination.latitude, - destination.longitude, - ), - onNavigateInApp: () => _startInAppNavigation( - zoneId: 0, - toLat: destination.latitude, - toLon: destination.longitude, + child: PointerInterceptor( + intercepting: kIsWeb, + child: _DestinationCard( + destination: destination, + onFindParking: isRoutingLoading ? null : _findParking, + onNavigate: () => openYandexNavigator( + destination.latitude, + destination.longitude, + ), + onNavigateInApp: () => _startInAppNavigation( + zoneId: 0, + toLat: destination.latitude, + toLon: destination.longitude, + ), + onClear: () { + ref.read(destinationProvider.notifier).state = null; + ref.read(routingProvider.notifier).reset(); + }, ), - onClear: () { - ref.read(destinationProvider.notifier).state = null; - ref.read(routingProvider.notifier).reset(); - }, ), ), // ─── Loading indicators ───────────────────────────────────── @@ -986,46 +1159,57 @@ class _MapScreenState extends ConsumerState { ), ), if (isNavigating) - const Positioned( + Positioned( top: 0, left: 0, right: 0, - child: NavigationTurnCard(), + child: PointerInterceptor( + intercepting: kIsWeb, + child: const NavigationTurnCard(), + ), ), if (isNavigating) Positioned( bottom: 0, left: 0, right: 0, - child: NavigationBottomBar( - onFinish: () { - ref.read(navigationProvider.notifier).stop(); - ref.read(routingProvider.notifier).reset(); - }, + child: PointerInterceptor( + intercepting: kIsWeb, + child: NavigationBottomBar( + onFinish: () { + ref.read(navigationProvider.notifier).stop(); + ref.read(routingProvider.notifier).reset(); + }, + ), ), ), if (_navBuilding) Positioned.fill( - child: ColoredBox( - color: Colors.black.withValues(alpha: 0.3), - child: Center( - child: Card( - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 14, - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox( - width: 18, - height: 18, - child: CircularProgressIndicator(strokeWidth: 2), - ), - const SizedBox(width: 12), - Text(s.searching), - ], + child: PointerInterceptor( + intercepting: kIsWeb, + child: ColoredBox( + color: Colors.black.withValues(alpha: 0.3), + child: Center( + child: Card( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 14, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator( + strokeWidth: 2, + ), + ), + const SizedBox(width: 12), + Text(s.searching), + ], + ), ), ), ), @@ -1038,34 +1222,37 @@ class _MapScreenState extends ConsumerState { left: 0, right: 0, child: Center( - child: DecoratedBox( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.circular(20), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.1), - blurRadius: 8, - ), - ], - ), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 14, - vertical: 8, - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator(strokeWidth: 2), + child: PointerInterceptor( + intercepting: kIsWeb, + child: DecoratedBox( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.1), + blurRadius: 8, ), - const SizedBox(width: 8), - Text(s.searching), ], ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 14, + vertical: 8, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 2), + ), + const SizedBox(width: 8), + Text(s.searching), + ], + ), + ), ), ), ), diff --git a/lib/presentation/screens/map/widgets/navigation_overlay.dart b/lib/presentation/screens/map/widgets/navigation_overlay.dart index aa35398..d6ac8f2 100644 --- a/lib/presentation/screens/map/widgets/navigation_overlay.dart +++ b/lib/presentation/screens/map/widgets/navigation_overlay.dart @@ -1,5 +1,7 @@ +import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:pointer_interceptor/pointer_interceptor.dart'; import '../../../../core/theme/app_colors.dart'; import '../../../../core/utils/nav_math.dart'; import '../../../../core/localization/app_localizations.dart'; @@ -36,7 +38,11 @@ class NavigationBottomBar extends ConsumerWidget { // ─── Turn instruction card ───────────────────────────────────────────────── class _TurnCard extends ConsumerWidget { - const _TurnCard({required this.turn, required this.isOffRoute, required this.hasArrived}); + const _TurnCard({ + required this.turn, + required this.isOffRoute, + required this.hasArrived, + }); final NavTurn? turn; final bool isOffRoute; @@ -128,24 +134,24 @@ class _TurnCard extends ConsumerWidget { } IconData _turnIcon(TurnDirection d) => switch (d) { - TurnDirection.left => Icons.turn_left_rounded, - TurnDirection.slightLeft => Icons.turn_slight_left_rounded, - TurnDirection.right => Icons.turn_right_rounded, - TurnDirection.slightRight => Icons.turn_slight_right_rounded, - TurnDirection.uTurn => Icons.u_turn_left_rounded, - TurnDirection.arrive => Icons.local_parking, - TurnDirection.straight => Icons.arrow_upward_rounded, - }; + TurnDirection.left => Icons.turn_left_rounded, + TurnDirection.slightLeft => Icons.turn_slight_left_rounded, + TurnDirection.right => Icons.turn_right_rounded, + TurnDirection.slightRight => Icons.turn_slight_right_rounded, + TurnDirection.uTurn => Icons.u_turn_left_rounded, + TurnDirection.arrive => Icons.local_parking, + TurnDirection.straight => Icons.arrow_upward_rounded, + }; String _turnLabel(TurnDirection d, AppStrings s) => switch (d) { - TurnDirection.left => s.turnLeft, - TurnDirection.slightLeft => s.keepLeft, - TurnDirection.right => s.turnRight, - TurnDirection.slightRight => s.keepRight, - TurnDirection.uTurn => s.uTurn, - TurnDirection.arrive => s.arrival, - TurnDirection.straight => s.straightAhead, - }; + TurnDirection.left => s.turnLeft, + TurnDirection.slightLeft => s.keepLeft, + TurnDirection.right => s.turnRight, + TurnDirection.slightRight => s.keepRight, + TurnDirection.uTurn => s.uTurn, + TurnDirection.arrive => s.arrival, + TurnDirection.straight => s.straightAhead, + }; } // ─── Bottom stats bar ────────────────────────────────────────────────────── @@ -159,22 +165,25 @@ class _NavBottomBar extends ConsumerWidget { void _confirmFinish(BuildContext context, AppStrings s) { showDialog( context: context, - builder: (_) => AlertDialog( - title: Text(s.finishConfirmTitle), - content: Text(s.finishConfirmContent), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(s.cancel), - ), - FilledButton( - onPressed: () { - Navigator.pop(context); - onFinish(); - }, - child: Text(s.finish), - ), - ], + builder: (_) => PointerInterceptor( + intercepting: kIsWeb, + child: AlertDialog( + title: Text(s.finishConfirmTitle), + content: Text(s.finishConfirmContent), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(s.cancel), + ), + FilledButton( + onPressed: () { + Navigator.pop(context); + onFinish(); + }, + child: Text(s.finish), + ), + ], + ), ), ); } @@ -182,11 +191,16 @@ class _NavBottomBar extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final s = ref.watch(l10nProvider); - final speedColor = nav.speedKmh > 90 ? Colors.red.shade600 : Theme.of(context).colorScheme.onSurface; + final speedColor = nav.speedKmh > 90 + ? Colors.red.shade600 + : Theme.of(context).colorScheme.onSurface; return Container( color: Theme.of(context).colorScheme.surface, padding: EdgeInsets.fromLTRB( - 12, 10, 4, MediaQuery.of(context).padding.bottom + 10, + 12, + 10, + 4, + MediaQuery.of(context).padding.bottom + 10, ), child: Row( children: [ diff --git a/lib/presentation/screens/map/widgets/parking_card_sheet.dart b/lib/presentation/screens/map/widgets/parking_card_sheet.dart index d3f3189..905e93f 100644 --- a/lib/presentation/screens/map/widgets/parking_card_sheet.dart +++ b/lib/presentation/screens/map/widgets/parking_card_sheet.dart @@ -1,5 +1,7 @@ +import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:pointer_interceptor/pointer_interceptor.dart'; import '../../../../domain/models/zone.dart'; import '../../../../core/theme/app_colors.dart'; import '../../../../presentation/providers/time_selector_provider.dart'; @@ -14,7 +16,10 @@ Future showParkingCard( return showModalBottomSheet( context: context, isScrollControlled: true, - builder: (_) => _ParkingCardSheet(zone: zone, onBuildRoute: onBuildRoute), + builder: (_) => PointerInterceptor( + intercepting: kIsWeb, + child: _ParkingCardSheet(zone: zone, onBuildRoute: onBuildRoute), + ), ); } @@ -28,21 +33,29 @@ class _ParkingCardSheet extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final s = ref.watch(l10nProvider); final timeMode = ref.watch(timeSelectorProvider); - final isFuture = timeMode.maybeWhen(future: (_) => true, orElse: () => false); + final isFuture = timeMode.maybeWhen( + future: (_) => true, + orElse: () => false, + ); final isPast = timeMode.maybeWhen(past: (_) => true, orElse: () => false); - final userSelectedAt = timeMode.maybeWhen(future: (at) => at, orElse: () => null); + final userSelectedAt = timeMode.maybeWhen( + future: (at) => at, + orElse: () => null, + ); final zonesAsync = ref.watch(rawZonesProvider); final isLoading = zonesAsync is AsyncLoading; - final currentZone = zonesAsync.valueOrNull + final currentZone = + zonesAsync.valueOrNull ?.where((z) => z.zoneId == zone.zoneId) .firstOrNull ?? zone; return Padding( padding: EdgeInsets.only( - bottom: MediaQuery.of(context).viewInsets.bottom + - MediaQuery.of(context).padding.bottom, + bottom: + MediaQuery.of(context).viewInsets.bottom + + MediaQuery.of(context).padding.bottom, ), child: SingleChildScrollView( child: Container( @@ -100,8 +113,9 @@ class _ParkingCardSheet extends ConsumerWidget { value: currentZone.hasForecast ? '${currentZone.freeCount} / ${currentZone.capacity} ${s.free.toLowerCase()}' : s.noForecast, - valueColor: - currentZone.hasForecast ? null : AppColors.textSecondary, + valueColor: currentZone.hasForecast + ? null + : AppColors.textSecondary, ), if (isFuture && currentZone.hasForecast && @@ -125,8 +139,7 @@ class _ParkingCardSheet extends ConsumerWidget { value: currentZone.pay == 0 ? s.free : '${currentZone.pay} ₽/${s.hourSign}', - valueColor: - currentZone.pay == 0 ? AppColors.primary : null, + valueColor: currentZone.pay == 0 ? AppColors.primary : null, ), if (!isPast) _InfoRow( @@ -190,12 +203,12 @@ class _ParkingCardSheet extends ConsumerWidget { String _confidenceLabel(Zone z) => '${(z.confidence * 100).round()}%'; String _locationTypeLabel(LocationType type, AppStrings s) => switch (type) { - LocationType.street => s.street, - LocationType.yard => s.yard, - LocationType.openLot => s.openLot, - LocationType.underground => s.underground, - LocationType.multilevel => s.multilevel, - }; + LocationType.street => s.street, + LocationType.yard => s.yard, + LocationType.openLot => s.openLot, + LocationType.underground => s.underground, + LocationType.multilevel => s.multilevel, + }; static String _formatTime(DateTime dt) { final l = dt.toLocal(); @@ -285,9 +298,10 @@ class _InfoRow extends StatelessWidget { children: [ Icon(icon, size: 20, color: Theme.of(context).hintColor), const SizedBox(width: 12), - Text(label, - style: - TextStyle(color: Theme.of(context).hintColor, fontSize: 14)), + Text( + label, + style: TextStyle(color: Theme.of(context).hintColor, fontSize: 14), + ), const Spacer(), Text( value, @@ -319,9 +333,14 @@ class _Badge extends StatelessWidget { borderRadius: BorderRadius.circular(8), border: Border.all(color: color.withValues(alpha: 0.5)), ), - child: Text(text, - style: TextStyle( - color: color, fontSize: 12, fontWeight: FontWeight.w600)), + child: Text( + text, + style: TextStyle( + color: color, + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), ); } } diff --git a/lib/presentation/screens/map/widgets/time_selector_widget.dart b/lib/presentation/screens/map/widgets/time_selector_widget.dart index 5812ef2..0af9ae5 100644 --- a/lib/presentation/screens/map/widgets/time_selector_widget.dart +++ b/lib/presentation/screens/map/widgets/time_selector_widget.dart @@ -1,6 +1,10 @@ +import 'dart:ui' show PointerDeviceKind; + +import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:pointer_interceptor/pointer_interceptor.dart'; import '../../../../presentation/providers/time_selector_provider.dart'; import '../../../../core/theme/app_colors.dart'; import '../../../../core/localization/app_localizations.dart'; @@ -24,9 +28,12 @@ class TimeSelectorWidget extends ConsumerWidget { context: context, isScrollControlled: true, backgroundColor: Colors.transparent, - builder: (_) => _TimePickerSheet( - initial: selectedDt, - onApply: (dt) => _apply(ref, dt), + builder: (_) => PointerInterceptor( + intercepting: kIsWeb, + child: _TimePickerSheet( + initial: selectedDt, + onApply: (dt) => _apply(ref, dt), + ), ), ); } @@ -213,27 +220,35 @@ class _TimePickerSheetState extends ConsumerState<_TimePickerSheet> { // Time picker SizedBox( height: 150, - child: CupertinoTheme( - data: CupertinoThemeData( - brightness: Theme.of(context).brightness, - ), - child: CupertinoDatePicker( - mode: CupertinoDatePickerMode.time, - use24hFormat: true, - minuteInterval: _step, - initialDateTime: _time, - onDateTimeChanged: (dt) { - setState( - () => _time = DateTime( - _time.year, - _time.month, - _time.day, - dt.hour, - dt.minute, - ), - ); + child: ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith( + dragDevices: { + ...ScrollConfiguration.of(context).dragDevices, + PointerDeviceKind.mouse, }, ), + child: CupertinoTheme( + data: CupertinoThemeData( + brightness: Theme.of(context).brightness, + ), + child: CupertinoDatePicker( + mode: CupertinoDatePickerMode.time, + use24hFormat: true, + minuteInterval: _step, + initialDateTime: _time, + onDateTimeChanged: (dt) { + setState( + () => _time = DateTime( + _time.year, + _time.month, + _time.day, + dt.hour, + dt.minute, + ), + ); + }, + ), + ), ), ), const SizedBox(height: 12), diff --git a/lib/presentation/screens/map/widgets/web_map_types.dart b/lib/presentation/screens/map/widgets/web_map_types.dart new file mode 100644 index 0000000..0b8cd5c --- /dev/null +++ b/lib/presentation/screens/map/widgets/web_map_types.dart @@ -0,0 +1,41 @@ +class WebMapCamera { + const WebMapCamera({ + required this.latitude, + required this.longitude, + required this.zoom, + required this.west, + required this.south, + required this.east, + required this.north, + }); + + final double latitude; + final double longitude; + final double zoom; + final double west; + final double south; + final double east; + final double north; +} + +class WebMapController { + WebMapCamera? camera; + void Function(double latitude, double longitude, double zoom)? moveHandler; + void Function(double zoom)? zoomHandler; + + bool get isReady => camera != null; + + void move(double latitude, double longitude, double zoom) => + moveHandler?.call(latitude, longitude, zoom); + + void zoomBy(double delta) { + final current = camera; + if (current != null) zoomHandler?.call(current.zoom + delta); + } + + void clear() { + camera = null; + moveHandler = null; + zoomHandler = null; + } +} diff --git a/lib/presentation/screens/map/widgets/web_map_view.dart b/lib/presentation/screens/map/widgets/web_map_view.dart new file mode 100644 index 0000000..fd25518 --- /dev/null +++ b/lib/presentation/screens/map/widgets/web_map_view.dart @@ -0,0 +1,2 @@ +export 'web_map_view_stub.dart' + if (dart.library.js_interop) 'web_map_view_web.dart'; diff --git a/lib/presentation/screens/map/widgets/web_map_view_stub.dart b/lib/presentation/screens/map/widgets/web_map_view_stub.dart new file mode 100644 index 0000000..2c24a78 --- /dev/null +++ b/lib/presentation/screens/map/widgets/web_map_view_stub.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:yandex_mapkit/yandex_mapkit.dart'; + +import '../../../../domain/models/zone.dart'; +import 'web_map_types.dart'; + +class WebMapView extends StatelessWidget { + const WebMapView({ + super.key, + required this.controller, + required this.zones, + required this.candidateIds, + required this.onZoneTap, + required this.onCameraChanged, + required this.onMapReady, + this.route, + this.activeRouteZoneId, + this.userLatitude, + this.userLongitude, + this.navigationLatitude, + this.navigationLongitude, + this.navigationHeading, + this.destinationLatitude, + this.destinationLongitude, + }); + + final WebMapController controller; + final List zones; + final Set candidateIds; + final void Function(Zone zone) onZoneTap; + final void Function(WebMapCamera camera) onCameraChanged; + final VoidCallback onMapReady; + final List? route; + final int? activeRouteZoneId; + final double? userLatitude; + final double? userLongitude; + final double? navigationLatitude; + final double? navigationLongitude; + final double? navigationHeading; + final double? destinationLatitude; + final double? destinationLongitude; + + @override + Widget build(BuildContext context) => const SizedBox.expand(); +} diff --git a/lib/presentation/screens/map/widgets/web_map_view_web.dart b/lib/presentation/screens/map/widgets/web_map_view_web.dart new file mode 100644 index 0000000..a30707f --- /dev/null +++ b/lib/presentation/screens/map/widgets/web_map_view_web.dart @@ -0,0 +1,225 @@ +import 'dart:convert'; +import 'dart:js_interop'; +import 'dart:ui_web' as ui_web; + +import 'package:flutter/material.dart'; +import 'package:web/web.dart' as web; +import 'package:yandex_mapkit/yandex_mapkit.dart'; + +import '../../../../core/theme/app_colors.dart'; +import '../../../../domain/models/zone.dart'; +import 'parking_zone_layer.dart'; +import 'web_map_types.dart'; + +@JS('parkTrackYandexMaps.create') +external void _createYandexMap( + JSString elementId, + JSFunction cameraCallback, + JSFunction zoneTapCallback, +); + +@JS('parkTrackYandexMaps.update') +external void _updateYandexMap(JSString elementId, JSString stateJson); + +@JS('parkTrackYandexMaps.move') +external void _moveYandexMap( + JSString elementId, + JSNumber latitude, + JSNumber longitude, + JSNumber zoom, +); + +@JS('parkTrackYandexMaps.setZoom') +external void _setYandexMapZoom(JSString elementId, JSNumber zoom); + +@JS('parkTrackYandexMaps.destroy') +external void _destroyYandexMap(JSString elementId); + +class WebMapView extends StatefulWidget { + const WebMapView({ + super.key, + required this.controller, + required this.zones, + required this.candidateIds, + required this.onZoneTap, + required this.onCameraChanged, + required this.onMapReady, + this.route, + this.activeRouteZoneId, + this.userLatitude, + this.userLongitude, + this.navigationLatitude, + this.navigationLongitude, + this.navigationHeading, + this.destinationLatitude, + this.destinationLongitude, + }); + + final WebMapController controller; + final List zones; + final Set candidateIds; + final void Function(Zone zone) onZoneTap; + final void Function(WebMapCamera camera) onCameraChanged; + final VoidCallback onMapReady; + final List? route; + final int? activeRouteZoneId; + final double? userLatitude; + final double? userLongitude; + final double? navigationLatitude; + final double? navigationLongitude; + final double? navigationHeading; + final double? destinationLatitude; + final double? destinationLongitude; + + @override + State createState() => _WebMapViewState(); +} + +class _WebMapViewState extends State { + late final String _elementId = + 'parktrack-yandex-map-${identityHashCode(this)}'; + late final String _viewType = 'parktrack-yandex-map-view-$_elementId'; + web.ResizeObserver? _attachmentObserver; + bool _created = false; + + @override + void initState() { + super.initState(); + ui_web.platformViewRegistry.registerViewFactory(_viewType, (_) { + final element = web.HTMLDivElement()..id = _elementId; + element.style + ..width = '100%' + ..height = '100%' + ..minWidth = '1px' + ..minHeight = '1px'; + + final observer = web.ResizeObserver( + (( + JSArray entries, + web.ResizeObserver observer, + ) { + if (!element.isConnected || !mounted) return; + final bounds = element.getBoundingClientRect(); + if (bounds.width <= 0 || bounds.height <= 0) return; + observer.disconnect(); + _attachmentObserver = null; + _initializeMap(); + }) + .toJS, + ); + _attachmentObserver = observer; + observer.observe(element); + return element; + }); + } + + @override + void didUpdateWidget(covariant WebMapView oldWidget) { + super.didUpdateWidget(oldWidget); + if (_created) _updateMap(); + } + + void _initializeMap() { + if (_created) return; + _created = true; + widget.controller.moveHandler = (latitude, longitude, zoom) { + _moveYandexMap(_elementId.toJS, latitude.toJS, longitude.toJS, zoom.toJS); + }; + widget.controller.zoomHandler = (zoom) { + _setYandexMapZoom(_elementId.toJS, zoom.toJS); + }; + + _createYandexMap( + _elementId.toJS, + ((JSString value) { + final data = jsonDecode(value.toDart) as Map; + final camera = WebMapCamera( + latitude: (data['latitude'] as num).toDouble(), + longitude: (data['longitude'] as num).toDouble(), + zoom: (data['zoom'] as num).toDouble(), + west: (data['west'] as num).toDouble(), + south: (data['south'] as num).toDouble(), + east: (data['east'] as num).toDouble(), + north: (data['north'] as num).toDouble(), + ); + final firstCamera = !widget.controller.isReady; + widget.controller.camera = camera; + widget.onCameraChanged(camera); + if (firstCamera) widget.onMapReady(); + }).toJS, + ((JSNumber zoneId) { + final id = zoneId.toDartInt; + for (final zone in widget.zones) { + if (zone.zoneId == id) { + widget.onZoneTap(zone); + break; + } + } + }).toJS, + ); + _updateMap(); + } + + void _updateMap() { + final state = { + 'zones': widget.zones + .map((zone) { + final color = zoneColor(zone); + return { + 'id': zone.zoneId, + 'type': zone.zoneType == ZoneType.parallel ? 'line' : 'polygon', + 'points': zone.geometry + .map((point) => [point.latitude, point.longitude]) + .toList(growable: false), + 'color': + '#${color.toARGB32().toRadixString(16).padLeft(8, '0').substring(2)}', + 'freeCount': zone.freeCount, + 'label': color == AppColors.parkingUnknown + ? null + : zone.freeCount, + 'hasForecast': zone.hasForecast, + 'candidate': widget.candidateIds.contains(zone.zoneId), + 'active': widget.activeRouteZoneId == zone.zoneId, + 'center': () { + final point = centroid(zone.geometry); + return [point.latitude, point.longitude]; + }(), + }; + }) + .toList(growable: false), + 'route': widget.route + ?.map((point) => [point.latitude, point.longitude]) + .toList(growable: false), + 'user': widget.userLatitude != null && widget.userLongitude != null + ? [widget.userLatitude, widget.userLongitude] + : null, + 'navigation': + widget.navigationLatitude != null && + widget.navigationLongitude != null + ? [ + widget.navigationLatitude, + widget.navigationLongitude, + widget.navigationHeading ?? 0, + ] + : null, + 'destination': + widget.destinationLatitude != null && + widget.destinationLongitude != null + ? [widget.destinationLatitude, widget.destinationLongitude] + : null, + }; + _updateYandexMap(_elementId.toJS, jsonEncode(state).toJS); + } + + @override + void dispose() { + _attachmentObserver?.disconnect(); + _attachmentObserver = null; + if (_created) _destroyYandexMap(_elementId.toJS); + widget.controller.clear(); + super.dispose(); + } + + @override + Widget build(BuildContext context) => HtmlElementView(viewType: _viewType); +} diff --git a/lib/presentation/screens/profile/profile_screen.dart b/lib/presentation/screens/profile/profile_screen.dart index 05a68e3..9ce621b 100644 --- a/lib/presentation/screens/profile/profile_screen.dart +++ b/lib/presentation/screens/profile/profile_screen.dart @@ -1,6 +1,8 @@ +import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; +import 'package:pointer_interceptor/pointer_interceptor.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../providers/auth_provider.dart'; import '../../providers/settings_provider.dart'; @@ -27,85 +29,91 @@ class ProfileScreen extends ConsumerWidget { orElse: () => null, ); - return Scaffold( - appBar: AppBar(title: Text(s.settings)), - body: ListView( - padding: const EdgeInsets.symmetric(vertical: 16), - children: [ - // --- Account Section --- - _buildSectionHeader(s.account), - if (user != null) - ListTile( - leading: const CircleAvatar(child: Icon(Icons.person)), - title: Text(user.fullName ?? user.email), - subtitle: Text(user.email), - trailing: const Icon(Icons.chevron_right), - onTap: () => GoRouter.of(context).push('/profile/edit'), - ) - else - ListTile( - leading: const CircleAvatar(child: Icon(Icons.person_outline)), - title: Text(s.notAuthenticated), - ), + return PointerInterceptor( + intercepting: kIsWeb, + child: Scaffold( + appBar: AppBar(title: Text(s.settings)), + body: ListView( + padding: const EdgeInsets.symmetric(vertical: 16), + children: [ + // --- Account Section --- + _buildSectionHeader(s.account), + if (user != null) + ListTile( + leading: const CircleAvatar(child: Icon(Icons.person)), + title: Text(user.fullName ?? user.email), + subtitle: Text(user.email), + trailing: const Icon(Icons.chevron_right), + onTap: () => GoRouter.of(context).push('/profile/edit'), + ) + else + ListTile( + leading: const CircleAvatar(child: Icon(Icons.person_outline)), + title: Text(s.notAuthenticated), + ), - const Divider(), + const Divider(), - // --- Appearance Section --- - _buildSectionHeader(s.appearance), - ListTile( - leading: const Icon(Icons.palette_outlined), - title: Text(s.theme), - subtitle: Text(_themeModeName(settings.themeMode, s)), - onTap: () => _showThemePicker(context, ref, s), - ), + // --- Appearance Section --- + _buildSectionHeader(s.appearance), + ListTile( + leading: const Icon(Icons.palette_outlined), + title: Text(s.theme), + subtitle: Text(_themeModeName(settings.themeMode, s)), + onTap: () => _showThemePicker(context, ref, s), + ), - const Divider(), + const Divider(), - // --- Language Section --- - _buildSectionHeader(s.language), - ListTile( - leading: const Icon(Icons.language_outlined), - title: Text(s.language), - subtitle: Text(_languageName(settings.locale?.languageCode, s)), - onTap: () => _showLanguagePicker(context, ref, s), - ), + // --- Language Section --- + _buildSectionHeader(s.language), + ListTile( + leading: const Icon(Icons.language_outlined), + title: Text(s.language), + subtitle: Text(_languageName(settings.locale?.languageCode, s)), + onTap: () => _showLanguagePicker(context, ref, s), + ), - const Divider(), + const Divider(), - // --- Information Section --- - _buildSectionHeader(s.info), - ListTile( - leading: const Icon(Icons.privacy_tip_outlined), - title: Text(s.privacyPolicy), - onTap: () => _launchUrl('https://parktrack.live/privacy'), - ), - ListTile( - leading: const Icon(Icons.delete_outline), - title: Text(s.dataErasure), - onTap: () => _launchUrl('https://parktrack.live/data-erasure'), - ), - ListTile( - leading: const Icon(Icons.code_outlined), - title: Text(s.github), - onTap: () => _launchUrl('https://github.com/ParkTrack-Project'), - ), + // --- Information Section --- + _buildSectionHeader(s.info), + ListTile( + leading: const Icon(Icons.privacy_tip_outlined), + title: Text(s.privacyPolicy), + onTap: () => _launchUrl('https://parktrack.live/privacy'), + ), + ListTile( + leading: const Icon(Icons.delete_outline), + title: Text(s.dataErasure), + onTap: () => _launchUrl('https://parktrack.live/data-erasure'), + ), + ListTile( + leading: const Icon(Icons.code_outlined), + title: Text(s.github), + onTap: () => _launchUrl('https://github.com/ParkTrack-Project'), + ), - const SizedBox(height: 32), + const SizedBox(height: 32), - if (user != null) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: OutlinedButton.icon( - onPressed: () => _showLogoutDialog(context, ref, s), - icon: const Icon(Icons.logout, color: Colors.red), - label: Text(s.logout, style: const TextStyle(color: Colors.red)), - style: OutlinedButton.styleFrom( - side: const BorderSide(color: Colors.red), - padding: const EdgeInsets.symmetric(vertical: 12), + if (user != null) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: OutlinedButton.icon( + onPressed: () => _showLogoutDialog(context, ref, s), + icon: const Icon(Icons.logout, color: Colors.red), + label: Text( + s.logout, + style: const TextStyle(color: Colors.red), + ), + style: OutlinedButton.styleFrom( + side: const BorderSide(color: Colors.red), + padding: const EdgeInsets.symmetric(vertical: 12), + ), ), ), - ), - ], + ], + ), ), ); } @@ -153,11 +161,35 @@ class ProfileScreen extends ConsumerWidget { children: [ Padding( padding: const EdgeInsets.all(16.0), - child: Text(s.selectTheme, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + child: Text( + s.selectTheme, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ), + _buildPickerItem( + context, + ref, + s.themeSystem, + ThemeMode.system, + currentMode == ThemeMode.system, + ), + _buildPickerItem( + context, + ref, + s.themeLight, + ThemeMode.light, + currentMode == ThemeMode.light, + ), + _buildPickerItem( + context, + ref, + s.themeDark, + ThemeMode.dark, + currentMode == ThemeMode.dark, ), - _buildPickerItem(context, ref, s.themeSystem, ThemeMode.system, currentMode == ThemeMode.system), - _buildPickerItem(context, ref, s.themeLight, ThemeMode.light, currentMode == ThemeMode.light), - _buildPickerItem(context, ref, s.themeDark, ThemeMode.dark, currentMode == ThemeMode.dark), const SizedBox(height: 8), ], ), @@ -165,7 +197,13 @@ class ProfileScreen extends ConsumerWidget { ); } - Widget _buildPickerItem(BuildContext context, WidgetRef ref, String title, ThemeMode mode, bool selected) { + Widget _buildPickerItem( + BuildContext context, + WidgetRef ref, + String title, + ThemeMode mode, + bool selected, + ) { return ListTile( title: Text(title), trailing: selected ? const Icon(Icons.check, color: Colors.green) : null, @@ -186,11 +224,38 @@ class ProfileScreen extends ConsumerWidget { children: [ Padding( padding: const EdgeInsets.all(16.0), - child: Text(s.selectLanguage, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + child: Text( + s.selectLanguage, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ), + _buildLangItem( + context, + ref, + s.langSystem, + null, + currentLang == null, + s, + ), + _buildLangItem( + context, + ref, + s.langRussian, + 'ru', + currentLang == 'ru', + s, + ), + _buildLangItem( + context, + ref, + s.langEnglish, + 'en', + currentLang == 'en', + s, ), - _buildLangItem(context, ref, s.langSystem, null, currentLang == null, s), - _buildLangItem(context, ref, s.langRussian, 'ru', currentLang == 'ru', s), - _buildLangItem(context, ref, s.langEnglish, 'en', currentLang == 'en', s), const SizedBox(height: 8), ], ), @@ -198,7 +263,14 @@ class ProfileScreen extends ConsumerWidget { ); } - Widget _buildLangItem(BuildContext context, WidgetRef ref, String title, String? code, bool selected, AppStrings s) { + Widget _buildLangItem( + BuildContext context, + WidgetRef ref, + String title, + String? code, + bool selected, + AppStrings s, + ) { return ListTile( title: Text(title), trailing: selected ? const Icon(Icons.check, color: Colors.green) : null, @@ -222,7 +294,10 @@ class ProfileScreen extends ConsumerWidget { title: Text(s.logout), content: Text(s.logoutConfirm), actions: [ - TextButton(onPressed: () => Navigator.pop(context), child: Text(s.cancel)), + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(s.cancel), + ), TextButton( onPressed: () { Navigator.pop(context); diff --git a/lib/presentation/screens/search/place_search_models.dart b/lib/presentation/screens/search/place_search_models.dart new file mode 100644 index 0000000..33cba0b --- /dev/null +++ b/lib/presentation/screens/search/place_search_models.dart @@ -0,0 +1,36 @@ +class PlaceSearchBounds { + const PlaceSearchBounds({ + required this.south, + required this.west, + required this.north, + required this.east, + }); + + final double south; + final double west; + final double north; + final double east; +} + +class PlaceSuggestion { + const PlaceSuggestion({ + required this.title, + required this.searchText, + this.subtitle, + this.latitude, + this.longitude, + }); + + final String title; + final String searchText; + final String? subtitle; + final double? latitude; + final double? longitude; +} + +class PlaceSearchPoint { + const PlaceSearchPoint({required this.latitude, required this.longitude}); + + final double latitude; + final double longitude; +} diff --git a/lib/presentation/screens/search/place_search_service.dart b/lib/presentation/screens/search/place_search_service.dart new file mode 100644 index 0000000..b4f3a78 --- /dev/null +++ b/lib/presentation/screens/search/place_search_service.dart @@ -0,0 +1,2 @@ +export 'place_search_service_native.dart' + if (dart.library.js_interop) 'place_search_service_web.dart'; diff --git a/lib/presentation/screens/search/place_search_service_native.dart b/lib/presentation/screens/search/place_search_service_native.dart new file mode 100644 index 0000000..e031a11 --- /dev/null +++ b/lib/presentation/screens/search/place_search_service_native.dart @@ -0,0 +1,72 @@ +import 'package:yandex_mapkit/yandex_mapkit.dart'; + +import 'place_search_models.dart'; + +class PlaceSearchService { + SuggestSession? _suggestSession; + SearchSession? _searchSession; + + Future> suggestions( + String text, + PlaceSearchBounds bounds, + ) async { + final resultWithSession = YandexSuggest.getSuggestions( + text: text, + boundingBox: _boundingBox(bounds), + suggestOptions: const SuggestOptions( + suggestType: SuggestType.unspecified, + ), + ); + _suggestSession?.close(); + _suggestSession = resultWithSession.session; + final result = await resultWithSession.result.timeout( + const Duration(seconds: 5), + ); + return (result.items ?? const []) + .map( + (item) => PlaceSuggestion( + title: item.title, + subtitle: item.subtitle, + searchText: item.searchText, + latitude: item.center?.latitude, + longitude: item.center?.longitude, + ), + ) + .toList(growable: false); + } + + Future pointByText( + String text, + PlaceSearchBounds bounds, + ) async { + final resultWithSession = YandexSearch.searchByText( + searchText: text, + geometry: Geometry.fromBoundingBox(_boundingBox(bounds)), + searchOptions: const SearchOptions(searchType: SearchType.geo), + ); + _searchSession?.close(); + _searchSession = resultWithSession.session; + final result = await resultWithSession.result.timeout( + const Duration(seconds: 5), + ); + final point = result.items?.firstOrNull?.geometry.firstOrNull?.point; + return point == null + ? null + : PlaceSearchPoint( + latitude: point.latitude, + longitude: point.longitude, + ); + } + + BoundingBox _boundingBox(PlaceSearchBounds bounds) => BoundingBox( + southWest: Point(latitude: bounds.south, longitude: bounds.west), + northEast: Point(latitude: bounds.north, longitude: bounds.east), + ); + + void dispose() { + _suggestSession?.close(); + _suggestSession = null; + _searchSession?.close(); + _searchSession = null; + } +} diff --git a/lib/presentation/screens/search/place_search_service_web.dart b/lib/presentation/screens/search/place_search_service_web.dart new file mode 100644 index 0000000..a06c744 --- /dev/null +++ b/lib/presentation/screens/search/place_search_service_web.dart @@ -0,0 +1,72 @@ +import 'dart:convert'; +import 'dart:js_interop'; + +import 'place_search_models.dart'; + +@JS('parkTrackYandexMaps.suggest') +external JSPromise _suggestPlaces( + JSString text, + JSNumber south, + JSNumber west, + JSNumber north, + JSNumber east, +); + +@JS('parkTrackYandexMaps.geocode') +external JSPromise _geocodePlace( + JSString text, + JSNumber south, + JSNumber west, + JSNumber north, + JSNumber east, +); + +class PlaceSearchService { + Future> suggestions( + String text, + PlaceSearchBounds bounds, + ) async { + final response = await _suggestPlaces( + text.toJS, + bounds.south.toJS, + bounds.west.toJS, + bounds.north.toJS, + bounds.east.toJS, + ).toDart; + final items = jsonDecode(response.toDart) as List; + return items + .map((value) { + final item = value as Map; + return PlaceSuggestion( + title: item['title'] as String, + subtitle: item['subtitle'] as String?, + searchText: item['searchText'] as String, + latitude: (item['latitude'] as num?)?.toDouble(), + longitude: (item['longitude'] as num?)?.toDouble(), + ); + }) + .toList(growable: false); + } + + Future pointByText( + String text, + PlaceSearchBounds bounds, + ) async { + final response = await _geocodePlace( + text.toJS, + bounds.south.toJS, + bounds.west.toJS, + bounds.north.toJS, + bounds.east.toJS, + ).toDart; + final value = jsonDecode(response.toDart); + if (value == null) return null; + final point = value as Map; + return PlaceSearchPoint( + latitude: (point['latitude'] as num).toDouble(), + longitude: (point['longitude'] as num).toDouble(), + ); + } + + void dispose() {} +} diff --git a/lib/presentation/screens/search/search_screen.dart b/lib/presentation/screens/search/search_screen.dart index 2399218..27c49e4 100644 --- a/lib/presentation/screens/search/search_screen.dart +++ b/lib/presentation/screens/search/search_screen.dart @@ -1,10 +1,13 @@ import 'dart:async'; +import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; -import 'package:yandex_mapkit/yandex_mapkit.dart'; +import 'package:pointer_interceptor/pointer_interceptor.dart'; import '../../providers/routing_provider.dart'; import '../../../core/localization/app_localizations.dart'; +import 'place_search_models.dart'; +import 'place_search_service.dart'; class SearchScreen extends ConsumerStatefulWidget { const SearchScreen({super.key}); @@ -14,22 +17,23 @@ class SearchScreen extends ConsumerStatefulWidget { } class _SearchScreenState extends ConsumerState { - // Wide bbox around bias point (covers ~1000 km in each direction). - // bbox center = current camera position, which correctly biases Yandex Suggest. - static const double _biasLatDelta = 10.0; - static const double _biasLonDelta = 15.0; + // Prefer results around the current map center without restricting a query + // that explicitly names another city. + static const double _biasLatDelta = 1.0; + static const double _biasLonDelta = 1.5; // Fallback if location/camera position unknown — center of Russia. - static const BoundingBox _russiaBoundingBox = BoundingBox( - southWest: Point(latitude: 41.0, longitude: 19.0), - northEast: Point(latitude: 82.0, longitude: 169.0), + static const PlaceSearchBounds _russiaBoundingBox = PlaceSearchBounds( + south: 41.0, + west: 19.0, + north: 82.0, + east: 169.0, ); final _controller = TextEditingController(); final _focusNode = FocusNode(); - SuggestSession? _suggestSession; - SearchSession? _searchSession; + final _searchService = PlaceSearchService(); Timer? _debounce; - List _suggestions = []; + List _suggestions = []; bool _loading = false; int _suggestSeq = 0; @@ -49,8 +53,7 @@ class _SearchScreenState extends ConsumerState { @override void dispose() { _debounce?.cancel(); - _suggestSession?.close(); - _searchSession?.close(); + _searchService.dispose(); _controller.dispose(); _focusNode.dispose(); super.dispose(); @@ -60,7 +63,11 @@ class _SearchScreenState extends ConsumerState { ref.read(searchQueryProvider.notifier).state = text; _debounce?.cancel(); if (text.trim().isEmpty) { - setState(() => _suggestions = []); + _suggestSeq++; + setState(() { + _suggestions = []; + _loading = false; + }); return; } _debounce = Timer( @@ -75,50 +82,32 @@ class _SearchScreenState extends ConsumerState { return value; } - BoundingBox _buildLocalBoundingBox(SearchBias bias) { + PlaceSearchBounds _buildLocalBoundingBox(SearchBias bias) { final south = _clamp(bias.latitude - _biasLatDelta, -90.0, 90.0); final north = _clamp(bias.latitude + _biasLatDelta, -90.0, 90.0); final west = _clamp(bias.longitude - _biasLonDelta, -180.0, 180.0); final east = _clamp(bias.longitude + _biasLonDelta, -180.0, 180.0); - return BoundingBox( - southWest: Point(latitude: south, longitude: west), - northEast: Point(latitude: north, longitude: east), + return PlaceSearchBounds( + south: south, + west: west, + north: north, + east: east, ); } - Future> _loadSuggestions( + Future> _loadSuggestions( String text, - BoundingBox boundingBox, - ) async { - final resultWithSession = YandexSuggest.getSuggestions( - text: text, - boundingBox: boundingBox, - suggestOptions: const SuggestOptions(suggestType: SuggestType.unspecified), - ); - _suggestSession?.close(); - _suggestSession = resultWithSession.session; - final result = await resultWithSession.result.timeout( - const Duration(seconds: 5), - ); - return result.items ?? []; - } + PlaceSearchBounds boundingBox, + ) => _searchService + .suggestions(text, boundingBox) + .timeout(const Duration(seconds: 5)); - Future _searchPointByText( + Future _searchPointByText( String text, - BoundingBox boundingBox, - ) async { - final resultWithSession = YandexSearch.searchByText( - searchText: text, - geometry: Geometry.fromBoundingBox(boundingBox), - searchOptions: const SearchOptions(searchType: SearchType.geo), - ); - _searchSession?.close(); - _searchSession = resultWithSession.session; - final result = await resultWithSession.result.timeout( - const Duration(seconds: 5), - ); - return result.items?.firstOrNull?.geometry.firstOrNull?.point; - } + PlaceSearchBounds boundingBox, + ) => _searchService + .pointByText(text, boundingBox) + .timeout(const Duration(seconds: 5)); Future _suggest(String text) async { if (!mounted) return; @@ -139,31 +128,34 @@ class _SearchScreenState extends ConsumerState { } on TimeoutException { if (!mounted || seq != _suggestSeq) return; setState(() => _loading = false); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(s.searchTimeout)), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(s.searchTimeout))); } catch (_) { if (!mounted || seq != _suggestSeq) return; setState(() => _loading = false); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(s.searchError)), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(s.searchError))); } } void _selectDestination(Destination destination) { + ref.read(routingProvider.notifier).reset(); ref.read(destinationProvider.notifier).state = destination; context.pop(); } - Future _onSuggestionTap(SuggestItem item) async { + Future _onSuggestionTap(PlaceSuggestion item) async { _focusNode.unfocus(); - if (item.center != null) { - _selectDestination(Destination( - latitude: item.center!.latitude, - longitude: item.center!.longitude, - name: item.title, - )); + if (item.latitude != null && item.longitude != null) { + _selectDestination( + Destination( + latitude: item.latitude!, + longitude: item.longitude!, + name: item.title, + ), + ); return; } @@ -181,28 +173,30 @@ class _SearchScreenState extends ConsumerState { if (!mounted) return; if (point == null) { setState(() => _loading = false); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(s.pointLookupError)), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(s.pointLookupError))); return; } - _selectDestination(Destination( - latitude: point.latitude, - longitude: point.longitude, - name: item.title, - )); + _selectDestination( + Destination( + latitude: point.latitude, + longitude: point.longitude, + name: item.title, + ), + ); } on TimeoutException { if (!mounted) return; setState(() => _loading = false); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(s.pointLookupTimeout)), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(s.pointLookupTimeout))); } catch (_) { if (!mounted) return; setState(() => _loading = false); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(s.searchError)), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(s.searchError))); } } @@ -210,72 +204,77 @@ class _SearchScreenState extends ConsumerState { Widget build(BuildContext context) { final s = ref.watch(l10nProvider); final theme = Theme.of(context); - return Scaffold( - appBar: AppBar( - backgroundColor: theme.colorScheme.surface, - elevation: 0, - leading: IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: () => context.pop(), - ), - titleSpacing: 0, - title: TextField( - controller: _controller, - focusNode: _focusNode, - onChanged: _onChanged, - onSubmitted: (text) { - if (text.trim().isNotEmpty) _suggest(text.trim()); - }, - textInputAction: TextInputAction.search, - decoration: InputDecoration( - hintText: s.searchHint, - hintStyle: TextStyle(color: theme.hintColor), - border: InputBorder.none, + return PointerInterceptor( + intercepting: kIsWeb, + child: Scaffold( + appBar: AppBar( + backgroundColor: theme.colorScheme.surface, + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => context.pop(), ), - ), - actions: [ - if (_controller.text.isNotEmpty) - IconButton( - icon: const Icon(Icons.close), - onPressed: () { - _controller.clear(); - ref.read(searchQueryProvider.notifier).state = ''; - setState(() => _suggestions = []); - }, + titleSpacing: 0, + title: TextField( + controller: _controller, + focusNode: _focusNode, + onChanged: _onChanged, + onSubmitted: (text) { + if (text.trim().isNotEmpty) _suggest(text.trim()); + }, + textInputAction: TextInputAction.search, + decoration: InputDecoration( + hintText: s.searchHint, + hintStyle: TextStyle(color: theme.hintColor), + border: InputBorder.none, ), - ], - bottom: PreferredSize( - preferredSize: const Size.fromHeight(1), - child: Divider(height: 1, color: theme.dividerColor), + ), + actions: [ + if (_controller.text.isNotEmpty) + IconButton( + icon: const Icon(Icons.close), + onPressed: () { + _controller.clear(); + _debounce?.cancel(); + _suggestSeq++; + ref.read(searchQueryProvider.notifier).state = ''; + setState(() { + _suggestions = []; + _loading = false; + }); + }, + ), + ], + bottom: PreferredSize( + preferredSize: const Size.fromHeight(1), + child: Divider(height: 1, color: theme.dividerColor), + ), ), + body: _loading + ? const Center(child: CircularProgressIndicator()) + : _suggestions.isEmpty + ? _EmptyState(hasQuery: _controller.text.isNotEmpty) + : ListView.builder( + itemCount: _suggestions.length, + itemBuilder: (_, i) { + final item = _suggestions[i]; + return ListTile( + leading: Icon(Icons.place_outlined, color: theme.hintColor), + title: Text(item.title), + subtitle: item.subtitle != null + ? Text( + item.subtitle!, + style: TextStyle( + color: theme.hintColor, + fontSize: 13, + ), + ) + : null, + onTap: () => _onSuggestionTap(item), + ); + }, + ), ), - body: _loading - ? const Center(child: CircularProgressIndicator()) - : _suggestions.isEmpty - ? _EmptyState(hasQuery: _controller.text.isNotEmpty) - : ListView.builder( - itemCount: _suggestions.length, - itemBuilder: (_, i) { - final item = _suggestions[i]; - return ListTile( - leading: Icon( - Icons.place_outlined, - color: theme.hintColor, - ), - title: Text(item.title), - subtitle: item.subtitle != null - ? Text( - item.subtitle!, - style: TextStyle( - color: theme.hintColor, - fontSize: 13, - ), - ) - : null, - onTap: () => _onSuggestionTap(item), - ); - }, - ), ); } } @@ -300,10 +299,7 @@ class _EmptyState extends ConsumerWidget { const SizedBox(height: 16), Text( hasQuery ? s.searchNoResults : s.searchHint, - style: TextStyle( - color: Theme.of(context).hintColor, - fontSize: 15, - ), + style: TextStyle(color: Theme.of(context).hintColor, fontSize: 15), ), ], ), diff --git a/pubspec.lock b/pubspec.lock index 6d3939b..aa77701 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -749,6 +749,38 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + pointer_interceptor: + dependency: "direct main" + description: + name: pointer_interceptor + sha256: "57210410680379aea8b1b7ed6ae0c3ad349bfd56fe845b8ea934a53344b9d523" + url: "https://pub.dev" + source: hosted + version: "0.10.1+2" + pointer_interceptor_ios: + dependency: transitive + description: + name: pointer_interceptor_ios + sha256: "03c5fa5896080963ab4917eeffda8d28c90f22863a496fb5ba13bc10943e40e4" + url: "https://pub.dev" + source: hosted + version: "0.10.1+1" + pointer_interceptor_platform_interface: + dependency: transitive + description: + name: pointer_interceptor_platform_interface + sha256: "0597b0560e14354baeb23f8375cd612e8bd4841bf8306ecb71fcd0bb78552506" + url: "https://pub.dev" + source: hosted + version: "0.10.0+1" + pointer_interceptor_web: + dependency: transitive + description: + name: pointer_interceptor_web + sha256: "460b600e71de6fcea2b3d5f662c92293c049c4319e27f0829310e5a953b3ee2a" + url: "https://pub.dev" + source: hosted + version: "0.10.3" pool: dependency: transitive description: @@ -1027,7 +1059,7 @@ packages: source: hosted version: "1.2.1" web: - dependency: transitive + dependency: "direct main" description: name: web sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" diff --git a/pubspec.yaml b/pubspec.yaml index b87cb5d..c791e93 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -38,6 +38,8 @@ dependencies: url_launcher: ^6.3.1 cupertino_icons: ^1.0.8 + pointer_interceptor: ^0.10.1+2 + web: ^1.1.1 dev_dependencies: flutter_test: diff --git a/test/presentation/time_selector_widget_test.dart b/test/presentation/time_selector_widget_test.dart new file mode 100644 index 0000000..0b3d13c --- /dev/null +++ b/test/presentation/time_selector_widget_test.dart @@ -0,0 +1,28 @@ +import 'dart:ui' show PointerDeviceKind; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mobile/core/localization/app_localizations.dart'; +import 'package:mobile/presentation/screens/map/widgets/time_selector_widget.dart'; + +void main() { + testWidgets('time picker accepts mouse dragging', (tester) async { + await tester.pumpWidget( + ProviderScope( + overrides: [l10nProvider.overrideWithValue(AppStrings.ru)], + child: const MaterialApp(home: Scaffold(body: TimeSelectorWidget())), + ), + ); + + await tester.tap(find.text(AppStrings.ru.time)); + await tester.pumpAndSettle(); + + final pickerContext = tester.element(find.byType(CupertinoDatePicker)); + expect( + ScrollConfiguration.of(pickerContext).dragDevices, + contains(PointerDeviceKind.mouse), + ); + }); +} diff --git a/web/index.html b/web/index.html index 9ffe9d1..aae712f 100644 --- a/web/index.html +++ b/web/index.html @@ -31,6 +31,15 @@ ParkTrack + + + + + + + + + **App integrity**. +4. Find the **App signing key certificate** section. +5. Copy the **SHA-256 certificate fingerprint**. + +If you are using a local keystore: +Run the following command: +```bash +keytool -list -v -keystore path/to/your/keystore.jks +``` +Then find the SHA-256 value. + +## Update assetlinks.json +Once you have the fingerprint, update `web/.well-known/assetlinks.json`: +```json +"sha256_cert_fingerprints": [ + "PASTE_YOUR_FINGERPRINT_HERE" +] +``` diff --git a/lib/core/localization/app_localizations.dart b/lib/core/localization/app_localizations.dart index 44054c0..7711fb0 100644 --- a/lib/core/localization/app_localizations.dart +++ b/lib/core/localization/app_localizations.dart @@ -5,6 +5,18 @@ import '../../presentation/providers/settings_provider.dart'; class AppStrings { final String appTitle; final String findParking; + final String noInternet; + final String requestTimedOut; + final String networkFailure; + final String mapLoadError; + final String routeLoadError; + final String unknownError; + final String retry; + final String pwaInstallTitle; + final String pwaInstallStep1; + final String pwaInstallStep2; + final String serviceUnavailable; + final String locationServicesDisabled; final String searching; final String routeReady; final String selectParking; @@ -105,6 +117,19 @@ class AppStrings { final String invalidData; final String serverError; final String requestError; + final String noInternet; + final String requestTimedOut; + final String networkFailure; + final String serviceUnavailable; + final String unknownError; + final String locationServicesDisabled; + final String mapLoadError; + final String routeLoadError; + final String retry; + final String installPwa; + final String pwaStep1; + final String pwaStep2; + final String ok; final String searchTimeout; final String pointLookupError; final String pointLookupTimeout; @@ -150,6 +175,18 @@ class AppStrings { AppStrings({ required this.appTitle, required this.findParking, + required this.noInternet, + required this.requestTimedOut, + required this.networkFailure, + required this.mapLoadError, + required this.routeLoadError, + required this.unknownError, + required this.retry, + required this.pwaInstallTitle, + required this.pwaInstallStep1, + required this.pwaInstallStep2, + required this.serviceUnavailable, + required this.locationServicesDisabled, required this.searching, required this.routeReady, required this.selectParking, @@ -250,6 +287,19 @@ class AppStrings { required this.invalidData, required this.serverError, required this.requestError, + required this.noInternet, + required this.requestTimedOut, + required this.networkFailure, + required this.serviceUnavailable, + required this.unknownError, + required this.locationServicesDisabled, + required this.mapLoadError, + required this.routeLoadError, + required this.retry, + required this.installPwa, + required this.pwaStep1, + required this.pwaStep2, + required this.ok, required this.searchTimeout, required this.pointLookupError, required this.pointLookupTimeout, @@ -296,6 +346,18 @@ class AppStrings { static AppStrings ru = AppStrings( appTitle: 'ParkTrack', findParking: 'Припарковаться', + noInternet: 'Нет интернета', + requestTimedOut: 'Время ожидания истекло', + networkFailure: 'Ошибка сети', + mapLoadError: 'Ошибка загрузки карты', + routeLoadError: 'Ошибка загрузки маршрута', + unknownError: 'Что-то пошло не так', + retry: 'Повторить', + pwaInstallTitle: 'Установка приложения', + pwaInstallStep1: 'Нажмите кнопку «Поделиться» в браузере', + pwaInstallStep2: 'Выберите «На экран Домой»', + serviceUnavailable: 'Сервис временно недоступен', + locationServicesDisabled: 'Геолокация отключена', searching: 'Ищем...', routeReady: 'Маршрут готов', selectParking: 'Выберите парковку', @@ -313,7 +375,8 @@ class AppStrings { filters: 'Фильтры', apply: 'Применить', reset: 'Сбросить', - passwordResetInstructions: 'Введите свой email чтобы получить инструкции по сбросу пароля.', + passwordResetInstructions: + 'Введите свой email чтобы получить инструкции по сбросу пароля.', logout: 'Выйти', settings: 'Настройки', appearance: 'Внешний вид', @@ -397,6 +460,19 @@ class AppStrings { invalidData: 'Неверные данные', serverError: 'Ошибка сервера', requestError: 'Ошибка запроса', + noInternet: 'Нет подключения к интернету', + requestTimedOut: 'Сервер не ответил вовремя', + networkFailure: 'Не удалось подключиться к серверу', + serviceUnavailable: 'Сервис временно недоступен', + unknownError: 'Что-то пошло не так', + locationServicesDisabled: 'Геолокация отключена', + mapLoadError: 'Не удалось загрузить карту', + routeLoadError: 'Не удалось загрузить маршрут', + retry: 'Повторить', + installPwa: 'Установить ParkTrack', + pwaStep1: '1. Нажмите «Поделиться»', + pwaStep2: '2. Выберите «На экран Домой»', + ok: 'ОК', searchTimeout: 'Поиск занял слишком много времени', pointLookupError: 'Не удалось определить координаты места', pointLookupTimeout: 'Определение точки заняло слишком много времени', @@ -459,6 +535,18 @@ class AppStrings { static AppStrings en = AppStrings( appTitle: 'ParkTrack', findParking: 'Park Now', + noInternet: 'No internet', + requestTimedOut: 'Request timed out', + networkFailure: 'Network failure', + mapLoadError: 'Map load error', + routeLoadError: 'Route load error', + unknownError: 'Something went wrong', + retry: 'Retry', + pwaInstallTitle: 'Install App', + pwaInstallStep1: 'Tap the Share button in your browser', + pwaInstallStep2: 'Select "Add to Home Screen"', + serviceUnavailable: 'Service temporarily unavailable', + locationServicesDisabled: 'Location services disabled', searching: 'Searching...', routeReady: 'Route Ready', selectParking: 'Select Parking', @@ -476,7 +564,8 @@ class AppStrings { filters: 'Filters', apply: 'Apply', reset: 'Reset', - passwordResetInstructions: 'Enter your email to receive password reset instructions.', + passwordResetInstructions: + 'Enter your email to receive password reset instructions.', logout: 'Logout', settings: 'Settings', appearance: 'Appearance', @@ -560,6 +649,19 @@ class AppStrings { invalidData: 'Invalid data', serverError: 'Server error', requestError: 'Request error', + noInternet: 'No internet connection', + requestTimedOut: 'The server took too long to respond', + networkFailure: 'Could not connect to the server', + serviceUnavailable: 'Service is temporarily unavailable', + unknownError: 'Something went wrong', + locationServicesDisabled: 'Location services are disabled', + mapLoadError: 'Could not load the map', + routeLoadError: 'Could not load the route', + retry: 'Retry', + installPwa: 'Install ParkTrack', + pwaStep1: '1. Tap “Share”', + pwaStep2: '2. Choose “Add to Home Screen”', + ok: 'OK', searchTimeout: 'Search timeout', pointLookupError: 'Could not determine coordinates', pointLookupTimeout: 'Point lookup timeout', diff --git a/lib/core/network/api_exception.dart b/lib/core/network/api_exception.dart index bad27c4..7042302 100644 --- a/lib/core/network/api_exception.dart +++ b/lib/core/network/api_exception.dart @@ -1,36 +1,181 @@ +import 'dart:async'; +import 'package:dio/dio.dart'; +import 'package:flutter/services.dart'; +import 'package:geolocator/geolocator.dart'; import '../localization/app_localizations.dart'; -class ApiException implements Exception { +enum AppFailureKind { + noInternet, + timeout, + network, + serviceUnavailable, + server, + unauthorized, + forbidden, + notFound, + conflict, + invalidData, + locationPermissionDenied, + locationServicesDisabled, + mapLoad, + routeLoad, + unknown, +} + +class AppFailure implements Exception { + const AppFailure(this.kind, {this.statusCode}); + + final AppFailureKind kind; final int? statusCode; - final String message; - const ApiException(this.message, {this.statusCode}); + bool get isRecoverable => switch (kind) { + AppFailureKind.noInternet || + AppFailureKind.timeout || + AppFailureKind.network || + AppFailureKind.serviceUnavailable || + AppFailureKind.server || + AppFailureKind.mapLoad || + AppFailureKind.routeLoad || + AppFailureKind.unknown => true, + _ => false, + }; + + factory AppFailure.from( + Object error, { + AppFailureKind fallback = AppFailureKind.unknown, + }) { + if (error is AppFailure) return error; + if (error is ApiException) return error.failure; + if (error is TimeoutException) { + return const AppFailure(AppFailureKind.timeout); + } + + // Check for network errors (SocketException etc) + final errorStr = error.toString().toLowerCase(); + if (errorStr.contains('socketexception') || + errorStr.contains('handshake') || + errorStr.contains('connection failed')) { + return const AppFailure(AppFailureKind.noInternet); + } - factory ApiException.fromStatusCode(int code) { - // We return a technical message by default, but it will be localized in UI + if (error is LocationServiceDisabledException) { + return const AppFailure(AppFailureKind.locationServicesDisabled); + } + if (error is PermissionDeniedException) { + return const AppFailure(AppFailureKind.locationPermissionDenied); + } + if (error is PlatformException) { + final code = error.code.toLowerCase(); + if (code.contains('permission') || code.contains('denied')) { + return const AppFailure(AppFailureKind.locationPermissionDenied); + } + if (code.contains('location') && + (code.contains('disabled') || code.contains('service'))) { + return const AppFailure(AppFailureKind.locationServicesDisabled); + } + if (code.contains('network')) { + return const AppFailure(AppFailureKind.network); + } + } + if (error is DioException) return _fromDio(error, fallback); + return AppFailure(fallback); + } + + static AppFailure _fromDio(DioException error, AppFailureKind fallback) { + // Check inner error for network issues + if (error.error != null) { + final innerStr = error.error.toString().toLowerCase(); + if (innerStr.contains('socketexception') || + innerStr.contains('handshake') || + innerStr.contains('connection failed')) { + return const AppFailure(AppFailureKind.noInternet); + } + } + + switch (error.type) { + case DioExceptionType.connectionTimeout: + case DioExceptionType.sendTimeout: + case DioExceptionType.receiveTimeout: + return const AppFailure(AppFailureKind.timeout); + case DioExceptionType.connectionError: + return const AppFailure(AppFailureKind.network); + case DioExceptionType.badResponse: + return AppFailure.fromStatusCode(error.response?.statusCode ?? 0); + case DioExceptionType.cancel: + return AppFailure(fallback); + case DioExceptionType.badCertificate: + case DioExceptionType.unknown: + return AppFailure(fallback); + } + } + + factory AppFailure.fromStatusCode(int code) { return switch (code) { - 401 => ApiException('Unauthorized', statusCode: 401), - 403 => ApiException('Forbidden', statusCode: 403), - 404 => ApiException('Not Found', statusCode: 404), - 409 => ApiException('Conflict', statusCode: 409), - 422 => ApiException('Invalid Data', statusCode: 422), - 500 => ApiException('Server Error', statusCode: 500), - _ => ApiException('Request Error ($code)', statusCode: code), + 401 => const AppFailure(AppFailureKind.unauthorized, statusCode: 401), + 403 => const AppFailure(AppFailureKind.forbidden, statusCode: 403), + 404 => const AppFailure(AppFailureKind.notFound, statusCode: 404), + 409 => const AppFailure(AppFailureKind.conflict, statusCode: 409), + 422 => const AppFailure(AppFailureKind.invalidData, statusCode: 422), + 503 => const AppFailure( + AppFailureKind.serviceUnavailable, + statusCode: 503, + ), + >= 500 => AppFailure(AppFailureKind.server, statusCode: code), + _ => AppFailure(AppFailureKind.unknown, statusCode: code), }; } - String getLocalizedMessage(AppStrings s) { - return switch (statusCode) { - 401 => s.unauthorized, - 403 => s.forbidden, - 404 => s.notFound, - 409 => s.emailAlreadyExists, - 422 => s.invalidData, - 500 => s.serverError, - _ => '${s.requestError} ($statusCode)', - }; + String localizedMessage(AppStrings strings) => switch (kind) { + AppFailureKind.noInternet => strings.noInternet, + AppFailureKind.timeout => strings.requestTimedOut, + AppFailureKind.network => strings.networkFailure, + AppFailureKind.serviceUnavailable => strings.serviceUnavailable, + AppFailureKind.server => strings.serverError, + AppFailureKind.unauthorized => strings.unauthorized, + AppFailureKind.forbidden => strings.forbidden, + AppFailureKind.notFound => strings.notFound, + AppFailureKind.conflict => strings.emailAlreadyExists, + AppFailureKind.invalidData => strings.invalidData, + AppFailureKind.locationPermissionDenied => strings.locationPermissionDenied, + AppFailureKind.locationServicesDisabled => strings.locationServicesDisabled, + AppFailureKind.mapLoad => strings.mapLoadError, + AppFailureKind.routeLoad => strings.routeLoadError, + AppFailureKind.unknown => strings.unknownError, + }; + + @override + String toString() => 'AppFailure(${kind.name}, statusCode: $statusCode)'; +} + +class ApiException implements Exception { + const ApiException(this.message, {this.statusCode, AppFailure? failure}) + : _failure = failure; + + final int? statusCode; + final String message; + final AppFailure? _failure; + + AppFailure get failure => + _failure ?? AppFailure.fromStatusCode(statusCode ?? 0); + + factory ApiException.fromStatusCode(int code) => ApiException( + 'HTTP $code', + statusCode: code, + failure: AppFailure.fromStatusCode(code), + ); + + factory ApiException.fromError(Object error) { + final failure = AppFailure.from(error); + return ApiException( + 'Request failed', + statusCode: failure.statusCode, + failure: failure, + ); } + String getLocalizedMessage(AppStrings strings) => + failure.localizedMessage(strings); + @override - String toString() => 'ApiException($statusCode): $message'; + String toString() => 'ApiException(statusCode: $statusCode): $message'; } diff --git a/lib/core/network/auth_interceptor.dart b/lib/core/network/auth_interceptor.dart index 66f5415..b795dba 100644 --- a/lib/core/network/auth_interceptor.dart +++ b/lib/core/network/auth_interceptor.dart @@ -67,8 +67,10 @@ class AuthInterceptor extends Interceptor { } Future _loginAgain() async { - final login = await _tokenStorage.getLogin(); - final password = await _tokenStorage.getPassword(); + final loginFuture = _tokenStorage.getLogin(); + final passwordFuture = _tokenStorage.getPassword(); + final login = await loginFuture; + final password = await passwordFuture; if (login == null || password == null) { throw StateError('No credentials available for token refresh'); } diff --git a/lib/core/network/network_error_classifier.dart b/lib/core/network/network_error_classifier.dart new file mode 100644 index 0000000..d029fcc --- /dev/null +++ b/lib/core/network/network_error_classifier.dart @@ -0,0 +1,2 @@ +export 'network_error_classifier_stub.dart' + if (dart.library.io) 'network_error_classifier_io.dart'; diff --git a/lib/core/network/network_error_classifier_io.dart b/lib/core/network/network_error_classifier_io.dart new file mode 100644 index 0000000..f2b7615 --- /dev/null +++ b/lib/core/network/network_error_classifier_io.dart @@ -0,0 +1,6 @@ +import 'dart:io'; + +bool isSocketNetworkError(Object error) => + error is SocketException || + error is HandshakeException || + error is HttpException; diff --git a/lib/core/network/network_error_classifier_stub.dart b/lib/core/network/network_error_classifier_stub.dart new file mode 100644 index 0000000..0b69727 --- /dev/null +++ b/lib/core/network/network_error_classifier_stub.dart @@ -0,0 +1 @@ +bool isSocketNetworkError(Object error) => false; diff --git a/lib/core/router/app_router.dart b/lib/core/router/app_router.dart index 410f4ad..6a77ea0 100644 --- a/lib/core/router/app_router.dart +++ b/lib/core/router/app_router.dart @@ -24,17 +24,25 @@ final routerProvider = Provider((ref) { return state.matchedLocation == '/' ? null : '/'; } - final isLoginRoute = state.matchedLocation == '/login'; - final isRegisterRoute = state.matchedLocation == '/register'; - final isPasswordResetRoute = state.matchedLocation == '/password-reset'; - final isSplashRoute = state.matchedLocation == '/'; + final location = state.uri.path; + final isLoginRoute = location == '/login'; + final isRegisterRoute = location == '/register'; + final isPasswordResetRoute = location == '/password-reset'; + final isSplashRoute = location == '/'; if (isUnauth) { if (isSplashRoute) return '/login'; - if (!isLoginRoute && !isRegisterRoute && !isPasswordResetRoute) return '/login'; + if (!isLoginRoute && !isRegisterRoute && !isPasswordResetRoute) { + final from = state.uri.toString(); + return '/login?from=${Uri.encodeComponent(from)}'; + } } - if (isAuth && (isSplashRoute || isLoginRoute || isRegisterRoute)) return '/map'; + if (isAuth && (isSplashRoute || isLoginRoute || isRegisterRoute)) { + final from = state.uri.queryParameters['from']; + if (from != null && from.isNotEmpty) return from; + return '/map'; + } return null; }, @@ -42,11 +50,33 @@ final routerProvider = Provider((ref) { GoRoute(path: '/', builder: (_, _) => const SplashScreen()), GoRoute(path: '/login', builder: (_, _) => const LoginScreen()), GoRoute(path: '/register', builder: (_, _) => const RegisterScreen()), - GoRoute(path: '/map', builder: (_, _) => const MapScreen()), + GoRoute( + path: '/map', + builder: (context, state) { + final id = int.tryParse(state.uri.queryParameters['id'] ?? ''); + final query = state.uri.queryParameters['q']; + return MapScreen(initialParkingId: id, searchQuery: query); + }, + routes: [ + GoRoute( + path: 'parking/:id', + builder: (context, state) { + final id = int.tryParse(state.pathParameters['id'] ?? ''); + return MapScreen(initialParkingId: id); + }, + ), + ], + ), GoRoute(path: '/profile', builder: (_, _) => const ProfileScreen(), routes: [ GoRoute(path: 'edit', builder: (_, _) => const EditProfileScreen()), ]), - GoRoute(path: '/search', builder: (_, _) => const SearchScreen()), + GoRoute( + path: '/search', + builder: (context, state) { + final q = state.uri.queryParameters['q']; + return SearchScreen(initialQuery: q); + }, + ), GoRoute(path: '/password-reset', builder: (_, _) => const PasswordResetScreen()), ], ); diff --git a/lib/core/router/deep_link_coordinator.dart b/lib/core/router/deep_link_coordinator.dart new file mode 100644 index 0000000..956bf7f --- /dev/null +++ b/lib/core/router/deep_link_coordinator.dart @@ -0,0 +1,78 @@ +class DeepLinkCoordinator { + Uri? _pendingDestination; + + void remember(Uri destination) { + _pendingDestination = Uri.parse(safeLocation(destination)); + } + + String takePendingOr(String? fallback) { + final pending = _pendingDestination; + _pendingDestination = null; + if (pending != null) return safeLocation(pending); + return safeLocation(Uri.tryParse(fallback ?? '')); + } + + String safeLocation(Uri? uri) { + if (uri == null || uri.hasScheme || uri.hasAuthority) return '/map'; + final segments = uri.pathSegments; + + if (uri.path == '/map') { + return Uri(path: '/map', queryParameters: _mapParameters(uri)).toString(); + } + if (segments.length == 2 && segments.first == 'parking') { + final zoneId = int.tryParse(segments[1]); + return zoneId != null && zoneId > 0 ? '/parking/$zoneId' : '/map'; + } + if (uri.path == '/destination') { + final latitude = double.tryParse(uri.queryParameters['lat'] ?? ''); + final longitude = double.tryParse(uri.queryParameters['lon'] ?? ''); + if (!_validCoordinates(latitude, longitude)) return '/map'; + final queryParameters = { + 'lat': latitude.toString(), + 'lon': longitude.toString(), + }; + final name = uri.queryParameters['name']; + if (name != null && name.isNotEmpty) queryParameters['name'] = name; + return Uri( + path: '/destination', + queryParameters: queryParameters, + ).toString(); + } + if (uri.path == '/search') { + final query = uri.queryParameters['q']; + return Uri( + path: '/search', + queryParameters: query == null || query.isEmpty ? null : {'q': query}, + ).toString(); + } + if (uri.path == '/profile' || uri.path == '/profile/edit') { + return uri.path; + } + return '/map'; + } + + Map? _mapParameters(Uri uri) { + final result = {}; + final zoneId = int.tryParse(uri.queryParameters['zoneId'] ?? ''); + if (zoneId != null && zoneId > 0) result['zoneId'] = zoneId.toString(); + final latitude = double.tryParse(uri.queryParameters['lat'] ?? ''); + final longitude = double.tryParse(uri.queryParameters['lon'] ?? ''); + if (_validCoordinates(latitude, longitude)) { + result['lat'] = latitude.toString(); + result['lon'] = longitude.toString(); + final name = uri.queryParameters['name']; + if (name != null && name.isNotEmpty) result['name'] = name; + } + return result.isEmpty ? null : result; + } + + bool _validCoordinates(double? latitude, double? longitude) => + latitude != null && + longitude != null && + latitude.isFinite && + longitude.isFinite && + latitude >= -90 && + latitude <= 90 && + longitude >= -180 && + longitude <= 180; +} diff --git a/lib/core/router/url_strategy.dart b/lib/core/router/url_strategy.dart new file mode 100644 index 0000000..b9c10a1 --- /dev/null +++ b/lib/core/router/url_strategy.dart @@ -0,0 +1,2 @@ +export 'url_strategy_stub.dart' + if (dart.library.js_interop) 'url_strategy_web.dart'; diff --git a/lib/core/router/url_strategy_stub.dart b/lib/core/router/url_strategy_stub.dart new file mode 100644 index 0000000..7af9f31 --- /dev/null +++ b/lib/core/router/url_strategy_stub.dart @@ -0,0 +1 @@ +void configureUrlStrategy() {} diff --git a/lib/core/router/url_strategy_web.dart b/lib/core/router/url_strategy_web.dart new file mode 100644 index 0000000..786fc3c --- /dev/null +++ b/lib/core/router/url_strategy_web.dart @@ -0,0 +1,3 @@ +import 'package:flutter_web_plugins/url_strategy.dart'; + +void configureUrlStrategy() => usePathUrlStrategy(); diff --git a/lib/core/storage/token_storage.dart b/lib/core/storage/token_storage.dart index 5f592e9..d114fb5 100644 --- a/lib/core/storage/token_storage.dart +++ b/lib/core/storage/token_storage.dart @@ -39,8 +39,10 @@ class TokenStorage { } Future saveCredentials(String login, String password) async { - await _storage.write(key: _keyLogin, value: login); - await _storage.write(key: _keyPassword, value: password); + await Future.wait([ + _storage.write(key: _keyLogin, value: login), + _storage.write(key: _keyPassword, value: password), + ]); } Future getLogin() => _storage.read(key: _keyLogin); @@ -50,9 +52,11 @@ class TokenStorage { _accessTokenRevision++; _cachedAccessToken = null; _accessTokenLoaded = true; - await _storage.delete(key: _keyAccess); - await _storage.delete(key: _keyLogin); - await _storage.delete(key: _keyPassword); + await Future.wait([ + _storage.delete(key: _keyAccess), + _storage.delete(key: _keyLogin), + _storage.delete(key: _keyPassword), + ]); } Future clearTokens() async { diff --git a/lib/core/utils/browser_locale.dart b/lib/core/utils/browser_locale.dart new file mode 100644 index 0000000..c8a4384 --- /dev/null +++ b/lib/core/utils/browser_locale.dart @@ -0,0 +1,2 @@ +export 'browser_locale_stub.dart' + if (dart.library.js_interop) 'browser_locale_web.dart'; diff --git a/lib/core/utils/browser_locale_stub.dart b/lib/core/utils/browser_locale_stub.dart new file mode 100644 index 0000000..0911ffc --- /dev/null +++ b/lib/core/utils/browser_locale_stub.dart @@ -0,0 +1 @@ +String? detectBrowserLanguage() => null; diff --git a/lib/core/utils/browser_locale_web.dart b/lib/core/utils/browser_locale_web.dart new file mode 100644 index 0000000..b5804a4 --- /dev/null +++ b/lib/core/utils/browser_locale_web.dart @@ -0,0 +1,6 @@ +import 'package:web/web.dart' as web; + +String? detectBrowserLanguage() { + final language = web.window.navigator.language.trim().toLowerCase(); + return language.isEmpty ? null : language; +} diff --git a/lib/core/utils/error_snackbar.dart b/lib/core/utils/error_snackbar.dart index 2e2a15b..3a60a20 100644 --- a/lib/core/utils/error_snackbar.dart +++ b/lib/core/utils/error_snackbar.dart @@ -4,16 +4,27 @@ import 'package:flutter/services.dart'; import 'package:pointer_interceptor/pointer_interceptor.dart'; import '../localization/app_localizations.dart'; +String? _lastShownMessage; +DateTime? _lastShownTime; + void showErrorSnackBar( BuildContext context, String message, { Object? error, StackTrace? stackTrace, AppStrings? s, + VoidCallback? onRetry, }) { - final details = _buildDetails(error, stackTrace); + final now = DateTime.now(); + if (_lastShownMessage == message && + _lastShownTime != null && + now.difference(_lastShownTime!) < const Duration(seconds: 3)) { + return; + } + _lastShownMessage = message; + _lastShownTime = now; - final label = s?.moreInfo ?? 'Details'; + final details = _buildDetails(error, stackTrace); ScaffoldMessenger.of(context) ..hideCurrentSnackBar() @@ -26,13 +37,18 @@ void showErrorSnackBar( Expanded(child: Text(message)), ], ), - action: details != null + action: onRetry != null ? SnackBarAction( - label: label, - onPressed: () => - _showDetailsDialog(context, message, details, s), + label: s?.retry ?? 'Retry', + onPressed: onRetry, ) - : null, + : details != null + ? SnackBarAction( + label: s?.moreInfo ?? 'Details', + onPressed: () => + _showDetailsDialog(context, message, details, s), + ) + : null, behavior: SnackBarBehavior.floating, duration: const Duration(seconds: 6), ), diff --git a/lib/core/utils/pwa_install_eligibility.dart b/lib/core/utils/pwa_install_eligibility.dart new file mode 100644 index 0000000..3c87542 --- /dev/null +++ b/lib/core/utils/pwa_install_eligibility.dart @@ -0,0 +1,2 @@ +export 'pwa_install_eligibility_stub.dart' + if (dart.library.js_interop) 'pwa_install_eligibility_web.dart'; diff --git a/lib/core/utils/pwa_install_eligibility_stub.dart b/lib/core/utils/pwa_install_eligibility_stub.dart new file mode 100644 index 0000000..9ccce19 --- /dev/null +++ b/lib/core/utils/pwa_install_eligibility_stub.dart @@ -0,0 +1,3 @@ +bool isIosPwaEligible() => false; + +void dismissIosPwaGuide() {} diff --git a/lib/core/utils/pwa_install_eligibility_web.dart b/lib/core/utils/pwa_install_eligibility_web.dart new file mode 100644 index 0000000..c033938 --- /dev/null +++ b/lib/core/utils/pwa_install_eligibility_web.dart @@ -0,0 +1,31 @@ +import 'package:flutter/foundation.dart'; +import 'package:web/web.dart' as web; + +bool isIosPwaEligible() { + try { + final navigator = web.window.navigator; + final userAgent = navigator.userAgent.toLowerCase(); + final isIos = + userAgent.contains('iphone') || + userAgent.contains('ipad') || + userAgent.contains('ipod') || + (userAgent.contains('macintosh') && navigator.maxTouchPoints > 0); + if (!isIos) return false; + + if (web.window.matchMedia('(display-mode: standalone)').matches) { + return false; + } + + const version = '1'; + return web.window.localStorage.getItem('pwa_install_dismissed_v$version') == + null; + } catch (error) { + debugPrint('PWA eligibility check failed: $error'); + return false; + } +} + +void dismissIosPwaGuide() { + const version = '1'; + web.window.localStorage.setItem('pwa_install_dismissed_v$version', 'true'); +} diff --git a/lib/data/repositories/auth_repository.dart b/lib/data/repositories/auth_repository.dart index 0cd32e9..6d51fa7 100644 --- a/lib/data/repositories/auth_repository.dart +++ b/lib/data/repositories/auth_repository.dart @@ -13,33 +13,43 @@ class AuthRepository { Future login(String login, String password) async { try { - final response = await _api.login(LoginRequestDto(login: login, password: password)); + final response = await _api.login( + LoginRequestDto(login: login, password: password), + ); await _tokenStorage.saveAccessToken(response.accessToken); await _tokenStorage.saveCredentials(login, password); return _mapUser(response.user); } on DioException catch (e) { - throw ApiException.fromStatusCode(e.response?.statusCode ?? 0); + throw ApiException.fromError(e); } on ApiException { rethrow; } catch (e) { - throw ApiException(e.toString()); + throw ApiException.fromError(e); } } - Future register(String email, String password, {String? fullName}) async { + Future register( + String email, + String password, { + String? fullName, + }) async { try { final response = await _api.register( - RegisterRequestDto(email: email, password: password, fullName: fullName), + RegisterRequestDto( + email: email, + password: password, + fullName: fullName, + ), ); await _tokenStorage.saveAccessToken(response.accessToken); await _tokenStorage.saveCredentials(email, password); return _mapUser(response.user); } on DioException catch (e) { - throw ApiException.fromStatusCode(e.response?.statusCode ?? 0); + throw ApiException.fromError(e); } on ApiException { rethrow; } catch (e) { - throw ApiException(e.toString()); + throw ApiException.fromError(e); } } @@ -65,9 +75,9 @@ class AuthRepository { try { await _api.requestPasswordReset(email); } on DioException catch (e) { - throw ApiException.fromStatusCode(e.response?.statusCode ?? 0); + throw ApiException.fromError(e); } catch (e) { - throw ApiException(e.toString()); + throw ApiException.fromError(e); } } @@ -75,20 +85,20 @@ class AuthRepository { try { final data = {}; if (fullName != null) data['full_name'] = fullName; - + final dto = await _api.updateProfile(data); return _mapUser(dto); } on DioException catch (e) { - throw ApiException.fromStatusCode(e.response?.statusCode ?? 0); + throw ApiException.fromError(e); } catch (e) { - throw ApiException(e.toString()); + throw ApiException.fromError(e); } } User _mapUser(UserDto dto) => User( - userId: dto.userId, - email: dto.email, - fullName: dto.fullName, - roles: dto.globalRole != null ? [dto.globalRole!] : [], - ); + userId: dto.userId, + email: dto.email, + fullName: dto.fullName, + roles: dto.globalRole != null ? [dto.globalRole!] : [], + ); } diff --git a/lib/main.dart b/lib/main.dart index f5a6577..1c913b2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,11 +2,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'core/router/app_router.dart'; +import 'core/router/url_strategy.dart'; import 'core/theme/app_theme.dart'; import 'presentation/providers/settings_provider.dart'; +import 'presentation/screens/map/widgets/pwa_install_guide.dart'; void main() { WidgetsFlutterBinding.ensureInitialized(); + configureUrlStrategy(); // MapKit API key is set via AndroidManifest.xml meta-data and ios/Runner/Info.plist runApp(const ProviderScope(child: ParkTrackApp())); } @@ -33,6 +36,25 @@ class ParkTrackApp extends ConsumerWidget { GlobalCupertinoLocalizations.delegate, ], supportedLocales: const [Locale('ru', 'RU'), Locale('en')], + builder: (context, child) => _AppBuilder(child: child), + ); + } +} + +class _AppBuilder extends StatelessWidget { + const _AppBuilder({this.child}); + final Widget? child; + + @override + Widget build(BuildContext context) { + if (child == null) return const SizedBox.shrink(); + return Material( + child: Stack( + children: [ + RepaintBoundary(child: child!), + const PwaInstallGuide(), + ], + ), ); } } diff --git a/lib/presentation/providers/auth_provider.dart b/lib/presentation/providers/auth_provider.dart index acbbdce..fdb9d75 100644 --- a/lib/presentation/providers/auth_provider.dart +++ b/lib/presentation/providers/auth_provider.dart @@ -41,6 +41,11 @@ class AuthNotifier extends StateNotifier { if (_isCheckingSession || _sessionChecked) return; _isCheckingSession = true; try { + final hasToken = await _ref.read(tokenStorageProvider).hasToken(); + if (!hasToken) { + state = const AuthState.unauthenticated(); + return; + } final user = await _ref .read(authRepositoryProvider) .getMe() @@ -63,12 +68,14 @@ class AuthNotifier extends StateNotifier { state = AuthState.authenticated(user); } - Future register(String email, String password, {String? fullName}) async { - final user = await _ref.read(authRepositoryProvider).register( - email, - password, - fullName: fullName, - ); + Future register( + String email, + String password, { + String? fullName, + }) async { + final user = await _ref + .read(authRepositoryProvider) + .register(email, password, fullName: fullName); state = AuthState.authenticated(user); } @@ -81,7 +88,9 @@ class AuthNotifier extends StateNotifier { _ref.read(authRepositoryProvider).requestPasswordReset(email); Future updateProfile({String? fullName}) async { - final user = await _ref.read(authRepositoryProvider).updateProfile(fullName: fullName); + final user = await _ref + .read(authRepositoryProvider) + .updateProfile(fullName: fullName); state = AuthState.authenticated(user); } } diff --git a/lib/presentation/providers/routing_provider.dart b/lib/presentation/providers/routing_provider.dart index 2086948..f03fbac 100644 --- a/lib/presentation/providers/routing_provider.dart +++ b/lib/presentation/providers/routing_provider.dart @@ -1,6 +1,7 @@ import 'package:dio/dio.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import '../../core/network/api_exception.dart'; import '../../domain/models/route_result.dart'; import 'app_providers.dart'; import 'filters_provider.dart'; @@ -36,12 +37,39 @@ final destinationModeProvider = StateProvider( ); class SearchBias { - const SearchBias({required this.latitude, required this.longitude}); + const SearchBias({ + required this.latitude, + required this.longitude, + this.south, + this.west, + this.north, + this.east, + }); final double latitude; final double longitude; + final double? south; + final double? west; + final double? north; + final double? east; } +class RoutingFailureEvent { + const RoutingFailureEvent({ + required this.id, + required this.failure, + required this.retry, + }); + + final int id; + final AppFailure failure; + final Future Function() retry; +} + +final routingFailureProvider = StateProvider( + (ref) => null, +); + final searchBiasProvider = StateProvider((ref) => null); final searchQueryProvider = StateProvider((ref) => ''); @@ -52,6 +80,41 @@ class RoutingNotifier extends StateNotifier { final Ref _ref; CancelToken? _cancelToken; int _requestGeneration = 0; + int _failureGeneration = 0; + RoutingState? _stableState; + + void _startRequest() { + _ref.read(routingFailureProvider.notifier).state = null; + final stable = state.maybeWhen( + candidates: (_) => state, + routePreview: (_) => state, + orElse: () => null, + ); + if (stable != null) { + _stableState = stable; + } else { + state = const RoutingState.searching(); + } + } + + void _publishFailure( + Object error, { + required AppFailureKind fallback, + required Future Function() retry, + }) { + final failure = AppFailure.from(error, fallback: fallback); + final cached = _stableState; + if (failure.isRecoverable && cached != null) { + state = cached; + } else { + state = RoutingState.error(failure.kind.name); + } + _ref.read(routingFailureProvider.notifier).state = RoutingFailureEvent( + id: ++_failureGeneration, + failure: failure, + retry: retry, + ); + } Future searchParking({ required double originLat, @@ -61,7 +124,7 @@ class RoutingNotifier extends StateNotifier { _cancelToken?.cancel('Superseded by a newer routing request'); final cancelToken = CancelToken(); _cancelToken = cancelToken; - state = const RoutingState.searching(); + _startRequest(); try { final destination = _ref.read(destinationProvider); final filters = _ref.read(filtersProvider); @@ -83,16 +146,15 @@ class RoutingNotifier extends StateNotifier { ); if (generation != _requestGeneration || cancelToken.isCancelled) return; state = RoutingState.candidates(candidates); + _stableState = state; } catch (e) { if (e is DioException && CancelToken.isCancel(e)) return; if (generation != _requestGeneration) return; - if (e is DioException && e.response != null) { - state = RoutingState.error( - '${e.response!.statusCode}: ${e.response!.data}', - ); - } else { - state = RoutingState.error(e.toString()); - } + _publishFailure( + e, + fallback: AppFailureKind.network, + retry: () => searchParking(originLat: originLat, originLon: originLon), + ); } } @@ -105,7 +167,7 @@ class RoutingNotifier extends StateNotifier { _cancelToken?.cancel('Superseded by a newer routing request'); final cancelToken = CancelToken(); _cancelToken = cancelToken; - state = const RoutingState.searching(); + _startRequest(); try { final destination = _ref.read(destinationProvider); final timeMode = _ref.read(timeSelectorProvider); @@ -126,16 +188,19 @@ class RoutingNotifier extends StateNotifier { ); if (generation != _requestGeneration || cancelToken.isCancelled) return; state = RoutingState.routePreview(route); + _stableState = state; } catch (e) { if (e is DioException && CancelToken.isCancel(e)) return; if (generation != _requestGeneration) return; - if (e is DioException && e.response != null) { - state = RoutingState.error( - '${e.response!.statusCode}: ${e.response!.data}', - ); - } else { - state = RoutingState.error(e.toString()); - } + _publishFailure( + e, + fallback: AppFailureKind.routeLoad, + retry: () => buildRoute( + originLat: originLat, + originLon: originLon, + selectedZoneId: selectedZoneId, + ), + ); } } @@ -143,6 +208,8 @@ class RoutingNotifier extends StateNotifier { _requestGeneration++; _cancelToken?.cancel('Routing reset'); _cancelToken = null; + _stableState = null; + _ref.read(routingFailureProvider.notifier).state = null; state = const RoutingState.idle(); } diff --git a/lib/presentation/providers/settings_provider.dart b/lib/presentation/providers/settings_provider.dart index 14de20d..5575b31 100644 --- a/lib/presentation/providers/settings_provider.dart +++ b/lib/presentation/providers/settings_provider.dart @@ -1,16 +1,15 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../core/utils/browser_locale.dart'; import '../../core/storage/settings_storage.dart'; class SettingsState { final ThemeMode themeMode; final Locale? locale; - SettingsState({ - required this.themeMode, - this.locale, - }); + SettingsState({required this.themeMode, this.locale}); SettingsState copyWith({ ThemeMode? themeMode, @@ -29,13 +28,27 @@ class SettingsNotifier extends StateNotifier { static const _channel = MethodChannel('com.parktrack.mobile/mapkit'); SettingsNotifier(this._storage) - : super(SettingsState(themeMode: ThemeMode.system)) { + : super(SettingsState(themeMode: ThemeMode.system)) { _loadSettings(); } Future _loadSettings() async { - final theme = await _storage.getThemeMode(); - final lang = await _storage.getLanguage(); + final themeFuture = _storage.getThemeMode(); + final languageFuture = _storage.getLanguage(); + final theme = await themeFuture; + String? lang = await languageFuture; + + if (lang == null) { + final browserLang = detectBrowserLanguage(); + if (browserLang != null) { + if (browserLang.startsWith('ru')) { + lang = 'ru'; + } else { + lang = 'en'; + } + } + } + if (lang != null) { _setMapKitLocale(lang); } @@ -61,6 +74,8 @@ class SettingsNotifier extends StateNotifier { ); } + Future setLocale(Locale locale) => setLanguage(locale.languageCode); + Future _setMapKitLocale(String lang) async { try { // MapKit uses language codes like "ru_RU" or "en_US" @@ -72,6 +87,8 @@ class SettingsNotifier extends StateNotifier { final settingsStorageProvider = Provider((ref) => SettingsStorage()); -final settingsProvider = StateNotifierProvider((ref) { - return SettingsNotifier(ref.watch(settingsStorageProvider)); -}); +final settingsProvider = StateNotifierProvider( + (ref) { + return SettingsNotifier(ref.watch(settingsStorageProvider)); + }, +); diff --git a/lib/presentation/providers/zones_provider.dart b/lib/presentation/providers/zones_provider.dart index 4384d99..add7b68 100644 --- a/lib/presentation/providers/zones_provider.dart +++ b/lib/presentation/providers/zones_provider.dart @@ -1,5 +1,6 @@ import 'package:dio/dio.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../core/network/api_exception.dart'; import '../../domain/models/zone.dart'; import 'app_providers.dart'; import 'time_selector_provider.dart'; @@ -40,12 +41,12 @@ class ZonesNotifier extends StateNotifier>> { ); if (generation != _requestGeneration || cancelToken.isCancelled) return; state = AsyncValue.data(zones); - } on DioException catch (e) { - if (CancelToken.isCancel(e) || generation != _requestGeneration) return; - state = AsyncValue.error(e, e.stackTrace); } catch (e, st) { if (generation != _requestGeneration) return; - state = AsyncValue.error(e, st); + if (e is DioException && CancelToken.isCancel(e)) return; + + final failure = AppFailure.from(e); + state = AsyncValue>.error(failure, st).copyWithPrevious(state); } finally { if (identical(_cancelToken, cancelToken)) _cancelToken = null; } @@ -75,10 +76,11 @@ class ZonesNotifier extends StateNotifier>> { } final filteredZonesProvider = Provider>((ref) { - final zonesAsync = ref.watch(rawZonesProvider); + final zones = + ref.watch(rawZonesProvider.select((value) => value.valueOrNull)) ?? + const []; final filters = ref.watch(filtersProvider); - final zones = zonesAsync.valueOrNull ?? []; return zones.where((z) { if (filters.hideInactive && !z.isActive) return false; if (filters.hideNoFreeSpots && z.freeCount == 0) return false; diff --git a/lib/presentation/screens/auth/login_screen.dart b/lib/presentation/screens/auth/login_screen.dart index 1c643d4..b744346 100644 --- a/lib/presentation/screens/auth/login_screen.dart +++ b/lib/presentation/screens/auth/login_screen.dart @@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart'; import '../../providers/auth_provider.dart'; import '../../../core/network/api_exception.dart'; import '../../../core/localization/app_localizations.dart'; +import 'widgets/language_switcher.dart'; class LoginScreen extends ConsumerStatefulWidget { const LoginScreen({super.key}); @@ -37,7 +38,10 @@ class _LoginScreenState extends ConsumerState { if (mounted) { final s = ref.read(l10nProvider); ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(e.getLocalizedMessage(s)), backgroundColor: Colors.red), + SnackBar( + content: Text(e.getLocalizedMessage(s)), + backgroundColor: Colors.red, + ), ); } } finally { @@ -49,6 +53,15 @@ class _LoginScreenState extends ConsumerState { Widget build(BuildContext context) { final s = ref.watch(l10nProvider); return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + actions: const [ + Padding( + padding: EdgeInsets.only(right: 8), + child: LanguageSwitcher(), + ), + ], + ), body: SafeArea( child: Center( child: SingleChildScrollView( @@ -58,16 +71,22 @@ class _LoginScreenState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - const Icon(Icons.local_parking, size: 64, color: Color(0xFF2E7D32)), + const Icon( + Icons.local_parking, + size: 64, + color: Color(0xFF2E7D32), + ), const SizedBox(height: 8), Text( s.appTitle, textAlign: TextAlign.center, style: Theme.of(context).textTheme.headlineMedium?.copyWith( - fontWeight: FontWeight.bold, - ), + fontWeight: FontWeight.bold, + ), ), - const SizedBox(height: 40), + const SizedBox(height: 8), + const Center(child: LanguageSwitcher()), + const SizedBox(height: 32), TextFormField( controller: _loginCtrl, decoration: InputDecoration( @@ -87,9 +106,11 @@ class _LoginScreenState extends ConsumerState { prefixIcon: const Icon(Icons.lock_outlined), border: const OutlineInputBorder(), suffixIcon: IconButton( - icon: Icon(_obscure - ? Icons.visibility_outlined - : Icons.visibility_off_outlined), + icon: Icon( + _obscure + ? Icons.visibility_outlined + : Icons.visibility_off_outlined, + ), onPressed: () => setState(() => _obscure = !_obscure), ), ), diff --git a/lib/presentation/screens/auth/password_reset_screen.dart b/lib/presentation/screens/auth/password_reset_screen.dart index a149feb..972b8c7 100644 --- a/lib/presentation/screens/auth/password_reset_screen.dart +++ b/lib/presentation/screens/auth/password_reset_screen.dart @@ -4,12 +4,14 @@ import 'package:go_router/go_router.dart'; import '../../providers/auth_provider.dart'; import '../../../core/network/api_exception.dart'; import '../../../core/localization/app_localizations.dart'; +import 'widgets/language_switcher.dart'; class PasswordResetScreen extends ConsumerStatefulWidget { const PasswordResetScreen({super.key}); @override - ConsumerState createState() => _PasswordResetScreenState(); + ConsumerState createState() => + _PasswordResetScreenState(); } class _PasswordResetScreenState extends ConsumerState { @@ -30,16 +32,19 @@ class _PasswordResetScreenState extends ConsumerState { try { await ref.read(authStateProvider.notifier).requestPasswordReset(email); if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Instructions sent to $email')), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('Instructions sent to $email'))); context.pop(); } } on ApiException catch (e) { if (mounted) { final s = ref.read(l10nProvider); ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(e.getLocalizedMessage(s)), backgroundColor: Colors.red), + SnackBar( + content: Text(e.getLocalizedMessage(s)), + backgroundColor: Colors.red, + ), ); } } finally { @@ -51,7 +56,15 @@ class _PasswordResetScreenState extends ConsumerState { Widget build(BuildContext context) { final s = ref.watch(l10nProvider); return Scaffold( - appBar: AppBar(title: Text(s.resetPassword)), + appBar: AppBar( + title: Text(s.resetPassword), + actions: const [ + Padding( + padding: EdgeInsets.only(right: 16), + child: Center(child: LanguageSwitcher()), + ), + ], + ), body: SafeArea( child: Center( child: SingleChildScrollView( @@ -61,7 +74,11 @@ class _PasswordResetScreenState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - const Icon(Icons.lock_reset_outlined, size: 64, color: Color(0xFF2E7D32)), + const Icon( + Icons.lock_reset_outlined, + size: 64, + color: Color(0xFF2E7D32), + ), const SizedBox(height: 16), Text( s.passwordResetInstructions, diff --git a/lib/presentation/screens/auth/register_screen.dart b/lib/presentation/screens/auth/register_screen.dart index 6c53285..b29e0e2 100644 --- a/lib/presentation/screens/auth/register_screen.dart +++ b/lib/presentation/screens/auth/register_screen.dart @@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart'; import '../../providers/auth_provider.dart'; import '../../../core/network/api_exception.dart'; import '../../../core/localization/app_localizations.dart'; +import 'widgets/language_switcher.dart'; class RegisterScreen extends ConsumerStatefulWidget { const RegisterScreen({super.key}); @@ -32,7 +33,9 @@ class _RegisterScreenState extends ConsumerState { if (!_formKey.currentState!.validate()) return; setState(() => _loading = true); try { - await ref.read(authStateProvider.notifier).register( + await ref + .read(authStateProvider.notifier) + .register( _emailCtrl.text.trim(), _passwordCtrl.text, fullName: _nameCtrl.text.trim(), @@ -41,7 +44,10 @@ class _RegisterScreenState extends ConsumerState { if (mounted) { final s = ref.read(l10nProvider); ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(e.getLocalizedMessage(s)), backgroundColor: Colors.red), + SnackBar( + content: Text(e.getLocalizedMessage(s)), + backgroundColor: Colors.red, + ), ); } } finally { @@ -53,7 +59,15 @@ class _RegisterScreenState extends ConsumerState { Widget build(BuildContext context) { final s = ref.watch(l10nProvider); return Scaffold( - appBar: AppBar(title: Text(s.registration)), + appBar: AppBar( + title: Text(s.registration), + actions: const [ + Padding( + padding: EdgeInsets.only(right: 16), + child: Center(child: LanguageSwitcher()), + ), + ], + ), body: SafeArea( child: SingleChildScrollView( padding: const EdgeInsets.all(24), @@ -69,7 +83,8 @@ class _RegisterScreenState extends ConsumerState { prefixIcon: const Icon(Icons.person_outlined), border: const OutlineInputBorder(), ), - validator: (v) => v == null || v.trim().isEmpty ? s.nameRequired : null, + validator: (v) => + v == null || v.trim().isEmpty ? s.nameRequired : null, ), const SizedBox(height: 16), TextFormField( @@ -80,7 +95,8 @@ class _RegisterScreenState extends ConsumerState { border: const OutlineInputBorder(), ), keyboardType: TextInputType.emailAddress, - validator: (v) => v == null || !v.contains('@') ? s.invalidEmail : null, + validator: (v) => + v == null || !v.contains('@') ? s.invalidEmail : null, ), const SizedBox(height: 16), TextFormField( @@ -90,9 +106,11 @@ class _RegisterScreenState extends ConsumerState { prefixIcon: const Icon(Icons.lock_outlined), border: const OutlineInputBorder(), suffixIcon: IconButton( - icon: Icon(_obscure - ? Icons.visibility_outlined - : Icons.visibility_off_outlined), + icon: Icon( + _obscure + ? Icons.visibility_outlined + : Icons.visibility_off_outlined, + ), onPressed: () => setState(() => _obscure = !_obscure), ), ), @@ -111,7 +129,9 @@ class _RegisterScreenState extends ConsumerState { height: 20, width: 20, child: CircularProgressIndicator( - strokeWidth: 2, color: Colors.white), + strokeWidth: 2, + color: Colors.white, + ), ) : Text(s.createAccount), ), diff --git a/lib/presentation/screens/auth/widgets/language_switcher.dart b/lib/presentation/screens/auth/widgets/language_switcher.dart new file mode 100644 index 0000000..c2ae07e --- /dev/null +++ b/lib/presentation/screens/auth/widgets/language_switcher.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../../providers/settings_provider.dart'; + +class LanguageSwitcher extends ConsumerWidget { + const LanguageSwitcher({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final settings = ref.watch(settingsProvider); + final locale = settings.locale ?? const Locale('ru'); + final isEn = locale.languageCode == 'en'; + + return Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerHighest.withValues(alpha: 0.5), + borderRadius: BorderRadius.circular(20), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + _LangButton( + label: 'RU', + isSelected: !isEn, + onTap: () => ref.read(settingsProvider.notifier).setLocale(const Locale('ru')), + ), + _LangButton( + label: 'EN', + isSelected: isEn, + onTap: () => ref.read(settingsProvider.notifier).setLocale(const Locale('en')), + ), + ], + ), + ); + } +} + +class _LangButton extends StatelessWidget { + const _LangButton({ + required this.label, + required this.isSelected, + required this.onTap, + }); + + final String label; + final bool isSelected; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: isSelected ? Theme.of(context).colorScheme.primary : Colors.transparent, + borderRadius: BorderRadius.circular(20), + ), + child: Text( + label, + style: TextStyle( + color: isSelected + ? Theme.of(context).colorScheme.onPrimary + : Theme.of(context).colorScheme.onSurfaceVariant, + fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, + fontSize: 12, + ), + ), + ), + ); + } +} diff --git a/lib/presentation/screens/map/map_screen.dart b/lib/presentation/screens/map/map_screen.dart index dc8b582..3d08a4b 100644 --- a/lib/presentation/screens/map/map_screen.dart +++ b/lib/presentation/screens/map/map_screen.dart @@ -9,6 +9,7 @@ import 'package:geolocator/geolocator.dart'; import 'package:go_router/go_router.dart'; import 'package:pointer_interceptor/pointer_interceptor.dart'; import 'package:yandex_mapkit/yandex_mapkit.dart'; +import '../../providers/app_providers.dart'; import '../../providers/zones_provider.dart'; import '../../../domain/models/zone.dart'; import '../../providers/filters_provider.dart'; @@ -16,6 +17,7 @@ import '../../providers/routing_provider.dart'; import '../../providers/navigation_provider.dart'; import '../../providers/time_selector_provider.dart'; import '../../../core/theme/app_colors.dart'; +import '../../../core/network/api_exception.dart'; import '../../../core/utils/navigation_deeplink.dart'; import '../../../core/utils/error_snackbar.dart'; import '../../../core/localization/app_localizations.dart'; @@ -31,7 +33,10 @@ import 'widgets/parking_card_sheet.dart'; import 'widgets/route_preview_sheet.dart'; class MapScreen extends ConsumerStatefulWidget { - const MapScreen({super.key}); + const MapScreen({super.key, this.initialParkingId, this.searchQuery}); + + final int? initialParkingId; + final String? searchQuery; @override ConsumerState createState() => _MapScreenState(); @@ -76,17 +81,57 @@ class _MapScreenState extends ConsumerState { @override void initState() { super.initState(); - _buildDestinationPinBitmap().then((b) { - if (mounted) setState(() => _destinationPinBytes = b); - }); - _buildUserLocationBitmap().then((b) { - if (mounted) setState(() => _userLocationBytes = b); + _loadMarkerBitmaps(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + if (widget.initialParkingId != null) { + _loadAndShowParking(widget.initialParkingId!); + } else if (widget.searchQuery != null) { + _performSearch(widget.searchQuery!); + } }); - _buildNavArrowBitmap().then((b) { - if (mounted) setState(() => _navArrowBytes = b); + } + + Future _loadMarkerBitmaps() async { + final bitmaps = await Future.wait([ + _buildDestinationPinBitmap(), + _buildUserLocationBitmap(), + _buildNavArrowBitmap(), + ]); + if (!mounted) return; + setState(() { + _destinationPinBytes = bitmaps[0]; + _userLocationBytes = bitmaps[1]; + _navArrowBytes = bitmaps[2]; }); } + Future _loadAndShowParking(int id) async { + try { + final repo = ref.read(zonesRepositoryProvider); + final zone = await repo.getZoneDetails(id); + if (!mounted) return; + _onZoneTap(zone); + final center = centroid(zone.geometry); + if (kIsWeb) { + _webMapController.move(center.latitude, center.longitude, 17); + } else { + _mapController?.moveCamera( + CameraUpdate.newCameraPosition( + CameraPosition(target: center, zoom: 17), + ), + animation: const MapAnimation(duration: 0.8), + ); + } + } catch (e) { + debugPrint('Failed to load deep-link zone $id: $e'); + } + } + + void _performSearch(String query) { + context.push('/search?q=${Uri.encodeComponent(query)}'); + } + @override void dispose() { _debounce?.cancel(); @@ -222,13 +267,18 @@ class _MapScreenState extends ConsumerState { return byteData!.buffer.asUint8List(); } - void _onCameraPositionChanged(CameraPosition position, _, _) { + void _onCameraPositionChanged( + CameraPosition position, + _, + bool cameraUpdateFinished, + ) { _lastCameraTarget = position.target; - _debounce?.cancel(); - _debounce = Timer(const Duration(milliseconds: 400), _fetchZones); if ((position.azimuth - _currentAzimuth).abs() > 2) { setState(() => _currentAzimuth = position.azimuth); } + if (!cameraUpdateFinished) return; + _debounce?.cancel(); + _debounce = Timer(const Duration(milliseconds: 400), _fetchZones); } void _onZoneTap(Zone zone) { @@ -508,7 +558,17 @@ class _MapScreenState extends ConsumerState { } Future _openSearch() async { - final bias = _userPosition != null + final webCamera = kIsWeb ? _webMapController.camera : null; + final bias = webCamera != null + ? SearchBias( + latitude: webCamera.latitude, + longitude: webCamera.longitude, + south: webCamera.south, + west: webCamera.west, + north: webCamera.north, + east: webCamera.east, + ) + : _userPosition != null ? SearchBias( latitude: _userPosition!.latitude, longitude: _userPosition!.longitude, @@ -580,8 +640,15 @@ class _MapScreenState extends ConsumerState { toLatitude: toLat, toLongitude: toLon, ); - if (webRoute == null) { - throw StateError('Yandex Maps did not return a route'); + if (webRoute == null || webRoute.points.length < 2) { + if (mounted) { + showErrorSnackBar( + context, + ref.read(l10nProvider).errorCreatingRoute, + s: ref.read(l10nProvider), + ); + } + return; } if (!mounted) return; route = webRoute.points; @@ -711,17 +778,21 @@ class _MapScreenState extends ConsumerState { }); ref.listen(rawZonesProvider, (_, next) { - next.whenOrNull( - error: (e, st) { - showErrorSnackBar( - context, - ref.read(l10nProvider).errorLoadingZones, - error: e, - stackTrace: st, - s: ref.read(l10nProvider), - ); - }, - ); + if (next.hasError && !next.isLoading) { + final error = next.error; + final message = error is AppFailure + ? error.localizedMessage(s) + : s.errorLoadingZones; + + showErrorSnackBar( + context, + message, + error: error, + stackTrace: next.stackTrace, + s: s, + onRetry: () => ref.read(rawZonesProvider.notifier).refresh(), + ); + } }); ref.listen(destinationProvider, (_, dest) { @@ -802,25 +873,47 @@ class _MapScreenState extends ConsumerState { final c = centroid(zone.geometry); zoneLat = c.latitude; zoneLon = c.longitude; - if (kIsWeb) { - _webMapController.move(c.latitude, c.longitude, 17); - } else { - await _mapController?.moveCamera( - CameraUpdate.newCameraPosition( - CameraPosition(target: c, zoom: 17), - ), - animation: const MapAnimation(duration: 0.8), - ); - } + } + } + + final selectedCandidates = route.candidates.where( + (candidate) => candidate.zoneId == route.selectedZoneId, + ); + final candidate = + selectedCandidates.firstOrNull ?? route.candidates.firstOrNull; + + if (zoneLat == null && + candidate?.routePolyline != null && + candidate!.routePolyline!.isNotEmpty) { + final lastPoint = candidate.routePolyline!.last; + zoneLat = lastPoint.latitude; + zoneLon = lastPoint.longitude; + } + + if (zoneLat != null && zoneLon != null) { + final target = Point(latitude: zoneLat, longitude: zoneLon); + final polyline = route.routePolyline ?? candidate?.routePolyline; + + if (polyline != null && polyline.isNotEmpty && kIsWeb) { + // On web, try to fit bounds for better UX + final south = polyline.map((p) => p.latitude).reduce(math.min); + final north = polyline.map((p) => p.latitude).reduce(math.max); + final west = polyline.map((p) => p.longitude).reduce(math.min); + final east = polyline.map((p) => p.longitude).reduce(math.max); + _webMapController.fitBounds(south, west, north, east); + } else if (kIsWeb) { + _webMapController.move(zoneLat, zoneLon, 17); + } else { + await _mapController?.moveCamera( + CameraUpdate.newCameraPosition( + CameraPosition(target: target, zoom: 17), + ), + animation: const MapAnimation(duration: 0.8), + ); } } setState(() { - final selectedCandidates = route.candidates.where( - (candidate) => candidate.zoneId == route.selectedZoneId, - ); - final candidate = - selectedCandidates.firstOrNull ?? route.candidates.firstOrNull; _routePolyline = route.routePolyline ?? candidate?.routePolyline; _routeDurationSeconds = candidate?.durationFromOriginSeconds?.toDouble() ?? 0; @@ -951,7 +1044,7 @@ class _MapScreenState extends ConsumerState { ); _fetchWebZones(camera); } - }, + }, onError: (Object error) { }, ) else YandexMap( @@ -970,21 +1063,6 @@ class _MapScreenState extends ConsumerState { ), ); _fetchZones(); - final pos = await _getCurrentPosition(); - if (pos != null && mounted) { - final target = Point( - latitude: 61.789114, - longitude: 34.359757, - ); - _lastCameraTarget = target; - await controller.moveCamera( - CameraUpdate.newCameraPosition( - CameraPosition(target: target, zoom: 15), - ), - animation: const MapAnimation(duration: 0.8), - ); - _fetchZones(); - } }, onCameraPositionChanged: _onCameraPositionChanged, ), @@ -1128,10 +1206,12 @@ class _MapScreenState extends ConsumerState { child: _DestinationCard( destination: destination, onFindParking: isRoutingLoading ? null : _findParking, - onNavigate: () => openYandexNavigator( - destination.latitude, - destination.longitude, - ), + onNavigate: kIsWeb + ? () {} + : () => openYandexNavigator( + destination.latitude, + destination.longitude, + ), onNavigateInApp: () => _startInAppNavigation( zoneId: 0, toLat: destination.latitude, @@ -1354,16 +1434,17 @@ class _DestinationCard extends ConsumerWidget { ), ), const SizedBox(width: 8), - Expanded( - child: FilledButton.tonal( - onPressed: onNavigate, - style: FilledButton.styleFrom( - textStyle: const TextStyle(fontSize: 13), - padding: const EdgeInsets.symmetric(vertical: 10), + if (!kIsWeb) + Expanded( + child: FilledButton.tonal( + onPressed: onNavigate, + style: FilledButton.styleFrom( + textStyle: const TextStyle(fontSize: 13), + padding: const EdgeInsets.symmetric(vertical: 10), + ), + child: Text(s.yandexNavigator), ), - child: Text(s.yandexNavigator), ), - ), ], ), ], diff --git a/lib/presentation/screens/map/widgets/pwa_install_guide.dart b/lib/presentation/screens/map/widgets/pwa_install_guide.dart new file mode 100644 index 0000000..2167023 --- /dev/null +++ b/lib/presentation/screens/map/widgets/pwa_install_guide.dart @@ -0,0 +1,2 @@ +export 'pwa_install_guide_stub.dart' + if (dart.library.js_interop) 'pwa_install_guide_web.dart'; diff --git a/lib/presentation/screens/map/widgets/pwa_install_guide_stub.dart b/lib/presentation/screens/map/widgets/pwa_install_guide_stub.dart new file mode 100644 index 0000000..94210d5 --- /dev/null +++ b/lib/presentation/screens/map/widgets/pwa_install_guide_stub.dart @@ -0,0 +1,8 @@ +import 'package:flutter/material.dart'; + +class PwaInstallGuide extends StatelessWidget { + const PwaInstallGuide({super.key}); + + @override + Widget build(BuildContext context) => const SizedBox.shrink(); +} diff --git a/lib/presentation/screens/map/widgets/pwa_install_guide_web.dart b/lib/presentation/screens/map/widgets/pwa_install_guide_web.dart new file mode 100644 index 0000000..48d9625 --- /dev/null +++ b/lib/presentation/screens/map/widgets/pwa_install_guide_web.dart @@ -0,0 +1,119 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:pointer_interceptor/pointer_interceptor.dart'; +import '../../../../core/localization/app_localizations.dart'; +import '../../../../core/utils/pwa_install_eligibility.dart'; + +class PwaInstallGuide extends ConsumerStatefulWidget { + const PwaInstallGuide({super.key}); + + @override + ConsumerState createState() => _PwaInstallGuideState(); +} + +class _PwaInstallGuideState extends ConsumerState { + bool _isVisible = false; + + @override + void initState() { + super.initState(); + _isVisible = isIosPwaEligible(); + } + + void _dismiss() { + dismissIosPwaGuide(); + setState(() => _isVisible = false); + } + + @override + Widget build(BuildContext context) { + if (!_isVisible) return const SizedBox.shrink(); + + final s = ref.watch(l10nProvider); + + return Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 32), + child: PointerInterceptor( + child: RepaintBoundary( + child: Material( + elevation: 8, + borderRadius: BorderRadius.circular(16), + color: Theme.of(context).colorScheme.surface, + child: Container( + constraints: const BoxConstraints(maxWidth: 400), + padding: const EdgeInsets.all(20), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + const Icon(Icons.install_mobile, color: Colors.blue), + const SizedBox(width: 12), + Expanded( + child: Text( + s.pwaInstallTitle, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ), + IconButton( + icon: const Icon(Icons.close), + onPressed: _dismiss, + visualDensity: VisualDensity.compact, + ), + ], + ), + const SizedBox(height: 16), + _StepRow(icon: Icons.ios_share, text: s.pwaInstallStep1), + const SizedBox(height: 12), + _StepRow( + icon: Icons.add_box_outlined, + text: s.pwaInstallStep2, + ), + const SizedBox(height: 20), + SizedBox( + width: double.infinity, + child: FilledButton( + onPressed: _dismiss, + child: const Text('OK'), + ), + ), + ], + ), + ), + ), + ), + ), + ), + ); + } +} + +class _StepRow extends StatelessWidget { + const _StepRow({required this.icon, required this.text}); + + final IconData icon; + final String text; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(8), + ), + child: Icon(icon, size: 20), + ), + const SizedBox(width: 16), + Expanded(child: Text(text, style: const TextStyle(fontSize: 14))), + ], + ); + } +} diff --git a/lib/presentation/screens/map/widgets/route_preview_sheet.dart b/lib/presentation/screens/map/widgets/route_preview_sheet.dart index 8c4ad6d..b3562e7 100644 --- a/lib/presentation/screens/map/widgets/route_preview_sheet.dart +++ b/lib/presentation/screens/map/widgets/route_preview_sheet.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../../core/theme/app_colors.dart'; @@ -51,9 +52,9 @@ class _RoutePreviewSheetState extends ConsumerState { } else { if (mounted) { final s = ref.read(l10nProvider); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(s.pointLookupError)), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(s.pointLookupError))); } return; } @@ -99,20 +100,36 @@ class _RoutePreviewSheetState extends ConsumerState { const SizedBox(height: 10), Text('${s.parkingZone}: #${widget.route.selectedZoneId}'), if (widget.route.arrivalTime != null) - Text('${s.arrival}: ${_formatArrival(widget.route.arrivalTime!, s)}'), + Text( + '${s.arrival}: ${_formatArrival(widget.route.arrivalTime!, s)}', + ), if (candidate != null) ...[ - Builder(builder: (_) { - final parts = []; - if (candidate.distanceToDestinationMeters != null) { - parts.add(formatNavDistance(candidate.distanceToDestinationMeters!, s)); - } - if (candidate.durationFromOriginSeconds != null) { - parts.add(formatNavDuration(candidate.durationFromOriginSeconds!, s)); - } - if (parts.isEmpty) return const SizedBox.shrink(); - return Text(parts.join(' • '), - style: TextStyle(color: Theme.of(context).hintColor)); - }), + Builder( + builder: (_) { + final parts = []; + if (candidate.distanceToDestinationMeters != null) { + parts.add( + formatNavDistance( + candidate.distanceToDestinationMeters!, + s, + ), + ); + } + if (candidate.durationFromOriginSeconds != null) { + parts.add( + formatNavDuration( + candidate.durationFromOriginSeconds!, + s, + ), + ); + } + if (parts.isEmpty) return const SizedBox.shrink(); + return Text( + parts.join(' • '), + style: TextStyle(color: Theme.of(context).hintColor), + ); + }, + ), ], const SizedBox(height: 18), if (widget.onNavigateInApp != null) @@ -124,23 +141,25 @@ class _RoutePreviewSheetState extends ConsumerState { icon: const Icon(Icons.map_outlined), label: Text(s.inAppRoute), ), - const SizedBox(height: 8), - FilledButton.icon( - style: FilledButton.styleFrom( - backgroundColor: widget.onNavigateInApp != null - ? Theme.of(context).colorScheme.surface - : null, - foregroundColor: widget.onNavigateInApp != null - ? AppColors.primary - : null, - side: widget.onNavigateInApp != null - ? const BorderSide(color: AppColors.primary) - : null, + if (!kIsWeb) ...[ + const SizedBox(height: 8), + FilledButton.icon( + style: FilledButton.styleFrom( + backgroundColor: widget.onNavigateInApp != null + ? Theme.of(context).colorScheme.surface + : null, + foregroundColor: widget.onNavigateInApp != null + ? AppColors.primary + : null, + side: widget.onNavigateInApp != null + ? const BorderSide(color: AppColors.primary) + : null, + ), + onPressed: _launching ? null : _launchNavigator, + icon: const Icon(Icons.navigation_outlined), + label: Text(_launching ? s.searching : s.yandexNavigator), ), - onPressed: _launching ? null : _launchNavigator, - icon: const Icon(Icons.navigation_outlined), - label: Text(_launching ? s.searching : s.yandexNavigator), - ), + ], const SizedBox(height: 8), TextButton( onPressed: () { diff --git a/lib/presentation/screens/map/widgets/web_map_types.dart b/lib/presentation/screens/map/widgets/web_map_types.dart index 0b8cd5c..1166003 100644 --- a/lib/presentation/screens/map/widgets/web_map_types.dart +++ b/lib/presentation/screens/map/widgets/web_map_types.dart @@ -22,6 +22,9 @@ class WebMapController { WebMapCamera? camera; void Function(double latitude, double longitude, double zoom)? moveHandler; void Function(double zoom)? zoomHandler; + void Function(double south, double west, double north, double east)? + fitBoundsHandler; + void Function()? retryHandler; bool get isReady => camera != null; @@ -33,9 +36,16 @@ class WebMapController { if (current != null) zoomHandler?.call(current.zoom + delta); } + void fitBounds(double south, double west, double north, double east) => + fitBoundsHandler?.call(south, west, north, east); + + void retry() => retryHandler?.call(); + void clear() { camera = null; moveHandler = null; zoomHandler = null; + fitBoundsHandler = null; + retryHandler = null; } } diff --git a/lib/presentation/screens/map/widgets/web_map_view_stub.dart b/lib/presentation/screens/map/widgets/web_map_view_stub.dart index 2c24a78..2689780 100644 --- a/lib/presentation/screens/map/widgets/web_map_view_stub.dart +++ b/lib/presentation/screens/map/widgets/web_map_view_stub.dart @@ -13,6 +13,7 @@ class WebMapView extends StatelessWidget { required this.onZoneTap, required this.onCameraChanged, required this.onMapReady, + required this.onError, this.route, this.activeRouteZoneId, this.userLatitude, @@ -30,6 +31,7 @@ class WebMapView extends StatelessWidget { final void Function(Zone zone) onZoneTap; final void Function(WebMapCamera camera) onCameraChanged; final VoidCallback onMapReady; + final void Function(Object error) onError; final List? route; final int? activeRouteZoneId; final double? userLatitude; diff --git a/lib/presentation/screens/map/widgets/web_map_view_web.dart b/lib/presentation/screens/map/widgets/web_map_view_web.dart index a30707f..bd9e054 100644 --- a/lib/presentation/screens/map/widgets/web_map_view_web.dart +++ b/lib/presentation/screens/map/widgets/web_map_view_web.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'dart:js_interop'; import 'dart:ui_web' as ui_web; +import 'package:flutter/foundation.dart' show setEquals; import 'package:flutter/material.dart'; import 'package:web/web.dart' as web; import 'package:yandex_mapkit/yandex_mapkit.dart'; @@ -13,9 +14,12 @@ import 'web_map_types.dart'; @JS('parkTrackYandexMaps.create') external void _createYandexMap( - JSString elementId, + web.HTMLElement element, + JSString locale, + JSString theme, JSFunction cameraCallback, JSFunction zoneTapCallback, + JSFunction errorCallback, ); @JS('parkTrackYandexMaps.update') @@ -32,6 +36,18 @@ external void _moveYandexMap( @JS('parkTrackYandexMaps.setZoom') external void _setYandexMapZoom(JSString elementId, JSNumber zoom); +@JS('parkTrackYandexMaps.retry') +external void _retryYandexMap(JSString elementId); + +@JS('parkTrackYandexMaps.fitBounds') +external void _fitYandexMapBounds( + JSString elementId, + JSNumber south, + JSNumber west, + JSNumber north, + JSNumber east, +); + @JS('parkTrackYandexMaps.destroy') external void _destroyYandexMap(JSString elementId); @@ -81,6 +97,14 @@ class _WebMapViewState extends State { late final String _viewType = 'parktrack-yandex-map-view-$_elementId'; web.ResizeObserver? _attachmentObserver; bool _created = false; + List? _serializedZonesSource; + Set _serializedCandidateIds = const {}; + int? _serializedActiveRouteZoneId; + List> _serializedZones = const []; + List? _serializedRouteSource; + List>? _serializedRoute; + Map _zonesById = const {}; + String? _lastStateJson; @override void initState() { @@ -103,7 +127,7 @@ class _WebMapViewState extends State { if (bounds.width <= 0 || bounds.height <= 0) return; observer.disconnect(); _attachmentObserver = null; - _initializeMap(); + _initializeMap(element); }) .toJS, ); @@ -119,7 +143,7 @@ class _WebMapViewState extends State { if (_created) _updateMap(); } - void _initializeMap() { + void _initializeMap(web.HTMLElement element) { if (_created) return; _created = true; widget.controller.moveHandler = (latitude, longitude, zoom) { @@ -128,9 +152,28 @@ class _WebMapViewState extends State { widget.controller.zoomHandler = (zoom) { _setYandexMapZoom(_elementId.toJS, zoom.toJS); }; + widget.controller.fitBoundsHandler = (south, west, north, east) { + _fitYandexMapBounds( + _elementId.toJS, + south.toJS, + west.toJS, + north.toJS, + east.toJS, + ); + }; + widget.controller.retryHandler = () { + _retryYandexMap(_elementId.toJS); + }; + + final isDark = Theme.of(context).brightness == Brightness.dark; + final locale = Localizations.localeOf(context).languageCode == 'ru' + ? 'ru_RU' + : 'en_RU'; _createYandexMap( - _elementId.toJS, + element, + locale.toJS, + (isDark ? 'dark' : 'light').toJS, ((JSString value) { final data = jsonDecode(value.toDart) as Map; final camera = WebMapCamera( @@ -148,48 +191,27 @@ class _WebMapViewState extends State { if (firstCamera) widget.onMapReady(); }).toJS, ((JSNumber zoneId) { - final id = zoneId.toDartInt; - for (final zone in widget.zones) { - if (zone.zoneId == id) { - widget.onZoneTap(zone); - break; - } - } + final zone = _zonesById[zoneId.toDartInt]; + if (zone != null) widget.onZoneTap(zone); + }).toJS, + ((JSString error) { + debugPrint('Yandex Maps Error: ${error.toDart}'); }).toJS, ); _updateMap(); } void _updateMap() { + final isDark = Theme.of(context).brightness == Brightness.dark; + final locale = Localizations.localeOf(context).languageCode == 'ru' + ? 'ru_RU' + : 'en_RU'; + final state = { - 'zones': widget.zones - .map((zone) { - final color = zoneColor(zone); - return { - 'id': zone.zoneId, - 'type': zone.zoneType == ZoneType.parallel ? 'line' : 'polygon', - 'points': zone.geometry - .map((point) => [point.latitude, point.longitude]) - .toList(growable: false), - 'color': - '#${color.toARGB32().toRadixString(16).padLeft(8, '0').substring(2)}', - 'freeCount': zone.freeCount, - 'label': color == AppColors.parkingUnknown - ? null - : zone.freeCount, - 'hasForecast': zone.hasForecast, - 'candidate': widget.candidateIds.contains(zone.zoneId), - 'active': widget.activeRouteZoneId == zone.zoneId, - 'center': () { - final point = centroid(zone.geometry); - return [point.latitude, point.longitude]; - }(), - }; - }) - .toList(growable: false), - 'route': widget.route - ?.map((point) => [point.latitude, point.longitude]) - .toList(growable: false), + 'theme': isDark ? 'dark' : 'light', + 'locale': locale, + 'zones': _zoneState(), + 'route': _routeState(), 'user': widget.userLatitude != null && widget.userLongitude != null ? [widget.userLatitude, widget.userLongitude] : null, @@ -208,7 +230,56 @@ class _WebMapViewState extends State { ? [widget.destinationLatitude, widget.destinationLongitude] : null, }; - _updateYandexMap(_elementId.toJS, jsonEncode(state).toJS); + final stateJson = jsonEncode(state); + if (stateJson == _lastStateJson) return; + _lastStateJson = stateJson; + _updateYandexMap(_elementId.toJS, stateJson.toJS); + } + + List> _zoneState() { + if (identical(_serializedZonesSource, widget.zones) && + setEquals(_serializedCandidateIds, widget.candidateIds) && + _serializedActiveRouteZoneId == widget.activeRouteZoneId) { + return _serializedZones; + } + + _serializedZonesSource = widget.zones; + _serializedCandidateIds = Set.unmodifiable(widget.candidateIds); + _serializedActiveRouteZoneId = widget.activeRouteZoneId; + _zonesById = {for (final zone in widget.zones) zone.zoneId: zone}; + _serializedZones = widget.zones + .map((zone) { + final color = zoneColor(zone); + final center = centroid(zone.geometry); + return { + 'id': zone.zoneId, + 'type': zone.zoneType == ZoneType.parallel ? 'line' : 'polygon', + 'points': zone.geometry + .map((point) => [point.latitude, point.longitude]) + .toList(growable: false), + 'color': + '#${color.toARGB32().toRadixString(16).padLeft(8, '0').substring(2)}', + 'freeCount': zone.freeCount, + 'label': color == AppColors.parkingUnknown ? null : zone.freeCount, + 'hasForecast': zone.hasForecast, + 'candidate': widget.candidateIds.contains(zone.zoneId), + 'active': widget.activeRouteZoneId == zone.zoneId, + 'center': [center.latitude, center.longitude], + }; + }) + .toList(growable: false); + return _serializedZones; + } + + List>? _routeState() { + if (identical(_serializedRouteSource, widget.route)) { + return _serializedRoute; + } + _serializedRouteSource = widget.route; + _serializedRoute = widget.route + ?.map((point) => [point.latitude, point.longitude]) + .toList(growable: false); + return _serializedRoute; } @override @@ -217,6 +288,8 @@ class _WebMapViewState extends State { _attachmentObserver = null; if (_created) _destroyYandexMap(_elementId.toJS); widget.controller.clear(); + _zonesById = const {}; + _lastStateJson = null; super.dispose(); } diff --git a/lib/presentation/screens/profile/profile_screen.dart b/lib/presentation/screens/profile/profile_screen.dart index 9ce621b..69f366d 100644 --- a/lib/presentation/screens/profile/profile_screen.dart +++ b/lib/presentation/screens/profile/profile_screen.dart @@ -112,6 +112,15 @@ class ProfileScreen extends ConsumerWidget { ), ), ), + + const SizedBox(height: 24), + Center( + child: Text( + 'ParkTrack v1.3.0+3', + style: TextStyle(color: Colors.grey.shade500, fontSize: 12), + ), + ), + const SizedBox(height: 16), ], ), ), diff --git a/lib/presentation/screens/search/place_search_service_native.dart b/lib/presentation/screens/search/place_search_service_native.dart index e031a11..09ec1c9 100644 --- a/lib/presentation/screens/search/place_search_service_native.dart +++ b/lib/presentation/screens/search/place_search_service_native.dart @@ -1,72 +1,191 @@ +import 'dart:async'; + import 'package:yandex_mapkit/yandex_mapkit.dart'; import 'place_search_models.dart'; class PlaceSearchService { + static const _suggestTimeout = Duration(seconds: 8); + static const _searchTimeout = Duration(seconds: 8); + static const _sessionCloseTimeout = Duration(seconds: 1); + SuggestSession? _suggestSession; SearchSession? _searchSession; + int _suggestGeneration = 0; Future> suggestions( String text, PlaceSearchBounds bounds, ) async { + final generation = ++_suggestGeneration; + final previousSession = _suggestSession; final resultWithSession = YandexSuggest.getSuggestions( text: text, boundingBox: _boundingBox(bounds), - suggestOptions: const SuggestOptions( + suggestOptions: SuggestOptions( suggestType: SuggestType.unspecified, + suggestWords: true, + userPosition: _center(bounds), ), ); - _suggestSession?.close(); _suggestSession = resultWithSession.session; - final result = await resultWithSession.result.timeout( - const Duration(seconds: 5), - ); - return (result.items ?? const []) - .map( - (item) => PlaceSuggestion( - title: item.title, - subtitle: item.subtitle, - searchText: item.searchText, - latitude: item.center?.latitude, - longitude: item.center?.longitude, - ), - ) - .toList(growable: false); + if (previousSession != null && !previousSession.isClosed) { + unawaited(_closeSuggestSession(previousSession)); + } + + try { + final result = await resultWithSession.result.timeout(_suggestTimeout); + if (generation != _suggestGeneration) return const []; + if (result.error != null) { + return _searchSuggestions(text, bounds); + } + return (result.items ?? const []) + .map( + (item) => PlaceSuggestion( + title: item.title, + subtitle: item.subtitle, + searchText: item.searchText, + latitude: item.center?.latitude, + longitude: item.center?.longitude, + ), + ) + .toList(growable: false); + } on TimeoutException { + if (generation != _suggestGeneration) return const []; + return _searchSuggestions(text, bounds); + } finally { + unawaited(_closeSuggestSession(resultWithSession.session)); + if (identical(_suggestSession, resultWithSession.session)) { + _suggestSession = null; + } + } } Future pointByText( String text, PlaceSearchBounds bounds, ) async { + final previousSession = _searchSession; final resultWithSession = YandexSearch.searchByText( searchText: text, geometry: Geometry.fromBoundingBox(_boundingBox(bounds)), - searchOptions: const SearchOptions(searchType: SearchType.geo), + searchOptions: SearchOptions( + searchType: SearchType.geo, + geometry: true, + resultPageSize: 1, + userPosition: _center(bounds), + ), ); - _searchSession?.close(); _searchSession = resultWithSession.session; - final result = await resultWithSession.result.timeout( - const Duration(seconds: 5), + if (previousSession != null && !previousSession.isClosed) { + unawaited(_closeSearchSession(previousSession)); + } + + try { + final result = await resultWithSession.result.timeout(_searchTimeout); + if (result.error != null) throw StateError(result.error!); + final point = _firstPoint(result.items?.firstOrNull); + return point == null + ? null + : PlaceSearchPoint( + latitude: point.latitude, + longitude: point.longitude, + ); + } finally { + unawaited(_closeSearchSession(resultWithSession.session)); + if (identical(_searchSession, resultWithSession.session)) { + _searchSession = null; + } + } + } + + Future> _searchSuggestions( + String text, + PlaceSearchBounds bounds, + ) async { + final previousSession = _searchSession; + final resultWithSession = YandexSearch.searchByText( + searchText: text, + geometry: Geometry.fromBoundingBox(_boundingBox(bounds)), + searchOptions: SearchOptions( + geometry: true, + resultPageSize: 10, + userPosition: _center(bounds), + ), ); - final point = result.items?.firstOrNull?.geometry.firstOrNull?.point; - return point == null - ? null - : PlaceSearchPoint( - latitude: point.latitude, - longitude: point.longitude, - ); + _searchSession = resultWithSession.session; + if (previousSession != null && !previousSession.isClosed) { + unawaited(_closeSearchSession(previousSession)); + } + + try { + final result = await resultWithSession.result.timeout(_searchTimeout); + if (result.error != null) throw StateError(result.error!); + return (result.items ?? const []) + .map((item) { + final point = _firstPoint(item); + final subtitle = + item.businessMetadata?.address.formattedAddress ?? + item.toponymMetadata?.address.formattedAddress; + return PlaceSuggestion( + title: item.name, + subtitle: subtitle == item.name ? null : subtitle, + searchText: [item.name, ?subtitle].join(', '), + latitude: point?.latitude, + longitude: point?.longitude, + ); + }) + .toList(growable: false); + } finally { + unawaited(_closeSearchSession(resultWithSession.session)); + if (identical(_searchSession, resultWithSession.session)) { + _searchSession = null; + } + } } + Point? _firstPoint(SearchItem? item) => + item?.geometry.map((geometry) => geometry.point).nonNulls.firstOrNull; + + Point _center(PlaceSearchBounds bounds) => Point( + latitude: (bounds.south + bounds.north) / 2, + longitude: (bounds.west + bounds.east) / 2, + ); + BoundingBox _boundingBox(PlaceSearchBounds bounds) => BoundingBox( southWest: Point(latitude: bounds.south, longitude: bounds.west), northEast: Point(latitude: bounds.north, longitude: bounds.east), ); + Future _closeSuggestSession(SuggestSession session) async { + if (session.isClosed) return; + try { + await session.close().timeout(_sessionCloseTimeout); + } catch (_) { + // The native session may already have been released after completion. + } + } + + Future _closeSearchSession(SearchSession session) async { + if (session.isClosed) return; + try { + await session.close().timeout(_sessionCloseTimeout); + } catch (_) { + // The native session may already have been released after completion. + } + } + void dispose() { - _suggestSession?.close(); + _suggestGeneration++; + final suggestSession = _suggestSession; _suggestSession = null; - _searchSession?.close(); + if (suggestSession != null) { + unawaited(_closeSuggestSession(suggestSession)); + } + final searchSession = _searchSession; _searchSession = null; + if (searchSession != null) { + unawaited(_closeSearchSession(searchSession)); + } } } diff --git a/lib/presentation/screens/search/search_screen.dart b/lib/presentation/screens/search/search_screen.dart index 27c49e4..dbd1285 100644 --- a/lib/presentation/screens/search/search_screen.dart +++ b/lib/presentation/screens/search/search_screen.dart @@ -10,7 +10,9 @@ import 'place_search_models.dart'; import 'place_search_service.dart'; class SearchScreen extends ConsumerStatefulWidget { - const SearchScreen({super.key}); + const SearchScreen({super.key, this.initialQuery}); + + final String? initialQuery; @override ConsumerState createState() => _SearchScreenState(); @@ -42,10 +44,11 @@ class _SearchScreenState extends ConsumerState { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { _focusNode.requestFocus(); - final saved = ref.read(searchQueryProvider); - if (saved.isNotEmpty) { - _controller.text = saved; - _suggest(saved); + final q = widget.initialQuery ?? ref.read(searchQueryProvider); + if (q != null && q.isNotEmpty) { + _controller.text = q; + ref.read(searchQueryProvider.notifier).state = q; + _suggest(q); } }); } @@ -62,8 +65,8 @@ class _SearchScreenState extends ConsumerState { void _onChanged(String text) { ref.read(searchQueryProvider.notifier).state = text; _debounce?.cancel(); + _suggestSeq++; if (text.trim().isEmpty) { - _suggestSeq++; setState(() { _suggestions = []; _loading = false; @@ -83,31 +86,45 @@ class _SearchScreenState extends ConsumerState { } PlaceSearchBounds _buildLocalBoundingBox(SearchBias bias) { - final south = _clamp(bias.latitude - _biasLatDelta, -90.0, 90.0); - final north = _clamp(bias.latitude + _biasLatDelta, -90.0, 90.0); - final west = _clamp(bias.longitude - _biasLonDelta, -180.0, 180.0); - final east = _clamp(bias.longitude + _biasLonDelta, -180.0, 180.0); + final south = bias.south; + final west = bias.west; + final north = bias.north; + final east = bias.east; + if (south != null && + west != null && + north != null && + east != null && + north > south && + east > west) { + return PlaceSearchBounds( + south: south, + west: west, + north: north, + east: east, + ); + } + + final fallbackSouth = _clamp(bias.latitude - _biasLatDelta, -90.0, 90.0); + final fallbackNorth = _clamp(bias.latitude + _biasLatDelta, -90.0, 90.0); + final fallbackWest = _clamp(bias.longitude - _biasLonDelta, -180.0, 180.0); + final fallbackEast = _clamp(bias.longitude + _biasLonDelta, -180.0, 180.0); return PlaceSearchBounds( - south: south, - west: west, - north: north, - east: east, + south: fallbackSouth, + west: fallbackWest, + north: fallbackNorth, + east: fallbackEast, ); } Future> _loadSuggestions( String text, PlaceSearchBounds boundingBox, - ) => _searchService - .suggestions(text, boundingBox) - .timeout(const Duration(seconds: 5)); + ) => _searchService.suggestions(text, boundingBox); Future _searchPointByText( String text, PlaceSearchBounds boundingBox, - ) => _searchService - .pointByText(text, boundingBox) - .timeout(const Duration(seconds: 5)); + ) => _searchService.pointByText(text, boundingBox); Future _suggest(String text) async { if (!mounted) return; diff --git a/pubspec.lock b/pubspec.lock index aa77701..273b4c0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -377,7 +377,7 @@ packages: source: sdk version: "0.0.0" flutter_web_plugins: - dependency: transitive + dependency: "direct main" description: flutter source: sdk version: "0.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index c791e93..696ed65 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: mobile description: "ParkTrack — мобильное приложение для поиска парковок" publish_to: 'none' -version: 1.2.0+2 +version: 1.3.0+3 environment: sdk: ^3.11.5 @@ -12,6 +12,8 @@ dependencies: sdk: flutter flutter_localizations: sdk: flutter + flutter_web_plugins: + sdk: flutter # State management flutter_riverpod: ^2.6.1 diff --git a/test/core/network/app_failure_test.dart b/test/core/network/app_failure_test.dart new file mode 100644 index 0000000..086c6b2 --- /dev/null +++ b/test/core/network/app_failure_test.dart @@ -0,0 +1,76 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:dio/dio.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:mobile/core/network/api_exception.dart'; + +void main() { + final request = RequestOptions(path: '/test'); + + test('classifies transport and timeout errors', () { + expect( + AppFailure.from(const SocketException('offline')).kind, + AppFailureKind.noInternet, + ); + expect( + AppFailure.from(TimeoutException('late')).kind, + AppFailureKind.timeout, + ); + expect( + AppFailure.from( + DioException( + requestOptions: request, + type: DioExceptionType.connectionTimeout, + ), + ).kind, + AppFailureKind.timeout, + ); + }); + + test('classifies HTTP failures without exposing the response body', () { + final unavailable = DioException( + requestOptions: request, + type: DioExceptionType.badResponse, + response: Response(requestOptions: request, statusCode: 503), + ); + final server = DioException( + requestOptions: request, + type: DioExceptionType.badResponse, + response: Response(requestOptions: request, statusCode: 502), + ); + + expect( + AppFailure.from(unavailable).kind, + AppFailureKind.serviceUnavailable, + ); + expect(AppFailure.from(server).kind, AppFailureKind.server); + }); + + test('classifies location platform failures', () { + expect( + AppFailure.from(const LocationServiceDisabledException()).kind, + AppFailureKind.locationServicesDisabled, + ); + expect( + AppFailure.from(const PermissionDeniedException('denied')).kind, + AppFailureKind.locationPermissionDenied, + ); + expect( + AppFailure.from(PlatformException(code: 'PERMISSION_DENIED')).kind, + AppFailureKind.locationPermissionDenied, + ); + }); + + test('uses a safe caller-provided fallback for unknown errors', () { + expect( + AppFailure.from( + StateError('technical detail'), + fallback: AppFailureKind.routeLoad, + ).kind, + AppFailureKind.routeLoad, + ); + }); +} diff --git a/test/core/router/deep_link_coordinator_test.dart b/test/core/router/deep_link_coordinator_test.dart new file mode 100644 index 0000000..c002222 --- /dev/null +++ b/test/core/router/deep_link_coordinator_test.dart @@ -0,0 +1,65 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mobile/core/router/deep_link_coordinator.dart'; + +void main() { + group('DeepLinkCoordinator', () { + late DeepLinkCoordinator coordinator; + + setUp(() => coordinator = DeepLinkCoordinator()); + + test('accepts the supported map URL', () { + expect(coordinator.safeLocation(Uri.parse('/map')), '/map'); + }); + + test('accepts a parking URL with a positive zone id', () { + expect(coordinator.safeLocation(Uri.parse('/parking/42')), '/parking/42'); + expect(coordinator.safeLocation(Uri.parse('/parking/nope')), '/map'); + }); + + test('preserves a search query', () { + expect( + coordinator.safeLocation( + Uri.parse('/search?q=Lenina%20Street&ignored=value'), + ), + '/search?q=Lenina+Street', + ); + }); + + test('validates destination coordinates and query parameters', () { + expect( + coordinator.safeLocation( + Uri.parse('/destination?lat=61.789114&lon=34.359757&name=Station'), + ), + '/destination?lat=61.789114&lon=34.359757&name=Station', + ); + expect( + coordinator.safeLocation(Uri.parse('/destination?lat=100&lon=0')), + '/map', + ); + }); + + test('keeps supported map query parameters only', () { + expect( + coordinator.safeLocation( + Uri.parse('/map?zoneId=7&lat=10&lon=20&name=Office&token=secret'), + ), + '/map?zoneId=7&lat=10.0&lon=20.0&name=Office', + ); + }); + + test('rejects unknown and external redirect locations', () { + expect(coordinator.safeLocation(Uri.parse('/unknown')), '/map'); + expect( + coordinator.safeLocation(Uri.parse('https://example.com/map')), + '/map', + ); + }); + + test('restores a remembered destination once after authentication', () { + coordinator.remember(Uri.parse('/parking/42')); + + expect(coordinator.takePendingOr('/map'), '/parking/42'); + expect(coordinator.takePendingOr('/search?q=park'), '/search?q=park'); + }); + }); +} diff --git a/test/core/utils/error_snackbar_test.dart b/test/core/utils/error_snackbar_test.dart new file mode 100644 index 0000000..48eae3b --- /dev/null +++ b/test/core/utils/error_snackbar_test.dart @@ -0,0 +1,25 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mobile/core/utils/error_snackbar.dart'; + +void main() { + test('suppresses only repeated messages inside the interval', () { + final deduplicator = ErrorMessageDeduplicator( + interval: const Duration(seconds: 5), + ); + final now = DateTime(2026); + + expect(deduplicator.shouldShow('offline', now), isTrue); + expect( + deduplicator.shouldShow('offline', now.add(const Duration(seconds: 2))), + isFalse, + ); + expect( + deduplicator.shouldShow('timeout', now.add(const Duration(seconds: 2))), + isTrue, + ); + expect( + deduplicator.shouldShow('offline', now.add(const Duration(seconds: 6))), + isTrue, + ); + }); +} diff --git a/test/core/utils/pwa_install_eligibility_test.dart b/test/core/utils/pwa_install_eligibility_test.dart new file mode 100644 index 0000000..4d06b83 --- /dev/null +++ b/test/core/utils/pwa_install_eligibility_test.dart @@ -0,0 +1,55 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mobile/core/utils/pwa_install_eligibility.dart'; + +void main() { + bool eligible({ + String userAgent = 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0)', + String platform = 'iPhone', + int maxTouchPoints = 5, + bool navigatorStandalone = false, + bool displayModeStandalone = false, + String? dismissedVersion, + bool isWeb = true, + }) => shouldShowPwaInstallGuide( + isWeb: isWeb, + userAgent: userAgent, + platform: platform, + maxTouchPoints: maxTouchPoints, + navigatorStandalone: navigatorStandalone, + displayModeStandalone: displayModeStandalone, + dismissedVersion: dismissedVersion, + ); + + test('shows in iPhone and desktop-mode iPad browsers', () { + expect(eligible(), isTrue); + expect( + eligible( + userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X)', + platform: 'MacIntel', + maxTouchPoints: 5, + ), + isTrue, + ); + }); + + test('never shows for installed standalone PWA', () { + expect(eligible(navigatorStandalone: true), isFalse); + expect(eligible(displayModeStandalone: true), isFalse); + }); + + test('does not show on Android, native, or after dismissal', () { + expect( + eligible( + userAgent: 'Mozilla/5.0 (Linux; Android 15)', + platform: 'Linux armv8l', + ), + isFalse, + ); + expect(eligible(isWeb: false), isFalse); + expect(eligible(dismissedVersion: pwaInstallGuideVersion), isFalse); + }); + + test('a future guide version can be shown again', () { + expect(eligible(dismissedVersion: '0'), isTrue); + }); +} diff --git a/test/presentation/auth/language_switcher_test.dart b/test/presentation/auth/language_switcher_test.dart new file mode 100644 index 0000000..60ecdeb --- /dev/null +++ b/test/presentation/auth/language_switcher_test.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mobile/core/storage/settings_storage.dart'; +import 'package:mobile/presentation/providers/settings_provider.dart'; +import 'package:mobile/presentation/screens/auth/register_screen.dart'; + +class _MemorySettingsStorage extends SettingsStorage { + String? language; + + @override + Future getThemeMode() async => ThemeMode.system; + + @override + Future getLanguage() async => language; + + @override + Future saveLanguage(String? languageCode) async { + language = languageCode; + } +} + +void main() { + testWidgets('switches language before authentication without overflow', ( + tester, + ) async { + tester.view.physicalSize = const Size(280, 600); + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + final storage = _MemorySettingsStorage()..language = 'ru'; + + await tester.pumpWidget( + ProviderScope( + overrides: [settingsStorageProvider.overrideWithValue(storage)], + child: const _TestApp(), + ), + ); + await tester.pumpAndSettle(); + + await tester.tap(find.text('EN')); + await tester.pumpAndSettle(); + + expect(storage.language, 'en'); + expect(find.text('Registration'), findsOneWidget); + expect(tester.takeException(), isNull); + }); +} + +class _TestApp extends ConsumerWidget { + const _TestApp(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final settings = ref.watch(settingsProvider); + return MaterialApp( + locale: settings.locale, + supportedLocales: const [Locale('ru'), Locale('en')], + localizationsDelegates: const [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + home: const RegisterScreen(), + ); + } +} diff --git a/test/presentation/map/web_map_types_test.dart b/test/presentation/map/web_map_types_test.dart new file mode 100644 index 0000000..25662d2 --- /dev/null +++ b/test/presentation/map/web_map_types_test.dart @@ -0,0 +1,31 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mobile/presentation/screens/map/widgets/web_map_types.dart'; + +void main() { + test('web map controller forwards zoom, move and retry controls', () { + final controller = WebMapController(); + controller.camera = const WebMapCamera( + latitude: 61.7, + longitude: 34.3, + zoom: 14, + west: 34, + south: 61, + east: 35, + north: 62, + ); + double? zoom; + (double, double, double)? move; + var retries = 0; + controller.zoomHandler = (value) => zoom = value; + controller.moveHandler = (lat, lon, value) => move = (lat, lon, value); + controller.retryHandler = () => retries++; + + controller.zoomBy(1); + controller.move(60, 30, 16); + controller.retry(); + + expect(zoom, 15); + expect(move, (60, 30, 16)); + expect(retries, 1); + }); +} diff --git a/test/presentation/providers/settings_provider_test.dart b/test/presentation/providers/settings_provider_test.dart new file mode 100644 index 0000000..2cddc0b --- /dev/null +++ b/test/presentation/providers/settings_provider_test.dart @@ -0,0 +1,39 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mobile/core/storage/settings_storage.dart'; +import 'package:mobile/presentation/providers/settings_provider.dart'; + +class _DelayedSettingsStorage extends SettingsStorage { + final saveCompleter = Completer(); + String? savedLanguage; + + @override + Future getThemeMode() async => ThemeMode.system; + + @override + Future getLanguage() async => null; + + @override + Future saveLanguage(String? languageCode) async { + savedLanguage = languageCode; + await saveCompleter.future; + } +} + +void main() { + test('updates locale before persistence completes', () async { + final storage = _DelayedSettingsStorage(); + final notifier = SettingsNotifier(storage); + await Future.delayed(Duration.zero); + + final pendingSave = notifier.setLanguage('en'); + + expect(notifier.state.locale, const Locale('en')); + expect(storage.savedLanguage, 'en'); + storage.saveCompleter.complete(); + await pendingSave; + notifier.dispose(); + }); +} diff --git a/test/presentation/providers/zones_provider_test.dart b/test/presentation/providers/zones_provider_test.dart new file mode 100644 index 0000000..0939964 --- /dev/null +++ b/test/presentation/providers/zones_provider_test.dart @@ -0,0 +1,64 @@ +import 'package:dio/dio.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mobile/data/api/forecasts_api.dart'; +import 'package:mobile/data/api/occupancy_api.dart'; +import 'package:mobile/data/api/zones_api.dart'; +import 'package:mobile/data/repositories/zones_repository.dart'; +import 'package:mobile/domain/models/zone.dart'; +import 'package:mobile/presentation/providers/app_providers.dart'; +import 'package:mobile/presentation/providers/zones_provider.dart'; + +class _FakeZonesRepository extends ZonesRepository { + _FakeZonesRepository() + : super(ZonesApi(Dio()), OccupancyApi(Dio()), ForecastsApi(Dio())); + + bool fail = false; + final cachedZone = const Zone( + zoneId: 7, + zoneType: ZoneType.standard, + capacity: 10, + freeCount: 3, + confidence: 0.8, + pay: 0, + geometry: [], + ); + + @override + Future> getZonesNow( + String bbox, { + CancelToken? cancelToken, + }) async { + if (fail) { + throw DioException.connectionError( + requestOptions: RequestOptions(path: '/zones'), + reason: 'offline', + ); + } + return [cachedZone]; + } +} + +void main() { + test('keeps cached zones when background refresh fails', () async { + final repository = _FakeZonesRepository(); + final container = ProviderContainer( + overrides: [zonesRepositoryProvider.overrideWithValue(repository)], + ); + addTearDown(container.dispose); + + final notifier = container.read(rawZonesProvider.notifier); + await notifier.fetchZones('1,2,3,4'); + expect(container.read(rawZonesProvider).valueOrNull, [ + repository.cachedZone, + ]); + + repository.fail = true; + await notifier.fetchZones('1,2,3,4', force: true); + + final state = container.read(rawZonesProvider); + expect(state.hasError, isTrue); + expect(state.hasValue, isTrue); + expect(state.valueOrNull, [repository.cachedZone]); + }); +} diff --git a/test/web/app_links_assets_test.dart b/test/web/app_links_assets_test.dart new file mode 100644 index 0000000..e7c8a25 --- /dev/null +++ b/test/web/app_links_assets_test.dart @@ -0,0 +1,48 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('PWA manifest is scoped for path-based app links', () { + final manifest = + jsonDecode(File('web/manifest.json').readAsStringSync()) + as Map; + + expect(manifest['id'], '/'); + expect(manifest['start_url'], '/map'); + expect(manifest['scope'], '/'); + expect( + (manifest['launch_handler'] as Map)['client_mode'], + 'navigate-existing', + ); + }); + + test('Android intent filter handles only the ParkTrack mobile host', () { + final manifest = File( + 'android/app/src/main/AndroidManifest.xml', + ).readAsStringSync(); + + expect(manifest, contains('android:autoVerify="true"')); + expect(manifest, contains('android:scheme="https"')); + expect(manifest, contains('android:host="m.parktrack.live"')); + expect(manifest, isNot(contains('android:host="*.parktrack.live"'))); + }); + + test('asset links placeholder stays valid and never uses a debug key', () { + final assetLinks = jsonDecode( + File('web/.well-known/assetlinks.json').readAsStringSync(), + ); + + expect(assetLinks, isA>()); + expect(assetLinks, isEmpty); + }); + + test('web deployment provides a direct-path fallback', () { + final workflow = File( + '.github/workflows/deploy-web.yml', + ).readAsStringSync(); + + expect(workflow, contains('cp build/web/index.html build/web/404.html')); + }); +} diff --git a/test/web/yandex_maps_bridge_test.dart b/test/web/yandex_maps_bridge_test.dart new file mode 100644 index 0000000..92cb3f9 --- /dev/null +++ b/test/web/yandex_maps_bridge_test.dart @@ -0,0 +1,30 @@ +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; + +void main() { + late String bridge; + + setUpAll(() { + bridge = File('web/yandex_maps.js').readAsStringSync(); + }); + + test('keeps rendering and service contracts implemented', () { + expect(bridge, contains('window.ymaps.route')); + expect(bridge, contains('window.ymaps.geocode')); + expect(bridge, contains('SearchControl')); + expect(bridge, isNot(contains("return JSON.stringify({ points: []"))); + expect(bridge, isNot(contains('return JSON.stringify([])'))); + }); + + test('uses supported v3 interactions and real themes', () { + expect( + bridge, + contains("behaviors: ['drag', 'scrollZoom', 'pinchZoom', 'dblClick']"), + ); + expect(bridge, contains("state.theme === 'dark' ? 'dark' : 'light'")); + expect(bridge, isNot(contains('filter: invert'))); + expect(bridge, isNot(contains('grayscale('))); + expect(bridge, isNot(contains('YMapOpenMapsButton'))); + }); +} diff --git a/web/.well-known/assetlinks.json b/web/.well-known/assetlinks.json new file mode 100644 index 0000000..643dd46 --- /dev/null +++ b/web/.well-known/assetlinks.json @@ -0,0 +1,12 @@ +[ + { + "relation": ["delegate_permission/common.handle_all_urls"], + "target": { + "namespace": "android_app", + "package_name": "com.parktrack.mobile", + "sha256_cert_fingerprints": [ + "B8:0A:91:ED:3C:71:8F:0B:21:53:82:9D:85:41:AA:93:CB:A0:54:1F:AF:E2:ED:4E:F4:61:4A:F9:75:A6:ED:D0" + ] + } + } +] diff --git a/web/index.html b/web/index.html index aae712f..61f45a7 100644 --- a/web/index.html +++ b/web/index.html @@ -1,5 +1,5 @@ - + + + + + + + **App integrity**. -4. Find the **App signing key certificate** section. -5. Copy the **SHA-256 certificate fingerprint**. +Backward-compatible map links are also accepted: -If you are using a local keystore: -Run the following command: -```bash -keytool -list -v -keystore path/to/your/keystore.jks -``` -Then find the SHA-256 value. +- `https://m.parktrack.live/map?id=42`; +- `https://m.parktrack.live/map?zoneId=42`; +- `https://m.parktrack.live/map?q=station`; +- `https://m.parktrack.live/map/parking/42`; +- the corresponding `parktrack://map?...` links. -## Update assetlinks.json -Once you have the fingerprint, update `web/.well-known/assetlinks.json`: -```json -"sha256_cert_fingerprints": [ - "PASTE_YOUR_FINGERPRINT_HERE" -] -``` +Only exact `https://m.parktrack.live` links, `parktrack://` links, and internal +relative paths are accepted as application destinations. Unknown or malformed +links safely fall back to `/map`. Protected sections preserve their destination +through authentication. diff --git a/lib/core/router/app_router.dart b/lib/core/router/app_router.dart index 6a77ea0..35b735e 100644 --- a/lib/core/router/app_router.dart +++ b/lib/core/router/app_router.dart @@ -1,6 +1,8 @@ +import 'package:flutter/widgets.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../presentation/providers/auth_provider.dart'; +import '../../presentation/providers/routing_provider.dart'; import '../../presentation/screens/splash/splash_screen.dart'; import '../../presentation/screens/auth/login_screen.dart'; import '../../presentation/screens/auth/register_screen.dart'; @@ -9,16 +11,33 @@ import '../../presentation/screens/profile/profile_screen.dart'; import '../../presentation/screens/profile/edit_profile_screen.dart'; import '../../presentation/screens/search/search_screen.dart'; import '../../presentation/screens/auth/password_reset_screen.dart'; +import 'deep_link_coordinator.dart'; final routerProvider = Provider((ref) { final authState = ref.watch(authStateProvider); + final deepLinks = DeepLinkCoordinator(); return GoRouter( initialLocation: '/', + onException: (_, state, router) { + router.go(deepLinks.safeLocation(state.uri)); + }, redirect: (context, state) { - final isLoading = authState.maybeWhen(loading: () => true, orElse: () => false); - final isAuth = authState.maybeWhen(authenticated: (_) => true, orElse: () => false); - final isUnauth = authState.maybeWhen(unauthenticated: () => true, orElse: () => false); + if (state.uri.hasScheme || state.uri.hasAuthority) { + return deepLinks.safeLocation(state.uri); + } + final isLoading = authState.maybeWhen( + loading: () => true, + orElse: () => false, + ); + final isAuth = authState.maybeWhen( + authenticated: (_) => true, + orElse: () => false, + ); + final isUnauth = authState.maybeWhen( + unauthenticated: () => true, + orElse: () => false, + ); if (isLoading) { return state.matchedLocation == '/' ? null : '/'; @@ -53,23 +72,56 @@ final routerProvider = Provider((ref) { GoRoute( path: '/map', builder: (context, state) { - final id = int.tryParse(state.uri.queryParameters['id'] ?? ''); + final id = int.tryParse( + state.uri.queryParameters['id'] ?? + state.uri.queryParameters['zoneId'] ?? + '', + ); final query = state.uri.queryParameters['q']; - return MapScreen(initialParkingId: id, searchQuery: query); + return MapScreen( + key: ValueKey(state.uri.toString()), + initialParkingId: id, + searchQuery: query, + initialDestination: destinationFromDeepLink(state.uri), + ); }, routes: [ GoRoute( path: 'parking/:id', builder: (context, state) { final id = int.tryParse(state.pathParameters['id'] ?? ''); - return MapScreen(initialParkingId: id); + return MapScreen( + key: ValueKey(state.uri.toString()), + initialParkingId: id, + ); }, ), ], ), - GoRoute(path: '/profile', builder: (_, _) => const ProfileScreen(), routes: [ - GoRoute(path: 'edit', builder: (_, _) => const EditProfileScreen()), - ]), + GoRoute( + path: '/parking/:id', + builder: (context, state) { + final id = int.tryParse(state.pathParameters['id'] ?? ''); + return MapScreen( + key: ValueKey(state.uri.toString()), + initialParkingId: id, + ); + }, + ), + GoRoute( + path: '/destination', + builder: (context, state) => MapScreen( + key: ValueKey(state.uri.toString()), + initialDestination: destinationFromDeepLink(state.uri), + ), + ), + GoRoute( + path: '/profile', + builder: (_, _) => const ProfileScreen(), + routes: [ + GoRoute(path: 'edit', builder: (_, _) => const EditProfileScreen()), + ], + ), GoRoute( path: '/search', builder: (context, state) { @@ -77,7 +129,30 @@ final routerProvider = Provider((ref) { return SearchScreen(initialQuery: q); }, ), - GoRoute(path: '/password-reset', builder: (_, _) => const PasswordResetScreen()), + GoRoute( + path: '/password-reset', + builder: (_, _) => const PasswordResetScreen(), + ), ], ); }); + +Destination? destinationFromDeepLink(Uri uri) { + final latitude = double.tryParse(uri.queryParameters['lat'] ?? ''); + final longitude = double.tryParse(uri.queryParameters['lon'] ?? ''); + if (latitude == null || + longitude == null || + !latitude.isFinite || + !longitude.isFinite || + latitude < -90 || + latitude > 90 || + longitude < -180 || + longitude > 180) { + return null; + } + return Destination( + latitude: latitude, + longitude: longitude, + name: uri.queryParameters['name'], + ); +} diff --git a/lib/core/router/deep_link_coordinator.dart b/lib/core/router/deep_link_coordinator.dart index 956bf7f..7758360 100644 --- a/lib/core/router/deep_link_coordinator.dart +++ b/lib/core/router/deep_link_coordinator.dart @@ -1,4 +1,6 @@ class DeepLinkCoordinator { + static const mobileHost = 'm.parktrack.live'; + Uri? _pendingDestination; void remember(Uri destination) { @@ -13,48 +15,82 @@ class DeepLinkCoordinator { } String safeLocation(Uri? uri) { - if (uri == null || uri.hasScheme || uri.hasAuthority) return '/map'; - final segments = uri.pathSegments; + final internal = _toInternalUri(uri); + if (internal == null) return '/map'; + final segments = internal.pathSegments; - if (uri.path == '/map') { - return Uri(path: '/map', queryParameters: _mapParameters(uri)).toString(); + if (internal.path == '/map') { + return Uri( + path: '/map', + queryParameters: _mapParameters(internal), + ).toString(); } if (segments.length == 2 && segments.first == 'parking') { final zoneId = int.tryParse(segments[1]); return zoneId != null && zoneId > 0 ? '/parking/$zoneId' : '/map'; } - if (uri.path == '/destination') { - final latitude = double.tryParse(uri.queryParameters['lat'] ?? ''); - final longitude = double.tryParse(uri.queryParameters['lon'] ?? ''); + if (segments.length == 3 && + segments.first == 'map' && + segments[1] == 'parking') { + final zoneId = int.tryParse(segments[2]); + return zoneId != null && zoneId > 0 ? '/parking/$zoneId' : '/map'; + } + if (internal.path == '/destination') { + final latitude = double.tryParse(internal.queryParameters['lat'] ?? ''); + final longitude = double.tryParse(internal.queryParameters['lon'] ?? ''); if (!_validCoordinates(latitude, longitude)) return '/map'; final queryParameters = { 'lat': latitude.toString(), 'lon': longitude.toString(), }; - final name = uri.queryParameters['name']; + final name = internal.queryParameters['name']; if (name != null && name.isNotEmpty) queryParameters['name'] = name; return Uri( path: '/destination', queryParameters: queryParameters, ).toString(); } - if (uri.path == '/search') { - final query = uri.queryParameters['q']; + if (internal.path == '/search') { + final query = internal.queryParameters['q']; return Uri( path: '/search', queryParameters: query == null || query.isEmpty ? null : {'q': query}, ).toString(); } - if (uri.path == '/profile' || uri.path == '/profile/edit') { - return uri.path; + if (internal.path == '/profile' || internal.path == '/profile/edit') { + return internal.path; + } + if (internal.path == '/login' || + internal.path == '/register' || + internal.path == '/password-reset') { + return internal.path; } return '/map'; } + Uri? _toInternalUri(Uri? uri) { + if (uri == null) return null; + if (!uri.hasScheme && !uri.hasAuthority) return uri; + + if (uri.scheme == 'https' && uri.host == mobileHost) { + return Uri(path: uri.path, query: uri.hasQuery ? uri.query : null); + } + if (uri.scheme != 'parktrack') return null; + + final path = uri.hasAuthority + ? '/${[uri.host, ...uri.pathSegments].join('/')}' + : uri.path; + return Uri(path: path, query: uri.hasQuery ? uri.query : null); + } + Map? _mapParameters(Uri uri) { final result = {}; - final zoneId = int.tryParse(uri.queryParameters['zoneId'] ?? ''); + final zoneId = int.tryParse( + uri.queryParameters['id'] ?? uri.queryParameters['zoneId'] ?? '', + ); if (zoneId != null && zoneId > 0) result['zoneId'] = zoneId.toString(); + final query = uri.queryParameters['q']; + if (query != null && query.isNotEmpty) result['q'] = query; final latitude = double.tryParse(uri.queryParameters['lat'] ?? ''); final longitude = double.tryParse(uri.queryParameters['lon'] ?? ''); if (_validCoordinates(latitude, longitude)) { diff --git a/lib/presentation/screens/map/map_screen.dart b/lib/presentation/screens/map/map_screen.dart index 8952ca8..a14e40b 100644 --- a/lib/presentation/screens/map/map_screen.dart +++ b/lib/presentation/screens/map/map_screen.dart @@ -90,10 +90,16 @@ bool shouldIgnoreMapBackgroundTap({ } class MapScreen extends ConsumerStatefulWidget { - const MapScreen({super.key, this.initialParkingId, this.searchQuery}); + const MapScreen({ + super.key, + this.initialParkingId, + this.searchQuery, + this.initialDestination, + }); final int? initialParkingId; final String? searchQuery; + final Destination? initialDestination; @override ConsumerState createState() => _MapScreenState(); @@ -159,7 +165,11 @@ class _MapScreenState extends ConsumerState { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { - if (widget.initialParkingId != null) { + if (widget.initialDestination != null) { + ref.read(routingProvider.notifier).reset(); + ref.read(destinationProvider.notifier).state = + widget.initialDestination; + } else if (widget.initialParkingId != null) { _loadAndShowParking(widget.initialParkingId!); } else if (widget.searchQuery != null) { _performSearch(widget.searchQuery!); diff --git a/test/core/router/app_router_deep_link_test.dart b/test/core/router/app_router_deep_link_test.dart new file mode 100644 index 0000000..33bd7c4 --- /dev/null +++ b/test/core/router/app_router_deep_link_test.dart @@ -0,0 +1,21 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mobile/core/router/app_router.dart'; + +void main() { + test('creates a destination from valid deep-link coordinates', () { + final destination = destinationFromDeepLink( + Uri.parse('/destination?lat=61.789114&lon=34.359757&name=Station'), + ); + + expect(destination?.latitude, 61.789114); + expect(destination?.longitude, 34.359757); + expect(destination?.name, 'Station'); + }); + + test('rejects invalid destination coordinates', () { + expect( + destinationFromDeepLink(Uri.parse('/destination?lat=100&lon=34.359757')), + isNull, + ); + }); +} diff --git a/test/core/router/deep_link_coordinator_test.dart b/test/core/router/deep_link_coordinator_test.dart index c002222..6d5e373 100644 --- a/test/core/router/deep_link_coordinator_test.dart +++ b/test/core/router/deep_link_coordinator_test.dart @@ -13,6 +13,16 @@ void main() { test('accepts a parking URL with a positive zone id', () { expect(coordinator.safeLocation(Uri.parse('/parking/42')), '/parking/42'); + expect( + coordinator.safeLocation( + Uri.parse('https://m.parktrack.live/parking/42'), + ), + '/parking/42', + ); + expect( + coordinator.safeLocation(Uri.parse('parktrack://parking/42')), + '/parking/42', + ); expect(coordinator.safeLocation(Uri.parse('/parking/nope')), '/map'); }); @@ -45,6 +55,50 @@ void main() { ), '/map?zoneId=7&lat=10.0&lon=20.0&name=Office', ); + expect( + coordinator.safeLocation(Uri.parse('parktrack://map?id=8&q=station')), + '/map?zoneId=8&q=station', + ); + }); + + test('supports every public app section for both link schemes', () { + const paths = [ + '/map', + '/search?q=park', + '/profile', + '/profile/edit', + '/login', + '/register', + '/password-reset', + ]; + + for (final path in paths) { + expect( + coordinator.safeLocation(Uri.parse('https://m.parktrack.live$path')), + coordinator.safeLocation(Uri.parse(path)), + ); + final uri = Uri.parse(path); + final custom = Uri( + scheme: 'parktrack', + host: uri.pathSegments.first, + pathSegments: uri.pathSegments.skip(1), + queryParameters: uri.queryParameters.isEmpty + ? null + : uri.queryParameters, + ); + expect(coordinator.safeLocation(custom), coordinator.safeLocation(uri)); + } + }); + + test('normalizes destination links and validates coordinates', () { + expect( + coordinator.safeLocation( + Uri.parse( + 'parktrack://destination?lat=61.789114&lon=34.359757&name=Station', + ), + ), + '/destination?lat=61.789114&lon=34.359757&name=Station', + ); }); test('rejects unknown and external redirect locations', () { @@ -53,6 +107,10 @@ void main() { coordinator.safeLocation(Uri.parse('https://example.com/map')), '/map', ); + expect( + coordinator.safeLocation(Uri.parse('parktrack-other://map')), + '/map', + ); }); test('restores a remembered destination once after authentication', () { diff --git a/test/web/app_links_assets_test.dart b/test/web/app_links_assets_test.dart index 04ea1a2..e3a71f5 100644 --- a/test/web/app_links_assets_test.dart +++ b/test/web/app_links_assets_test.dart @@ -26,6 +26,7 @@ void main() { expect(manifest, contains('android:autoVerify="true"')); expect(manifest, contains('android:scheme="https"')); expect(manifest, contains('android:host="m.parktrack.live"')); + expect(manifest, contains('android:scheme="parktrack"')); expect(manifest, isNot(contains('android:host="*.parktrack.live"'))); }); @@ -47,7 +48,7 @@ void main() { expect( target['sha256_cert_fingerprints'], contains( - 'B8:0A:91:ED:3C:71:8F:0B:21:53:82:9D:85:41:AA:93:CB:A0:54:1F:AF:E2:ED:4E:F4:61:4A:F9:75:A6:ED:D0', + '52:9C:D3:24:95:AD:28:AB:08:F6:AE:C5:09:19:87:43:E4:9D:E8:0D:39:55:1B:A2:60:00:C6:AB:B9:B9:6D:BE', ), ); }); diff --git a/web/.well-known/assetlinks.json b/web/.well-known/assetlinks.json index 643dd46..eb60f20 100644 --- a/web/.well-known/assetlinks.json +++ b/web/.well-known/assetlinks.json @@ -1,12 +1,15 @@ [ { - "relation": ["delegate_permission/common.handle_all_urls"], + "relation": [ + "delegate_permission/common.handle_all_urls", + "delegate_permission/common.get_login_creds" + ], "target": { "namespace": "android_app", "package_name": "com.parktrack.mobile", "sha256_cert_fingerprints": [ - "B8:0A:91:ED:3C:71:8F:0B:21:53:82:9D:85:41:AA:93:CB:A0:54:1F:AF:E2:ED:4E:F4:61:4A:F9:75:A6:ED:D0" + "52:9C:D3:24:95:AD:28:AB:08:F6:AE:C5:09:19:87:43:E4:9D:E8:0D:39:55:1B:A2:60:00:C6:AB:B9:B9:6D:BE" ] } } -] +] \ No newline at end of file From a2b88c396807536e3136db1f95e6383d47b09d75 Mon Sep 17 00:00:00 2001 From: Nikita Aksenov Date: Tue, 28 Jul 2026 19:06:58 +0300 Subject: [PATCH 109/145] Optimize map updates and remove unused routing code --- lib/core/localization/app_localizations.dart | 4 - .../providers/routing_provider.dart | 6 - lib/presentation/screens/map/map_screen.dart | 35 ++++-- .../screens/map/widgets/web_map_view_web.dart | 112 ++++++++++++------ test/web/yandex_maps_bridge_test.dart | 6 + web/yandex_maps.js | 84 +++++++------ 6 files changed, 153 insertions(+), 94 deletions(-) diff --git a/lib/core/localization/app_localizations.dart b/lib/core/localization/app_localizations.dart index 80895eb..40a9df9 100644 --- a/lib/core/localization/app_localizations.dart +++ b/lib/core/localization/app_localizations.dart @@ -55,7 +55,6 @@ class AppStrings { final String logoutConfirm; final String cancel; final String inAppRoute; - final String yandexNavigator; final String buildRoute; final String searchParkingNear; final String selectedPlace; @@ -240,7 +239,6 @@ class AppStrings { required this.logoutConfirm, required this.cancel, required this.inAppRoute, - required this.yandexNavigator, required this.buildRoute, required this.searchParkingNear, required this.selectedPlace, @@ -426,7 +424,6 @@ class AppStrings { logoutConfirm: 'Вы уверены, что хотите выйти из аккаунта?', cancel: 'Отмена', inAppRoute: 'В путь', - yandexNavigator: 'Яндекс Карты', buildRoute: 'Маршрут', searchParkingNear: 'Искать парковку рядом', selectedPlace: 'Выбранное место', @@ -629,7 +626,6 @@ class AppStrings { logoutConfirm: 'Are you sure you want to sign out?', cancel: 'Cancel', inAppRoute: 'Go', - yandexNavigator: 'Yandex Maps', buildRoute: 'Route', searchParkingNear: 'Search parking nearby', selectedPlace: 'Selected place', diff --git a/lib/presentation/providers/routing_provider.dart b/lib/presentation/providers/routing_provider.dart index e08d8b4..a88bdde 100644 --- a/lib/presentation/providers/routing_provider.dart +++ b/lib/presentation/providers/routing_provider.dart @@ -32,12 +32,6 @@ class Destination with _$Destination { final destinationProvider = StateProvider((ref) => null); -enum DestinationMode { routeToAddress, nearestParking } - -final destinationModeProvider = StateProvider( - (ref) => DestinationMode.nearestParking, -); - class SearchBias { const SearchBias({ required this.latitude, diff --git a/lib/presentation/screens/map/map_screen.dart b/lib/presentation/screens/map/map_screen.dart index a14e40b..94f8164 100644 --- a/lib/presentation/screens/map/map_screen.dart +++ b/lib/presentation/screens/map/map_screen.dart @@ -260,28 +260,37 @@ class _MapScreenState extends ConsumerState { ); if (same) return; final generation = ++_bitmapGeneration; - final newCache = {}; final newById = {}; final usedStyles = <({int? count, int color, int textColor})>{}; for (final zone in zones) { - if (_bitmapGeneration != generation) return; if (zone.geometry.length < 3) continue; final style = styles[zone.zoneId]!; usedStyles.add(style); - var bitmap = _zoneBitmapCache[style]; - if (bitmap == null) { - bitmap = await buildCountBitmap( - style.count, - Color(style.color), - textColor: Color(style.textColor), - ); - if (_bitmapGeneration != generation) return; - _zoneBitmapCache[style] = bitmap; - } - newCache[zone.zoneId] = bitmap; newById[zone.zoneId] = zone; } + + final missingStyles = usedStyles + .where((style) => !_zoneBitmapCache.containsKey(style)) + .toList(growable: false); + final generated = await Future.wait( + missingStyles.map( + (style) async => MapEntry( + style, + await buildCountBitmap( + style.count, + Color(style.color), + textColor: Color(style.textColor), + ), + ), + ), + ); if (_bitmapGeneration != generation) return; + _zoneBitmapCache.addEntries(generated); + + final newCache = { + for (final entry in newById.entries) + entry.key: _zoneBitmapCache[styles[entry.key]!]!, + }; if (mounted) { setState(() { _markerBrightness = brightness; diff --git a/lib/presentation/screens/map/widgets/web_map_view_web.dart b/lib/presentation/screens/map/widgets/web_map_view_web.dart index 5ae97af..b76c954 100644 --- a/lib/presentation/screens/map/widgets/web_map_view_web.dart +++ b/lib/presentation/screens/map/widgets/web_map_view_web.dart @@ -25,6 +25,12 @@ external void _createYandexMap( @JS('parkTrackYandexMaps.update') external void _updateYandexMap(JSString elementId, JSString stateJson); +@JS('parkTrackYandexMaps.updatePosition') +external void _updateYandexMapPosition( + JSString elementId, + JSString positionJson, +); + @JS('parkTrackYandexMaps.move') external void _moveYandexMap( JSString elementId, @@ -128,11 +134,13 @@ class _WebMapViewState extends State { Set _serializedCandidateIds = const {}; int? _serializedSelectedZoneId; int? _serializedActiveRouteZoneId; + Brightness? _serializedBrightness; List> _serializedZones = const []; List? _serializedRouteSource; List>? _serializedRoute; Map _zonesById = const {}; String? _lastStateJson; + String? _lastPositionJson; @override void initState() { @@ -168,9 +176,39 @@ class _WebMapViewState extends State { @override void didUpdateWidget(covariant WebMapView oldWidget) { super.didUpdateWidget(oldWidget); + if (!_created) return; + if (_hasStructuralChanges(oldWidget)) { + _updateMap(); + } else if (_hasPositionChanges(oldWidget)) { + _updatePosition(); + } + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); if (_created) _updateMap(); } + bool _hasStructuralChanges(WebMapView oldWidget) { + return !identical(oldWidget.zones, widget.zones) || + !setEquals(oldWidget.candidateIds, widget.candidateIds) || + oldWidget.selectedZoneId != widget.selectedZoneId || + oldWidget.activeRouteZoneId != widget.activeRouteZoneId || + !identical(oldWidget.route, widget.route); + } + + bool _hasPositionChanges(WebMapView oldWidget) { + return oldWidget.userLatitude != widget.userLatitude || + oldWidget.userLongitude != widget.userLongitude || + oldWidget.userHeading != widget.userHeading || + oldWidget.navigationLatitude != widget.navigationLatitude || + oldWidget.navigationLongitude != widget.navigationLongitude || + oldWidget.navigationHeading != widget.navigationHeading || + oldWidget.destinationLatitude != widget.destinationLatitude || + oldWidget.destinationLongitude != widget.destinationLongitude; + } + void _initializeMap(web.HTMLElement element) { if (_created) return; _created = true; @@ -259,40 +297,54 @@ class _WebMapViewState extends State { ? 'ru_RU' : 'en_RU'; + final position = _positionState(); final state = { 'theme': isDark ? 'dark' : 'light', 'locale': locale, 'zones': _zoneState(), 'route': _routeState(), - 'user': widget.userLatitude != null && widget.userLongitude != null - ? [widget.userLatitude, widget.userLongitude, widget.userHeading ?? 0] - : null, - 'navigation': - widget.navigationLatitude != null && - widget.navigationLongitude != null - ? [ - widget.navigationLatitude, - widget.navigationLongitude, - widget.navigationHeading ?? 0, - ] - : null, - 'destination': - widget.destinationLatitude != null && - widget.destinationLongitude != null - ? [widget.destinationLatitude, widget.destinationLongitude] - : null, + ...position, }; final stateJson = jsonEncode(state); if (stateJson == _lastStateJson) return; _lastStateJson = stateJson; + _lastPositionJson = jsonEncode(position); _updateYandexMap(_elementId.toJS, stateJson.toJS); } + void _updatePosition() { + final positionJson = jsonEncode(_positionState()); + if (positionJson == _lastPositionJson) return; + _lastPositionJson = positionJson; + _updateYandexMapPosition(_elementId.toJS, positionJson.toJS); + } + + Map _positionState() => { + 'user': widget.userLatitude != null && widget.userLongitude != null + ? [widget.userLatitude, widget.userLongitude, widget.userHeading ?? 0] + : null, + 'navigation': + widget.navigationLatitude != null && widget.navigationLongitude != null + ? [ + widget.navigationLatitude, + widget.navigationLongitude, + widget.navigationHeading ?? 0, + ] + : null, + 'destination': + widget.destinationLatitude != null && + widget.destinationLongitude != null + ? [widget.destinationLatitude, widget.destinationLongitude] + : null, + }; + List> _zoneState() { + final brightness = Theme.of(context).brightness; if (identical(_serializedZonesSource, widget.zones) && setEquals(_serializedCandidateIds, widget.candidateIds) && _serializedSelectedZoneId == widget.selectedZoneId && - _serializedActiveRouteZoneId == widget.activeRouteZoneId) { + _serializedActiveRouteZoneId == widget.activeRouteZoneId && + _serializedBrightness == brightness) { return _serializedZones; } @@ -300,6 +352,7 @@ class _WebMapViewState extends State { _serializedCandidateIds = Set.unmodifiable(widget.candidateIds); _serializedSelectedZoneId = widget.selectedZoneId; _serializedActiveRouteZoneId = widget.activeRouteZoneId; + _serializedBrightness = brightness; _zonesById = {for (final zone in widget.zones) zone.zoneId: zone}; final markerState = resolveParkingMarkerState( allIds: _zonesById.keys.toSet(), @@ -308,27 +361,15 @@ class _WebMapViewState extends State { ); _serializedZones = widget.zones .map((zone) { - final colors = parkingZoneColors( - zone, - brightness: Theme.of(context).brightness, - ); + final colors = parkingZoneColors(zone, brightness: brightness); final center = centroid(zone.geometry); final isCandidate = widget.candidateIds.contains(zone.zoneId); final isSelected = widget.selectedZoneId == zone.zoneId; final isDimmed = !markerState.activeIds.contains(zone.zoneId); final displayedColors = isDimmed ? colors.dimmed() : colors; - final clusterFull = parkingClusterColor( - 0, - brightness: Theme.of(context).brightness, - ); - final clusterOne = parkingClusterColor( - 1, - brightness: Theme.of(context).brightness, - ); - final clusterFree = parkingClusterColor( - 3, - brightness: Theme.of(context).brightness, - ); + final clusterFull = parkingClusterColor(0, brightness: brightness); + final clusterOne = parkingClusterColor(1, brightness: brightness); + final clusterFree = parkingClusterColor(3, brightness: brightness); return { 'id': zone.zoneId, 'type': zone.zoneType == ZoneType.parallel ? 'line' : 'polygon', @@ -343,7 +384,7 @@ class _WebMapViewState extends State { 'candidate': isCandidate, 'active': widget.activeRouteZoneId == zone.zoneId || isSelected, 'markerOpacity': isDimmed ? parkingCounterDimmedOpacity : 1.0, - 'markerTextColor': Theme.of(context).brightness == Brightness.dark + 'markerTextColor': brightness == Brightness.dark ? '#09090b' : '#ffffff', 'clusterFull': _cssColor(clusterFull), @@ -382,6 +423,7 @@ class _WebMapViewState extends State { widget.controller.clear(); _zonesById = const {}; _lastStateJson = null; + _lastPositionJson = null; super.dispose(); } diff --git a/test/web/yandex_maps_bridge_test.dart b/test/web/yandex_maps_bridge_test.dart index a1016e9..4249042 100644 --- a/test/web/yandex_maps_bridge_test.dart +++ b/test/web/yandex_maps_bridge_test.dart @@ -38,4 +38,10 @@ void main() { expect(bridge, contains('M16 39C13 32')); expect(bridge, contains('viewBox="0 0 48 48" width="24" height="24"')); }); + + test('updates moving markers without rebuilding parking zones', () { + expect(bridge, contains('function renderPositions(entry, state)')); + expect(bridge, contains('updatePosition(id, state)')); + expect(bridge, contains('renderPositions(entry, entry.latestState)')); + }); } diff --git a/web/yandex_maps.js b/web/yandex_maps.js index b9b3ccf..4d05810 100644 --- a/web/yandex_maps.js +++ b/web/yandex_maps.js @@ -401,6 +401,46 @@ }; } + function renderPositions(entry, state) { + if (!entry.map || !renderingApi) return; + const { YMapMarker } = renderingApi; + const positionSignature = JSON.stringify([ + state.navigation || null, + state.user || null, + state.destination || null, + ]); + if (positionSignature === entry.positionSignature) return; + entry.positionSignature = positionSignature; + clearObjectGroup(entry, 'positionObjects'); + + if (state.navigation) { + const angle = state.navigation[2] || 0; + const el = document.createElement('div'); + el.style.cssText = `position:absolute;left:-14px;top:-14px;width:28px;height:28px;z-index:2400`; + el.innerHTML = ``; + addObject(entry, new YMapMarker({ + coordinates: [state.navigation[1], state.navigation[0]] + }, el), 'positionObjects'); + } + if (state.user) { + const el = document.createElement('div'); + const angle = Number(state.user[2] || 0); + el.style.cssText = 'position:absolute;left:-12px;top:-12px;width:24px;height:24px;z-index:2300'; + el.innerHTML = ``; + addObject(entry, new YMapMarker({ + coordinates: [state.user[1], state.user[0]] + }, el), 'positionObjects'); + } + if (state.destination) { + const el = document.createElement('div'); + el.style.cssText = 'position:absolute;left:-16px;top:-40px;width:32px;height:40px;z-index:2300'; + el.innerHTML = ''; + addObject(entry, new YMapMarker({ + coordinates: [state.destination[1], state.destination[0]] + }, el), 'positionObjects'); + } + } + function render(entry, state) { entry.latestState = state; if (!entry.map || !renderingApi) return; @@ -527,42 +567,7 @@ }), 'routeObjects'); } - const positionSignature = JSON.stringify([ - state.navigation || null, - state.user || null, - state.destination || null, - ]); - if (positionSignature !== entry.positionSignature) { - entry.positionSignature = positionSignature; - clearObjectGroup(entry, 'positionObjects'); - - if (state.navigation) { - const angle = state.navigation[2] || 0; - const el = document.createElement('div'); - el.style.cssText = `position:absolute;left:-14px;top:-14px;width:28px;height:28px;z-index:2400`; - el.innerHTML = ``; - addObject(entry, new YMapMarker({ - coordinates: [state.navigation[1], state.navigation[0]] - }, el), 'positionObjects'); - } - if (state.user) { - const el = document.createElement('div'); - const angle = Number(state.user[2] || 0); - el.style.cssText = 'position:absolute;left:-12px;top:-12px;width:24px;height:24px;z-index:2300'; - el.innerHTML = ``; - addObject(entry, new YMapMarker({ - coordinates: [state.user[1], state.user[0]] - }, el), 'positionObjects'); - } - if (state.destination) { - const el = document.createElement('div'); - el.style.cssText = 'position:absolute;left:-16px;top:-40px;width:32px;height:40px;z-index:2300'; - el.innerHTML = ''; - addObject(entry, new YMapMarker({ - coordinates: [state.destination[1], state.destination[0]] - }, el), 'positionObjects'); - } - } + renderPositions(entry, state); } async function initializeMap(entry) { @@ -649,6 +654,13 @@ entry.latestStateJson = state; render(entry, JSON.parse(state)); }, + updatePosition(id, state) { + const entry = entries.get(id); + if (!entry) return; + const positions = JSON.parse(state); + entry.latestState = Object.assign(entry.latestState || {}, positions); + renderPositions(entry, entry.latestState); + }, move(id, lat, lon, zoom) { const entry = entries.get(id); if (entry && entry.map) entry.map.setLocation({ center: [lon, lat], zoom, duration: 300 }); From be030f547634cc2af995db7db038e0dfdfab32fd Mon Sep 17 00:00:00 2001 From: Nikita Aksenov Date: Tue, 28 Jul 2026 19:10:19 +0300 Subject: [PATCH 110/145] Finalize ParkTrack 1.4.0 release notes --- .../releases/1.4.0/github-release-notes.md | 50 +++++++++++++++---- distribution/releases/1.4.0/whatsnew-en-US | 8 +-- distribution/releases/1.4.0/whatsnew-ru-RU | 8 +-- 3 files changed, 48 insertions(+), 18 deletions(-) diff --git a/distribution/releases/1.4.0/github-release-notes.md b/distribution/releases/1.4.0/github-release-notes.md index 3d21fa7..0e40f39 100644 --- a/distribution/releases/1.4.0/github-release-notes.md +++ b/distribution/releases/1.4.0/github-release-notes.md @@ -1,18 +1,48 @@ # ParkTrack 1.4.0 -This release makes it easier to compare nearby parking options, inspect them on the map, and preview a route before starting a trip. +ParkTrack 1.4.0 makes parking selection and route planning faster, clearer, and +more reliable across Android and the web app. ## What's new -- Added a compact, scrollable parking results panel ranked in the order returned by the service. -- Synchronized result selection with parking markers on Android and the web. -- Added a detailed parking view with a return path that preserves the result list and scroll position. -- Added quick “Go” and “Open in Yandex Maps” actions. -- Improved route preview so the complete route fits around the map controls and remains visible while the map is moved or zoomed. -- Replaced the destination circle with a precise pin marker. +### Android + +- Added a compact, scrollable parking results panel synchronized with markers + on the map. +- Added detailed parking cards, route previews, and quick Route and Yandex Maps + actions. +- Added complete verified `m.parktrack.live` App Links and `parktrack://` + custom links for the map, parking cards, destinations, search, profile, and + authentication screens. +- Added a Yandex Maps-style location marker that follows device heading and + refreshed the compass and map controls. + +### Web/PWA + +- Added synchronized parking results and parking cards to the Yandex Maps web + experience. +- Added destination markers, route previews, and in-app navigation controls. +- Added direct path links for map, parking, destination, search, profile, and + authentication sections. ## Fixes and improvements -- Added localized loading, empty, error, action, and parking-data labels in Russian and English. -- Improved result formatting, touch targets, small-screen behavior, and light/dark theme support. -- Added tests for search state, marker emphasis, formatting, external map links, route camera bounds, and destination markers. +### Android + +- Restored reliable parking-card opening from map taps. +- Improved route-building progress feedback and navigation distance formatting. +- Improved zoom-button hold behavior, map-control placement on small screens, + and location-marker contrast. +- Parallelized parking marker bitmap generation and removed unused routing and + localization code. +- Added focused coverage for map controls, parking cards, route previews, + location markers, navigation statistics, deep links, and routing states. + +### Web/PWA + +- Stopped serializing and redrawing all parking zones for every GPS position + update; moving markers now use a lightweight update path. +- Improved map theme synchronization, compass behavior, marker selection, + routing overlays, and small-screen control placement. +- Improved deep-link fallback behavior and preserved protected destinations + through authentication. diff --git a/distribution/releases/1.4.0/whatsnew-en-US b/distribution/releases/1.4.0/whatsnew-en-US index da3d591..7e071ca 100644 --- a/distribution/releases/1.4.0/whatsnew-en-US +++ b/distribution/releases/1.4.0/whatsnew-en-US @@ -1,4 +1,4 @@ -• A convenient new parking results list is synchronized with markers on the map. -• Added quick “Go” and “Open in Yandex Maps” actions. -• Improved route preview while keeping map controls available. -• Search destinations are now marked with a precise pin. +• Added convenient search results and detailed parking cards. +• Added m.parktrack.live and parktrack:// links for opening app sections. +• Refreshed the location marker, compass, map controls, and route-building feedback. +• Accelerated map, marker, and navigation updates and fixed parking-card opening. diff --git a/distribution/releases/1.4.0/whatsnew-ru-RU b/distribution/releases/1.4.0/whatsnew-ru-RU index 6b06f0c..abd8fe7 100644 --- a/distribution/releases/1.4.0/whatsnew-ru-RU +++ b/distribution/releases/1.4.0/whatsnew-ru-RU @@ -1,4 +1,4 @@ -• Новый удобный список найденных парковок синхронизирован с маркерами на карте. -• Добавлены быстрые действия «В путь» и «Открыть в Яндекс Картах». -• Улучшен предварительный просмотр маршрута с сохранением управления картой. -• Найденное место теперь отмечается точным pin-маркером. +• Добавлены удобные результаты поиска и подробные карточки парковок. +• Добавлены ссылки m.parktrack.live и parktrack:// для открытия нужного раздела приложения. +• Обновлены маркер геолокации, компас, кнопки карты и отображение построения маршрута. +• Ускорены обновления карты, маркеров и навигации, исправлено открытие карточек парковок. From d5ddfe30e72830a76aae74eecf96f0a4a17382df Mon Sep 17 00:00:00 2001 From: Nikita Aksenov Date: Tue, 28 Jul 2026 19:12:07 +0300 Subject: [PATCH 111/145] Remove obsolete pending deep link state --- lib/core/router/deep_link_coordinator.dart | 13 ------------- test/core/router/deep_link_coordinator_test.dart | 7 ------- 2 files changed, 20 deletions(-) diff --git a/lib/core/router/deep_link_coordinator.dart b/lib/core/router/deep_link_coordinator.dart index 7758360..a08305a 100644 --- a/lib/core/router/deep_link_coordinator.dart +++ b/lib/core/router/deep_link_coordinator.dart @@ -1,19 +1,6 @@ class DeepLinkCoordinator { static const mobileHost = 'm.parktrack.live'; - Uri? _pendingDestination; - - void remember(Uri destination) { - _pendingDestination = Uri.parse(safeLocation(destination)); - } - - String takePendingOr(String? fallback) { - final pending = _pendingDestination; - _pendingDestination = null; - if (pending != null) return safeLocation(pending); - return safeLocation(Uri.tryParse(fallback ?? '')); - } - String safeLocation(Uri? uri) { final internal = _toInternalUri(uri); if (internal == null) return '/map'; diff --git a/test/core/router/deep_link_coordinator_test.dart b/test/core/router/deep_link_coordinator_test.dart index 6d5e373..f2389bd 100644 --- a/test/core/router/deep_link_coordinator_test.dart +++ b/test/core/router/deep_link_coordinator_test.dart @@ -112,12 +112,5 @@ void main() { '/map', ); }); - - test('restores a remembered destination once after authentication', () { - coordinator.remember(Uri.parse('/parking/42')); - - expect(coordinator.takePendingOr('/map'), '/parking/42'); - expect(coordinator.takePendingOr('/search?q=park'), '/search?q=park'); - }); }); } From 59a6baa462e7063cd9cb71babc2cca4af757c272 Mon Sep 17 00:00:00 2001 From: Nikita Aksenov Date: Tue, 28 Jul 2026 19:29:45 +0300 Subject: [PATCH 112/145] Fix web routing and panel drag tracking --- .../screens/map/widgets/candidates_sheet.dart | 2 +- .../map/candidates_sheet_test.dart | 41 +++++++++++++++++ test/web/yandex_maps_bridge_test.dart | 14 ++++++ web/index.html | 2 +- web/yandex_maps.js | 46 ++++++++++++++++++- 5 files changed, 102 insertions(+), 3 deletions(-) diff --git a/lib/presentation/screens/map/widgets/candidates_sheet.dart b/lib/presentation/screens/map/widgets/candidates_sheet.dart index 0403f03..eb7f66a 100644 --- a/lib/presentation/screens/map/widgets/candidates_sheet.dart +++ b/lib/presentation/screens/map/widgets/candidates_sheet.dart @@ -83,7 +83,7 @@ class _CandidatesSheetState extends ConsumerState { required double maxHeight, }) { _setPanelHeight( - (_panelHeight ?? fallbackHeight) - deltaY * 1.5, + (_panelHeight ?? fallbackHeight) - deltaY, minHeight, maxHeight, ); diff --git a/test/presentation/map/candidates_sheet_test.dart b/test/presentation/map/candidates_sheet_test.dart index 1bfa399..3c0ddb9 100644 --- a/test/presentation/map/candidates_sheet_test.dart +++ b/test/presentation/map/candidates_sheet_test.dart @@ -207,6 +207,47 @@ void main() { expect(closeCount, 1); }); + testWidgets('panel height follows the drag distance one to one', ( + tester, + ) async { + tester.view.physicalSize = const Size(340, 700); + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + final heights = []; + + await tester.pumpWidget( + ProviderScope( + overrides: [l10nProvider.overrideWithValue(AppStrings.en)], + child: MaterialApp( + home: Scaffold( + body: CandidatesSheet( + candidates: const [], + zones: const [], + lastViewedZoneId: null, + initialScrollOffset: 0, + onSelect: (_) {}, + onAction: (_, _, _) {}, + onPanelHeightChanged: heights.add, + onScrollOffsetChanged: (_) {}, + onClose: () {}, + ), + ), + ), + ), + ); + await tester.pump(); + final initialHeight = heights.last; + final header = find.byKey(const Key('parking_results_drag_header')); + final gesture = await tester.startGesture(tester.getCenter(header)); + + await gesture.moveBy(const Offset(0, 40)); + await tester.pump(); + + expect(heights.last, closeTo(initialHeight - 40, 0.01)); + await gesture.up(); + }); + testWidgets( 'renders explicit loading, route building, empty, and error states', (tester) async { diff --git a/test/web/yandex_maps_bridge_test.dart b/test/web/yandex_maps_bridge_test.dart index 4249042..ed80b1b 100644 --- a/test/web/yandex_maps_bridge_test.dart +++ b/test/web/yandex_maps_bridge_test.dart @@ -11,6 +11,20 @@ void main() { test('keeps rendering and service contracts implemented', () { expect(bridge, contains('window.ymaps.route')); + expect(bridge, contains('const responses = await api.route({')); + expect( + bridge, + contains('api.getDefaultConfig().setApikeys({ router: routerApiKey })'), + ); + expect(bridge, contains('points: [[fLon, fLat], [tLon, tLat]]')); + expect( + bridge, + contains( + '.map(coordinate => [Number(coordinate[1]), Number(coordinate[0])])', + ), + ); + expect(bridge, contains('duration: Number(properties.duration) || 0')); + expect(bridge, contains('distance: Number(properties.length) || 0')); expect(bridge, contains('window.ymaps.geocode')); expect(bridge, contains('searchViaServices')); expect(bridge, contains('api-maps.yandex.ru/services/search/')); diff --git a/web/index.html b/web/index.html index 61f45a7..2ac8358 100644 --- a/web/index.html +++ b/web/index.html @@ -50,7 +50,7 @@ } - +