From e5cfb8b4baf412522f8c01a55fcea473152be92b Mon Sep 17 00:00:00 2001 From: Sven Date: Mon, 20 Apr 2026 19:12:13 +0200 Subject: [PATCH] =?UTF-8?q?Weitere=20Konsolidierung=20Momente,=20Splash=20?= =?UTF-8?q?Absicherung.=20=C3=9Cbersetzung...?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nahbar/nahbar.xcodeproj/project.pbxproj | 10 +- .../UserInterfaceState.xcuserstate | Bin 94432 -> 97063 bytes nahbar/nahbar/AddMomentView.swift | 135 +- nahbar/nahbar/CalendarManager.swift | 175 +++ nahbar/nahbar/ContactPickerView.swift | 12 +- nahbar/nahbar/IchView.swift | 33 +- nahbar/nahbar/Localizable.xcstrings | 1081 +++++++++++++++-- nahbar/nahbar/LogbuchView.swift | 87 +- nahbar/nahbar/NahbarContact.swift | 12 +- nahbar/nahbar/OnboardingContainerView.swift | 1 + nahbar/nahbar/PersonDetailView.swift | 437 +++++-- nahbar/nahbar/PersonalityEngine.swift | 89 +- nahbar/nahbar/SplashView.swift | 34 +- nahbar/nahbarTests/CalendarManagerTests.swift | 270 ++++ nahbar/nahbarTests/ContactPickerTests.swift | 44 + .../nahbarTests/NahbarPersonalityTests.swift | 62 + nahbar/nahbarTests/SplashViewTests.swift | 38 + 17 files changed, 2272 insertions(+), 248 deletions(-) create mode 100644 nahbar/nahbar/CalendarManager.swift create mode 100644 nahbar/nahbarTests/CalendarManagerTests.swift create mode 100644 nahbar/nahbarTests/SplashViewTests.swift diff --git a/nahbar/nahbar.xcodeproj/project.pbxproj b/nahbar/nahbar.xcodeproj/project.pbxproj index d993050..8825d25 100644 --- a/nahbar/nahbar.xcodeproj/project.pbxproj +++ b/nahbar/nahbar.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 2670595C2F96640E00956084 /* CalendarManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2670595B2F96640E00956084 /* CalendarManager.swift */; }; 269ECE662F92B5C700444B14 /* NahbarMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 269ECE652F92B5C700444B14 /* NahbarMigration.swift */; }; 26B1E2DB2F93985A009CF58B /* CloudSyncMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26B1E2DA2F93985A009CF58B /* CloudSyncMonitor.swift */; }; 26B2CAB42F93B52B0039BA3B /* UserProfileStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26B2CAB32F93B52B0039BA3B /* UserProfileStore.swift */; }; @@ -98,6 +99,7 @@ /* Begin PBXFileReference section */ 265F92202F9109B500CE0A5C /* nahbar.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = nahbar.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 2670595B2F96640E00956084 /* CalendarManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarManager.swift; sourceTree = ""; }; 269ECE652F92B5C700444B14 /* NahbarMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NahbarMigration.swift; sourceTree = ""; }; 26B1E2DA2F93985A009CF58B /* CloudSyncMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudSyncMonitor.swift; sourceTree = ""; }; 26B2CAB32F93B52B0039BA3B /* UserProfileStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileStore.swift; sourceTree = ""; }; @@ -284,6 +286,7 @@ 26F8B0CE2F94E7B1004905B9 /* PersonalityQuizView.swift */, 26F8B0D02F94E7D5004905B9 /* PersonalityResultView.swift */, 26F8B0D22F94E7ED004905B9 /* PersonalityComponents.swift */, + 2670595B2F96640E00956084 /* CalendarManager.swift */, ); path = nahbar; sourceTree = ""; @@ -463,6 +466,7 @@ 26F8B0D12F94E7D5004905B9 /* PersonalityResultView.swift in Sources */, 26EF66472F91351800824F91 /* AppLockView.swift in Sources */, 26B2CAB82F93B7570039BA3B /* NahbarLogger.swift in Sources */, + 2670595C2F96640E00956084 /* CalendarManager.swift in Sources */, 26F8B0C72F94E499004905B9 /* NahbarInsightStyle.swift in Sources */, 26B2CABA2F93B76E0039BA3B /* LogExportView.swift in Sources */, 26B2CAB42F93B52B0039BA3B /* UserProfileStore.swift in Sources */, @@ -653,7 +657,8 @@ ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_CFBundleDisplayName = nahbar; - INFOPLIST_KEY_NSCalendarsWriteOnlyAccessUsageDescription = "nahbar erstellt KAlendereinträge für geplante Treffen"; + INFOPLIST_KEY_NSCalendarsFullAccessUsageDescription = "nahbar liest deine Kalender, damit du beim Erstellen eines Termins den richtigen Kalender wählen kannst."; + INFOPLIST_KEY_NSCalendarsWriteOnlyAccessUsageDescription = "nahbar erstellt Kalendereinträge für geplante Treffen"; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; @@ -693,7 +698,8 @@ ENABLE_TESTABILITY = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_CFBundleDisplayName = nahbar; - INFOPLIST_KEY_NSCalendarsWriteOnlyAccessUsageDescription = "nahbar erstellt KAlendereinträge für geplante Treffen"; + INFOPLIST_KEY_NSCalendarsFullAccessUsageDescription = "nahbar liest deine Kalender, damit du beim Erstellen eines Termins den richtigen Kalender wählen kannst."; + INFOPLIST_KEY_NSCalendarsWriteOnlyAccessUsageDescription = "nahbar erstellt Kalendereinträge für geplante Treffen"; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; diff --git a/nahbar/nahbar.xcodeproj/project.xcworkspace/xcuserdata/sven.xcuserdatad/UserInterfaceState.xcuserstate b/nahbar/nahbar.xcodeproj/project.xcworkspace/xcuserdata/sven.xcuserdatad/UserInterfaceState.xcuserstate index 9a1d95e1ffbb3ddc99a97cdd25b8d739c19c96e1..c7103d516331baba405294a3856064fce1528096 100644 GIT binary patch literal 97063 zcmeEv2Ygf2`}aNP-Xu3S>n3Ts!k#i(I?@q#CzK6kubQ?g6uM|ymWcO=A}&Bt0T(o7 z%TPo>P-HkzL1ieoaN*u8Q4rtf+?(4$3W6{Ge((SN{OKnlxjFZ&@AEw8S?Ao`vZ6v? zMNG_51~Hgn8II8~T1LkRBU?@Nmir1zOGdTwl;=$>gl|JzRg{*EY*m^&$(vW~VZ!!kP$b>QBOav3fL^GY37$%m9W8xVX zlfWc0NlY>`fEmaPVg@rqm~3V!GmII|j9`kH5~h?XW2P|WjE|{cDw(Ow?aUnJ4(3i~ zE^`mFka>Vv%q(FZXP#jE%u41NW;63FvxRw%*~z@X>}K{bdzt;r0p=j{F>`|Xg!z>D zjQO1Tf;q{2$^6QkVooz>n6u39%pZtBEaH%W3@8FcA}4Bu8lxtt6>5#zpth(JNS-qk8^MVo`g$q zDfZzRcox1D&&G4{U3eazj~C!&csZ`Zeq4=L;Fb6(ydH1C&*80jKYks*f#1Xj@Im|* zej6XbAK(x1XZUmc1O5^Jgn!1T@j3hlzQD39$7)zDt78qUku|ec*1?9dVQe_th;7Pt zU^}u=Y&6@6?ZS3tyRqHb9&84i$@XQ3vBTM$Sr41bPGzUD)7cs9O!gLb7JDl@o4t*l z!`{WtW9PFA*v0G;b}74!-OfJG?qGMaFR(ANyV#f5-RvHAFT0O@nLWh5%f82cz<$Vn z!hXu0WWQv;Wxr#8Vt-~&vuD`z?C?Ka*telOrb0M6A3+2MN3@(f7!wujDa@pKa zZX`E~8^?|3a=AQi0ymK>;)=N`Tsb$Do5tP3&Eh1EaCdTZxqG;Kxdq%p?ji1BZYlRD z_awKBtL9d4Yq+)C25uv_ncK?k;C69)xmURp+$Y?p+-Kb9+!x$Q?n~|~?rZKF?g#EP zcY(Xe{i)Guc#U2oXv`X$CQ{?nG}1KIG|{xtwAFObbkf9Yk~Cd3Jv3>WUYfp|JWamF zt0~Y-&`i`6Y9?tWYl<|*nkky;ni-mznp-q;GRmF8>BH=0wLbDBT2Sj%cTtx;>zI<-x;&9tqwt+m~> zDcauJLE6FEA==T}F$Ezv&Y}y`h3lH=n(A8X+UOE>NxEd6Ti03FMb}lAuFKG6>IUcr z>IUg@brW@kI-hQu?iSq~-5t6+b#ry|bqjP=y4AWhy3M++x*fW`x_!Erb#Lh2)E&?r z)E&_s)xE3xSa(AAiSA3?SGse$-*o47zw7?cUC>?RIbOqSc{6X}MZN{!l5fSg=G*XX z`J4E5d=&5EJM&%m3_g?Z#rNj>@q_r0{3w1jKZeia^ZBX#OrG$!^Yi$-`9=I<{xQCq zU%{{BSMg8r>-i1*X8u|J82=&v5&tRw8UH2!75^RoJ^wTR3x9?`%m2>*!C&J4(zAL_ z&+GMilisYi>FxS3eYieGAFGek$Ln4C1bw1DNuR8D>pSba>ofHI^#k++^`rEo^<(s7 z^*8JD^+oz(eTjamewu!Pexd#W{e${N`iJxn>mSiC)-Ta7)jy$Mpz9s^6yH zqu;CFr+-<$Uw=S8(+!h~=k zLTD^B7upH!g$_cz;1YTYDMG4{CZr1)LY6R47$gi9#t37Dal&{ZSI8442-Ae=!VF=i zaEmZYxK$v+9l|`}USXl|h_F~#B0Mgv5LODSg!RG(;YDGW@RG1w*dy!}_6bLXqr$tw zd%`i{xbVL4nee&rg>Y6lC;TRy7k(H15H1K84U9o&;0^(o zhG;`4Ly{rc(8n;qFw8K zb{qB>_8Q(WylME@@R{Ko!%v2v4Zj=yG$NzkC>RY!yD`Mr#@OB%YfLgG8&i#$#y-X& z#%$wI<5=T3W2w<+oN1Jd#CWf9fpL-XG2`RLCyah$wQ+@Ut#O@kuklsmLF2o|_l(Dk zpBcY2erx>Ic*=O%c*c0v_^0ub37dG6-efU}re>z*rWU4_rdFobrZ%P+Q>-b@6mRNc z>T2p{8fY418f+S3$~Fx(4KocljWvxkO)yO~6`CfQN=)UZnWkGzvrM;|?ljFcEipZ5 zT4q{n+F*Ld^t@?@X{TwQ>1ES0e{7xS;?Q|3R+ z7t9weT8m&YTS6^imQI#%Q7>9W zn;0%eh)u<2Vn;Dbj21hIG2%#ZtT;~eh`C~cI6<5&7Kvrz6mgn(t9ZY-SX?456(1G- zVzv03xKn&Vd{ulyd`o;!JSH9&KNUX{Pm8~ce^@yyZ#7y&tPX2qYZGgXHPM=6O|fQJ zv#f)yL#)}>5!NwQpLLpbwso%cF6%t&gVsl^k6QiKYU@+h_134YTddoyFIeBP9+jY-tQV|**|1G(6KrOi%@%5lv^i}} zZOv@WZLMwXY|*w(wisKot*fn@Eyb2)>tpL{8*Ce5%eCd%@@-yQfo+0qqOHVMYMW-8 zZku77X}iyMziqy4fo-Af0o#MNMYhGZCvB^2t8E)>8*STd&)as`cG_OD?X$gR+iyEy zJ7_y%J8JvD_Mz>2+Yh!MZ9mz5w*6xJ)po&l(e|h9l3i=p*?D`U-Dz)RZ)|U3Z)$I5 zZ*FgEzsVkFkGH$*33j)=n?1vxY42t4Z69PGY|pb7+9%m7?9=VD?04Ajw9mEAw=b|) z*;m`w*f-m^+IQIZ+V|OCw(qweuphU-Z~wslq5UKK$MzHU&+T8^zp;O7|J8ohe$M`f z{Za@H(S|e$X&TZjqLRyEk327U0Q%J{<_>e9kT|>Hs^a|-6k`>Y?WI)J} zkkKJyLh?fLLneikg-9XuLhcT^C*;A9MIkFgo(fqXvNdE!$gYrALtYDcJLH{^&qKZm z`8MQq$ZsJR9IS(L2o8h8=rB3#4yU7$qp_om z9Q_>q9fKVs9OE3tj%kkRjv0=bj@un`91lC5a6IW)=2+=i<#^h$#j)M-ykn1Juj8oW zBge;%6ONOPFC9NSPCI^c{1wWC+CsxaBSKq-whiqN8XxKkO$c>|b`I?xIx(~;v^aE1 zXnE+=&}pH!gw6`RJ#=2^-JuIY7ltkmtqS#rR)?+#T^YJ6bYtkI(5FMUhdv*=BlJM% z;m{+YABTPx`eo>kp+ANG9QsS>`Ox3P3}N=LkT6GB)3BCdZNoZ+#e~I%xx+e#^$bf1 zOASj4%L*G9mK`=KEGH~4Y(m)Nuqk1cVYi0O4!a}lp|B^zmW3@3TNAc6Y-iXDVK0X5 z3VSJRci5h=y}c4#VV{J38unS(=V4!keHV5r>~z?fu(M%*hFuC5!$ZQI z;f=yugtrWj3Xcs>2=5l&J-kPFukhaCqr=CCd&3LDi^7Y;r-k1Vep~om;q$`p4!=Kq ze)xmoOTw3juLxfgzCQfv@U7w7!e0#E75-NE+u`qo9|}JlekA;8`0?=f!#@r`5q>`W zV)&mCC;~?`if9(mHlkBROhjyiJEC($dPMJtei1_>hDCTHawGC0CPWlP6h~A>%!rs7 zF+1Y+h({t8M=XhWJfbSXAF(cCW5lM2XCwAR?2XtL@p8l~5wAwP7O_9#K*T!{??${A zaV+AKh%X{eMtm9ZZN$%!x=2H$Eiy9F8QCbZO=SDX=*Yy#q{tqTJtId(j*ZNVoE%vc zSsYmzIWyd!d6|z-H=PHZ2c7RY4-IQoQdv}VgfTH@#=?k< zoe3Q=AUk@pcZP2?{2#FKhPTQq^7wpy#>&_vEU|tjgmHjgjGR*Y9I=%BlcJL2ljBn35;J3ClM-{pkgW8K5vApmePy0JZ+dB7WwE!ULTx#c zX~}e4!#J5nOk<`A)0An(G-p~!8c8eZBwo@>f@F}4YnWC{Yo-mnH)Gn-|0R=TmMrit zNTU3IY>w!l?G%>emrl#_Wy0XH^UA&6lJPZHGs`{2-W)LsF7)>J`WH?p2OBkixTmPn z>&q-JEgq0P#OtdlE%)RWd1d=G-BaPo5!=*MFw~Qq>M0-L@eMEZ6+)pAg%uOC3ri*x zdEu8Fu_+Wx$*TZMxn{I*J*~8)qP(=I$XlKxcBfUTGg1Gyj2O2oGb1H2DkdfwrqY#? z66H=!ii^rjaV4h5#wW&Qq-Exa5w(3OEiKCRlxL}Z$q~bAf0O%J^An#VM#eOYX%_2B zifQJKbHT?tU-T*xZYGWCxR&Y6bYZ$O-I(r752h!R!lX)8$tKyQ5Xm8hN?}sC6tNZ% z$Y3&=UQBN$i|GU3`T+)!(llwhbekj*6e?K=3YP#A5jvGUv2@x1&(y*R9++I^Qq9+t zaxYA=6-o`OC@d)iSY3V}}=dr=@#y zD<@2#Q?7QPrurPw-rrkXT0Wy!xuYYj!` zi19T=vPvqvMMZ@Zyd^N=i5_UIrna=wd|;XspAQ^Qo{|b+oSM7pH=9~oI=R?WK3N54 z8Z(ROxPqC^%wT3Rw@9s}Hd0&ZrWMSs%xva1=2oel)IsV9?=a~hnT16a-g23ZQ)l!C z0KiEFlhx1TtElM-y__S44l5~WuyD9ihc|y%R{eU^{@lgPV?viNm)aj@?q%*{?q}wY zR^(*~2%3?~N3U<}fb8@_5DTRxp7I%fD79eZfb4;?4^3g(di}{R1ulSsY8wwS4>6%t z%pxhOig{Ryrec>edV}{I21&k@d6an!7#?Vj%CSMzy9WeQ?)8Dt_0V2OF;b_2;^th{ z@rN+WX|y_!5+ca@ij+K{4xcw4gr9d*VTHF?9{V_7fXcGqif_CxkBYQGlnkN9_>|Y) z?DNwr-xn=k?$;arf}tf7_}6lGGv$-tWNz7WhVr>(&$~XAKk?R9(Ovrv9X5Q_SZ~Rc znaZySDgVP*>i?Ug%<_}WGLXp`)60q!Njnc2N%4|PN{|v)FzcBO z%tmIDlq7YQx=3ARng_ao#yutZ-h3IRp|Jib{yqu>eZC$EhsJbD*OO<(u&m%Ze_Mp0^KGhS252^Zc3mQTCu#Wruga>eUaJ4 zbOe5h9R!Q@Ew{vUY!;P#YsY4{xMJXAk}KxcTh&VUF|X8A`l{4TYOntAI`f87#dXP5 z_3pQrcbSf>n75gCm_y8A<_L3C>M5m2sZyGhE@i9&X+DlQ&Uly)nDHRTn@X8dOQ{zK zd=MxJH8SdoQd}0g(Nr436s47x$>O<1&3*M`n^HE*=L6j%M{IqyLI74}UPWa&K-ubQ z^^y8Y{Z=qPFh4Rs zF+WKCr2*1Fc>hmH`E$%~ij=>K8Af%%it?T^xcD&~?jh^`E>NJbc>?%*3Pbze{T z>HL<8Yn8h56nT1`Qa3O(q(M5SV-?a$*;R;_hSKSem{RF2pE1Zo^=c3+zFJjRy~&7d zOvjbTgv`i-L}ZnQNyDWP(nx94N~RSGK@Jp(!a$23Esc}LOL?@?R=&!zvLY|F5j;hp zDtNQK9tbUgYF=4X;matY(h`)BFuGO^gLSI5NOoRnnRkrbC#r^*mgI;w`CB$9ksy2X z~YKPi07SsWCL}4g;l-$-(Z+Y=(y8h?O-2s}BFK0}z@RrbMTJ1t0Q4hsTO7&FaO{Cl`51jVdk9yLDlG;$gKgzvQsz^iW zIbxHbQuU_Tk1}Xoe^W}G?p~-L@H^^_vQQt?S1OPuNE4+(;CD0t4V3wPk~H~0!0&(0 z-e@EmO(|oPR8)n=NX6HpjG>!>^GgO=aj(l1E?3h3mBRJL?I9ZuZ`&l=bY@#fXk?=% z&04g&seQ-jnArHkyk0O3E%Q`N90dq_r}r)_sqh<&!%I99!D1X$?k$oTVJ1)(vWV94 zUaB)t+LZMbQ0Csa-N^0UB_gQEZt@$AP|RUcufGjf!y^2K@G(k#L&0YDi7b?H2Flp= zZYa~(o>Fkon5Igd!nb@Tf;9RhT`q4O6yHu z9ty=fSe2Til&j`6cLZGRWU5j+3ZLVk1pOkKsuJL9A~fnx?&5cMZa^{Z+O3XaI&XJC zMuNkm6$GohGJPQ+T?nCQA6OX@a~A}i7eUZ@HS;uto3}ALn3o{Pd=T`Bk08kW1*}oO zLRk5Cgpm&ELFk1+Fu5r(Z!!dsGf`il=xj6=g2s7hA_R=fP$im%W}(@LK+yPp^axst z{7MI~ss(pfzN_!(DV1&VPyy4?kMfaMDwC%8(FA6LR1V4@h|sV+YH)dER|Lpp8pjWa zKAZdt6`ffnWt9~}LD+!AO-YYWj!BBmjB>@q!J8{JE-EE1BQ`2AGsB%2ACuyak4sjY zEkV93tV2r8%!jkxU7suQG?vmKDXIW1f5Os`q zqIndHx#%uwhBVWU?nd`Ww@9~AEZSE3yyb(+y#?O#a!|`vhlZ?A)ew1BIw*n7>y=D{ zRcQivDe9IvOnr4|0b00Rnx#nN2hk$%+NG42duI62gUkY@#KY*3<%X6fDn52yfxXjY03R@2MOTHolprCXMj z7lP|xhUyxU6Ik-=8^W;KsXu!?a<^mw2vFEzeG4=vcbmGO#`pJ3FDx#c2@5;TZ2?VL zb-Y|wNfwp4U}%94t688+(CtFdY2-ErDIM$QnNeD)dO^`Hw3{kdFG=@Rp*_<5%7hIc zS~FL&Ij4a83VOtkUPZ4-^QDKR*(!K%ptqRNYV;;LfDTFvq=nK0)#z=e4LT$}2(GzB zQu}dW(AMf6W9heA*P#esDJ1KF zx{a?LfHJ?18rRd1($WfQjBA{N%x`F66*@0HA6-UAP zKpl}$6F|)cmAG@KPB}T#6pI$L>lvLWbNV{@rg}>{sWa*8G^o53eCfr-;N5`hBdJE- zNm05qn>6Cjxy5{1+_` zwyeB#lG3?w95DvErV>vzj>Jx8LETHBT@Ui)P4?zf;>rMT{tV^L#<)q2coRUTIOD)B zp=?@Si;%QuGh zzAq3ucuMQY(s2P92B5~qcK|`Ie23@-#6kk1(30g-z6azsbpZt`%TbDp;B1jYO}HCQ zVLDdh?zji;DQ%ISleSjlRGfy>rESs~=^X8+4Jz}c_7a5uGKn+dDuTmzTGD<;X?7zQYXH zyN9-3mw};9wU@QJL5+72WXo=8uk<`j=6ZbIG4Q`)YfSZmrTdapt+35Ze9N!_ z*&vGicqZitcV206w1?XK(fJh(g-L2XYBSOl@zhI^PB~&)ji-xx*A!Ayg-mokM_q6w zYB`lKZ^O4Up?)l3BJGo2_TxGD4(S!?G+of_6H7tk$XDvGX}o5ssN?ffd#DtR&M)^A z$P89k(I33(o(Zbk6yJ^Sg%uy)BfVOM?~`7WSNwYQ)Ad;G+CsdT^85q%LA(e*gdfI_ zNc*MNr8lHEr32EzRd@+ritfgb;m6V4(p%DxpnLu-{R;2EOJQD_#AHTIol%!@nikej zjc;H<#`KAWxrG(JtPK29c`MKk#Gi-87=)Rqm+0NW-C-h2=Ef;43SI4ViEfA_#^AG}^++JM~s5MC$X^$lo8FoNbP zRpxsGG~xqsOqa~GQZUqjLSWv5%gE6e*`?Jq_(!N9t7NJYlct75P>CX0*{l&4P^M8( z8QP7M%8H4lreb0yYaiiWaT_b73t$ zhSq|Kuvq#SUb94Ao!F1?2@3Vc(kE5;6X{brQOyQHuO~npvjo(3+4rZY%r&D>@BIRQ z4GJkfiNC~ONuNodOJ7vuZ}7MHJL#nKmGm{Ot4+{o%Py-t8&(>a`A%0UIJ9&|dZCZr zSnKBmCs)uNzQTMj_*tT-(Kz;D{0j{KS9}VhfYFteh55NND!jgdBA5new-?^pzx@&> zW#G*Ciyui}D$?-`K06$wCSHcPd1M7mr7r@ z?3uruq5ss0xrqOwWdA3=Bz-G=2UaPIKqh=IwI5ahK>Eh3%Sb`Os|or z%CPE5q<|EN9^{?i@m2b22ObspB_J7P$XOmpfYnPsNwcn(Z4WUP)+GHR&j)K^MVOBP zpsx5?OTfToZM1H?bV{1lvS%xK%*>b(-a>GjdYkp7e&1FZkTa2-mg$O0jE#+pa-}6? zM7d+)Q=$^wiOF%UnB=6mj5wu%2sTnlQBlTPN9pjhpa@>!H)V{CWm7yb-)u9sHMmCD z=4=bLCEH3mFa0k4AzfI(wqe_%OKdyoB0)%6Mvz{L9of%Y5HNzN$F;`JtFbmmE5uGs z-%&#fCrp$Js}04l3ACYDHja&FUDBV@CFw7Mm=)ksUyJURrOskN+Ct^Ob2ymR)MpOX zo_88m{d_|_)0F5OFmnB012Nu!=ek+FALZL&SxEOn@VBrG_yy!dXuTqG?NmKdJKB>? zg+-1{AqZEoX#}zIG6&jr&46X)PMOSJVA!&~*(`!M0$~Q#v}OCTgDy8X*};%=(wiU+ zjPW+9ea&pD?T%o_1l=;0X~T{uNGCyR3NN*vy=(zHft|<} zvXj`!1PKHg2r?36BFIb-)FTpPrKw5uk0vL{e<17WA5BhD{s4sz6l67y2O*0RUult- zn#TPKbIYllLJ4cgtF)4#)L`kMs_j<4eETcaJ}4`FDEwBhavJ1qu=4)i3J*1M>ldf? zN`u8I&h?AZ#Ht31W>@C=W`F`!T)#ZEup2B7_9V?gpqg*}JFeLG8gE&hp@PZ5N-P2K ziI9uE9rWq}*#Tp^Vxl7Rz{gPMn#>dgg(!024)#vaU~6vhvva{JtaAa(6op4~C@&8! zmAO?q1T`h789~hnYC%v-f?5&OnxHlWwI%4L4eVNW z9s3ko%d}!Q!s}`F8Fn-KEW3q)0__OuO0>7b>TV+1JL%h8qMb*yj}z@iqJ2Y|iEv0a zfKDm`S5?EoAb_z%2HbASZ!$mlGE2+pk7JeGy6n7(5F7*%Kcun*WKl(7iR{~!&F%_N z{vc2;&o9j;gN&cnkhqZNO|2{}%7=PVii+wM^ko$c@OmL#0!%lxA4&!xbj7aF-q4A_ zruj8Wf28s|^`wHMgc{DW%+4w)C z^J|@&%J5Rbe#)0`gh7DSD68WZ)dH^RVn{0VZE{&D1bBRMX|O)?6>0hkTDXpV6^~>0 zv#+ynuy3*l*n{j_?Arvjmv#}u3?L;xzkeyEJSsqLY~R07`VPY8seh&Kwa&N8N#I7!{x`}9;&a4#bw>a`hT_7C zsHrex-tvDyLpQ2>KPufz$PxW@yLSVqn+iQyq-GYCPP;l~Mn8YM^Y;sawQf^+nJ0`X+Ri9Ei4ngZRH@M>|1vK3OT&qJske5vOce?+Z z(tUT1xZ&UGKFy|8lT7-1O2CpYzcO?F69_IUV}NXrf1R|ixqFIJm;H-_U#3GWJXsrbcI`=i)T(|s<@I2PdDr{hw z>0Ss!QSZDGFZGtIDoT!+r1VyPj|X*`dcCqCZwmB;%e|oW%KXhZWrZ9ZArM})XccJl)9~qP+IACovpajGUHt7$x-ga#P}$e zJ2@jNIVmkQDkGWBSXvsmY?A7>GFEA2!1cA_N=iwMNl9`=rKTmvM7fe;-BGDYuJkBZ ziaRMY#vPXwo0?F!6_3&iBq(1G`e}fEQXC8;EyBlGBnC z>y4#AX=O-`I89xR@(X>i(aKw#TSSAd;FZpsoKrv(D|-}qDogSvW`jWty6F&aLATsI zZ=5SGH#w$_Tuk291RI(rc8hZ*#ll<@=B5WseWyVFN#kymoTtI?RDQPgXDX#cbcWiuGiaRc`ZYyO-v#u60|fR9YE%eXRhK#w90a#z!T`#3llM2}x1z zl-?7+|jCSlIEE5|a`WpPZWP zj?JhiiEdL`8GC)LWB{wiC&a}>#iziuxm>`#$>|`8GF@qI*r6AfnChxGV|OU6K=ku^ zC_X7EGbuhJ)fJThz(lzc-7!(Asj0D12`RA|sen*?tgD_Rx?5@G=Id((cEDw3CMLv3 z#l*Uxm5i8ppt0EWsLa&Fj5v2%QbtB>dR_GAE3Lp@yz3cDVoD64l9Ud@8U%$aHO&>3 zoRAh1l^B;2mzt585gVUUPev?KS^=lQ^|X?ik{stwazp3i(o#VF075A#u)!}iAtNC^ zK0PrZGrk_bEKyo1_-8W06XPjJ%u9?3k`eIqiDiqQp#hRAOpetSdI2(n`IttXEorV8lO>@gAtLAU@_F;TKcjNpi;|$EeENChln^ z+e*FgEVrfpg>BsS`WJR`FVw&A5(f!tbnA{<$3E`m`WIf~4nz7Nx1W2RdxLwEJHQ>} z-s0Zo-r)`rG?k!f1WhMs20=3kx`m)w1l>x|Y=UlE#~o38=iD*wICUy=A5ur6q&UzC zx}Bf}vO5tiRJ?~*jgB@P*X$2^T(PIDq0p!=DEfM10<0}-jRi%Oj#l=}$u7<4QaLEz zV1MMDbxJ9^ukGIs)pFlLERFk)AQFhxaz6%QwKQG}6R-TuQgJ`S!EP8oca}Rx&>Tox z_H*aC-wC>tpnI<=YAd^V!LCszMe(3t9@6D5X)wfZxW6=v1`#xupt}f~SFK?+9EP6W zO(4DYza7`r7#NGj7#x@VedVU-UOF)#Z*}V>%UWwAmqA=tV}YV#aM8u=j0t<%3@nzk zCp-JLyl|iNWnhvub`2aI;n##{90c7*(EWZ*7#;O|2=Sd zHSYQsx@x-BztB?yX<&74N!Mi5ztCHgRsTXi%?OY%n*N#rnt_@@n!%bOnrzKb%`nYy zf*vR634)#^XcBDFG3q0TX?7K*VUuLBwc$nhJQq(y)f0wetHqNC{~ta$>Yf zinBDgQ9ig;0}I7^f;RXyl7pNSU%lgL%`^2`dyD3|`WLoqp09u51rZ#6$QM5jM# zex`K#3PG<@Iz6m84Fqr|m;ml-($xR;U;SRZck{`U-A}gaL1_hYi!=@77De{)O6R`m zb=%J7+4l-RRE({ql?$3nbf6dM!n9vrnEs**)9Vc^OjhIb0l2jqEiV&@R!0fsO-dkt zQ388Ch``i4%-RsxHwxawrd*WPriD!;;2i|_VKG4m33_{_)}am6hH1mK5d?vLd4izt z2|7=3>w3OCZ4*TYf9pTsG0%%naOZhEx!`v3lNXnq5SyO| zSx0qEJZ(#*d++?aIr!?Fw$Z|>{ZE!pZJfeE+C+tew4Esj9acH$Xe|eQM3+yQgS6d& zgS6oKKN8>|ZR!kL2AI2^u}w?7Sv{I zhf@l6Xotzt?)cwJEA3eA%?&V~)}zg(jQ0USAIglUEnqC#3BiolfRNTsf})dyi(XMk zYfH3K6c#F@EcEd;^m>~1_J3xNY3BsszDu#kwD(fDKT+WZ5f`w>zO6+Y_M}y57ZTV6 zq$GT57hO$WY9G};L6Lq8(n8SZ1byMxKB)!84MgTwHxB6)+SLlut7N3Vq;qz4)mOVg z`%D9Nhjz2}S&H=61bw3*y$#^o9t>Xty1#ZO6n!DM=oNK;?MvFd3etNh(ll-J?|Qvo zd*mN7_R#>+#{-Q05k>k373rU98T(u<)SptQfe3z7p+0$4sK3|#M4|pc`y)X=6ZDH; z`?K~Jf_^3F%ne%_wP&@zH^iZTXfIIQP7!ok!R;@`qGN)=Xkcm7u~3u?E_%hKQK!=h zGP&vWGP#|lz+HWn(}_Abbqx}9bv6q1Z`W`~gw6?hWx7a$ey`FsBIpkqE44SUJ0T!b zbh^iM4sXeFvQy$io3|&1GfE%9Nv1kHb^Qp|6KtdH z1aBj{%BBR}VBIL#lmKS2VieaJ#5LA%zz$vwM+QMUp>7U%wg^oo{6*W z9q4=__xIo4%lqt2&{K8Oq3Dd@qBAdLJEwHW*l_oXgMTbLQ~GUf| z(vB-2t=_g!w}fIk5KyI#jsT_rC@h$Yhoc75Ruom;Q z)dYuC=~faPeqETZ)oo}9(~Y`K6w?TTBNa@a1)#PB1GPG9{GH~VeSX+E4IDVcjv^ae~_r+}5xAK=&cRHxYdEl~#y4Tb=*G{x#jFy3gst z@)^PHs&rov-2Und%h$S}=)&@i?pxh=y6<&A=zb))1Hl~$jv_dk;7$a`tkV6g`$b+@ zPSb@Ymf*3Yc+?n7m1a~F48^PUSI~?zXS7UfJrQ(U;9`r4RzGYGQ)L;xGC?<2&n9q#~ z*fbnDx=}t*zCEbTJWzbkfZEJQUrlZ16Zj+$*L)(usZ~5oY})mRYrZQFdQ3InjqlEb zCX-HZ2Em!tdkSXE7GOPw-Ol(3mHEOErB?-PbSf zXxE#{Xk+;t%4p*#qYaQ5?PlIX@IZpcG{k7?ZC<_@SPH_YO7OHca9R^J4a7@V^CeV^ z!i}3GNmkIDQsdOJ4*JkrvBu;BvRCeVD`F zNr~eKOczh@}M2RfL}<7ZaBdsWF3Kj2+({u7|jSbNZv*)D+9E4>#*VinlJ|2 ztea(&PlL^mDvDe9S7@%L$Unz#<+t(MwRV07*PefYf05tCHR5;kd-%QlKK^Bba|s3= z9~AtF;8P`d5>3FwMFf`+4B3=@2`(qNg5aqH10BtzRldr<#_#7}=ilJp=fdu%8k3bHe^YIEHXm!Zoeu zt>r(E-Jv)yM=ZJ_`%UT}B-W|^*V+vEItfJlCrTGUPlGf5Zorn8PAT>6zuG)g{-n|r z?4}-nLzt+ZMEA8_@@<|QS?lZR=<>wJCAnf<-k8|j+(dUm zQhq{GT-_W?{=CvX*duoHbx%pnfqk7C`V`pl*DyW*sWdw!M;v|K&DOJs)H8HavY@?! z!U>gdc0Qz|Mo)rMyXru%$4Y~+@2&WTG+3W$6&|KTrK46()ej;Cy-w*vWsW%HhV-Em z-GyD`3vS4$Gz2G^d2UEUH7uL~R%J!M(g_W7vEHh*3%eU{WV`YPB4rQG71|9|+Px)5 zyjM+Imd}j($9t=4=((ZoRf*m_IC3b?Tac6#p9h)Sp5)x5+;~r%JKhb+x}JP^$V6gn zexiCZp*}+23f$oONWD|vNZ(lBMBh~3Oy6AJLJ!9Ctpv{|_%?zif(gO56Fi4t&~xr2 zc$vV>s5`BhRuloO+ce3oCFf#>Yo*& z?*fXd9_+<=0Yz2c<7$?XK2x6sDxSWVzBj@55PYv+-$xIs%6$Yscunf+RRf6u9ZEk) z4+ccFez1OsKAYhC37${zf@=LR{cx(wE+qJY|GELCAIDhqT&OK1~Jf)%(=FbftzBzP~u`v`uyTCfRr%6+d8EGM-6SG|-% zBx4br!7L>#E@_qUa9TvwF9!-b&p)*pxKC&TMVkf}Ee?Gr^4=ajP0yZQT2=7XqP!aJ z6IuwZWq~EMk_Fa&U_YTPU1Wpv;;fE#0huLq6gtT~D@0SCrO9+xx=FnwLFf#}MG1*Q zl8`KbhyEbJZxIX*pLbRWU4*VeH=#ShhX_7QFx2=1RdGAY0a0~lv7)!ZA=Xn1ODo}| z&+-Z0!2aMgI7%FBy@rabhn1+Cmen&~p}4Zwx_<3BN{jMAE%2BHdJ=$7X&27WZlE`T z^4W!OVsXg?wPeHc-Hb_iB2!S4|a z&a9&Ze{w64L6?-7oejtH3bzsbB|UCkeoBWhM}i01bwJ7x29{ATiEq}d zi~%XB{W5ZfW@HadNzLk)HFVUhSwrBsH8|%-sp(Ey(_DhTuC3{wzpE*0K(CzajFcg1 zy>kYo4DFpQA6zBeC)^K5{nlfd%XzR`m@h1#XWL!9Si>hXt7Q9t0B2QI2@ewdU6t?< z!QWpFbbU6?l%KIi&ldK|hjcZo@T$jN2}^}X>t%ZjPYBB)o-I5n044oI@XvmsO7Ii> z3t|7Ic($-wSW8E>b@;R6AV&-V)-3Lg;`5f=M}6T&BiWeFQWXXLt4zl4**4-hmIz7)O^ zz81a_z7@U`z9%e4SPfydgw+w2C#;^ZK;1vmprP;!yiS4Q%^GCi7Hfu}AsY%xIJLrp zcP!SPIvaeZ?7y{fvSA0>jf%qyf712s5@C%2Mc9A>im-uGwmT9ydz79{s|m29EMO(a@i2VI{xb>rlB!o zF*FI*T!$Z?KCbigX_FuB+HP2v9#cL8CD+g#ina(YI{VL-U50xGYsKplZ>YYKV=1-(r$=u6?8yc=@rusVA)4YyLzZ=s;K zlR=+N1#J5(2w3&DIfi?wDE$hAX&q4taulR#eNhTv-w&?^@OnT2y#vKNioV6wg8ncC z{Sm@;R6$>QHH+J@+~B96R}nV4%1}+%PS*wcYQs|vNtyMA4HWbk!p16~KLbc^4n|Tt z+cLiNy|feSzAnwUWt)rUUKpN(qFaNDelz5e{jZMfxB9s+H$`k-pLBaI=+7I#nFcwx zhMg4jcnbP13c4#8^stZAY2Rmfm7?`BMJrK8>otm2(%+zUz;N{1(RvS%Iu5T76tt2l zP&7-H?NN)?35pgp>{ikG{HkbuYxtg`1#{b_%J2hWyIvPszZlLmgw|QZIf_;{!gg2C z`U4=i5DdY&#TC1aQlEBfgp(cJXLBPcT9=^cU%^HDyb@BLbw@w4Ht`qB)Ymq@UxStr z8#OXoMvkJ@6VNj1C|W7OXf^E^K+9+}THq$5iK3N8(K3oupr&74pc);<#@8%RjZK+W z#^&&9si2iXf$Bxy=rhI@fog0EXc_E}F1lA|Kq-7vb=}R?Wjd4a7pk<6F zZ0{-~^f&8z&@#G>-5L<6#_q-*6s{5NQgHx^twS`z`ON$@IC&>BL48cN?r)uL5K(SkIfY!$7F ztMQic79$AoD&s7|4y!WGChYL*LhE+p+=kG)%Q%ms1#9X^1+Du4g8PFZIJ9N>i5CtJ z82tQ){;sW&r8z}wAryTexafxqqn{txuV?98H~w(LqFK{wdFvtLVg;>7C|aW_T1zQf zkN|kO&J+H-tRfg8e&RPSGcG6WIKo~%ee7>d4W-7F^)tqX$(a-?X*^}z`p;tJ#%+ol z-?-EGg7HNF{v~Qn6%F4ssSZ+296s)0y?=o!fX84c!JW&M8Xy-bn-cX|3xtP zqef5cfAM_JIqP4xo@o<3fbLQ;eg#Fp4lbJc{;6)x(%#~laRc}^CsKf<)INM?gdN=w zWi|dl9l-2lN+>^5LMaL+l*o#fJ=-hU|6;gW!8s%BdxIs{c%Bkk*;VdW@A%8a)bq5M zSd#|)9wyLod{rh;bSkLd<0?DKOoGWsZ!i$HvdRQFPra(6#AG#vgQLV`GucfcCWk51 z6h_!-gq=><8HAllSQzcBRi+42Bz2US8iS*Ry_K*V<-~vXng8jD|E9L44nSt6n@sIY z?FkFg>^8zm)uxW7C{r|H31OEIwwh9P+j{9%wRv@ypOc&;whNT3IXCx;2hTRN+k~Dp zUiZ}N0AZM1CP+B-Ri@E|g#&u7eOtR}yeXH`S&r#ulZUVi2)mH54^*4-O!-utJxJI^RGj@+ zV`(NZBTWsMk<0(sQI_{t<`=g={Ai)JI;QBab5pc}IwH)&p<(TD^W7Ha#njWL{1Pwo4WqO>jPh1y0EjPh-!C-n? zX<9|;=}E#alb1)+IsodaV4%JzerfGUL(jR(!#x!{Q+iq`rW>K?rr@HFJlQpG@_W6K z9@*O@>e*)o9=#Tg(fNOo&i@K|{&&;)U->ul z|BC70_09hwn1499PL)`z==?ubcm9u=-ly|_oUp5_Odk+-&2`QH2@{+s6FmQ4m`>99 zUrX3^%KU!=llpD&q`ooMm|1$HZ^pdUM?1Lt43_Bp!!uS*KL!__v%mY74~PhVlL<+U$h za;0YQ(AM4i5J1|jHQShuPnmUQ-mEtZW`o&iHkr+4i&->-oAp`3ZXxV*gxyNmZG_!U z*yjnmgRnaZ`vPHKe9CNB6j^f^cibF7&mA;3qN?mJz))6YcSA-BVGn>E3CJFVtDqNM z)kWU0jvi zu&79WdBLw29Vs2|nD*%DU9W!G2c(L53=|z3Ty$cV`|iCf`}0qy#-HdvWpmdWsbbDC z-(1fEG3S}V-vCC8*-ORDTOel4=~T?T5;TkI6c(AM{WI&;JUzhlv&^@eX9LSil;z)1 zS^g+tKPD_ZtdX#vP=;5;&mENXKzBW)lHNQZJ@eh>d+3X>hY1VDDSd~aQ6nq5L+#}Q z=7%VGJZN4-*mnv0p5Oei`4Pe%BkTv)v=m&GOw~pnGsA|rAT!py++0Pe2^OFC<#ocm z5@1>t3{wKjhBoWdr_Z*$i#B`KsRD}VS}3|MxM-h6U%b3*#eiW?`tN<9=X{>jBD%p$ zAFpZNNcrGHnGc?!eDG02e84~3vgb_!GuEsQ;(4>;IX26l<4-9CfaOPPxSWsFJNBCQ z)hD7?&4>OWNgNK4#Cu9!q8Yrzg#AJ#i7(Z>ME0jzilEM7GdPP+suXcmXR-NPGel|q z=I_i9oBfKgU;E8JntvkfH-!EE#tEa-=5x@9#$i4y`x3sTlX#v^;&(xlXk|~S^Z2I) zH9$TVY+)(+fMfednS9{X9E*hyrir*FY43O2)Hm%JuxSK zLcyv2)3)|-KK7e=dQ=VhSS%JB^c??bu~Pp3`6?X`^x6_`Y5C8_Vb2lv_Z!CcmJADQf(T-JOCL*Lira65J+I(45WpA|jN99@;j> zd@YJwHWVEiT=W7PHcanKYxLA-i$6TyqwG*EZX+zC=|D$Pw*P~&y=4q#`wI=ReZ;hu zJ!1lFzgvYd&!Px^i!Av60^luUD8*g~qFD8gA`2D#rk<7(@GXF6!BTGVsosUTghR5A zL3S{3m~fC&0sR3HeeXI;K=4~;DO_%$f}i68z;jxa%Q*|h{Yqyp*XVLL>TqX;p6;Rq zHji)`mB8-3DuF#{d6>@oBFjUB(-DsMTOP42CY+vd*LF_NwX7?a$1Tf&z%KVESgI_3 zATUlKoIxS5RX{bXgQ=zidu)!-Iz90*>&2g>v3KvH1O}#}r2$iM|M+Hq6lC_9v-JE| zC!a3Jh6mZIXm7N@X>1_UEN~#}{e&}70(lk)gfj;bh&61Sde?T#PPhxi$qo=F96kEy zN;j!@?6Dl6;^g{s+ARl_wZn2KKvnNjs@=NdNJ+ZfdlK75owoCq zi$Efmr*B*Sv|OSj;v`%nnM6d)SVT6MM2wHVduCVfl!xv!&k7H}zl9%2MASl2U2xH= zVaIno`+3i=pYhN7_59g0O>0O*6hxB@wrHeaH@Qm31HBgQq66I5VhG`yRf(a5YyLO> zZZT49L~nrcw5Spr6Rzb|{oP`7u?-|!i!H>KVk@yV;aU+6-hnLIu3&zkK5lqKdw&p- z;OD0mUHS3sH{jvxAy;|2x)>{_(_;c`VjRB}9@*3k9^urA-!8h@x5X}86R{iTuQFAfj~ii5<#;t(-g94ZbIhZ8QIaAOHqNVrLan@qSO!W9$l z5yI^z+$Ewh5sjH>EJPy`O?#rrBAT&8Gp^pz0pb{WXDSD#-M{{5G*40zJc=f+ASN%- zo$vMJCBdPuu0(i>Xi{>1er&uaJ{h*H=Ela=*%d9`taPvAzkJYZ?dbxdS7|Z&pX`CI z*_Eqq`c?uCwK0jhLz|>D6mxwK(||3#aS3s0Nl~%%=`OB}q{OJ?6nFwlW_pUtl@t$8 zA&jrPeO@e8T8V?h66)Wi8H*_YmO`rxFT9C}O_>_TSIv_z#OdP9ngEyxLNcNDr`h72e;@1;=ZbexK_K1(>JpboxY1M`5Ds*o z+M4Ez4}x$N7l;eR2MCu$xMae)tHnj)L*m1P1Fp*;+(22M3`B$E-P?hOmuJJ%;OORj z`OLxV2`&Bo;biLkJSNgZ!^OwJ&B}G5ZdP#_xLLWbS9i0DE5!BJfSBcLPuE#Yg+3GM}6!$cs=7@X6eRK`$Nw^ec4SNk1 zrv1SSQ~9=!7yY%NkNwVH)=V&cG(VQEVQ)gw1Hnbz!@HRhj%OZOFzDcl6_x#`)UIJ~ zi-(mp>=0eU(qIkyKh)g^K$Yd+`2QO`_i-y{F9#|r4ir!jM=sn7Dj*6DWC9v z?s4wvc3#=|b}G5?HC=E9{+S!6uXEilDEaSV&;Law|6Ttj|6jV@RPx{6vHSdJ*I79? z_m2~EyOaxt-G0Dr^6z+7BrnG!|3O9b|Faw4J9FIb303zTw_k;-$1~;sf!p7u^GYx)CBgsRAOHS(2^uor) z-Fs|WHv5ZVp4-FwJW75KO^_q`y_Dn+Ao-f!%HWSEIr!^+W&gUQ31bi1Z80mw!aZos z0De$Lm&x~YXQl^f5|lmqXTPmnljzu^*Cadk=ryC2J^I1qTsbG8ih8(5pTLhC#|JfB z&6J~I6TwwGjsV9$X^&nrO*35?Jee9HR6~U-I#)A8GgGKygev~2s`oTs-!yYI3n}eC zZ*0~q(kxa=8!J?Cj?yltUZuQOqLiD9HLIC4yJTq}dvmd7twz~I$Oeu5y%MTn)VAzn z9@y!u+h&dO$Zpp^HCvU@koXjz*rnNB>_DJqpJspY9~{tdsZ)^`&1hcHyjuJRuW8}$iScNz4IeuopK`dl!a{R;F92@=1 z#C;OETkL73E_@-Z@=ki^y@I9Pa_PLGvv6|9q))h`(>Z-9H-MHl`zPJe`HVM=)_Z(r ziU#8xMxSXe6tD0_%{Nlxe9a}zWz7}MRn3>0Ynrb#*EM{n9xqfAgleKtvCf$+R8xd% zs!&Z6s?2=N4QZF=rsg}%EzS3uA2dH|cx;)-WY255P_ewu5~|rkl_gXhxS6Yz+U(0K z{95|@1p0Pq$uprk@{-NK&fXpTc+8x8k%|{B;=^0wxMA=ylW^Z+Z1SkI_&DWm$u#bs z^KHVewzI8HCEIfTs!OTA>ew;BuTx8Y)o3RG_mKOI54&^@zXP_;sEmy3@+KO7PODnRRYpZJAivK{T<)tD; zr!Z-`Hcj~f&%&ML70{$*KtOYUKmQiZoB8sIqglU9??=>Up6$q7q(gyRzO&cUsYjU)`g=($R&SP)! z)P`y!`7);s)ArMbYx`>lXd{Gbolvb8strQ5QK%TOIa{>@wS!zcxvtel@t9Rru23D$ z{6+aP_d0(u_s1XJshJeVGro9XJlEs(9#;5_@wDzK${mBsor7%V9rvTh#})pv`=bv; zxz&jupO_Srl>X?4{gcuYyCkKirH90)jUL5gAyYYwJ)yV#MT3uN;Yr7bv`N|#>|1Jw z3sqi@mP0a|mA#W^dvv996phxVv02xS(XtiYB2-&*wdq=Rn6?R({i2xC9&TUS6^H-H z+k~`}v@_Tl*G|?>(N5J)(`IUgmKA%xP!$N(4x!p9RJ(*~w@~fj5=HGSz6;gk@44D} z%5LCZ_6FG_6sr9~^`erNr@1@wiPw(&`y-4?e*EEMv>tc5;%V=hEE#=r^n_7k(;vQ; z(Q&i8c7?KDS}9cf3U_q1*@Zj0_S@K$_e2)GjZM2z%N;L-C1pW&Lx8YO0(#!?;=o!VqvQe1lC zIqhMos`j;#`_S#O_Isyq>pFbabGesyN91w}(0gLD zKfYj6sS1<6rPPLfR-W(jtV^k51}B+;<4e@Q{ya=JJ~iIGCy#b?+_kJdqy0cxb-piD zM{~4ih3d`6t)iVD_(=PS@_~R68UQ=xk6$?MuLwAYk%?M3Y+?Pcv1?N#lULPc-h z7OHoI>V#086smW(YQNH6R}P$MZzu=OP6^c?jss_Z{@bDS|LfPaceNZu$v9v>%<+;cI=WoYTTpS(xcpJ6g_~b zds(oyb&}3Ss6G&?v$?u5I+akJ6RPVTab{r45*|8x~xvJ(&2?u7-=)z*2icGT4os?T$Db%lzHW1fEPsB56}Vua}$>Kf@9 z3)MxTx+GMW^K?ygO_jCd6`{K7SUc7$u?l;%c4Wt5Y;t?x8s!yw<%Et2|vRCT(4E($sY`SKX@7)hGAt-Sp^5()SOKGwIsu zI@mWxy7tP(=o&{Bbb-oormsqDjWlk&F7Us0eYmc>PI<|lu7|Q5ePdsaf|TXx#$%Qv zXZ1sLTx_w8<)uQ=C5FX?h2|eijQx%Z-4K=-UUTa_cKEHb#3+|AIS-uc;&fbnk)w+j zs+&2w1fjBDi{$>4k2TPZ(2Xi>JdM_kQO46Pp>kX}q#H}2GD;R|$gMr*I+MD6w5;t> z_jd-q?XJ{x0+UWGIq9~~H+xpw);n@Vmn~mgD!;6MRMRQCOh-+pDK%9t9MVl!YI?h5 zO)FTezyFtBma+H$fNpM$^UOf^Pj6zUdW@% z6RLYTx-CNW%QG#{`MO=D<$t$Mc_EMLSD|uT0HouEJi1a|$dhpY=I5&;g1Wyt?VhDz z`G}TE{$FC!QeMcT@(cVnJEr@co$3X38z!B2^gps+- z*L|e>Sjc6BtP-*;r^vOA&Uv>A39T(6&(EU;T2Y>5}-?FGL zqgNH1LSI(Tod5;;a{BW63i^urO8UzBD*CE=H@!y4{J&PnIw9)?7plodA)AD37P3Xi z)&hOuO#pgs0#I%N(AzlNFDviOQV#da)t!g?Wlw&@Kr6YKF0!xj|Je2aC3kAl{+BQ6 z*Ee8*=((h$TH)G2@AafJC;H}koHxx&|2>Jx?B=;4JA`Vsn(%CGD%tkkCn zx$&c4jnR+iOX9!0oKinQKT-LTsGq`@MA=Ko+)Ka#Ik}tiCD9qN(9cvlIbA@MKU>J<suq<_B*g8lu%uT9P`S;gP;;rQCDpRebYIJx=-Ww~gH*O`^e)i37l zf3mMA{XLJ)@K)&8v9s~?Z*Oj&sa6xrF(Z`Jbv<{bStAve#_=L^~I zsTLS#JiGK*P(rLl$mY@(h$2z@Dg5dQZrei^Z) z^Mv`WTbXOVAE7(}Lw|@#OL;A>`}Ef$4tMIY3n)c;Ug@_*FdR+8Ud$c{@?^gol)dnH#2+Z)|K_wDyR zZtp&I_q*8GB(C2+r@zmnzm=S{+L}Q%1KW3+y5iMc^X8SEnf@sGf9SbPgF|`xzm()F zm#94MOs;)b#US(YLW9~+*1#@FXCZeHa@WV)3plLUV)W<|vxm36|1ZCH(NLw7_bwI= z4TIibVxPxgFt8WVL&!aI4Q6SVkb7};3_Az^`7;)V8U}Y(hK8C#4$3jGEed|(Q8Ytc zLt|EuhI)qj22Vo+LqkI&A@>$?A0hV@a)^*ag&ek3x@BmhtRM|O%9W@>?&nxPDi@*s zzj?yKG30oQ#AZVq!*hnVhIT>@7jl0g4-j&MGVZE69?n?g6$H%6{CdPA8nAedy{xvc>g;TL3Ijz)WbaGN!x+5_>@_4i3MWTit znRgwp5*6}5d{45JLA4P%5H zE#w#>$L1N*4C#ikLXH#iXd!d2<+EJjSg+XnrfDGg4;{;cJkyj5CW{^x{r7zUXOxo+ zQ&~A0CToe2qbXZ*4bu#nLLMgMQA%w8JTmJ1*i6F$wlkb~Qcgb!`-vz{K<#p#ed5$p zzWrpBaso;@{bX3epSujpm9Hk5Tn(L|9NG~wkHm4#ah2hD`&!zNt*oUJIpJbhoB4~7 zlS&>Bd2NRCLz@horO7em2zf+~Ay3FWdgdv1rJbMNX4pZ6wELCQE}U+0oOj7{y=mBG z;MEC29x3D$9@=H#xpRh6o;!Ey%Mn|&4T8N6d|3BHHS3KZ*f<$@?wp~N=g#$S^}gG$ z=5D@g{dP`u_3b<@^ZI}BD4pR|!)v5cJH~KWIb$i0$u(pej!;k`r#`8s#|-RpC?^$_ zie}nREjmsraypTMQrrT2eeE@^XE>u&RPr#Kb5t}DLCX5zXT?+Fs>E8fVcsm-b4Y;XA`E zWrt&ekSE&r_6)Zv)SZ%h5%0e8=_l|EU@loYvPQ|XpG`2H_D)*V_+NV1 ztFc?T!>T%NE$#Q1c&e(a8mnp%?za191` zot(DcV-hP|f`rQm;lieA2`w1IjQxz^#{R|u#t37iaiDRKaj{ z@kUn5%Y?jK$VY|zrjU;b`7I$I7xLReen-eBgnUxS?+SH!p{^j*6@|KzP*)b}DniWx zpd$A@7)RLeNRa3MFWhm`yk)FUfUjSy_pq2@{=Ts-y?wj`Vq#jx`LvAlj&A1f9o;;B zSbThJv!daRc7(^1;CqkvO5&l{Nzut(@%A?XdBu&29jojmaJxiIa&*dwhd*fA+26l| zZeyj0$;~HgW-?-jou8=nf`LJVepX2oZkM8X|v$f60#Ui=J z?Z$i|Zxr&TT;mSoP9d`&vs+n3KkZtJXTTTVBYfPeMjc0VjQfl)D$VXUz98gWA?M{9 z4;T*$d9#ptDo)9rJ-!vjk57+JNn`Kw(WP3>J{&S0V{^r2>W+7xJ$moi8s|&Tc>RI$ zy0gO5;Kp~@Xq|j?s9}q;)hZ}*oX2?D_`b5aIwRz*ImQo!yzQAbSLcoFY?Qca-1wRC zg0kh`F64asmfv`ZZP4YC+n{@aKjn0q&^=^-v+ylnPuhB0*qnhqhYWixCnwlz^suGB)vZ;!Y_Y3)j zT$7tgBjgu_{L)iZ_h}A$n~WxFDWx~rOx2Xq9}x0Ed+AN?QdLu}lBGX1V3M#M=={e^ zQ}1|Q8!3_~y@{O~Qz<(&yH33QW0}r97iD^+{~i$ClShserq1MPYD7n6gQ+3MorU}| zwKsVwwST2#?R{@JKjmxkvkw?kb7jCBdcue7dnhLMQ1VP|OwXCx3i&l5zb<5+DHKuU z*nnH%3Zcl4o_1~GXra^N4>eT#klHiemZFHospzfF;*LCUX+ zUXSIR%}`Tx@!7%&Y7O^cL`ooT7Eu{$HwYWv1cT~689 zIU8AN;!AR#X_a=kiLE6G`lO2jaAwt!S=U< z1SKUDz9HFu6Q;BE91|z~b4|HieIRq`)Y)9q7SmQCe<%k=J!o!*?OAT zZeGqYofGnvr&_-{KmD=kf-UYxv zA>R`6uTNU8{x+9oxq7;*s?6mj4|7FSb}U!lD~r@^<&S&Ha%CTNW(}jxtQGPPg`>`F zVALruMKhZV-!3En$aT__-P{UCjk&tHCS%Ub`RqG6W_KaqeU>q2_Av8yl@gcrnH!iJ zDr4>^A^&XOY@3@%Rn1LH7V~pe@Y>tMgDz!vyL9}=+o|=byV;jXo0Xh2en;>Hm-rsh ztNU!L{`;oy2S2R4+27pKUUzeVQukk|JBJ;qd&$F&|JkCO+nbf=@0mL&Mdvcvv$^I@ z%F6WjC$CJ+-OYWUdSz-3rQrS0-%<1jO1=N$4|SPGMIWdXoo7<~;Ve4u+jA5>##!_~ z?L~jdS@Z;Rl2UZeeE*$e9xl|tQ;2q=ZeOhW=^N*Y8Rn)brgL(m7Gwrl4Uy7 zoA>tC&Vl=1Y?IY%@BGzD(I+$Ml#-L)&1-aIY(clEB||4QKe&AEJCBN>qG|LS=bNBfIuzu2KE^K$b_b~wsfh0AX_ z#V`P0R`VO?qvkiw$INe;j|;V4s0~7G z6l#-Dn}yoKDM9lIWyi^U%6>{vZFTH7Ra5qx{-3;u$x-l+lmYj#P}`INSG~jN(eB-2 zN2L^A;TDy|zoUG-eVaEKos!`Azy;+4p9^($PL91X^^Kw@$IO?_-|@8Ba-Zh!TjuZ0KbU_s z-!|Vd-!=ba{@Hxb{EJZ65o&LtZYk8Qgu1m*cM@to+Fzz9(rrxrWpXntTn4yJaG zatj@nlpN3DyQhD^N%4amqfi;qqpn;M_iB#rms#g9lzFLK1qA~{;B9pyIQnOk~v zDPbx9dUPtaa3Qjku~hvZJjK`IrX0wZJS=+WQ+zEJF5y$#kLW9_>3Uqk=X{EXh12L#XQ(uCpz*3fI}n#+sG2Sj)V@O9tnd=wXT4h1qP)k}!ZOsn$knozL`#zWYFV|vQ2RNqmaXHsuvMpZ4#*VC znBo_dvp#pgvc$4f^PXjS z(dQ1R+X(e@%5w+QZ3~|}@c49voirph@*=E@;)E$L7(3!9uMG{s@0M0q>wd^aNp%*P|?6z1ASPoiV zvb=0zqt`{Ky9#wTq3$l!J+@d5Sq@uXv%GFOBGf&Fx|dJ~33aeghb!By|MFGgPk35G z;icb?F5rIVH)KEM0}t3Z{~z3MZaHar*O6yO=Wt2yqLn{uIcNEh3(M6ZLfu!W z``9lm|F3Ngl%;TPw|!@GokQVM%jf?XR9{#w3U#PZ_j3-aD@xc`g*uGGcMmr;QEpEd zUte2z3`CJ5VV0YgTkK2~J4fRT;I`#~@(3Agd0Y8v<*S#kQNCt*_wu#M*Dha2xK0tS zQ-$j^;hHI2g>aoNTxST^nZk9JaGhOX`NOeEvPzC;%vx2-21$7xmU2zGdf>xn%&KD_ zZjP+w=!>nPh6_H~BZw=LHCQdO&G$qhy+&zQ9~V$#MXCw=TQX01)FzDh^E zxf)p=^Hf(OTLY}E?VBTOD`j&O$L8o?u12t_jTMpVC|=rdYH4+ z>$HSUp z9YZ6s!8+Q$@g2obqJ<2gS1QnlNvf`yYAJ#gH zNoSXw^s&!}wa&G21r^^xt@D*ik9mryms!^<3-z-<>D9`uT*`i?HMg+RTa`+uIV(N( zVWp=$s&s);=^a9y?yU6gCsz7^^<|~f2dys&b%s!n%eB5@eO0K(3$^_cw|~7e@2QsQ zBUa8hmUw2X^)2ghrEC*~+Hq}<^(3`;w`46!d1kBiG?ShwIq74c*=jv&J?|K8AKFLT zWTkG8dD^UVv|X@XbX58arP5QM;uF`bU$gx%U*n1rzLUvIa;@K3ZwPg!P>YA>0<7Oz zS^XE;9$0_0-udUZac2nktiN)|#>#iL89CPbLOnCH;b2a7D39d{O;6>4li`l9SoTe- zRe8&_q{(!B_((FFi>(Z^R(_UaQweoe;a1F6)@7Ar%Bh7d*(%yN-)5ee5{F@j?6N>Xr7n;?paSB60?7wpEiF=ehi8xoWdzZeW8f z)C==$)onFwHHCVSP%jbcrOFPO-jN~uiwGY+TjHFpj#Sm=VXNCeIJ8N6(#Uvr1y1?= zZu)J7P%pMW56D*E<{2Iw+9^6cKG#-XTI~2lLt9<@PDA9utZB-W_StyZe|XX_NOSuO ze&`I)+s0FD904{f3~+g#&Clj&{ekvUO#-uywX| z5$bHAem>XM&DLG0+3#PgWZ-|$zh~)`y;#BaVkx!$OL|zrzK#ld$cgiRh4D6Y9-Ey+x?E7D)AM(aNgZ z7H5mM4YMWK5^YIbJ*(cPth$Ao?4^Qf&EC-Q}zkC83H*=1{ zDYoex0I*H9O|xa%gi!Ah>b*j}PpJ2Aw#~53w9T^37U~y-`bD8WAk+tyk!fU|)Pcv^ zW+bIg2#HsokQA1buGGj};uqmOQi)YsX}{_{T3Ojq5bqM7;*|90=nGe_Ft88mn z>)2M?vTe@`^~*xd*Vk9`Y-?@nY%_$KL$`6tEj z+$GajAH)!bHs&$(>&voeDqi3(6(EbAkMfLMY8JnE^VdW}S-89+(JT%U+dG?4+ zj!sMC!Ezna`9aLs^!PNk&*scB5usyaV|jjeeB6fyV-=Idro=uXenMI#e-{RnW6o6N zSTp5J9b2Lej2TY@I@rCdSj&urA5b2}&buT4Pg_MpFpp(k(iz zzw!Verc?G8>a?%tNSbDjTV~mEnd(dvWJ_}TJ&n>EiYpIITZVxE7CfXqsKB{gK~ zSb80^P&)DO!`uZB92yv=1k%NEteB_ovAd9vR%8;hbCQ1D?au*x_^WfqDs9{6w9BlL zsaM+W*x8mF*ok$nTW*)GnVL+k(yEQQJLjBC*+->5x7PIczfoP><_hthZLU(>o?@#Qw2MurbbW53kwgS*7koh z*c^RTl5Xz`pNNQL&Hc~p<_-)g<)=(Ncu3*c$Q>NQe-33cuPrj#;vC~JH-?Xw@^h)2 zq>-$A$7mq=N-d=BQZK2m6e^99#!8c=S<(XOd15k#UoPcc+*~X!)m&=0)O4xkQpcr@OQ6eOmx(TuU4+XFmw7IWT$Z>jb6Me% z?Xt#Yoy!K7O)dvrK6ClmZFnbZzb$?ApgQ#I?U` zjOz&3@vf6xXS&XH&30Yqy4m%h>mk=Ou4i4pa{bozw(C!>_sdi+ebli}cffXV@WX1bYZskoJ79ZBT1*O+|8lN|Ua`Hw=_}Tuhir#!&)Z(NvD#rx zdQ7O_Vr44S$A$Xst+t~sf7y=N-s0u&e+l(Fnd}Oj5bBe>1Ae2jdaeEB^`J7k{&gZ^ zVxsy_o|7_BX!-|~t zRhpm~{15=MnpYe+% z>385oTm*G8R7NT$VJVoOf%zGBV=wlD=?w=#pAC!$1LMJP9E=CUNnF7LNitRdxioeG zbu{(_aU030F$8fK1KKmDVJtGR2y3wcn~(=$Fm3~F8xMf?jW6Rm?&5DrGBL(XWgvrk zo63V)n9Q)E8fw5DwZRxPF$PW4(KG|(!gNlO%vIonFw6sE%1j(w93YvgCD#Ni0SOmNmwvXBo;D8exS1Hl-!L?IEwkt|6Z zt(B}zKrGf~@J9ezfzMiFkpjlPl^j~rk%94`KOBFPtXY_g`Cyz{7vms4z!$h9Nj7q1 zBS$tHs)PL4$d8Tu*u2pk)X~-h zHE&6hJLAory1Fw@cjn{X9v#sM)Y!c@`hr@!_d|csmOE{@Ct(DrpZjP~Kle$Pf@zXe zt2v$nIj_ZhYB8T$%%|1>L}CzzU=GM{EyiFia$JiX*E))mAl_Q!xz?w+fNQvm-`K>s zqAI9y?RuaewaI(!#%Ka>kgwXrQQIE@2m-OxCYIX7Qky>2rcbr$Q*Fj@ZQ`jN4f0r< zKGvpgwW&$%ckvM(NKzdg8i4uL;WKrng0Wd=5vXsSXqRxk4 zo^|M7ozHLuU*ao#jT`tyl03@52*$Jr?RwA`5Ay2K4jm8(a_rFy!RUh!&qLt!^mDTiqBi|GH@yi*c9$ z=3jR@$VuG-?7zKk%2tfe?`QdI@0c)Z?@DvXKK~thXC`@j#O56JLEd zm{13PXbZ+-{S?rz`nlKw>Q+A=2XPE62lZJF>Yv0ZoW}e30+(`W8PC>2IgS_7GVjNfp|Qrz30cc4BGOfEl=8N zP!aXf6N5p$8Y~642)Lt<>0iNliAh;|w= z|3=Ka5%X?jMs?H#d22*ljap(f$WBzu%Oa%REG8OZ&2unaeo2CIgqm^jF%=C z!FXwM71t!GX(hBp6o|DcwQV{F^T0ftuE9o-x26Yh0`KBOT)}mG1M=AP2aw06ckwfR zmn84HE`;dR1Q5XaA=|et!rh$C<F`B{` zeh5HoQ1fQwtyu`d5RL(e1a)dQ1Vcfen#F;SPfCnwDr1I<}x z{M?{JHF&}cEfEZA$dg|_j)MOC-IgSO z`siN;8t7qy71hDm@UMeK&NdYcU zp)4w(GOB{S1=Ilj2%sMUvq1g>R$vvLm!y_mKp$E%?ppQ-eP~7PTk-i;sZjI*7T`$I>t#-8`^9`uG%yL^=jh{VsCQ;H*pKp{<)1{S$S?Z_TmLe zYTFj{zb!GeC68?xH*IO7?Jy)E8KW>20yB|?c_624mtZ+o;v|O)$ZI=)Ova1&L6X{& zkM_jYo_e((h#_DMwPy^qPrz`D1pR8C2I|nB@!6hvv}ZYQKON+;{T$54A}j?tZND1i zsr@^67pL(7$XWZ3@fp6rWqgV2ATRB|gA&^v{ET1mJN}fU4id^hh8B8Ij}8`$z<5jq z@pT}+4%Dc_C0qe*bfArnEzk+nx??x=#CjBf8g!%v9rsC6AngRYfp!9EC(s1)9!L%X zX(y0&0;hoZI;lW@JC#F43Ft2urXGjHPbh;Ai}T`*9}9z-33=yx#v4K~3H3v8f|!41$5jnNYHH@G$Eb8rvP*Wg|V#y~J`f(IiC z$ryL{-qf=<{p(!`RiS|n-slb1hrL-IdQ*qqmnErB8Bps!9WVq57!LZ< zXB4PIAL8#b6IqxCYSM?k^jV7KV19k5O&{vrrvTKZ&wd=h%Xk&U-sg7?{Ssf_Dj=@D zdQgYHjJdwG-~q;4Uuw~}FJ@pfm|tJ!(f2go$ETpieaS&z<{82~L+EcvRcOIHL#S&A z%VY@i4)Fs0522|IgOHJ!h)H0%37LxdSb&9~?;-239veU% zLw4a!P{R;v7(xw0sA0%iP{WWg)jU-J^HspM|4J45O05C>>q_xq=OvwpNI9>2y)(kA6@`sqyHhihBxpg z$aQ~m-Tx=BPVLW_=>G@)mZSl$AhrSJP!Uz2K|M4;BY1&02N34~;v7JJ2ebtF9YB5u zklz8sIiMGMqc0*sE(gRT0ZABvk(h{Sn2uSXF9Q~W+zwcVJvfKU_zK_PCVs>n{0#aL zLEI5qQ2z+(A5jyvLEI6ILCg_8Xa-`9AjXJ6pww|bB z$#Z0LklV;sAl}H%V4OxWP9sAQhHwl(JcfgOMvlfYyIT*&yN@M2!de zf;b0t1UVbj72VMb#65`o4I)>A79bz*;|EC^EJF(utf&s^GPo{SHU~EZpC3$)2Qy{| zPs4P~!aOi$2h+#F^l|WJyoz`5F+Rf=xQs7x9XCL(2mg*g@R$8msx+hwe-q1)a;Shx zV5|(OhMFLzA;dJKK6;=($p4UN#32DG7z64sBm<0vA=G2YBIFwkX<*qMfKDBx4k4Cu$tXOVni0PSjcyU>Ej+T1QdqDB6rV zj3c1ks1I-s=kW?-) z3K2uzVdEfW&<{Xcw)AIcw!jyG4w0ux+KNA!V_IF1S2pSX~@7N zOa)^nb_Vu>9K_Q1So$4%5BKq>BrznUxH2FYaVB`76^J{I8pQQR2&hHe01O0e#*GAH zC@vL@qd4jlN8jS;TO4i2WrAGB9mO?CiZ2V6`S{L=0Wrsu`*_BCJaNbKx%gSg!d#G# z_=Q-Eb=U~<5x)i7L4D#+;{vEb{8#t})Ht4TAOACc1$BvkAW6eK5DwxTMr^~F_pnW1 zp2K$FWt;-@9L79{T?X?V#(al;i|_CQZsTuBN^k`^N?@J|pg?@GjoN`#1~6UBVY2u7oc^TnXRf7tqH9 z`k3&SBqd59wnP;)(1W@rTEV(8u_ii#x+W5PVgv>u3dEjB?1{vlNbHG>sYGg;I2+Gn zEjELA6W_wyAg;tuLCz8{g4hy?EsGzK}Ko=JPa zypvwUL6C!_Pw+b!tHbHr@G?-NJSw3ov@pO78)~2yJWwAE5d_*EPTRw2dpP|bPQQoK z@8L0s2gw^g0`z@2aSdl&4`;j#pNJ`#h6PxJC0H&=BV0iYBQ(&#h>_qkBPN4oVT1sA z8Sx|T;3xbdNy**7a+Ta4#FI=s$%k1;jDh9bRY(A2i3Ccpubw zG<56W`-D=+|iaHTpOFfxjhb4EY&jf)&+3ZN@O)F?CQ6o}fNsXlD%VjG>(|!5E39 zcmbb)`lnLU)b@zNWX!@G%*R5k!bapE4=iV?+mMfgIE2@66pYg4og;!54lAKr6HX<0Wl1UIz0^V;*T&K`qj5fgGe)0`p8~p6T>Coj#}2=XBLpt+L?*M9$-WTB*fPol_7|>RF0x~cI*;tDW$iZgN&-4Q9!d{TKbo!jmI7%nB z^bf$8NoOpilh5=E_yU(e-08%feqEBrQm?VK&>6HjmfVirhmY{HBxUG8+!-xF9x@n1 z8N{E_1EJ`L{)hm5$QX>_V9aKuVk}rTGFYB6GBF+0EaOcut}=)(gZMItFXJ+ZErYR^ z!T8R&CrRUoZyd}1IC49#Dzvbm8ft>r$C2l8y)YHbdmLkX+yR`xyZ8`S@DrHlc;-32 zEST?j<~v>k`Z?YRGwOq}KfXJd@A%#bK|fHd@$_^25JV#miHy66 zHZblcl8=duyNQguiA~^xW(Y?bsKdl8%mcAaT!PhDgY_VX6LUcBCjKEwld6OHO=2FC zx}X~vyOYSlq%mNglbGiu`aFp~PomG0nCB$=J831D_oS`J$3YyzYj^|4@eXKf(tG$E zH*g1xze&I1cl;?ylhvSRlj-kd#=>MR^aw;Ma=|<%^ZChqelnk*%;zVwY)&ChQ40gdYOX8g0=5ozN9wpe|D*Fc5!J2ATQJVBN7Q1j*(!#)6lZ00$e9M5LHvzhPgbYy@W&nCyS=V2kp(QIX&m26L#v9NL^in{z(LC6Kc@v^nQnkh3{INYY%|noC=A ztDz=nZ!TkKt|w@BZWH*RJvyQ@x}pbqp*Q*>6y$3zam|eZam^hG>Nl5s&7FwJn2Jm+ z#1bsWDm)MJHurU$0?X&zkMJpof9{vKjvKfM;-5>t=DETE6Uf&*FA(cIVx329^TH60 z2n@kc#3CMyuX*Dz6C`5Z0xZS~ti~Fw1Iy67LwFab@c}*rxt;eJzQARW&w12x9<`jO z#C9LQ<4^o8N%LJ$0hQqfEo|^WJ&^PH4bd3neST}SMF(^Od7s}M$(R80IG-^%e>RAJ zKKYzaKIfCq`Pm@;`FYrjV>pg8AfNMzbw07qC$Tdh;6}EY{w4l!XCVgLwFr; z;3OCi3(kX_FZdJ}K;9R8i(B{+cR}74{3=Nct*8(3xRCL?usMi-A^BWLJ{OYDg6q0^i>8B|FPe=x zn1|I^gZ0=1^1f&bPT(Vu$3=|aMPGvW7m?3Jiy5bjA4t*? zH|Su51=T<-OBkO^>cSI^K)sg)q6@l#_?8gg5^}jD1pUw-kr)JGUXlP}T|%r&h;_+$ z5bKf|m<7h~62`-l`N#%&TtXg~1doCPs0 z{TQF(8&K1w_ds2j-j}3hGRmPMsz3uB8o(bd(FX0%5iC2)1|S~9xoj-PV-l$IGGboF z7+glI%T|CIFMA$qu>s_6SuPIa5~%O;T4)byvpfzXkb*HF56hYNa^}69c`s+)%gM=d z^1b{ENumj91^Hc}g#l(%hdb(k@v))>fap@O5dX?+_!{5hJA5xmtBhd$t|Hb|#JFl2mVtS$B8FAeeAR1Uo~xMWsuPGXItQD@?#Iu^(uD*(|z_?p|6SpKOyDF-I+GLZvY{pr3V>E+57;o9F z@f^A!6vIHA*<(PBvX^2xHem~hFPr$X_h27h#6b{SHnC-YgbN@q+2kerI&Oe`XOr*j zza{DUO0b~@YQY2b(GXtnMmuysAUdNPdLRhB(HBDy4aURs2^fxKEC%D?`8;ezK6c^- z5bN`=;1FKJ+xQS)gE*hRgTEwcjRrcnqb`VV4e_mM3Li8FIb1_*Ylv+P@vI3)1O|cF z){Mb=5W|`s*ad33W0s3A~HAZ6w}}b>RtWv(XFwV61QK2=cU%JZ&UT8-u_&-xz`@#DRWo z9F9>)#aPh4jcf1{sMW?(ApVWSzmfPiQm2iVa23S9@j9r}#s`wLsS1d5lLhtA60JcD zn`nO%wc13jHVwc)3zWH$0STeCg?}b8#s+~_z0ikA})jFHiv%X5O)r7=avEEAh$By zpo0~}oJ-8PEPJ^g@B%UB27y}VlIPrb(0=YjWMKiwZSGPqUURdt7Q~dh6R+S5sCO>) z&iw=zKy0~JK+SU*ueprZ+*^{A$9T$Y&nwzPxA*LlTma0{W1*8;9^Fj^hLvvw7q-kG$r6 z3gXZE0@ra@k~YgI3u4?%4mW$l7sRo-6KH>P50JynAqWGpY>o%D-8=@=b#o?WfxK>> zhs7YK%_~4|H*djyyn}af8Xw?8e2mXPT$`!+W@^6qI)2BW_*;^;klQWPehazXQXUmS zZnqHk7A+XVTbhA!w`B@uV=k!i7HYg@H8vp^o3Rz!QGl0l7)L1mkV%14-Ie4%OifV%t^^#I}vtwl#$>{15=*+tvrf zwyhufgM4ipgeWksw#9@wgC+_X-&;{Mm3%wBzV%|>7+gTR24@DA) zar<1%2V-t~4rqV-K^(&gP}A+ybo)7+$0s1B?Kf}__d!m#|0PNJjJNzUAhvvRn%@BA zG@sb=i7mf5x`90A6GuL|%a20>$Xh;n%O8tzm;iE@&$!GdPx-`^&vKZ52eegC7oKQ@ zCh!6CFKB^QU`!RXMY0r@B(9|h#2AOyrvFaQHF9kVb8tTp zK+HQkqZ?>@=QhyZ&K=k-NxQ0|8fv08$i*&CGz9(M)e7`~7yaMGymm3KUG#Yu<8@aI z;z2CCMj!=aFckuf)m^hO2lK(W-L)9&K;L)K_g%!Yi@xuA0gRPhui!9V$5lz%T@g*u z2J~llCv*k*-`yL;vO5gqWH&Y6P5yV!0&VW5&E4dGH~HU9{&$oA-L$!TI|@L%yZ2xp zUIgv$ChxnC;{;BDobNu15Alm6?J0u_s0`M9dvvg(I^01H_jteyg#{qKJ;b+X6|%7wjE_CUwujjEkc&M>@fH{hd)~wQIEVB28b3+WURTH{2XeNz z3N+BegnIAviG^q>$;!6auwzsV%{O+naFL(6!Hjjc@^^xS;X6zcZfbimhc&NEaXRi!k&fv z&T2vk!*>>vKoTjKcSsr;=+vhegvuLg-l2mq?@(Qa9*3-br-snFVJDAY@xD+$`iVdXktiaGjfIMGc=!i8fihNp&j@Q3+qK+ z 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

w>7u3<=IyHj!NkRh1&Je(g&G?&0V-& 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 Oqulxb`)A0o&;JVouoUwE diff --git a/nahbar/nahbar/AddMomentView.swift b/nahbar/nahbar/AddMomentView.swift index 4a0267a..acb6960 100644 --- a/nahbar/nahbar/AddMomentView.swift +++ b/nahbar/nahbar/AddMomentView.swift @@ -26,6 +26,9 @@ struct AddMomentView: View { return cal.date(bySettingHour: hour + 1, minute: 0, second: 0, of: Date()) ?? Date() }() @State private var eventDuration: Double = 3600 // Sekunden; -1 = Ganztag + @State private var availableCalendars: [EKCalendar] = [] + @State private var selectedCalendarID: String = "" + @State private var eventAlarmOffset: Double = -3600 // Sekunden; 0 = keine Erinnerung // Vorhaben: Erinnerung @State private var addReminder = false @@ -203,11 +206,67 @@ struct AddMomentView: View { } .padding(.horizontal, 16) .padding(.vertical, 4) + + RowDivider() + + HStack { + Image(systemName: "bell") + .font(.system(size: 13)) + .foregroundStyle(eventAlarmOffset != 0 ? theme.accent : theme.contentTertiary) + Text("Erinnerung") + .font(.system(size: 15)) + .foregroundStyle(theme.contentPrimary) + Spacer() + Picker("", selection: $eventAlarmOffset) { + Text("Keine").tag(0.0) + Text("5 Min vorher").tag(-300.0) + Text("15 Min vorher").tag(-900.0) + Text("1 Std vorher").tag(-3600.0) + Text("1 Tag vorher").tag(-86400.0) + } + .tint(theme.accent) + } + .padding(.horizontal, 16) + .padding(.vertical, 4) + + if availableCalendars.count > 1 { + RowDivider() + + HStack { + Text("Kalender") + .font(.system(size: 15)) + .foregroundStyle(theme.contentPrimary) + Spacer() + Picker("", selection: $selectedCalendarID) { + ForEach(availableCalendars, id: \.calendarIdentifier) { cal in + HStack(spacing: 6) { + Circle() + .fill(Color(cgColor: cal.cgColor)) + .frame(width: 10, height: 10) + Text(cal.title) + } + .tag(cal.calendarIdentifier) + } + } + .tint(theme.accent) + } + .padding(.horizontal, 16) + .padding(.vertical, 4) + } } } .background(theme.surfaceCard) .clipShape(RoundedRectangle(cornerRadius: theme.radiusCard)) .padding(.horizontal, 20) + .task(id: addToCalendar) { + guard addToCalendar && availableCalendars.isEmpty else { return } + let calendars = await CalendarManager.shared.availableCalendars() + availableCalendars = calendars + // Vorauswahl: gespeicherter Kalender oder der Standard + if selectedCalendarID.isEmpty || !calendars.map(\.calendarIdentifier).contains(selectedCalendarID) { + selectedCalendarID = CalendarManager.shared.defaultCalendarIdentifier ?? "" + } + } } // MARK: - Erinnerungs-Sektion (Vorhaben) @@ -290,8 +349,9 @@ struct AddMomentView: View { ) modelContext.insert(calEntry) person.logEntries?.append(calEntry) - createCalendarEvent(notes: trimmed) { - // Callback nach Dismiss + let momentID = moment.id + Task { + await createAndStoreCalendarEvent(for: momentID, notes: trimmed) } } dismiss() @@ -299,21 +359,7 @@ struct AddMomentView: View { return } - guard addToCalendar else { - dismiss() - return - } - - let dateStr = eventDate.formatted(.dateTime.day().month(.abbreviated).hour().minute()) - let calEntry = LogEntry( - type: .calendarEvent, - title: String.localizedStringWithFormat(String(localized: "Treffen mit %@ — %@"), person.firstName, dateStr), - person: person - ) - modelContext.insert(calEntry) - person.logEntries?.append(calEntry) - - createCalendarEvent(notes: trimmed) {} + dismiss() } // MARK: - Vorhaben-Erinnerung @@ -341,40 +387,35 @@ struct AddMomentView: View { } } - // MARK: - EventKit (callback-basiert, kein Swift Concurrency) + // MARK: - EventKit (async via CalendarManager) - private func createCalendarEvent(notes: String, completion: @escaping () -> Void) { - let store = EKEventStore() + private func createAndStoreCalendarEvent(for momentID: UUID, notes: String) async { + let isAllDay = eventDuration < 0 + let startDate: Date + let endDate: Date - let handler: (Bool, Error?) -> Void = { [store] granted, _ in - guard granted, let calendar = store.defaultCalendarForNewEvents else { - DispatchQueue.main.async { self.dismiss() } - return - } - - let event = EKEvent(eventStore: store) - event.title = String.localizedStringWithFormat(String(localized: "Treffen mit %@"), self.person.firstName) - event.notes = notes.isEmpty ? nil : notes - event.calendar = calendar - - if self.eventDuration < 0 { - event.isAllDay = true - let dayStart = Calendar.current.startOfDay(for: self.eventDate) - event.startDate = dayStart - event.endDate = Calendar.current.date(byAdding: .day, value: 1, to: dayStart) ?? dayStart - } else { - event.startDate = self.eventDate - event.endDate = self.eventDate.addingTimeInterval(self.eventDuration) - } - - try? store.save(event, span: .thisEvent) - DispatchQueue.main.async { self.dismiss() } + if isAllDay { + startDate = Calendar.current.startOfDay(for: eventDate) + endDate = Calendar.current.date(byAdding: .day, value: 1, to: startDate) ?? startDate + } else { + startDate = eventDate + endDate = eventDate.addingTimeInterval(eventDuration) } - if #available(iOS 17.0, *) { - store.requestWriteOnlyAccessToEvents(completion: handler) - } else { - store.requestAccess(to: .event, completion: handler) + let calendarID = selectedCalendarID.isEmpty ? nil : selectedCalendarID + let title = String.localizedStringWithFormat(String(localized: "Treffen mit %@"), person.firstName) + let alarmOffset: TimeInterval? = eventAlarmOffset == 0 ? nil : eventAlarmOffset + + if let identifier = await CalendarManager.shared.createEvent( + title: title, + notes: notes.isEmpty ? nil : notes, + startDate: startDate, + endDate: endDate, + isAllDay: isAllDay, + calendarIdentifier: calendarID, + alarmOffset: alarmOffset + ) { + CalendarEventStore.save(momentID: momentID, eventIdentifier: identifier) } } } diff --git a/nahbar/nahbar/CalendarManager.swift b/nahbar/nahbar/CalendarManager.swift new file mode 100644 index 0000000..2d08f76 --- /dev/null +++ b/nahbar/nahbar/CalendarManager.swift @@ -0,0 +1,175 @@ +import EventKit +import Foundation + +// MARK: - CalendarEventStore +// Speichert die Zuordnung Moment-UUID → EKEvent-Identifier in UserDefaults. +// Dieser Mapping-Store ist bewusst device-lokal (EKEvent-IDs sind nicht cloud-sync-fähig). + +enum CalendarEventStore { + private static let key = "nahbar.momentCalendarEvents" + + /// Feuert wenn sich das Mapping ändert. Das `object` ist die betroffene Moment-UUID. + static let didChangeNotification = Notification.Name("CalendarEventStoreDidChange") + + static func save(momentID: UUID, eventIdentifier: String) { + var map = load() + map[momentID.uuidString] = eventIdentifier + UserDefaults.standard.set(map, forKey: key) + DispatchQueue.main.async { + NotificationCenter.default.post(name: didChangeNotification, object: momentID) + } + } + + static func identifier(for momentID: UUID) -> String? { + load()[momentID.uuidString] + } + + static func remove(momentID: UUID) { + var map = load() + map.removeValue(forKey: momentID.uuidString) + UserDefaults.standard.set(map, forKey: key) + DispatchQueue.main.async { + NotificationCenter.default.post(name: didChangeNotification, object: momentID) + } + } + + private static func load() -> [String: String] { + UserDefaults.standard.dictionary(forKey: key) as? [String: String] ?? [:] + } +} + +// MARK: - CalendarManager + +final class CalendarManager { + static let shared = CalendarManager() + private init() {} + + private let store = EKEventStore() + + // MARK: Berechtigungen + + /// Fordert Full Access an und gibt zurück, ob Zugriff gewährt wurde. + func requestFullAccess() async -> Bool { + do { + if #available(iOS 17.0, *) { + return try await store.requestFullAccessToEvents() + } else { + return try await store.requestAccess(to: .event) + } + } catch { + return false + } + } + + /// Schreibt einen neuen Kalender-Eintrag und gibt den EKEvent-Identifier zurück. + /// Gibt nil zurück, wenn kein Zugriff gewährt wurde oder ein Fehler auftritt. + /// `alarmOffset`: negativer Zeitabstand vor dem Start in Sekunden (nil = keine Erinnerung). + func createEvent( + title: String, + notes: String?, + startDate: Date, + endDate: Date, + isAllDay: Bool, + calendarIdentifier: String?, + alarmOffset: TimeInterval? = nil + ) async -> String? { + let granted = await requestFullAccess() + guard granted else { return nil } + + let calendar: EKCalendar? = { + if let id = calendarIdentifier { + return store.calendar(withIdentifier: id) + } + return store.defaultCalendarForNewEvents + }() + guard let calendar else { return nil } + + let event = EKEvent(eventStore: store) + event.title = title + event.isAllDay = isAllDay + event.startDate = startDate + event.endDate = endDate + event.calendar = calendar + + // nahbar-Markierung in den Notizen + let marker = "— via nahbar" + if let existingNotes = notes, !existingNotes.isEmpty { + event.notes = "\(existingNotes)\n\n\(marker)" + } else { + event.notes = marker + } + + // Erinnerung (EKAlarm) wenn gewünscht + if let offset = alarmOffset { + event.addAlarm(EKAlarm(relativeOffset: offset)) + } + + do { + try store.save(event, span: .thisEvent) + return event.eventIdentifier + } catch { + return nil + } + } + + /// Aktualisiert Titel, Notizen und Startdatum eines bestehenden Kalendereintrags. + /// Die Dauer sowie Ganztages-/Alarm-Einstellungen des Eintrags bleiben erhalten. + /// Gibt true zurück, wenn der Eintrag gefunden und gespeichert wurde. + func updateEvent(identifier: String, title: String, notes: String?, newStartDate: Date) async -> Bool { + let granted = await requestFullAccess() + guard granted else { return false } + guard let event = store.event(withIdentifier: identifier) else { return false } + + event.title = title + + let marker = "— via nahbar" + if let n = notes, !n.isEmpty { + event.notes = "\(n)\n\n\(marker)" + } else { + event.notes = marker + } + + // Startdatum verschieben, Dauer beibehalten (nur für zeitgebundene Einträge) + if !event.isAllDay { + let duration = event.endDate.timeIntervalSince(event.startDate) + event.startDate = newStartDate + event.endDate = newStartDate.addingTimeInterval(duration) + } + + do { + try store.save(event, span: .thisEvent) + return true + } catch { + return false + } + } + + /// Löscht einen Kalendereintrag anhand seines Identifiers. + /// Gibt true zurück, wenn der Eintrag gefunden und gelöscht wurde. + func deleteEvent(identifier: String) async -> Bool { + let granted = await requestFullAccess() + guard granted else { return false } + + guard let event = store.event(withIdentifier: identifier) else { return false } + do { + try store.remove(event, span: .thisEvent) + return true + } catch { + return false + } + } + + /// Gibt alle Benutzer-Kalender zurück (sortiert nach Titel). + func availableCalendars() async -> [EKCalendar] { + let granted = await requestFullAccess() + guard granted else { return [] } + return store.calendars(for: .event) + .filter { $0.allowsContentModifications } + .sorted { $0.title < $1.title } + } + + /// Gibt den Standardkalender für neue Einträge zurück. + var defaultCalendarIdentifier: String? { + store.defaultCalendarForNewEvents?.calendarIdentifier + } +} diff --git a/nahbar/nahbar/ContactPickerView.swift b/nahbar/nahbar/ContactPickerView.swift index cb86679..5c77947 100644 --- a/nahbar/nahbar/ContactPickerView.swift +++ b/nahbar/nahbar/ContactPickerView.swift @@ -238,8 +238,16 @@ struct ContactImport { let location: String if let postal = contact.postalAddresses.first?.value { - // Bundesstaat/Region einbeziehen, falls vorhanden - location = [postal.city, postal.state, postal.country].filter { !$0.isEmpty }.joined(separator: ", ") + var parts: [String] = [] + // Straße (mehrzeilig → ", " zusammenführen) + let street = postal.street.replacingOccurrences(of: "\n", with: ", ") + if !street.isEmpty { parts.append(street) } + // PLZ + Stadt zusammen, Bundesstaat separat + let cityPart = [postal.postalCode, postal.city].filter { !$0.isEmpty }.joined(separator: " ") + if !cityPart.isEmpty { parts.append(cityPart) } + if !postal.state.isEmpty { parts.append(postal.state) } + if !postal.country.isEmpty { parts.append(postal.country) } + location = parts.joined(separator: ", ") } else { location = "" } diff --git a/nahbar/nahbar/IchView.swift b/nahbar/nahbar/IchView.swift index 7ec2263..f2a0183 100644 --- a/nahbar/nahbar/IchView.swift +++ b/nahbar/nahbar/IchView.swift @@ -1,6 +1,7 @@ import SwiftUI import PhotosUI import SwiftData +import Contacts private let socialStyleOptions = [ "Introvertiert", @@ -319,6 +320,7 @@ struct IchEditView: View { @State private var socialStyle: String @State private var selectedPhoto: UIImage? @State private var photoPickerItem: PhotosPickerItem? = nil + @State private var showingContactPicker = false init() { let store = UserProfileStore.shared @@ -443,9 +445,22 @@ struct IchEditView: View { .font(.system(size: 15, weight: .semibold)) .foregroundStyle(theme.accent) } + ToolbarItem(placement: .bottomBar) { + Button { + showingContactPicker = true + } label: { + Label("Vom Kontakt übernehmen", systemImage: "person.crop.circle") + .font(.system(size: 14)) + } + .foregroundStyle(theme.accent) + } } } - + .overlay(alignment: .center) { + SingleContactPickerTrigger(isPresented: $showingContactPicker, onSelect: applyContact) + .frame(width: 0, height: 0) + .allowsHitTesting(false) + } .onChange(of: photoPickerItem) { _, item in Task { guard let item else { return } @@ -531,6 +546,22 @@ struct IchEditView: View { .padding(.vertical, 12) } + // MARK: - Contact Import + + private func applyContact(_ contact: CNContact) { + let imported = ContactImport.from(contact) + if !imported.name.isEmpty { name = imported.name } + if !imported.occupation.isEmpty { occupation = imported.occupation } + if !imported.location.isEmpty { location = imported.location } + if let bday = imported.birthday { + birthday = bday + hasBirthday = true + } + if let data = imported.photoData { + selectedPhoto = UIImage(data: data) + } + } + // MARK: - Helpers @ViewBuilder diff --git a/nahbar/nahbar/Localizable.xcstrings b/nahbar/nahbar/Localizable.xcstrings index 7af5b8b..a457786 100644 --- a/nahbar/nahbar/Localizable.xcstrings +++ b/nahbar/nahbar/Localizable.xcstrings @@ -2,23 +2,62 @@ "sourceLanguage" : "de", "strings" : { "" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "" + } + } + } }, " " : { "comment" : "A placeholder text for the error message.", - "isCommentAutoGenerated" : true + "isCommentAutoGenerated" : true, + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : " " + } + } + } }, "— %@" : { "comment" : "A quote author", - "isCommentAutoGenerated" : true + "isCommentAutoGenerated" : true, + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "— %@" + } + } + } }, "·" : { "comment" : "A period.", - "isCommentAutoGenerated" : true + "isCommentAutoGenerated" : true, + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "·" + } + } + } }, "[%@]" : { "comment" : "A label displaying the log category of a log entry. The argument is the log category of the log entry.", - "isCommentAutoGenerated" : true + "isCommentAutoGenerated" : true, + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "[%@]" + } + } + } }, "%@ analysieren" : { "comment" : "LogbuchView – AI analysis button label with person's first name", @@ -49,6 +88,12 @@ "state" : "new", "value" : "%1$@: %2$@, %3$@" } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@: %2$@, %3$@" + } } } }, @@ -61,15 +106,60 @@ "state" : "new", "value" : "%1$@%2$@%3$@" } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@%2$@%3$@" + } } } }, "%lld ausgewählt" : { - + "localizations" : { + "en" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld selected" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld selected" + } + } + } + } + } + } }, "%lld Einträge" : { "comment" : "A label showing the number of log entries. The argument is the number of entries.", - "isCommentAutoGenerated" : true + "isCommentAutoGenerated" : true, + "localizations" : { + "en" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld entry" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld entries" + } + } + } + } + } + } }, "%lld Einträge – Export als Textdatei" : { "comment" : "SettingsView / LogExportView – entry count with export hint", @@ -118,7 +208,14 @@ } }, "%lld Kontakte ausgewählt. Weiter." : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld contacts selected. Continue." + } + } + } }, "%lld von %lld Kontakten – Pro für mehr" : { "comment" : "A text label that shows the number of contacts that can be made for free, followed by a call to action to upgrade to Pro.", @@ -129,14 +226,34 @@ "state" : "new", "value" : "%1$lld von %2$lld Kontakten – Pro für mehr" } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$lld of %2$lld contacts – Pro for more" + } } } }, "%lld von 100 Zeichen" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld of 100 characters" + } + } + } }, "%lld/100" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld/100" + } + } + } }, "📱 Kontakte werden ausschließlich lokal gespeichert und niemals mit Servern geteilt." : { "comment" : "PrivacyBadgeView – contacts context message", @@ -172,7 +289,14 @@ } }, "🔒 Diese Daten bleiben ausschließlich auf deinem iPhone und werden niemals übertragen." : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "🔒 This data stays exclusively on your iPhone and is never transmitted." + } + } + } }, "1 Monat" : { "comment" : "Settings – look-ahead / period picker option", @@ -196,6 +320,26 @@ } } }, + "1 Std vorher" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "1 hr before" + } + } + } + }, + "1 Tag vorher" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "1 day before" + } + } + } + }, "1 Woche" : { "comment" : "Settings – look-ahead period picker option", "localizations" : { @@ -252,6 +396,16 @@ } } }, + "5 Min vorher" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "5 min before" + } + } + } + }, "10 kurze Situationen. Keine falschen Antworten. Dauert etwa 2 Minuten." : { "localizations" : { "en" : { @@ -262,6 +416,16 @@ } } }, + "15 Min vorher" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "15 min before" + } + } + } + }, "30 Min" : { "comment" : "AddMomentView – calendar event duration option", "localizations" : { @@ -297,7 +461,14 @@ } }, "Abonnement" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Subscription" + } + } + } }, "Abonnement verlängert sich automatisch. In den iPhone-Einstellungen jederzeit kündbar." : { "comment" : "PaywallView – subscription legal notice", @@ -332,6 +503,28 @@ } } }, + "Alle %lld Einträge anzeigen" : { + "localizations" : { + "en" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Show all %lld entry" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Show all %lld entries" + } + } + } + } + } + } + }, "Alle %lld Einträge werden entfernt." : { "comment" : "LogExportView – clear log confirmation message", "localizations" : { @@ -356,10 +549,24 @@ } }, "Alle %lld Tage – basierend auf deinem Profil" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Every %lld days – based on your profile" + } + } + } }, "Alle Features freigeschaltet" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "All features unlocked" + } + } + } }, "Alle Momente und Notizen zu dieser Person werden unwiderruflich gelöscht." : { "comment" : "AddPersonView – delete confirmation message", @@ -373,7 +580,14 @@ } }, "Alle Personen, Momente, Besuche und dein Profil werden unwiderruflich gelöscht. Die App startet neu." : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "All persons, moments, visits and your profile will be permanently deleted. The app will restart." + } + } + } }, "Alle Pro-Features freigeschaltet" : { "comment" : "SettingsView – Pro subscription active subtitle", @@ -434,7 +648,14 @@ } }, "Alles löschen und Onboarding starten" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Delete everything and start onboarding" + } + } + } }, "Alles, was du in nahbar eingibst, wird ausschließlich auf deinem iPhone gespeichert und verarbeitet." : { "comment" : "OnboardingPrivacyView – subtitle below headline", @@ -562,10 +783,24 @@ } }, "App wirklich zurücksetzen?" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Really reset the app?" + } + } + } }, "App zurücksetzen" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reset app" + } + } + } }, "App-Schutz" : { "comment" : "SettingsView – section header for app lock settings", @@ -613,10 +848,24 @@ } }, "Auf Max upgraden – KI-Analyse freischalten" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Upgrade to Max – unlock AI analysis" + } + } + } }, "Aus Kontakten ausfüllen" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fill from contacts" + } + } + } }, "Aus Kontakten auswählen" : { "comment" : "AddPersonView – import from contacts button", @@ -792,7 +1041,14 @@ } }, "Bitte gib zuerst deinen Vornamen ein." : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Please enter your first name first." + } + } + } }, "Chat" : { "comment" : "MomentSource.chat raw value", @@ -908,7 +1164,15 @@ }, "Daten werden in dieser Sitzung nicht gespeichert." : { "comment" : "A description of the data that is not saved in the current session.", - "isCommentAutoGenerated" : true + "isCommentAutoGenerated" : true, + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Data is not saved in this session." + } + } + } }, "Daten werden nur lokal gespeichert" : { "comment" : "SettingsView – iCloud sync disabled subtitle", @@ -923,7 +1187,15 @@ }, "Datenbankfehler" : { "comment" : "A title of a banner that appears when the app is in degraded mode.", - "isCommentAutoGenerated" : true + "isCommentAutoGenerated" : true, + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Database error" + } + } + } }, "Datenschutz" : { "comment" : "SettingsView – privacy info row label", @@ -949,7 +1221,14 @@ } }, "Datenschutzhinweis: Diese Daten werden ausschließlich lokal auf deinem Gerät gespeichert und niemals übertragen." : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Privacy notice: This data is stored exclusively locally on your device and is never transmitted." + } + } + } }, "Datenschutzhinweis: Diese Funktion sendet Daten an einen externen KI-Dienst." : { "comment" : "PrivacyBadgeView – aiFeature context accessibility label", @@ -997,7 +1276,15 @@ }, "Datum angeben" : { "comment" : "A label displayed in a form section.", - "isCommentAutoGenerated" : true + "isCommentAutoGenerated" : true, + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Set date" + } + } + } }, "Dauer" : { "comment" : "AddMomentView – calendar event duration section label", @@ -1021,7 +1308,14 @@ } }, "Dein Geschlecht hilft, die Auswertung besser einzuordnen." : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Your gender helps to better contextualise the evaluation." + } + } + } }, "Dein nächstes Gespräch kann hier beginnen." : { "comment" : "PersonDetailView – moments empty state message", @@ -1093,7 +1387,14 @@ } }, "Details" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Details" + } + } + } }, "Diagnose" : { "comment" : "SettingsView – section header for developer diagnostics", @@ -1162,6 +1463,16 @@ } } }, + "Dieser Moment hat einen verknüpften Kalendereintrag. Soll dieser ebenfalls gelöscht werden?" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "This moment has a linked calendar event. Should it also be deleted?" + } + } + } + }, "Distanzierter" : { "comment" : "RatingQuestion – negative pole for relationship closeness question", "extractionState" : "stale", @@ -1296,7 +1607,14 @@ } }, "Du kannst Kontakte jederzeit später in der App hinzufügen." : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "You can add contacts at any time later in the app." + } + } + } }, "Du meldest dich locker – er ist wahrscheinlich einfach beschäftigt." : { "extractionState" : "stale", @@ -1490,7 +1808,14 @@ } }, "Eigene Kontaktdaten aus Adressbuch übernehmen" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Import your own contact details from the address book" + } + } + } }, "Ein Freund erzählt von einem Plan, den du für einen Fehler hältst." : { "extractionState" : "stale", @@ -1626,7 +1951,14 @@ } }, "Ergebnis bestätigen und fortfahren" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Confirm result and continue" + } + } + } }, "Erinnern" : { "comment" : "PersonDetailView – set reminder confirmation button", @@ -1640,6 +1972,16 @@ } } }, + "Erinnerung" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reminder" + } + } + } + }, "Erinnerung setzen" : { "comment" : "AddMomentView – toggle label to enable reminder for intention moments", "localizations" : { @@ -1664,7 +2006,14 @@ } }, "Erinnerungen" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reminders" + } + } + } }, "Erneut" : { "localizations" : { @@ -1710,7 +2059,14 @@ } }, "Erzähl uns kurz, wer du bist." : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tell us briefly who you are." + } + } + } }, "Etwas Neues ausprobieren" : { "comment" : "PersonDetailView – activity suggestion: try something new", @@ -1844,6 +2200,12 @@ "state" : "new", "value" : "Frage %1$lld von %2$lld" } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Question %1$lld of %2$lld" + } } } }, @@ -1861,7 +2223,15 @@ }, "Füge deine eigenen Infos hinzu – damit nahbar noch besser versteht, in welchem Kontext du Beziehungen pflegst." : { "comment" : "A description of the benefits of adding your own information.", - "isCommentAutoGenerated" : true + "isCommentAutoGenerated" : true, + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Add your own info – so nahbar better understands the context in which you nurture relationships." + } + } + } }, "Fühlt sich die Beziehung gestärkt an?" : { "comment" : "RatingQuestion – relationship question text", @@ -1943,6 +2313,17 @@ } } }, + "Geplante Treffen" : { + "comment" : "TodayView – section title for planned future meeting moments", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Planned Meetings" + } + } + } + }, "Geschenkidee anzeigen" : { "comment" : "TodayView GiftSuggestionRow – collapsed state button", "localizations" : { @@ -1978,10 +2359,24 @@ } }, "Geschlecht" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gender" + } + } + } }, "Geschlecht (optional)" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gender (optional)" + } + } + } }, "Gesellig" : { "extractionState" : "stale", @@ -2017,17 +2412,6 @@ } } }, - "Geplante Treffen" : { - "comment" : "TodayView – section title for planned future meeting moments", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Planned Meetings" - } - } - } - }, "Gesprächszeit" : { "comment" : "SettingsView section header / CallWindowSetupView nav title", "localizations" : { @@ -2229,7 +2613,14 @@ } }, "Idee: %@" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Idea: %@" + } + } + } }, "Ideen werden generiert…" : { "comment" : "TodayView GiftSuggestionRow – loading state text", @@ -2391,6 +2782,16 @@ } } }, + "Kalender" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Calendar" + } + } + } + }, "Kauf wiederherstellen" : { "comment" : "PaywallView – restore purchases button", "localizations" : { @@ -2402,6 +2803,16 @@ } } }, + "Keine" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "None" + } + } + } + }, "Keine Einträge für diesen Filter" : { "comment" : "LogExportView – empty state when filter shows no entries", "localizations" : { @@ -2438,7 +2849,15 @@ }, "Keine Treffer." : { "comment" : "A label displayed when there are no search results.", - "isCommentAutoGenerated" : true + "isCommentAutoGenerated" : true, + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "No results." + } + } + } }, "KI-Analyse" : { "comment" : "SettingsView – section header for AI settings", @@ -2531,19 +2950,54 @@ } }, "Kontakte aus Adressbuch auswählen" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Select contacts from address book" + } + } + } }, "Kontakte auswählen" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Select contacts" + } + } + } }, "Kontakte hinzufügen" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Add contacts" + } + } + } }, "Kontakte überspringen" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skip contacts" + } + } + } }, "Kontakte überspringen?" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skip contacts?" + } + } + } }, "Kontakte und Momente bleiben lokal auf deinem Gerät – keine Cloud-Synchronisation." : { "comment" : "OnboardingPrivacyView – local storage privacy row text", @@ -2592,7 +3046,14 @@ } }, "Kurze Frage vorab" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Quick question first" + } + } + } }, "Limit erreicht" : { "comment" : "LogbuchView – AI refresh button label when at request limit", @@ -2639,7 +3100,14 @@ } }, "Los geht's – nahbar starten" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Let's go – start nahbar" + } + } + } }, "Löschen" : { "comment" : "Universal delete button / swipe action", @@ -2699,7 +3167,14 @@ } }, "Max aktiv" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Max active" + } + } + } }, "Menschen" : { "comment" : "Tab label for people list", @@ -2747,7 +3222,15 @@ }, "Mittwoch, 16. April" : { "comment" : "A label that displays the date.", - "isCommentAutoGenerated" : true + "isCommentAutoGenerated" : true, + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wednesday, 16 April" + } + } + } }, "Möchtest du die Notiz anpassen?" : { "comment" : "VisitEditFlowView – note step title", @@ -2808,6 +3291,26 @@ } } }, + "Moment + Kalendereintrag löschen" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Delete moment + calendar event" + } + } + } + }, + "Moment bearbeiten" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Edit moment" + } + } + } + }, "Moment festhalten" : { "comment" : "AddMomentView – sheet navigation title", "localizations" : { @@ -2819,6 +3322,26 @@ } } }, + "Moment löschen" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Delete moment" + } + } + } + }, + "Moment…" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Moment…" + } + } + } + }, "Momente" : { "comment" : "PersonDetailView – moments section header", "localizations" : { @@ -3059,7 +3582,15 @@ }, "nahbar" : { "comment" : "The name of the app.", - "isCommentAutoGenerated" : true + "isCommentAutoGenerated" : true, + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "nahbar" + } + } + } }, "nahbar erinnert dich täglich in deinem Zeitfenster und schlägt einen Kontakt vor — mit Notizen, damit du vorbereitet bist." : { "comment" : "CallWindowSetupView – feature description", @@ -3073,10 +3604,24 @@ } }, "Nahbar erinnert dich, wenn du diese Person seit der gewählten Zeit nicht mehr kontaktiert hast." : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "nahbar reminds you when you haven't contacted this person for the selected period." + } + } + } }, "nahbar erinnert dich, wenn du lange nichts von jemandem gehört hast." : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "nahbar reminds you when you haven't heard from someone for a long time." + } + } + } }, "nahbar Max freischalten für KI-Analyse" : { "comment" : "LogbuchView – upsell button for AI analysis", @@ -3115,7 +3660,15 @@ }, "nahbar-log.txt" : { "comment" : "The file name of the log export.", - "isCommentAutoGenerated" : true + "isCommentAutoGenerated" : true, + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "nahbar-log.txt" + } + } + } }, "Natürlich & verbunden" : { "comment" : "Theme tagline for Grove", @@ -3186,7 +3739,15 @@ }, "Nicht angegeben" : { "comment" : "A placeholder value for the social style picker.", - "isCommentAutoGenerated" : true + "isCommentAutoGenerated" : true, + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Not specified" + } + } + } }, "Nicht jetzt" : { "comment" : "CallSuggestionView – dismiss / defer button", @@ -3234,11 +3795,26 @@ } }, "Noch keine Kontakte" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "No contacts yet" + } + } + } }, "Noch keine Menschen hier." : { "comment" : "A description of the empty state when there are no people in the list.", - "isCommentAutoGenerated" : true + "isCommentAutoGenerated" : true, + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "No people here yet." + } + } + } }, "Noch keine Momente festgehalten" : { "comment" : "TodayView lastSeenHint – no moments recorded at all", @@ -3286,7 +3862,14 @@ } }, "Notiz anpassen" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Adjust note" + } + } + } }, "Notizen" : { "comment" : "AddPersonView / PersonDetailView – notes field label (plural)", @@ -3300,6 +3883,16 @@ } } }, + "Nur Moment löschen" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Delete moment only" + } + } + } + }, "Nur Smalltalk" : { "comment" : "RatingQuestion – negative pole for conversation depth", "extractionState" : "stale", @@ -3381,7 +3974,14 @@ } }, "Onboarding, Profil und alle Daten löschen" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Delete onboarding, profile and all data" + } + } + } }, "Optional" : { "comment" : "AddPersonView – optional field placeholder hint", @@ -3431,7 +4031,15 @@ }, "Person hinzufügen" : { "comment" : "A button that adds a new person.", - "isCommentAutoGenerated" : true + "isCommentAutoGenerated" : true, + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Add person" + } + } + } }, "Personalisierte Vorschläge in 2 Minuten" : { "localizations" : { @@ -3454,7 +4062,14 @@ } }, "Persönlichkeits-Pentagon-Diagramm" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Personality Pentagon diagram" + } + } + } }, "Persönlichkeitsempfehlung: Passend für dich" : { "extractionState" : "stale", @@ -3468,7 +4083,14 @@ } }, "Persönlichkeitsprofil-Details anzeigen" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Show personality profile details" + } + } + } }, "Persönlichkeitsquiz starten" : { "localizations" : { @@ -3515,18 +4137,48 @@ } }, "Pro oder Max-Abo" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pro or Max subscription" + } + } + } }, "Profil bearbeiten" : { "comment" : "The title of the screen where a user can edit their profile.", - "isCommentAutoGenerated" : true + "isCommentAutoGenerated" : true, + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Edit profile" + } + } + } }, "Profil einrichten" : { "comment" : "A button to create a user's profile.", - "isCommentAutoGenerated" : true + "isCommentAutoGenerated" : true, + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Set up profile" + } + } + } }, "Profilfoto auswählen" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Select profile photo" + } + } + } }, "Push-Benachrichtigung nach dem Besuch" : { "comment" : "SettingsView – aftermath notification toggle subtitle", @@ -3582,7 +4234,14 @@ } }, "Quiz überspringen" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skip quiz" + } + } + } }, "Quiz zurücksetzen" : { "localizations" : { @@ -3634,6 +4293,12 @@ "state" : "new", "value" : "Schritt %1$lld von %2$lld" } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Step %1$lld of %2$lld" + } } } }, @@ -3814,10 +4479,24 @@ } }, "Spitzname (optional)" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nickname (optional)" + } + } + } }, "Spitzname, optional" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nickname, optional" + } + } + } }, "Suchen…" : { "comment" : "ShareExtensionView – contact search placeholder", @@ -3912,7 +4591,15 @@ }, "Tippe auf + um jemanden hinzuzufügen." : { "comment" : "A description of how to add a new contact.", - "isCommentAutoGenerated" : true + "isCommentAutoGenerated" : true, + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tap + to add someone." + } + } + } }, "Tippe auf + um loszulegen" : { "comment" : "VisitHistorySection – empty state subtitle", @@ -3960,7 +4647,14 @@ } }, "Treffen bewerten" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rate meeting" + } + } + } }, "Treffen mit %@" : { "comment" : "AddMomentView – calendar event / LogEntry title with person name", @@ -3985,7 +4679,14 @@ } }, "Trotzdem überspringen" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skip anyway" + } + } + } }, "Typ" : { "comment" : "ShareExtensionView – moment type section header", @@ -4022,10 +4723,24 @@ } }, "Über mich (optional)" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "About me (optional)" + } + } + } }, "Über mich, maximal 100 Zeichen" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "About me, maximum 100 characters" + } + } + } }, "Über nahbar" : { "comment" : "SettingsView – about section header", @@ -4098,7 +4813,14 @@ } }, "Uns fehlt noch was – wir würden gerne mehr von dir erfahren." : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "We're still missing something – we'd like to know more about you." + } + } + } }, "Unternehmung" : { "comment" : "MomentType.intention displayName – shown in type picker and feature tour", @@ -4145,6 +4867,16 @@ } } }, + "Verlauf" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "History" + } + } + } + }, "Version" : { "comment" : "SettingsView – version info row label", "extractionState" : "stale", @@ -4224,6 +4956,16 @@ } } }, + "Vom Kontakt übernehmen" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Import from contact" + } + } + } + }, "Von" : { "comment" : "CallWindowSetupView – start of time window label", "localizations" : { @@ -4281,10 +5023,24 @@ } }, "Vorname" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "First name" + } + } + } }, "Vorname, erforderlich" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "First name, required" + } + } + } }, "Wähle deinen Plan" : { "comment" : "PaywallView – header title", @@ -4309,10 +5065,24 @@ } }, "Wähle Menschen aus deinem Adressbuch, die dir wichtig sind." : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Choose people from your address book who matter to you." + } + } + } }, "Wähle Menschen aus, die dir wichtig sind." : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Choose people who matter to you." + } + } + } }, "Wann?" : { "comment" : "AddMomentView – calendar event date picker label", @@ -4395,22 +5165,64 @@ } }, "Weiter (%lld ausgewählt)" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Continue (%lld selected)" + } + } + } }, "Weiter zu den Fragen" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Continue to questions" + } + } + } }, "Weiter zum nächsten Schritt" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Continue to next step" + } + } + } }, "Weiter zum Persönlichkeitsquiz" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Continue to personality quiz" + } + } + } }, "Weiter, kein Kontakt ausgewählt" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Continue, no contact selected" + } + } + } }, "Weitere hinzufügen" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Add more" + } + } + } }, "Wenn du magst, kannst du das Treffen kurz reflektieren." : { "extractionState" : "stale", @@ -4425,7 +5237,15 @@ }, "Wer bist du?" : { "comment" : "A title for the empty state view.", - "isCommentAutoGenerated" : true + "isCommentAutoGenerated" : true, + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Who are you?" + } + } + } }, "Wichtig" : { "comment" : "LogbuchView swipe action – mark moment as important", @@ -4486,7 +5306,15 @@ }, "Wie heißt du?" : { "comment" : "A label for the user's name.", - "isCommentAutoGenerated" : true + "isCommentAutoGenerated" : true, + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "What's your name?" + } + } + } }, "Wie ist dein Energielevel nach dem Treffen?" : { "comment" : "RatingQuestion – energy level question text", @@ -4501,10 +5329,24 @@ } }, "Wie kennen dich deine Freunde?" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "How do your friends know you?" + } + } + } }, "Wie nennen dich deine Freunde?" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "What do your friends call you?" + } + } + } }, "Wie oft erinnern?" : { "comment" : "AddPersonView – nudge frequency picker label", @@ -4552,7 +5394,14 @@ } }, "Willkommen bei nahbar" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Welcome to nahbar" + } + } + } }, "Wir erinnern dich an die Nachwirkung." : { "comment" : "VisitSummaryView – aftermath reminder subtitle", @@ -4624,10 +5473,24 @@ } }, "z. B. Max" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "e.g. Max" + } + } + } }, "Zeigt eine Bestätigungsabfrage." : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Shows a confirmation prompt." + } + } + } }, "Zeitfenster" : { "comment" : "SettingsView / CallWindowSetupView – time window section header and row label", @@ -4640,9 +5503,27 @@ } } }, + "Zeitpunkt" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Time" + } + } + } + }, "Zeitraum" : { "comment" : "A generic term for a billing period.", - "isCommentAutoGenerated" : true + "isCommentAutoGenerated" : true, + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Period" + } + } + } }, "Zu Max upgraden" : { "comment" : "PaywallView – CTA button when upgrading from Pro to Max", diff --git a/nahbar/nahbar/LogbuchView.swift b/nahbar/nahbar/LogbuchView.swift index 3751235..979b416 100644 --- a/nahbar/nahbar/LogbuchView.swift +++ b/nahbar/nahbar/LogbuchView.swift @@ -73,6 +73,16 @@ struct LogbuchView: View { @State private var remainingRequests: Int = AIAnalysisService.shared.remainingRequests @AppStorage("aiConsentGiven") private var aiConsentGiven = false + // Kalender-Lösch-Bestätigung + @State private var momentPendingDelete: Moment? = nil + @State private var showCalendarDeleteDialog = false + + // Moment-Bearbeitung + @State private var momentForTextEdit: Moment? = nil + + /// Inkrementiert bei jeder CalendarEventStore-Änderung → triggert Re-Render der Rows. + @State private var calendarEventsVersion = 0 + var body: some View { ScrollView { VStack(alignment: .leading, spacing: 28) { @@ -104,6 +114,31 @@ struct LogbuchView: View { Task { await runAnalysis() } } } + .sheet(item: $momentForTextEdit) { moment in + EditMomentView(moment: moment) + } + .onReceive(NotificationCenter.default.publisher(for: CalendarEventStore.didChangeNotification)) { _ in + calendarEventsVersion += 1 + } + .confirmationDialog( + "Moment löschen", + isPresented: $showCalendarDeleteDialog, + presenting: momentPendingDelete + ) { moment in + Button("Moment + Kalendereintrag löschen", role: .destructive) { + performDelete(moment, deleteCalendarEvent: true) + momentPendingDelete = nil + } + Button("Nur Moment löschen", role: .destructive) { + performDelete(moment, deleteCalendarEvent: false) + momentPendingDelete = nil + } + Button("Abbrechen", role: .cancel) { + momentPendingDelete = nil + } + } message: { _ in + Text("Dieser Moment hat einen verknüpften Kalendereintrag. Soll dieser ebenfalls gelöscht werden?") + } .onReceive( NotificationCenter.default.publisher( for: Notification.Name("NSManagedObjectContextObjectsDidChangeNotification") @@ -135,6 +170,7 @@ struct LogbuchView: View { DeletableLogbuchRow( isImportant: moment.isImportant, isLast: index == items.count - 1, + onEdit: { momentForTextEdit = moment }, onDelete: { deleteMoment(moment) }, onToggleImportant: { toggleImportant(moment) } ) { @@ -152,6 +188,24 @@ struct LogbuchView: View { } private func deleteMoment(_ moment: Moment) { + if CalendarEventStore.identifier(for: moment.id) != nil { + momentPendingDelete = moment + showCalendarDeleteDialog = true + } else { + performDelete(moment, deleteCalendarEvent: false) + } + } + + private func performDelete(_ moment: Moment, deleteCalendarEvent: Bool) { + let momentID = moment.id + if deleteCalendarEvent, let eventID = CalendarEventStore.identifier(for: momentID) { + Task { + _ = await CalendarManager.shared.deleteEvent(identifier: eventID) + CalendarEventStore.remove(momentID: momentID) + } + } else { + CalendarEventStore.remove(momentID: momentID) + } modelContext.delete(moment) person.touch() } @@ -200,6 +254,13 @@ struct LogbuchView: View { .font(.system(size: 10)) .foregroundStyle(.orange) } + if case .moment(let m) = item, + calendarEventsVersion >= 0, // Dependency auf calendarEventsVersion + CalendarEventStore.identifier(for: m.id) != nil { + Image(systemName: "calendar") + .font(.system(size: 10)) + .foregroundStyle(theme.contentTertiary) + } Text(LocalizedStringKey(item.label)) .font(.system(size: 12)) .foregroundStyle(theme.contentTertiary) @@ -483,12 +544,13 @@ struct LogbuchView: View { } // MARK: - Deletable Logbuch Row -// Rechts wischen → Wichtig (orange), Links wischen → Löschen (rot) +// Rechts wischen → Bearbeiten (accent) + Wichtig (orange), Links wischen → Löschen (rot) private struct DeletableLogbuchRow: View { @Environment(\.nahbarTheme) var theme let isImportant: Bool let isLast: Bool + let onEdit: () -> Void let onDelete: () -> Void let onToggleImportant: () -> Void @ViewBuilder let content: Content @@ -499,6 +561,23 @@ private struct DeletableLogbuchRow: View { var body: some View { ZStack { HStack(spacing: 0) { + // Links: Bearbeiten-Button + Button { + withAnimation(.spring(response: 0.32, dampingFraction: 0.8)) { offset = 0 } + onEdit() + } label: { + VStack(spacing: 4) { + Image(systemName: "pencil") + .font(.system(size: 15, weight: .medium)) + Text("Bearbeiten") + .font(.system(size: 11, weight: .medium)) + } + .foregroundStyle(.white) + .frame(width: actionWidth) + .frame(maxHeight: .infinity) + } + .background(theme.accent) + // Links: Wichtig-Button Button { onToggleImportant() @@ -550,18 +629,18 @@ private struct DeletableLogbuchRow: View { let x = value.translation.width guard abs(x) > abs(value.translation.height) * 0.6 else { return } if x > 0 { - offset = min(x, actionWidth + 16) + offset = min(x, actionWidth * 2 + 16) } else { offset = max(x, -(actionWidth + 16)) } } .onEnded { value in let x = value.translation.width - if x > actionWidth + 20 { + if x > actionWidth * 2 + 20 { onToggleImportant() withAnimation(.spring(response: 0.32, dampingFraction: 0.8)) { offset = 0 } } else if x > actionWidth / 2 { - withAnimation(.spring(response: 0.32, dampingFraction: 0.8)) { offset = actionWidth } + withAnimation(.spring(response: 0.32, dampingFraction: 0.8)) { offset = actionWidth * 2 } } else if x < -(actionWidth / 2) { withAnimation(.spring(response: 0.32, dampingFraction: 0.8)) { offset = -actionWidth } } else { diff --git a/nahbar/nahbar/NahbarContact.swift b/nahbar/nahbar/NahbarContact.swift index 6157509..f22e729 100644 --- a/nahbar/nahbar/NahbarContact.swift +++ b/nahbar/nahbar/NahbarContact.swift @@ -19,6 +19,8 @@ struct NahbarContact: Identifiable, Codable, Equatable { /// Firma oder Organisation des Kontakts. var organizationName: String var notes: String + /// Formatierte Postanschrift (Straße, PLZ Stadt, Land). + var location: String /// Original CNContact identifier for stable matching against the system address book. var cnIdentifier: String? @@ -30,6 +32,7 @@ struct NahbarContact: Identifiable, Codable, Equatable { emailAddresses: [String] = [], organizationName: String = "", notes: String = "", + location: String = "", cnIdentifier: String? = nil ) { self.id = id @@ -39,6 +42,7 @@ struct NahbarContact: Identifiable, Codable, Equatable { self.emailAddresses = emailAddresses self.organizationName = organizationName self.notes = notes + self.location = location self.cnIdentifier = cnIdentifier } @@ -52,15 +56,16 @@ struct NahbarContact: Identifiable, Codable, Equatable { self.organizationName = contact.organizationName // CNContactNoteKey requires a special entitlement – omitted intentionally. self.notes = "" + self.location = ContactImport.from(contact).location self.cnIdentifier = contact.identifier } // MARK: - Codable (rückwärtskompatibel) - // Neue Felder (emailAddresses, organizationName) mit decodeIfPresent lesen, - // damit bestehende NahbarContacts.json-Dateien ohne diese Felder weiterhin laden. + // Neue Felder mit decodeIfPresent lesen, damit bestehende NahbarContacts.json + // ohne diese Felder weiterhin geladen werden können. enum CodingKeys: String, CodingKey { - case id, givenName, familyName, phoneNumbers, emailAddresses, organizationName, notes, cnIdentifier + case id, givenName, familyName, phoneNumbers, emailAddresses, organizationName, notes, location, cnIdentifier } init(from decoder: Decoder) throws { @@ -72,6 +77,7 @@ struct NahbarContact: Identifiable, Codable, Equatable { emailAddresses = try c.decodeIfPresent([String].self, forKey: .emailAddresses) ?? [] organizationName = try c.decodeIfPresent(String.self, forKey: .organizationName) ?? "" notes = try c.decodeIfPresent(String.self, forKey: .notes) ?? "" + location = try c.decodeIfPresent(String.self, forKey: .location) ?? "" cnIdentifier = try c.decodeIfPresent(String.self, forKey: .cnIdentifier) } // encode(to:) wird vom Compiler synthetisiert, da alle Felder Encodable sind. diff --git a/nahbar/nahbar/OnboardingContainerView.swift b/nahbar/nahbar/OnboardingContainerView.swift index d3b4a64..93dc19f 100644 --- a/nahbar/nahbar/OnboardingContainerView.swift +++ b/nahbar/nahbar/OnboardingContainerView.swift @@ -111,6 +111,7 @@ struct OnboardingContainerView: View { // 3. Import each selected contact as a Person in SwiftData for contact in coordinator.selectedContacts { let person = Person(name: contact.fullName) + if !contact.location.isEmpty { person.location = contact.location } modelContext.insert(person) } if !coordinator.selectedContacts.isEmpty { diff --git a/nahbar/nahbar/PersonDetailView.swift b/nahbar/nahbar/PersonDetailView.swift index 4d59719..78c7c1e 100644 --- a/nahbar/nahbar/PersonDetailView.swift +++ b/nahbar/nahbar/PersonDetailView.swift @@ -18,13 +18,22 @@ struct PersonDetailView: View { @State private var momentForEdit: Moment? = nil @State private var momentForSummary: Moment? = nil + // Moment-Bearbeiten + @State private var momentForTextEdit: Moment? = nil + + // Kalender-Lösch-Bestätigung + @State private var momentPendingDelete: Moment? = nil + @State private var showCalendarDeleteDialog = false + @StateObject private var personalityStore = PersonalityStore.shared + @State private var activityHint: String = "" var body: some View { ScrollView { VStack(alignment: .leading, spacing: 28) { personHeader momentsSection + if !person.sortedLogEntries.isEmpty { logbuchSection } if hasInfoContent { infoSection } } .padding(.horizontal, 20) @@ -77,6 +86,28 @@ struct PersonDetailView: View { } } } + .sheet(item: $momentForTextEdit) { moment in + EditMomentView(moment: moment) + } + .confirmationDialog( + "Moment löschen", + isPresented: $showCalendarDeleteDialog, + presenting: momentPendingDelete + ) { moment in + Button("Moment + Kalendereintrag löschen", role: .destructive) { + performDelete(moment, deleteCalendarEvent: true) + momentPendingDelete = nil + } + Button("Nur Moment löschen", role: .destructive) { + performDelete(moment, deleteCalendarEvent: false) + momentPendingDelete = nil + } + Button("Abbrechen", role: .cancel) { + momentPendingDelete = nil + } + } message: { _ in + Text("Dieser Moment hat einen verknüpften Kalendereintrag. Soll dieser ebenfalls gelöscht werden?") + } // Schützt vor Crash wenn der ModelContext durch Migration oder // CloudKit-Sync intern reset() aufruft und Person-Objekte ungültig werden. .onReceive( @@ -119,15 +150,6 @@ struct PersonDetailView: View { HStack { SectionHeader(title: "Momente", icon: "clock") Spacer() - NavigationLink { - LogbuchView(person: person) - } label: { - Image(systemName: "book.closed") - .font(.system(size: 13, weight: .medium)) - .foregroundStyle(theme.contentTertiary) - .padding(.horizontal, 8) - .padding(.vertical, 5) - } Button { showingAddMoment = true } label: { @@ -164,6 +186,7 @@ struct PersonDetailView: View { isLast: index == person.sortedMoments.count - 1, onDelete: { deleteMoment(moment) }, onToggleImportant: { toggleImportant(moment) }, + onEdit: { momentForTextEdit = moment }, onRateMeeting: { momentForRating = moment }, onAftermathMeeting: { momentForAftermath = moment }, onViewSummary: { momentForSummary = moment }, @@ -178,53 +201,125 @@ struct PersonDetailView: View { } } - // MARK: - Vorhaben-Vorschlag (ersetzt nextStepSection/nextStepSuggestionsView) + // MARK: - Vorhaben-Vorschlag private func intentionSuggestionButton(profile: PersonalityProfile) -> some View { - let preferred = PersonalityEngine.preferredActivityStyle(for: profile) - let highlightNew = PersonalityEngine.highlightNovelty(for: profile) + let hint = activityHint.isEmpty ? refreshActivityHint(profile: profile) : activityHint - let activities: [(String, ActivityStyle?, Bool)] = [ - ("Kaffee trinken", .oneOnOne, false), - ("Spazieren gehen", .oneOnOne, false), - ("Zusammen essen", .group, false), - ("Etwas unternehmen", .group, false), - ("Etwas Neues ausprobieren", nil, true), - ("Anrufen", nil, false), - ] - - func score(_ item: (String, ActivityStyle?, Bool)) -> Int { - var s = 0 - if item.1 == preferred { s += 2 } - if item.2 && highlightNew { s += 1 } - return s - } - let sorted = activities.sorted { score($0) > score($1) } - let topTwo = sorted.prefix(2).map { $0.0 } - let hint = topTwo.joined(separator: " oder ") - let topActivity = sorted.first?.0 ?? "" - - return Button { - // AddMomentView mit vorausgefülltem Intention-Typ öffnen - // (PersonDetailView übergibt den Vorschlagstext via AddMomentView-Initialisierung) - showingAddMoment = true - _ = topActivity // Vorschlag wird in AddMomentView als Standardtyp .intention öffnen - } label: { - HStack(spacing: 6) { - Image(systemName: "brain") - .font(.system(size: 11)) - .foregroundStyle(NahbarInsightStyle.accentPetrol) - Text("Idee: \(hint)") - .font(.system(size: 13)) - .foregroundStyle(theme.contentSecondary) - .lineLimit(1) + return HStack(spacing: 0) { + Button { + showingAddMoment = true + } label: { + HStack(spacing: 6) { + Image(systemName: "brain") + .font(.system(size: 11)) + .foregroundStyle(NahbarInsightStyle.accentPetrol) + Text("Idee: \(hint)") + .font(.system(size: 13)) + .foregroundStyle(theme.contentSecondary) + .lineLimit(1) + } + .padding(.leading, 14) + .padding(.vertical, 7) + .frame(maxWidth: .infinity, alignment: .leading) + } + + // Neue Idee würfeln + Button { + activityHint = refreshActivityHint(profile: profile) + } label: { + Image(systemName: "arrow.clockwise") + .font(.system(size: 12)) + .foregroundStyle(theme.contentTertiary) + .padding(.horizontal, 12) + .padding(.vertical, 7) } - .padding(.horizontal, 14) - .padding(.vertical, 7) - .frame(maxWidth: .infinity, alignment: .leading) } } + @discardableResult + private func refreshActivityHint(profile: PersonalityProfile) -> String { + let suggestions = PersonalityEngine.suggestedActivities( + for: profile, tag: person.tag, count: 2 + ) + let hint = suggestions.joined(separator: " oder ") + activityHint = hint + return hint + } + + // MARK: - Logbuch Vorschau + + private let logbuchPreviewLimit = 5 + + private var logbuchSection: some View { + let entries = person.sortedLogEntries + let preview = Array(entries.prefix(logbuchPreviewLimit)) + let hasMore = entries.count > logbuchPreviewLimit + + return VStack(alignment: .leading, spacing: 10) { + HStack { + SectionHeader(title: "Verlauf", icon: "book.closed") + Spacer() + } + + VStack(spacing: 0) { + ForEach(Array(preview.enumerated()), id: \.element.id) { index, entry in + logEntryPreviewRow(entry) + if index < preview.count - 1 || hasMore { RowDivider() } + } + if hasMore { + NavigationLink(destination: LogbuchView(person: person)) { + HStack { + Text("Alle \(entries.count) Einträge anzeigen") + .font(.system(size: 14)) + .foregroundStyle(theme.accent) + Spacer() + Image(systemName: "chevron.right") + .font(.system(size: 12, weight: .medium)) + .foregroundStyle(theme.contentTertiary) + } + .padding(.horizontal, 16) + .padding(.vertical, 12) + } + } + } + .background(theme.surfaceCard) + .clipShape(RoundedRectangle(cornerRadius: theme.radiusCard)) + } + } + + private func logEntryPreviewRow(_ entry: LogEntry) -> some View { + HStack(spacing: 12) { + Image(systemName: entry.type.icon) + .font(.system(size: 14, weight: .light)) + .foregroundStyle(theme.accent) + .frame(width: 20) + + VStack(alignment: .leading, spacing: 3) { + Text(entry.title) + .font(.system(size: 15, design: theme.displayDesign)) + .foregroundStyle(theme.contentPrimary) + .fixedSize(horizontal: false, vertical: true) + + HStack(spacing: 6) { + Text(LocalizedStringKey(entry.type.rawValue)) + .font(.system(size: 12)) + .foregroundStyle(theme.contentTertiary) + Text("·") + .font(.system(size: 12)) + .foregroundStyle(theme.contentTertiary) + Text(entry.loggedAt.formatted(.dateTime.day().month(.abbreviated).year())) + .font(.system(size: 12)) + .foregroundStyle(theme.contentTertiary) + } + } + + Spacer() + } + .padding(.horizontal, 16) + .padding(.vertical, 12) + } + // MARK: - Info private var hasInfoContent: Bool { @@ -266,6 +361,25 @@ struct PersonDetailView: View { // MARK: - Aktionen private func deleteMoment(_ moment: Moment) { + // Prüfen ob ein Kalendereintrag verknüpft ist → ggf. Bestätigung anfordern + if CalendarEventStore.identifier(for: moment.id) != nil { + momentPendingDelete = moment + showCalendarDeleteDialog = true + } else { + performDelete(moment, deleteCalendarEvent: false) + } + } + + private func performDelete(_ moment: Moment, deleteCalendarEvent: Bool) { + let momentID = moment.id + if deleteCalendarEvent, let eventID = CalendarEventStore.identifier(for: momentID) { + Task { + _ = await CalendarManager.shared.deleteEvent(identifier: eventID) + CalendarEventStore.remove(momentID: momentID) + } + } else { + CalendarEventStore.remove(momentID: momentID) + } modelContext.delete(moment) person.touch() } @@ -307,9 +421,9 @@ struct PersonDetailView: View { } // MARK: - Deletable Moment Row -// Links wischen → Löschen (rot) -// Rechts wischen → Als wichtig markieren (orange) -// Vollständig rechts wischen → sofortiger Toggle, Zeile springt zurück +// Links wischen → Löschen (rot) +// Rechts wischen → Bearbeiten (accent) + Wichtig (orange) +// Vollständig rechts wischen → sofortiger Wichtig-Toggle, Zeile springt zurück private struct DeletableMomentRow: View { @Environment(\.nahbarTheme) var theme @@ -317,6 +431,7 @@ private struct DeletableMomentRow: View { let isLast: Bool let onDelete: () -> Void let onToggleImportant: () -> Void + let onEdit: () -> Void let onRateMeeting: () -> Void let onAftermathMeeting: () -> Void let onViewSummary: () -> Void @@ -328,28 +443,47 @@ private struct DeletableMomentRow: View { var body: some View { ZStack { - // Hintergrund: beide Aktions-Buttons + // Hintergrund-Buttons HStack(spacing: 0) { - // Links: Wichtig-Button (sichtbar bei Rechts-Wischen) - Button { - onToggleImportant() - withAnimation(.spring(response: 0.32, dampingFraction: 0.8)) { offset = 0 } - } label: { - VStack(spacing: 4) { - Image(systemName: moment.isImportant ? "star.slash.fill" : "star.fill") - .font(.system(size: 15, weight: .medium)) - Text(moment.isImportant ? "Entfernen" : "Wichtig") - .font(.system(size: 11, weight: .medium)) + + // Linke Seite (sichtbar bei Rechts-Wischen): Bearbeiten + Wichtig + HStack(spacing: 0) { + Button { + withAnimation(.spring(response: 0.32, dampingFraction: 0.8)) { offset = 0 } + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { onEdit() } + } label: { + VStack(spacing: 4) { + Image(systemName: "pencil") + .font(.system(size: 15, weight: .medium)) + Text("Bearbeiten") + .font(.system(size: 11, weight: .medium)) + } + .foregroundStyle(.white) + .frame(width: actionWidth) + .frame(maxHeight: .infinity) } - .foregroundStyle(.white) - .frame(width: actionWidth) - .frame(maxHeight: .infinity) + .background(theme.accent) + + Button { + onToggleImportant() + withAnimation(.spring(response: 0.32, dampingFraction: 0.8)) { offset = 0 } + } label: { + VStack(spacing: 4) { + Image(systemName: moment.isImportant ? "star.slash.fill" : "star.fill") + .font(.system(size: 15, weight: .medium)) + Text(moment.isImportant ? "Entfernen" : "Wichtig") + .font(.system(size: 11, weight: .medium)) + } + .foregroundStyle(.white) + .frame(width: actionWidth) + .frame(maxHeight: .infinity) + } + .background(Color.orange) } - .background(Color.orange) Spacer() - // Rechts: Löschen-Button (sichtbar bei Links-Wischen) + // Rechte Seite (sichtbar bei Links-Wischen): Löschen Button { withAnimation(.spring(response: 0.28, dampingFraction: 0.75)) { offset = -UIScreen.main.bounds.width @@ -383,8 +517,6 @@ private struct DeletableMomentRow: View { } .background(theme.surfaceCard) .offset(x: offset) - // simultaneousGesture erlaubt dem übergeordneten ScrollView weiterhin zu scrollen. - // Der Winkeltest (Faktor 2.5) lässt nur klar horizontale Gesten durch. .simultaneousGesture( DragGesture(minimumDistance: 20, coordinateSpace: .local) .onChanged { value in @@ -392,8 +524,10 @@ private struct DeletableMomentRow: View { let y = value.translation.height guard abs(x) > abs(y) * 2.5 else { return } if x > 0 { - offset = min(x, actionWidth + 16) + // Rechts: bis zu zwei Button-Breiten + offset = min(x, actionWidth * 2 + 16) } else { + // Links: ein Button offset = max(x, -(actionWidth + 16)) } } @@ -404,12 +538,16 @@ private struct DeletableMomentRow: View { withAnimation(.spring(response: 0.32, dampingFraction: 0.8)) { offset = 0 } return } - if x > actionWidth + 20 { - // Vollständiges Rechts-Wischen: sofort togglen, zurückspringen + if x > actionWidth * 2 + 20 { + // Vollständiges Rechts-Wischen → Wichtig-Toggle, zurückspringen onToggleImportant() withAnimation(.spring(response: 0.32, dampingFraction: 0.8)) { offset = 0 } + } else if x > actionWidth { + // Mehr als eine Button-Breite → beide linken Buttons zeigen + withAnimation(.spring(response: 0.32, dampingFraction: 0.8)) { offset = actionWidth * 2 } } else if x > actionWidth / 2 { - withAnimation(.spring(response: 0.32, dampingFraction: 0.8)) { offset = actionWidth } + // Knapp eine Button-Breite → beide linken Buttons zeigen + withAnimation(.spring(response: 0.32, dampingFraction: 0.8)) { offset = actionWidth * 2 } } else if x < -(actionWidth / 2) { withAnimation(.spring(response: 0.32, dampingFraction: 0.8)) { offset = -actionWidth } } else { @@ -435,14 +573,23 @@ struct MomentRowView: View { var onEditMeeting: (() -> Void)? = nil var onToggleIntention: (() -> Void)? = nil + /// Wird lokal gecacht, damit die Ansicht auf CalendarEventStore-Änderungen reagieren kann. + @State private var hasCalendarEvent = false + var body: some View { - switch moment.type { - case .meeting: - meetingRow - case .intention: - intentionRow - default: - standardRow + Group { + switch moment.type { + case .meeting: meetingRow + case .intention: intentionRow + default: standardRow + } + } + .onAppear { + hasCalendarEvent = CalendarEventStore.identifier(for: moment.id) != nil + } + .onReceive(NotificationCenter.default.publisher(for: CalendarEventStore.didChangeNotification)) { n in + guard (n.object as? UUID) == moment.id else { return } + hasCalendarEvent = CalendarEventStore.identifier(for: moment.id) != nil } } @@ -609,6 +756,11 @@ struct MomentRowView: View { .font(.system(size: 10)) .foregroundStyle(.orange) } + if hasCalendarEvent { + Image(systemName: "calendar") + .font(.system(size: 10)) + .foregroundStyle(theme.contentTertiary) + } Text(moment.createdAt, format: .dateTime.day().month(.abbreviated).year().locale(Locale(identifier: "de_DE"))) .font(.system(size: 12)) .foregroundStyle(theme.contentTertiary) @@ -656,6 +808,129 @@ struct MomentRowView: View { } } +// MARK: - Edit Moment View + +struct EditMomentView: View { + @Environment(\.nahbarTheme) var theme + @Environment(\.modelContext) var modelContext + @Environment(\.dismiss) var dismiss + + let moment: Moment + + @State private var text: String + @State private var createdAt: Date + @FocusState private var isFocused: Bool + + init(moment: Moment) { + self.moment = moment + self._text = State(initialValue: moment.text) + self._createdAt = State(initialValue: moment.createdAt) + } + + private var isValid: Bool { !text.trimmingCharacters(in: .whitespaces).isEmpty } + + var body: some View { + NavigationStack { + VStack(alignment: .leading, spacing: 16) { + + // Zeitpunkt + VStack(spacing: 0) { + DatePicker( + "Zeitpunkt", + selection: $createdAt, + displayedComponents: [.date, .hourAndMinute] + ) + .font(.system(size: 15)) + .foregroundStyle(theme.contentPrimary) + .tint(theme.accent) + .environment(\.locale, Locale.current) + .padding(.horizontal, 16) + .padding(.vertical, 10) + } + .background(theme.surfaceCard) + .clipShape(RoundedRectangle(cornerRadius: theme.radiusCard)) + .padding(.horizontal, 20) + + // Text + ZStack(alignment: .topLeading) { + if text.isEmpty { + Text("Moment…") + .font(.system(size: 16)) + .foregroundStyle(theme.contentTertiary) + .padding(.horizontal, 16) + .padding(.vertical, 14) + .allowsHitTesting(false) + } + TextEditor(text: $text) + .font(.system(size: 16, design: theme.displayDesign)) + .foregroundStyle(theme.contentPrimary) + .tint(theme.accent) + .scrollContentBackground(.hidden) + .padding(.horizontal, 12) + .padding(.vertical, 10) + .focused($isFocused) + } + .frame(minHeight: 180) + .background(theme.surfaceCard) + .clipShape(RoundedRectangle(cornerRadius: theme.radiusCard)) + .padding(.horizontal, 20) + + Spacer() + } + .padding(.top, 16) + .background(theme.backgroundPrimary.ignoresSafeArea()) + .navigationTitle("Moment bearbeiten") + .navigationBarTitleDisplayMode(.inline) + .themedNavBar() + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button("Abbrechen") { dismiss() } + .foregroundStyle(theme.contentSecondary) + } + ToolbarItem(placement: .topBarTrailing) { + Button("Fertig") { save() } + .font(.system(size: 15, weight: .semibold)) + .foregroundStyle(isValid ? theme.accent : theme.contentTertiary) + .disabled(!isValid) + } + } + } + .onAppear { isFocused = true } + } + + private func save() { + let trimmed = text.trimmingCharacters(in: .whitespaces) + guard !trimmed.isEmpty else { return } + moment.text = trimmed + moment.createdAt = createdAt + moment.updatedAt = Date() + moment.person?.touch() + do { + try modelContext.save() + } catch { + AppEventLog.shared.record( + "Fehler beim Bearbeiten des Moments: \(error.localizedDescription)", + level: .error, category: "Moment" + ) + } + // Verknüpften Kalendereintrag aktualisieren, falls vorhanden + if let eventID = CalendarEventStore.identifier(for: moment.id) { + let firstName = moment.person?.firstName ?? "" + let title = String.localizedStringWithFormat(String(localized: "Treffen mit %@"), firstName) + let savedDate = createdAt + Task { + await CalendarManager.shared.updateEvent( + identifier: eventID, + title: title, + notes: trimmed, + newStartDate: savedDate + ) + } + } + dismiss() + } +} + // MARK: - Info Row struct InfoRowView: View { diff --git a/nahbar/nahbar/PersonalityEngine.swift b/nahbar/nahbar/PersonalityEngine.swift index 2d9811b..977f4a5 100644 --- a/nahbar/nahbar/PersonalityEngine.swift +++ b/nahbar/nahbar/PersonalityEngine.swift @@ -160,11 +160,83 @@ enum PersonalityEngine { } } - /// Gibt an, ob "Etwas Neues ausprobieren" hervorgehoben werden soll. + /// Gibt an, ob Erlebnis-Aktivitäten hervorgehoben werden sollen. static func highlightNovelty(for profile: PersonalityProfile?) -> Bool { profile?.level(for: .openness) == .high } + /// Gibt `count` Aktivitätsvorschläge zurück, gewichtet nach Persönlichkeit und Kontakt-Tag. + /// Innerhalb gleicher Scores wird zufällig variiert – jeder Aufruf kann andere Ergebnisse liefern. + static func suggestedActivities( + for profile: PersonalityProfile?, + tag: PersonTag?, + count: Int = 2 + ) -> [String] { + let preferred = preferredActivityStyle(for: profile) + let highlightNew = highlightNovelty(for: profile) + + func score(_ s: ActivitySuggestion) -> Int { + var p = 0 + if s.style == preferred { p += 2 } + if s.isNovelty && highlightNew { p += 1 } + if let t = s.preferredTag, t == tag { p += 1 } + return p + } + + // Nach Score gruppieren, innerhalb jeder Gruppe mischen → Abwechslung + let grouped = Dictionary(grouping: activityPool) { score($0) } + var result: [String] = [] + for key in grouped.keys.sorted(by: >) { + guard result.count < count else { break } + let bucket = (grouped[key] ?? []).shuffled() + for item in bucket { + guard result.count < count else { break } + result.append(item.text) + } + } + return result + } + + // MARK: - Aktivitäts-Pool (intern, für Tests zugänglich via suggestedActivities) + + static let activityPool: [ActivitySuggestion] = [ + // ── 1:1 ────────────────────────────────────────────────────────────── + ActivitySuggestion("Kaffee trinken", style: .oneOnOne, isNovelty: false, preferredTag: nil), + ActivitySuggestion("Spazieren gehen", style: .oneOnOne, isNovelty: false, preferredTag: nil), + ActivitySuggestion("Zusammen frühstücken", style: .oneOnOne, isNovelty: false, preferredTag: nil), + ActivitySuggestion("Mittagessen", style: .oneOnOne, isNovelty: false, preferredTag: .work), + ActivitySuggestion("Auf ein Getränk treffen", style: .oneOnOne, isNovelty: false, preferredTag: nil), + ActivitySuggestion("Zusammen kochen", style: .oneOnOne, isNovelty: false, preferredTag: .family), + ActivitySuggestion("Bummeln gehen", style: .oneOnOne, isNovelty: false, preferredTag: nil), + ActivitySuggestion("Rad fahren", style: .oneOnOne, isNovelty: false, preferredTag: nil), + ActivitySuggestion("Joggen gehen", style: .oneOnOne, isNovelty: false, preferredTag: nil), + ActivitySuggestion("Picknick", style: .oneOnOne, isNovelty: false, preferredTag: nil), + ActivitySuggestion("Besuch machen", style: .oneOnOne, isNovelty: false, preferredTag: .family), + ActivitySuggestion("Gemeinsam lesen", style: .oneOnOne, isNovelty: false, preferredTag: nil), + // ── Gruppe ─────────────────────────────────────────────────────────── + ActivitySuggestion("Abendessen", style: .group, isNovelty: false, preferredTag: nil), + ActivitySuggestion("Spieleabend", style: .group, isNovelty: false, preferredTag: .friends), + ActivitySuggestion("Kino", style: .group, isNovelty: false, preferredTag: .friends), + ActivitySuggestion("Konzert oder Show", style: .group, isNovelty: false, preferredTag: nil), + ActivitySuggestion("Museum besuchen", style: .group, isNovelty: false, preferredTag: nil), + ActivitySuggestion("Wandern", style: .group, isNovelty: false, preferredTag: nil), + ActivitySuggestion("Grillabend", style: .group, isNovelty: false, preferredTag: .friends), + ActivitySuggestion("Sportevent", style: .group, isNovelty: false, preferredTag: nil), + ActivitySuggestion("Veranstaltung besuchen", style: .group, isNovelty: false, preferredTag: .community), + // ── Erlebnis ───────────────────────────────────────────────────────── + ActivitySuggestion("Etwas Neues ausprobieren", style: nil, isNovelty: true, preferredTag: nil), + ActivitySuggestion("Escape Room", style: nil, isNovelty: true, preferredTag: .friends), + ActivitySuggestion("Kochkurs", style: nil, isNovelty: true, preferredTag: nil), + ActivitySuggestion("Weinprobe oder Tasting", style: nil, isNovelty: true, preferredTag: nil), + ActivitySuggestion("Kletterpark", style: nil, isNovelty: true, preferredTag: .friends), + ActivitySuggestion("Workshop besuchen", style: nil, isNovelty: true, preferredTag: .community), + ActivitySuggestion("Karaoke", style: nil, isNovelty: true, preferredTag: .friends), + // ── Einfach / Remote ───────────────────────────────────────────────── + ActivitySuggestion("Anrufen", style: nil, isNovelty: false, preferredTag: nil), + ActivitySuggestion("Nachricht schicken", style: nil, isNovelty: false, preferredTag: nil), + ActivitySuggestion("Artikel oder Tipp teilen", style: nil, isNovelty: false, preferredTag: nil), + ] + // MARK: - Intervall-Empfehlung für Einstellungen /// Gibt den empfohlenen Benachrichtigungs-Intervall für das Einstellungsmenü zurück. @@ -197,3 +269,18 @@ enum ActivityStyle { case group case oneOnOne } + +/// Ein einzelner Aktivitätsvorschlag aus dem Pool. +struct ActivitySuggestion { + let text: String + let style: ActivityStyle? + let isNovelty: Bool + let preferredTag: PersonTag? + + init(_ text: String, style: ActivityStyle?, isNovelty: Bool, preferredTag: PersonTag?) { + self.text = text + self.style = style + self.isNovelty = isNovelty + self.preferredTag = preferredTag + } +} diff --git a/nahbar/nahbar/SplashView.swift b/nahbar/nahbar/SplashView.swift index 20effde..af41aba 100644 --- a/nahbar/nahbar/SplashView.swift +++ b/nahbar/nahbar/SplashView.swift @@ -1,4 +1,15 @@ import SwiftUI +import NaturalLanguage + +// MARK: - Language Detection + +/// Gibt die dominante Sprache eines Textes zurück (via NLLanguageRecognizer). +/// Interne Sichtbarkeit, damit Unit-Tests darauf zugreifen können. +func detectsDominantLanguage(_ text: String) -> NLLanguage? { + let recognizer = NLLanguageRecognizer() + recognizer.processString(text) + return recognizer.dominantLanguage +} // MARK: - API Response Models @@ -167,16 +178,25 @@ struct SplashView: View { } /// https://api.zitat-service.de – kostenlos, Deutsch + /// Wiederholt den Abruf bis zu 3x, falls die API ein nicht-deutsches Zitat liefert. private func fetchZitatService() async -> (text: String, author: String)? { guard let url = URL(string: "https://api.zitat-service.de/v1/quote?language=de") else { return nil } - do { - let (data, _) = try await URLSession.shared.data(for: URLRequest(url: url, timeoutInterval: 1)) - let r = try JSONDecoder().decode(ZitatServiceResponse.self, from: data) - let author = (r.authorName == "Unbekannt") ? "" : r.authorName - return (r.quote, author) - } catch { - return nil + for _ in 0..<3 { + do { + let (data, _) = try await URLSession.shared.data(for: URLRequest(url: url, timeoutInterval: 1)) + let r = try JSONDecoder().decode(ZitatServiceResponse.self, from: data) + guard isGermanText(r.quote) else { continue } + let author = (r.authorName == "Unbekannt") ? "" : r.authorName + return (r.quote, author) + } catch { + return nil + } } + return nil + } + + private func isGermanText(_ text: String) -> Bool { + detectsDominantLanguage(text) == .german } /// https://zenquotes.io – kostenlos, kein API-Key, Englisch diff --git a/nahbar/nahbarTests/CalendarManagerTests.swift b/nahbar/nahbarTests/CalendarManagerTests.swift new file mode 100644 index 0000000..3549e8d --- /dev/null +++ b/nahbar/nahbarTests/CalendarManagerTests.swift @@ -0,0 +1,270 @@ +import Testing +import Foundation +@testable import nahbar + +// MARK: - CalendarEventStore Tests +// +// Testet das UserDefaults-basierte Mapping Moment-UUID → EKEvent-Identifier. +// CalendarManager selbst (EKEventStore, Berechtigungsfluss) erfordert einen +// echten Gerätezugang und ist daher nicht sinnvoll unit-testbar. + +// Serialisiert, weil CalendarEventStore UserDefaults.standard shared state nutzt. +// Parallele Ausführung würde dazu führen, dass init()-Aufrufe verschiedener Tests +// den Key gegenseitig wegräumen. +@Suite("CalendarEventStore – CRUD", .serialized) +struct CalendarEventStoreCRUDTests { + + private let storeKey = "nahbar.momentCalendarEvents" + + /// Löscht den Mapping-Key vor jedem Test, um Seiteneffekte zu vermeiden. + init() { + UserDefaults.standard.removeObject(forKey: storeKey) + } + + @Test("initial: kein Identifier für unbekannte UUID") + func noIdentifierForUnknownUUID() { + let unknownID = UUID() + #expect(CalendarEventStore.identifier(for: unknownID) == nil) + } + + @Test("save + identifier: gespeicherter Wert wird korrekt zurückgegeben") + func saveAndRetrieve() { + let momentID = UUID() + let eventID = "EK-ABC-123" + + CalendarEventStore.save(momentID: momentID, eventIdentifier: eventID) + + #expect(CalendarEventStore.identifier(for: momentID) == eventID) + } + + @Test("remove: nach dem Entfernen ist identifier nil") + func removeDeletesEntry() { + let momentID = UUID() + CalendarEventStore.save(momentID: momentID, eventIdentifier: "to-delete") + CalendarEventStore.remove(momentID: momentID) + + #expect(CalendarEventStore.identifier(for: momentID) == nil) + } + + @Test("remove: nicht vorhandener Eintrag wirft keinen Fehler") + func removeOfMissingEntryIsNoop() { + let unknownID = UUID() + // Darf keinen Crash oder Exception auslösen + CalendarEventStore.remove(momentID: unknownID) + #expect(CalendarEventStore.identifier(for: unknownID) == nil) + } + + @Test("mehrere Einträge: jeder UUID erhält seinen eigenen Identifier") + func multipleEntriesAreIndependent() { + let ids = (0..<5).map { _ in UUID() } + let eventIDs = ids.map { "event-\($0)" } + + for (id, eventID) in zip(ids, eventIDs) { + CalendarEventStore.save(momentID: id, eventIdentifier: eventID) + } + + for (id, eventID) in zip(ids, eventIDs) { + #expect(CalendarEventStore.identifier(for: id) == eventID) + } + } + + @Test("remove betrifft nur den spezifischen Eintrag, andere bleiben erhalten") + func removeDoesNotAffectOtherEntries() { + let id1 = UUID() + let id2 = UUID() + CalendarEventStore.save(momentID: id1, eventIdentifier: "event-1") + CalendarEventStore.save(momentID: id2, eventIdentifier: "event-2") + + CalendarEventStore.remove(momentID: id1) + + #expect(CalendarEventStore.identifier(for: id1) == nil) + #expect(CalendarEventStore.identifier(for: id2) == "event-2") + } + + @Test("überschreiben: zweites save ersetzt ersten Identifier") + func overwriteUpdatesValue() { + let momentID = UUID() + CalendarEventStore.save(momentID: momentID, eventIdentifier: "old-event") + CalendarEventStore.save(momentID: momentID, eventIdentifier: "new-event") + + #expect(CalendarEventStore.identifier(for: momentID) == "new-event") + } + + @Test("identifier ist nach save deterministisch (mehrfache Abfrage gibt gleichen Wert)") + func identifierIsDeterministic() { + let momentID = UUID() + let eventID = "stable-\(UUID().uuidString)" + CalendarEventStore.save(momentID: momentID, eventIdentifier: eventID) + + let first = CalendarEventStore.identifier(for: momentID) + let second = CalendarEventStore.identifier(for: momentID) + + #expect(first == second) + #expect(first == eventID) + } + + @Test("UUID-String-Roundtrip: Schlüssel überlebt Serialisierung") + func uuidStringKeyRoundTrip() { + // Stellt sicher, dass UUID().uuidString als Dictionary-Key korrekt round-trippt + let momentID = UUID() + let eventID = "ek-\(UUID().uuidString)" + + CalendarEventStore.save(momentID: momentID, eventIdentifier: eventID) + + // Lade den Raw-Store und prüfe den Schlüssel direkt + let raw = UserDefaults.standard.dictionary(forKey: storeKey) as? [String: String] + #expect(raw?[momentID.uuidString] == eventID) + } +} + +// MARK: - Update-Logik + +// CalendarManager.updateEvent greift auf EKEventStore (Gerätezugang) zu und ist +// daher nicht vollständig unit-testbar. Die folgenden Tests verifizieren die +// reinen Berechnungslogiken, die updateEvent intern verwendet. + +@Suite("CalendarManager – Update-Logik") +struct CalendarManagerUpdateLogicTests { + + private let marker = "— via nahbar" + + // MARK: Notiz-Formatierung + + @Test("Notizen mit Text: marker wird durch zwei Zeilenumbrüche getrennt angehängt") + func notesWithTextAppendMarker() { + let text = "Schönes Treffen" + let expected = "\(text)\n\n\(marker)" + + let result: String = { + if !text.isEmpty { + return "\(text)\n\n\(marker)" + } else { + return marker + } + }() + + #expect(result == expected) + } + + @Test("Leerer Text: Notizen bestehen nur aus dem marker") + func emptyTextProducesOnlyMarker() { + let text = "" + + let result: String = { + if !text.isEmpty { + return "\(text)\n\n\(marker)" + } else { + return marker + } + }() + + #expect(result == marker) + } + + @Test("Marker ist exakt '— via nahbar'") + func markerFormat() { + #expect(marker == "— via nahbar") + } + + // MARK: Dauer-Berechnung + + @Test("Dauer bleibt erhalten wenn Startdatum verschoben wird") + func durationPreservedOnShift() { + let originalStart = Date(timeIntervalSinceReferenceDate: 0) + let originalEnd = Date(timeIntervalSinceReferenceDate: 3600) // 1 Stunde + let newStart = Date(timeIntervalSinceReferenceDate: 7200) // 2 Stunden später + + let duration = originalEnd.timeIntervalSince(originalStart) + let newEnd = newStart.addingTimeInterval(duration) + + #expect(duration == 3600) + #expect(newEnd.timeIntervalSince(newStart) == 3600) + } + + @Test("Startdatum-Verschiebung ändert Enddatum proportional") + func endDateMovesWithStart() { + let base = Date(timeIntervalSinceReferenceDate: 1_000_000) + let duration = TimeInterval(90 * 60) // 90 Minuten + let shift = TimeInterval(24 * 3600) // 1 Tag vorwärts + + let originalEnd = base.addingTimeInterval(duration) + let newStart = base.addingTimeInterval(shift) + let newEnd = newStart.addingTimeInterval(duration) + + #expect(newEnd.timeIntervalSince(newStart) == originalEnd.timeIntervalSince(base)) + } + + // MARK: isAllDay-Schutz + + @Test("isAllDay: Startdatum wird nicht verändert (Schutz-Flag)") + func allDayEventSkipsDateShift() { + let isAllDay = true + let origStart = Date(timeIntervalSinceReferenceDate: 0) + let newStart = Date(timeIntervalSinceReferenceDate: 86400) + + // Simuliert den Guard in updateEvent + var effectiveStart = origStart + if !isAllDay { + effectiveStart = newStart + } + + #expect(effectiveStart == origStart) + } + + @Test("nicht-Ganztages: Startdatum wird auf newStart gesetzt") + func nonAllDayEventShiftsDate() { + let isAllDay = false + let origStart = Date(timeIntervalSinceReferenceDate: 0) + let newStart = Date(timeIntervalSinceReferenceDate: 86400) + + var effectiveStart = origStart + if !isAllDay { + effectiveStart = newStart + } + + #expect(effectiveStart == newStart) + } +} + +// MARK: - Alarm-Offset Semantik + +@Suite("CalendarManager – Alarm-Offset-Semantik") +struct CalendarAlarmOffsetTests { + + @Test("Offset 0 wird als 'keine Erinnerung' interpretiert") + func zeroOffsetMeansNoAlarm() { + // Konvention: AddMomentView wandelt 0.0 → nil vor dem createEvent-Aufruf + let rawOffset: Double = 0.0 + let alarmOffset: TimeInterval? = rawOffset == 0 ? nil : rawOffset + #expect(alarmOffset == nil) + } + + @Test("negativer Offset entspricht einem Zeitpunkt vor dem Event") + func negativeOffsetIsBeforeEvent() { + let oneHourBefore: TimeInterval = -3600 + #expect(oneHourBefore < 0) + } + + @Test("Alarm-Offsets haben erwartete Werte") + func alarmOffsetValues() { + let offsets: [(String, Double)] = [ + ("5 Min", -300), + ("15 Min", -900), + ("1 Std", -3600), + ("1 Tag", -86400), + ] + for (label, offset) in offsets { + #expect(offset < 0, "Offset '\(label)' muss negativ sein") + } + } + + @Test("1 Tag entspricht 86400 Sekunden") + func oneDayIs86400Seconds() { + #expect(-86400.0 == -(24 * 60 * 60)) + } + + @Test("1 Stunde entspricht 3600 Sekunden") + func oneHourIs3600Seconds() { + #expect(-3600.0 == -(60 * 60)) + } +} diff --git a/nahbar/nahbarTests/ContactPickerTests.swift b/nahbar/nahbarTests/ContactPickerTests.swift index 5b68358..c6648cf 100644 --- a/nahbar/nahbarTests/ContactPickerTests.swift +++ b/nahbar/nahbarTests/ContactPickerTests.swift @@ -215,6 +215,50 @@ struct ContactImportTests { #expect(ContactImport.from(CNMutableContact()).location == "") } + @Test("Vollständige deutsche Adresse: Straße + PLZ + Stadt + Land") + func fullGermanAddress() { + let contact = CNMutableContact() + let address = CNMutablePostalAddress() + address.street = "Musterstraße 12" + address.postalCode = "10115" + address.city = "Berlin" + address.country = "Deutschland" + contact.postalAddresses = [CNLabeledValue(label: CNLabelHome, value: address)] + #expect(ContactImport.from(contact).location == "Musterstraße 12, 10115 Berlin, Deutschland") + } + + @Test("Vollständige US-Adresse: Straße + PLZ + Stadt + State + Land") + func fullUSAddress() { + let contact = CNMutableContact() + let address = CNMutablePostalAddress() + address.street = "1 Infinite Loop" + address.postalCode = "95014" + address.city = "Cupertino" + address.state = "CA" + address.country = "USA" + contact.postalAddresses = [CNLabeledValue(label: CNLabelHome, value: address)] + #expect(ContactImport.from(contact).location == "1 Infinite Loop, 95014 Cupertino, CA, USA") + } + + @Test("Mehrzeilige Straße: Zeilenumbrüche werden zu Komma-Leerzeichen") + func multiLineStreet() { + let contact = CNMutableContact() + let address = CNMutablePostalAddress() + address.street = "Musterstraße 12\nHinterhaus" + address.city = "Hamburg" + contact.postalAddresses = [CNLabeledValue(label: CNLabelHome, value: address)] + #expect(ContactImport.from(contact).location == "Musterstraße 12, Hinterhaus, Hamburg") + } + + @Test("Nur PLZ ohne Stadt → PLZ als cityPart") + func postalCodeOnly() { + let contact = CNMutableContact() + let address = CNMutablePostalAddress() + address.postalCode = "10115" + contact.postalAddresses = [CNLabeledValue(label: CNLabelHome, value: address)] + #expect(ContactImport.from(contact).location == "10115") + } + @Test("Geburtstag mit vollständigem Datum → wird übernommen") func birthdayFullDate() { let contact = CNMutableContact() diff --git a/nahbar/nahbarTests/NahbarPersonalityTests.swift b/nahbar/nahbarTests/NahbarPersonalityTests.swift index e383d81..67c8ce1 100644 --- a/nahbar/nahbarTests/NahbarPersonalityTests.swift +++ b/nahbar/nahbarTests/NahbarPersonalityTests.swift @@ -429,6 +429,68 @@ struct PersonalityEngineBehaviorTests { } } +// MARK: - suggestedActivities Tests + +@Suite("PersonalityEngine – suggestedActivities") +struct SuggestedActivitiesTests { + + @Test("Gibt genau count Elemente zurück") + func returnsRequestedCount() { + let result = PersonalityEngine.suggestedActivities(for: nil, tag: nil, count: 2) + #expect(result.count == 2) + } + + @Test("count: 1 → genau ein Vorschlag") + func countOne() { + let result = PersonalityEngine.suggestedActivities(for: nil, tag: nil, count: 1) + #expect(result.count == 1) + } + + @Test("Alle zurückgegebenen Texte stammen aus dem Pool") + func resultsAreFromPool() { + let poolTexts = Set(PersonalityEngine.activityPool.map(\.text)) + let result = PersonalityEngine.suggestedActivities(for: nil, tag: nil, count: 5) + for text in result { + #expect(poolTexts.contains(text), "'\(text)' nicht im Pool") + } + } + + @Test("Pool hat mindestens 20 Einträge") + func poolIsSufficient() { + #expect(PersonalityEngine.activityPool.count >= 20) + } + + @Test("Keine Duplikate in einem Ergebnis") + func noDuplicates() { + let result = PersonalityEngine.suggestedActivities(for: nil, tag: nil, count: 5) + #expect(result.count == Set(result).count) + } + + @Test("Ergebnis ist nicht leer wenn Pool vorhanden") + func notEmptyWhenPoolExists() { + let result = PersonalityEngine.suggestedActivities(for: nil, tag: nil, count: 2) + #expect(!result.isEmpty) + } + + @Test("Pool enthält Erlebnis-Aktivitäten (isNovelty)") + func poolContainsNoveltyActivities() { + #expect(PersonalityEngine.activityPool.contains { $0.isNovelty }) + } + + @Test("Pool enthält 1:1 und Gruppen-Aktivitäten") + func poolContainsBothStyles() { + #expect(PersonalityEngine.activityPool.contains { $0.style == .oneOnOne }) + #expect(PersonalityEngine.activityPool.contains { $0.style == .group }) + } + + @Test("Pool enthält Tag-spezifische Aktivitäten") + func poolContainsTagSpecificActivities() { + #expect(PersonalityEngine.activityPool.contains { $0.preferredTag == .friends }) + #expect(PersonalityEngine.activityPool.contains { $0.preferredTag == .family }) + #expect(PersonalityEngine.activityPool.contains { $0.preferredTag == .work }) + } +} + // MARK: - GenderSelectionScreen Skip-Logik @Suite("PersonalityQuiz – Geschlechtsabfrage überspringen") diff --git a/nahbar/nahbarTests/SplashViewTests.swift b/nahbar/nahbarTests/SplashViewTests.swift new file mode 100644 index 0000000..63c2499 --- /dev/null +++ b/nahbar/nahbarTests/SplashViewTests.swift @@ -0,0 +1,38 @@ +import Testing +import NaturalLanguage +@testable import nahbar + +@Suite("SplashView – Spracherkennung") +struct SplashLanguageDetectionTests { + + @Test("Deutscher Text wird als Deutsch erkannt") + func germanTextIsGerman() { + let text = "Der Mensch ist dem Menschen am nötigsten." + #expect(detectsDominantLanguage(text) == .german) + } + + @Test("Niederländischer Text wird nicht als Deutsch erkannt") + func dutchTextIsNotGerman() { + let text = "De mens heeft de medemens het meest nodig." + #expect(detectsDominantLanguage(text) != .german) + } + + @Test("Englischer Text wird nicht als Deutsch erkannt") + func englishTextIsNotGerman() { + let text = "Happiness is only real when shared." + #expect(detectsDominantLanguage(text) != .german) + } + + @Test("Französischer Text wird nicht als Deutsch erkannt") + func frenchTextIsNotGerman() { + let text = "Le bonheur n'est réel que lorsqu'il est partagé." + #expect(detectsDominantLanguage(text) != .german) + } + + @Test("Leerer String liefert nil oder nicht Deutsch") + func emptyStringIsNotGerman() { + let result = detectsDominantLanguage("") + // NLLanguageRecognizer liefert nil bei leerem Text + #expect(result == nil || result != .german) + } +}