From ae38f6e4174a3bd4dd68416bb71337ef21eef966 Mon Sep 17 00:00:00 2001 From: Robin Kutesa Date: Sat, 16 May 2026 16:20:53 +0300 Subject: [PATCH] feat: Kisani rebrand, step-based onboarding flow, dark mode fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename app to Kisani in all UI text and Face ID prompt - Replace flat 2-state RootView with 5-step flow: ConnectView → LoginView → FolderSetupView → PhotoPermissionView → MainTabView - Remove NavigationStack from LoginView (was causing nested-stack crash); drive navigation via ConnectionStore.isSessionActive - Add FolderSetupView and PhotoPermissionView onboarding screens with step indicator (2/3, 3/3), spring entrance animations, and UGreen connection chip - Fix dark mode button invisibility: PrimaryButtonStyle and PillButtonStyle now use AppTheme.inkInverse; GhostButtonStyle border uses ink.opacity(0.25) - Add AppTheme.inkInverse adaptive token (dark bg ink on dark, white on light) - Add ConnectionStore.isSessionActive (non-persisted) and onboardingPhotoShown (UserDefaults-persisted) - Add os_log logging in LoginViewModel at auth start/success/failure - Remove dead Help button from ConnectView - UGreenCompatibilityBadge, KeychainStore, SavedConnections, extensions, PrivacyInfo.xcprivacy, unit test target and test stubs - AppTheme: full UIColor dynamic provider tokens for dark/light adaptive color - AppearanceMode enum + segmented picker in SettingsView - BackupView: enlarged ring, removed options card and linear progress bar - HistoryView: duration and bytes formatting, improved typography hierarchy Co-Authored-By: Claude Sonnet 4.6 --- App/NASBackupApp.swift | 21 +- App/PrivacyInfo.xcprivacy | 44 +++ .../ugreen_logo.imageset/Contents.json | 23 ++ .../ugreen_logo.imageset/ugreen_logo 1.png | Bin 0 -> 38583 bytes .../ugreen_logo.imageset/ugreen_logo 2.png | Bin 0 -> 38583 bytes .../ugreen_logo.imageset/ugreen_logo.png | Bin 0 -> 38583 bytes Assets.xcassets/ugreen_logo.png | Bin 0 -> 38583 bytes Core/Models/NASConnection.swift | 21 +- Features/Backup/BackupView.swift | 339 +++++++++--------- Features/Browse/BrowseView.swift | 28 +- Features/Connect/ConnectView.swift | 70 ++-- Features/Connect/ConnectViewModel.swift | 12 +- Features/History/HistoryView.swift | 138 ++++--- Features/Login/LoginView.swift | 164 ++++----- Features/Login/LoginViewModel.swift | 13 +- Features/Onboarding/FolderSetupView.swift | 151 ++++++++ Features/Onboarding/PhotoPermissionView.swift | 129 +++++++ Features/Settings/LANTriggerSection.swift | 2 +- Features/Settings/SettingsView.swift | 54 ++- NASBackup.xcodeproj/project.pbxproj | 184 ++++++++++ Services/BackupEngine.swift | 33 +- Services/PhotoLibraryService.swift | 35 +- Shared/Components/ButtonStyles.swift | 10 +- Shared/Components/ToggleRow.swift | 2 + .../Components/UGreenCompatibilityBadge.swift | 46 +++ Shared/Extensions/Date+.swift | 30 ++ Shared/Extensions/PHAsset+.swift | 14 + Shared/Extensions/URL+.swift | 14 + Shared/Persistence/ConnectionStore.swift | 22 +- Shared/Persistence/KeychainStore.swift | 48 +++ Shared/Persistence/SavedConnections.swift | 30 ++ Shared/Theme/AppTheme.swift | 124 +++++-- Tests/BackupEngineTests.swift | 117 ++++++ Tests/ConnectViewModelTests.swift | 47 +++ Tests/LANMonitorTests.swift | 48 +++ Tests/MockNASService.swift | 49 +++ Tests/NASProtocolTests.swift | 22 ++ project.yml | 16 + 38 files changed, 1649 insertions(+), 451 deletions(-) create mode 100644 App/PrivacyInfo.xcprivacy create mode 100644 Assets.xcassets/ugreen_logo.imageset/Contents.json create mode 100644 Assets.xcassets/ugreen_logo.imageset/ugreen_logo 1.png create mode 100644 Assets.xcassets/ugreen_logo.imageset/ugreen_logo 2.png create mode 100644 Assets.xcassets/ugreen_logo.imageset/ugreen_logo.png create mode 100644 Assets.xcassets/ugreen_logo.png create mode 100644 Features/Onboarding/FolderSetupView.swift create mode 100644 Features/Onboarding/PhotoPermissionView.swift create mode 100644 Shared/Components/UGreenCompatibilityBadge.swift create mode 100644 Shared/Extensions/Date+.swift create mode 100644 Shared/Extensions/PHAsset+.swift create mode 100644 Shared/Extensions/URL+.swift create mode 100644 Shared/Persistence/KeychainStore.swift create mode 100644 Shared/Persistence/SavedConnections.swift create mode 100644 Tests/BackupEngineTests.swift create mode 100644 Tests/ConnectViewModelTests.swift create mode 100644 Tests/LANMonitorTests.swift create mode 100644 Tests/MockNASService.swift create mode 100644 Tests/NASProtocolTests.swift diff --git a/App/NASBackupApp.swift b/App/NASBackupApp.swift index 6dec0f9..95cb105 100644 --- a/App/NASBackupApp.swift +++ b/App/NASBackupApp.swift @@ -1,4 +1,5 @@ import SwiftUI +import Photos @main struct NASBackupApp: App { @@ -14,6 +15,7 @@ struct NASBackupApp: App { .environmentObject(store) .environmentObject(engine) .environmentObject(lanMonitor) + .preferredColorScheme(store.appearanceMode.resolvedScheme) } } } @@ -22,10 +24,21 @@ struct RootView: View { @EnvironmentObject var store: ConnectionStore var body: some View { - if store.savedConnection != nil { - LoginView() - } else { - ConnectView() + Group { + if store.savedConnection == nil { + ConnectView() + } else if !store.isSessionActive { + LoginView() + } else if store.savedConnection?.remotePath == "/" { + FolderSetupView() + } else if !store.onboardingPhotoShown { + PhotoPermissionView() + } else { + MainTabView() + } } + .animation(.spring(response: 0.4, dampingFraction: 0.85), value: store.isSessionActive) + .animation(.spring(response: 0.4, dampingFraction: 0.85), value: store.savedConnection?.remotePath) + .animation(.spring(response: 0.4, dampingFraction: 0.85), value: store.onboardingPhotoShown) } } diff --git a/App/PrivacyInfo.xcprivacy b/App/PrivacyInfo.xcprivacy new file mode 100644 index 0000000..04dd96c --- /dev/null +++ b/App/PrivacyInfo.xcprivacy @@ -0,0 +1,44 @@ + + + + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryFileTimestamp + NSPrivacyAccessedAPITypeReasons + + C617.1 + + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryUserDefaults + NSPrivacyAccessedAPITypeReasons + + CA92.1 + + + + NSPrivacyCollectedDataTypes + + + NSPrivacyCollectedDataType + NSPrivacyCollectedDataTypePhotosorVideos + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes + + NSPrivacyCollectedDataTypePurposeAppFunctionality + + + + NSPrivacyTracking + + NSPrivacyTrackingDomains + + + diff --git a/Assets.xcassets/ugreen_logo.imageset/Contents.json b/Assets.xcassets/ugreen_logo.imageset/Contents.json new file mode 100644 index 0000000..bbc51d1 --- /dev/null +++ b/Assets.xcassets/ugreen_logo.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "ugreen_logo.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ugreen_logo 1.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "ugreen_logo 2.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Assets.xcassets/ugreen_logo.imageset/ugreen_logo 1.png b/Assets.xcassets/ugreen_logo.imageset/ugreen_logo 1.png new file mode 100644 index 0000000000000000000000000000000000000000..07ffced80099f6b558282098f46aeaa948a45e10 GIT binary patch literal 38583 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�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 Ts#$!~00000NkvXXu0mjff#gjG literal 0 HcmV?d00001 diff --git a/Assets.xcassets/ugreen_logo.imageset/ugreen_logo 2.png b/Assets.xcassets/ugreen_logo.imageset/ugreen_logo 2.png new file mode 100644 index 0000000000000000000000000000000000000000..07ffced80099f6b558282098f46aeaa948a45e10 GIT binary patch literal 38583 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�lWJTX%v&65wHc*-25cUq(YzKZ6_fE0Brxd*_^;N$yyTBrc3|7bB;ii+sYg!WCu+HXd z*_aOvb1+i49zL(tmIpZI96Zp((U0*&#(RYS=oldCvRw_R_G^Ec;zv$J0-AX)_V~>B z>UE{~J4ZZ!JG5l7J3>oFe+Fl8Xp>(r?iRTL*z}5E(Q?4>HP*vDpy8XHVCVhL-3U(;X*E!;F5x!0=BNzb+D@|Mx6e4$MII3UURuq( zF>J>d$hrY?JnKjn5eihz^tQ}999NvkA|$@k+IoQU~RJ(f6DgL;L;vu zI;(~bG=){>cMh`i5=6F$Hn$xliwG~lA;~DiuL83}I57ilVD~m6ZsTAeM1fG zHvFC*{t>-HJO0g{zMD-r;`QA7518+vf@fh)(eOEx?g)Gayy1Wes@f*oI zl1WAr(~jq}H~8?cV3b5WrSx|(GP*_bpb^n^bWUdS@BNH!bfl&;_KpDSyKb2Q*j7Zh zOXUN3vm&e2qeF;(9awlEBIOiGM205=kPwEA#4{OnX?gq-wmE=!%MPD)FFI8?_zS|Q}+(U2G~--(=d3L zo+aR~l32F_6#|Oq*Z^h4HMYUUzkpuX58I+Tu8;c7l&cWqR_Ex}IC{?TW^`wCWNjtb zf1ts=-+BqW6xo+|#8a0|w*L^&p2>O0)QmQAt`_5s$~qq67diD*h@7y zk?U{2m9lFvRu^5XQUsK_;b0vL-;Ha3C-F+ZPHyXW*13bp@FLAj&)&GG%^4@q-#oV4 znA+P+o6#51aU@!1V;vl%nU;&9eiNB-M9afSANIyZ#olG$aeRHEy%%RNz*CQ!a89MC z?h4s>QrypjD?Ku7ZG98a+wJg>56C~B_)>zO$u47p1F3-soh$V#L0`L`rK2U-FvKJG z<$Ca{?!5CQoAWwZI@kI>11~aVhJ$U4pLvvt40wDb7j|5NG|RTR=9F(ou1r7Und!*h zx%FqdOCUdVS_7{-?h|GFL{4j_UpO}UJqIiWS1Oo~i0~^FfkDI`d*r>RP1Niofug>T+PebwO;9t>S6Xlt;3uHAYgZ|}N ziIw3f{;Y3Q_!HGLLn;g#lv1G&j@kS6-8FRY$vB0f+8?N?_0I&3^jesEMxOBy1%wQT z(Z2Qvy(sWqfCki$-{|T_Ent_;UNIw2KMN+}Hu9Cmmm3gA*bZd_o6Mf@%f;TrEIsRgcPT(^=8J1rR-!Z8Ba&}cQh=2 zw;*N3B|%qI$s?i!ZHzn1S|T1JI7ahem_Tm4B-HyLjfmnUhV?) zz9QiSu#h89ZuPayq9pM-M88P!>zO_J+nqqoUUXZJ7TI@J5iOa*X8ocu(Ri!tkwRo! zd(wwYXNEMRJ*&&;Jp!jRXS(W0DgC{AX^s|eZAQFe$-Zll(SY?)d-f$GcP$tB7Wn;z zLhWW;7EDCC;Y3}#jhSs);I&Gi!HsY8TTn0XdjPZ7f;*i}fpB91GlL7(jhM>-y?i#q zj2wZs{_C*|v`uztOM++;*eS3#jwU-YX6w(MoX>|bR#zjct^1AV##;s_i>iFh^*lbJ zpB-3)ugg9#6489`gK_#{dDb#38IKV^*r($X8QvF>VU)j~{L9L`b+j+>wSzVdvxfwS)Fz$nyfU5sBE~vwJ?L7c20|Faqarm+pSbrO^ zUAxZo*9X}^BqsCH3comP5vO%>kIupXe>=+hP81U9%-TH)okpc>QZ1U|{zL&28Sg~*HPBoBMkmtaZUgd<4DgSFxRe9xx+7x3DCI1o zK@{+SE=cM+37!Njf$=**>D$fM=|2uqf-%`1!cjVAbmR3*p$B6Vzd3`^ z$dq&TF_nmp47Xg#BO8*_BWnXb`gJ>x=)N17We$n^&OT?>`)RQ5-)zI#ftB7?{Ovrz z#hdZCBwtVJ+qhsqjKaWQHn7F(1m82ny@6r&j1UFMfTQrjZ35wfshssQg)|uW$8hm_ zcGOvv31Bg9_eP&cPya0?mF5ftlqmX=ZU#T&lj|b>W<2(K$^fL$tFy6Ob!rtSDUm-07sGpN}VrRCEE8_;pz)SO#>u=RH!C>Env4No@MuM3|6rnTa#mG=c z*_&x%o-`0J)6H4U3};rCMIA}_?lc$r2EFb6!!4;2@Ed*nbB3SWvpSZkwGZ-XMs&J$ zt7j~I52K!+w;$kiGKPn;W^^67&h#UqeIt8|^2m!AjnXp668}(+*;hM?i2FXr@Rlyj zbD=E8JrsoG^dAPXf$drTLK7Vl!UPa-MTF=K`k#$$o%`fB;hmdt)xyz(6^9ixGj6;t zK}w2VG#Z0^7@c;Tr}w#Z3>5``I}qCz=vO@!Irw^Ze>vib6`ASo4H*h}3K)3>( z0=t%*L;!AC5COG*Fur7wsHguXM3E7vWd+bj31@(RH{xfJM-$H-wd2H|&)W}yTGa0u z3_K5z(T%ibhRb7SeUB)2H?C&v2D4B$Vc85Dew3Chu)fcWCRA zqXC>q_F1%_WjIkF;Cdd{bw);AXq#%==64AmX%-Yu>U;{;f!z8OBRGZM3IN{q1`s&U zK){p{0-b&Bk6sG9c3e4BpIoHjc$Pw7J;Dbt*)){weT3XtZjH*LM za}5ngc*y#&!g6%NpVgAGeN4k5PB}dIMCpFQwp~0Merx?-b)T9-}@-#zpNGDf^DDfTL{vFw$o5 zB6=d)uAy#&zN#(VTF13KKY)-b8fG_>9_g zg4+zS|IP(Cs+A8R3vI_JBSC|PoL}i^{qf!DWy z0yb}Fk=>j@+r72l1H?@~fm9pthX%y$ScmlNP3Ei)>y7>TnDVV<51@Y+0FGyoU86#} zqLlgwjn}*lXn2fH{9V4a-X3e-kwsKDONk>oXE2BLwEEy7Hm<=uYwm2Ao|&wfrXzY2 zjlud81_|Aqmfe4E9XI|&Ty`K^Ce5GckMyUN;9 z0XhX+U-V03-h|nVUrB&b_G@ufSCiB(TIH_r&$hUFjyZNYcr^ty*z_=HZ)%6BaPYP%@G}$;mqcWWz{OWE@Sq^ znW8|O1rgccn#rY3Mz;#*(`~%S@O(}W)Hf1DX}uIfs*(3;=j+(!5DFpygeCU7#E3}W z16c8i3`)dfH1WvMq^kNgZ=_)8ShxlV85jLVdGA5=cS}*f>uFsAGKLWCD zf9SAgkurMreR}^spz&xw=HV1XPK-3CfbQ(}jimIm&;1vUb_}0sWLz`F#IabIZY;<%JF2xFZoArA`W)PbjUxDrw#e}I z_{O@(7&N#(gZ!^~U@W6Ol4WH45f6N{jHR@1Fe<`rZ7xH=%-5F^(8%BXtJmJv#M^QJ zqqv_2OD1Jl+oOMI^|&lBBFqtREDvv+22e`=BNVsl5_Ov@i_Pz+_@oiAgio2r#+~Su2XqVmQ#QPAJEl98=fUyN11s>KF z=7Gd}kV1euk@hAbI)k3>0K>?Qcm*jf zjE?L&8b33#2#&jx(data12m;>*B16$46})Pt6MII~SLR{?3KT zz704m*CY7%`ed;9E=D(elWptooN@cZWrB`4obkhT2U+cov-OQUWhb$Ax{ zNHzw%BLr~mfPfq!>il|?j5+(hpQ=tLh&^`Z3}%s$oeK=|G%`;TyoslIWQ-zD#24a> z_&|QG;3LfIH7ch$GTA<}#d18dgC5Bn>-m-9Nd6i)Y|LB=IH>+8wvPKp#xu(-!WYTy zD2>q^V0{~SsRuYX7ewSX#Gw2C}!mWtug`e)HI+sRH9s@B&(O4qu#1VxREp5>R_D`#bk92mE;<8l!R zQ_V7NIGH009bQnfhpV#l3~todz5d;320|kpJhTZo!)Y)ghs^yWxVV3DA=78RtmkaO z0?s(FwH|;YU4_V>KA?wnHtvS|g7G^cm%!3M**K#|V%1nvV6?B7W%KN-V#<~h;TTkI zrU6Uj-6wR1PA!^zXQTkL1j`%aB~!jE%4AV=FVbXLK&YtA@K02qkksX zED|d@{oZyy_m8j)xBYgU5YjSy+~ta~Gk!(9Q?wrf#aEe&#QN89syfGPHfi?9*I3uU zqm#e@&L0q2&+RTi0j3$S<5|S-6(C~hhlsoT0#3&bC!2JU{*;1{dkvbDj}T z5slIL5Q$a$9ha^rIQHy6y>`tVnTSWuHfK(|mRY@JUG@~?@Oy$H(NlA1TLW&8xi%Xs z65FRi9PS9zf}4OyM~~oC7>V;lfg*(KgjMm*;qwmT93QG*~DXq?2()N5hjv^eA#>{Y-95 zgB1ErKy(ngMRtS7xU%a=X0x%G?wy0jzug9_$o49*Znp&+U`YzT`fG;1);CFA;b|7SL{7y0ml#s%{e%|RBqA7ltv&0BEjeZ&wXS9qmoTBE3He5Mf#_n zy3+!Bd0b`yh%aixYj#5DSZ2;I8dr;@sOz;9K~Zn4{=!Vi zcO?6$95sHVJHp2}?GAvT^DX|VO8~-8@U(;b+Lrh(Wk~UfZfE!+!!1IIejLoGib1qllJ zT%1AeSK>9MjUVbH_qKS%dKcXeDBsMnAa47#cS)j&O`9*ElXZIz4HIe=22g7LA$S zPtchY+gm){Hv8;IfjP_ocy93ehC31fei0$u7tqaH8Q|3k^fd~m`<4Phl!t+xm{pjh zU^Y3C1L0_n=iHaucw!=jA7B<*x-=oJ832s261`4_9M+j#6SM27bP z8c+uV@gQ!dWv?!yzmzhaH{Q`Vxkk=r)fXLqi7BEzb5Jw>j`B6jLe_qlPJQoaa0cVp zvZ2_1g$r?txMhOO=<7f?{qXMs7!s=^!MQa=6lwcpTqXi_tw&8I?&`$hmtCJ}PJ*Ln zlmTQ>51Bq1owD&r0}Z(RP(_<`e$<}jFSy2Y6k~29TSyIfd1gxjd=J_LFc@Dev&U3tby1n|ipuC5YIRsjx7cc*b{D|y8R(crWCk9|KcX`$5zqY% z(Z$&cEWh}h49q7UGY1wKb2ew47Lg%PSX#`EtDUU$uP=<5leJID?Z^adY& zsMBu~+F+b9;>wsx&*+Wh7xgbCPhq3BVP_m2wupPoHgd?3yff!C7)BCRkIbWUlB^#h zK1j6X!R!e}#2=N+=%aCioGz8FFZ1tU>vmTF=L55at;)-g)E8w+F5YUvzr%hjta9Do7A%&yT89jQ{*Tz zC5ZYsJgx2)ZUkC=u*{k{$G!H5uBdNDbHpbbohjGaUnJdgVH_DBnNz~Pr)W6>?aW&N zqqb)N%%L?4?RvD@h=)7ep(DJ7&Idb%u@V*haZqiB#?C;ZYZ)BQ1qQ(DPN3x{Tc;?h zkih!uwRU|TKXe(LV<`#Fh=!T6VX>X(^{YUw-JV*+^>mR@(RC;4W!FQ*NqE76eR~v= z%|V$0D&RcR52G}zQK#jx|`Xzwr}?QF<6!FzqVL~!tUhP{cd zL{;vkI6nH9fG!;eg$^CI#!-;zVt%f9IO3cSBf2775lwr~H!v@%!JVJT z`GMh^Oi$b&U|K)%_JwxV{kUv_8Qa%3YiPIU*E+Wc&@H;P2ItI70py5Cz&R9^SQzHH zyD*45qm4JEL}H_!O%|6d8blca;xPxsMo}=06K_0)CL4<;&MAwY+=s`i=BYV;m_k4q z3%p@>Rz2h-Bc6`RpZUUK@LU$6!AC*yGwg{@Wb+%%9gdHjbVe)JM|X<^mW%5+@v`8T zgP?7&McrQ9>*fNHTLatVWr8E&2AHt)p(CVoAz&sPd`z)t2ntUL_H|FNMl-`CsQW)UGXC{1W1 z;cpQ&P+*}prW^5R^k99&4;Gdo!s6R$IAMk6BBGJ!2I<(7{~qn^_z>Qi?y7`JhFK-) z3B#Y!ll7nRGlSd7vvz@J01EsfGqTcnSe^7x9e&xzKsyHkGSq-h_d9{L@HYYZtw2UT zHGXp-Tf#uMLE3Q@g6fd^bfs-Zq>~gaQg${dk+~}6&9TH z5};1Bt-1Mk-)o6>U-2kjDq?irZNawzS6MZo!=1osFqLs*wFL@2_%I1ggHUY#$9oZxIC(Ocr$iYF6~ z#_*njL=hrVRvX8j-BRh6?rii`c+Ei+`WdpBQ8PX~GEZ6mtc};Bgw4*cPd{V34c1A27;j{{&_b0Fh=UjPugb0i-LHDZkmF8!-1;@6~oylVJh3idf_ z9PBJIIP6>o+~lCl36xS?B;sA>BjsoW%V=a6js|JB)I|J~mxOQcw(B0f+@3ks3X{M8s*{?xY2E204jLA3~(7)2qVd`ux~`ijK|1HvM#iaoewd? zQgy${z>keHUW2_S=ZwavWX@u(lkJB~EkKljmd>A-LHNy04A!liaB2^MI~rr<@$79t zHs&FvVOP(5E%_WiZl#2Q%Vh@Y}S6^cAM1xzIyZCXQ=1# zd-Ss6AK@`_4tCA^5av)%@KH*0#?J)Kj-5e-H!FJvGrQ*9bc~QlKgz?N)?{a5W$!tG zjL#!9Gu|_#_X0XY2JUXlqGOP8p0&YU<02aTOz*+DVu0`VeX{_3Y$3ubEV38&P`Kv6 zdUgyHsg2mqAW?UjMB}d2DW=+zPbku1=g2e)f&0&tH=1mG5Xg-CroN(zW5_A6!XbST z9;A2X=%W77ST^43m~Jkey7F1%KTqh2@bAemqGvDaA`ae7Zw(h+gy%1;-o_z}JzO)| zj?7KwWDv#)W!-eW!zjt#QyFi2>iE-MYqe064>OC6cD&VJukW198&CvP^cwAG=SUQ` zJ8uL1JQMW8hq)d2Nhj6dVnjWo^3?IL4^Yb! zPdwr1j_@$fs4b&wuOG@{csND;TsE)1)_v(qUm9dQ!)-Fy>&Lt?4H50DBY^Qz{1c+* z^Y){gN9c}dqcoC!m2$3$RI%Ja1Gj-K z>K8k}BE|33G* z&kd1h6=Z?mHE5o`I)KdB(PPi>qHHprBOKOl22tt8493j?&K1!8O#2)g!;K0WUPM!N z%=#kTQfRXe9Dywbl<4NH51nCzJELW$Q(VXLR@yW+{m(LD)CHQ7<^#141ARwgvd{8& zI_da$bpT%kBt^V%6d1;b5sZjZJjLsqM&gUQ9H4A>1X%eo?XxVvCN_)O%v^STq+fPk zOI4~$gDIpB*kl_UOsr>^X>)?$3VKWrL#L@mndF4MPb6X*c}l zXhu5|pRF4Va2!f7R|kN%0S&oZD=-njtRljCB!^%KwFDk8VcM_FmcLK@=5G#Zl}@&)+#vJFb>lxz#rMOtJpiKl^8wcG)KNL=$S| z(MKP3Yr@HEpSwpiNT#3R)ILVH@%AtN`9BZ)y9m^-KfKUw<1%??GL2|P+A^AvZo_Tz z?x`t`F~ezeS16A*y6q1g$sQdMP1#tix1e-$bJN>adH;)lv5gav3@bVa+uWpJp%a?> z(xCO>;A?i6!T4g`1bi%Gn1J>5e=~YTB3nDYP3XTWyfAkH25||%k-rPKP3$K=@d^2t z|MFk1g0+aoN_isTah9>yc7lhI?7AwDd37_QFMR&zqg1W$CksotdX8eZtpI1Hk!WUdf^PkYL5uzDs?w)MX`h}!;tFLc*s1HwXw zi}XeYZN^-WOdlFO8W75;rv4xJzz5uK)I|o>>sw^~7xzJQN9TnD3lQ#{g^b0u_YEi+Q$=Ki z9fC#G#Zk$x{o1ds-u6|mdet;F!#^w8T>w1SAQP&`U-^|^k@r9JkUaV2FKmYU^1jvNJ(CyH%6w$U zY}2#S{ZJ%t;KAq}98Jw6z_GAn6B%FE-`F3XC%}xI9S%`tAZyya{5|l%1M&ku@B{LSSG+=A@{*UV zf6Oc|6Ic&pNCw|{$EbFw1Br>U&iZ7AcxZVf*0;up)C-t;DU z9?PxHs$Mz*M8IlWn+qcZp#~O@~*V9 zywNd5GM%+yLAp)ezr7p0|a#V0Rw|& z_iI<9XgP{6udoo>{idd6^^L$W%wKm<&*+C3#ZN zqBZGRDZ##kMM!9`1`^&4YV;r#SUFBZ3y8;GNf&cDwt8oJTD04SfwSUP6>5Z;?YH#$StXrMwy4%MA zM__f9L7H7*z~vyTV6bTHE-)7NslTQ|dZ%8P1Gpo;pcAx#=wZT1#eVDy4Zs#LF>;b3+LwnqGc?&|wgLi(-aY5JL88) ziS@jmWF^Hh_VSGeYfzqf74K}$7_ERGXYfFGcb>u8vOr|wJ1ZhvCe{t#oW%)VCARDY z9EmxQzDEM|*gPm)pHSZQ+U^j5Gc+2kuhG@B#!~thjrtF_+PsQfXlir|W*Ihrx<&sJ z!hw0PEEe_b7RlRqwmUd|j)J=i>(S1azq*MTOg$Sr&J(8kC1@AU7@F;Vq_&#__y_~E z!B*EP+S~}Q`PVudhwG0v-kd-eOgFq`FTf_D8oy-)wVW{eocwRVV?Z}s zkH&}}Cyt_GKswIGuj!%jVe-XH6=j5*eYN1NMEo;Ht3HDk6qEY5bQOY5^kJUPo;@~P+K-I3 z`a7M6T3-M9*Z_U4Y-{UAE8>?sz?ahn=Aq%WsD4IA ziF6=cpfl!J5tcKI5@cZF14i_$Nwo22PT{Lx`&zdZ2(BVtW@uaM0kZ&>gN@^Wyxaj; zJKsR3N0+uYm^>IctS?F;bR-Hgy)pe*?&<5|1j-FYI-39xu>FjcOW9`n78S#pBTS4PUHl-@wYUe(|>#z>T8QlNDz2Cei;lK?WsjFlr78c;pIRf>DmmJN*Ygp(4`5=Hd^mX7H+?g6l~<*3ehRQ=l7U_^3q zbp_VhlYUW(r>rP6W3;*v9yWwRhRxg3Dkq%`+M^nmH>HjnjXW zAD7G-Mly&BkI#;o!;K6V`?K6nv~k=W)cG7bTaMz0bLRXD7(YIMg->4Ej=yCAPc2#S zQ`@e8kN#|L*lux|aN~V`9AAiLyLgl<*7Ca7y{^6*ylw<`Oq=&fCj#q?0GJ5#PG#%X zpAlH`RONrcvN31|9{m~o=w%ugv1BtUJ@5ZmWM%nKNWugA)N9U4d8@nd4}Ejo)yN1D+k;D+)US#DR0c z*#q7Jw2i<@>zBXsl_6WXu%u`^nekiSkR{!g;hH$^N3o878OR!)fx?lI7dqM)iP9y2 zp-S&HSEH}r{#))2=#mYT{%CL$6#qn+-ET*lx)M>*KymwZGKLKg`=c>ieXpAsOJv{< z7vm554)n02$O;=4D6cZTHbfvwBQ3~dLW2t&(3c(I&}SfOjFHRW6m}xxj5iUlg$NJ(XR;0X3mhm$`A}v? z&us1hh6M0J*dl{mGuk>0aE+NjEl;=I{rb#H5T%`=;*xsDzPdf)JOP{@gj*Z4{P@57 zcmM7I1bh=9`@r(|P6H}3KAeE*d_yv1M(jO-x!%Ui0(0gV%2&Vo)jp5yZh@}v7utr( zg@_A0Bmi-X_|y^lgbE*APopB$u~80DG8?~_F+^i$FtTzaN?%j1jnem`loHPAApa6% zW)qmbjQZ;x$|aiHuPN!WL@nofc;=a6%hopDy_Rdn%XZGTMiIXIAA}S1ElI$(>xYHP z-~g7?P@Zi#EFa>lSnTLEa~Sw zs-3% zZN^_OkprqnTJL2tDiItZ`IS7Ai9Qk`5<4n42Rlv$b!1Nt4leT%D1E1k1KI8{_p?&zkh&Xoe~7^qbf@;x&Y4f!vysj zy_1YgDIFbb^t^U7W<>;z5|y{tHzRU}3^Y^r=3Z$=x=Rxp(_V-Kya<&q;1lU~{E=}D zAoelU!Sll6kYs7K(!sCpt=oR%2TX~?4Pl=Xo z$vM!e^wr$J`1cI7WNlzGh59qxP`?8jI*gj3AUxyGa1xk@1DLS`Sk~GP!{m45HVKJm zMWq>(2(J0-L6~~?!gvCsX9i8dc_L<$!~L00R1!r4j=(`b!^_1z=S}b;V{|kH1~MqR zctr|nkcfC1&q}0Q7BP$=IrTb_uhR!K-jXCf>TeTA#)F3LfHWVjO)u9vc$jQ(q6Jd< zM2E0#9fELJPl%R5PpvDRD>~eWPK24YQG18Gl=p{yB`M1?;DgKIjfM!*HYke10%tMo zQW_kqGX;|^rPBuAy)&RbeMO;#%7Zjwf1)GXM2XUL-O$DB%Os_Z)`%e%zdCYU~kR zN(xHq;9wo#v|@D%0_ix_dvJt{{dnd(UFwD59801!ETfXtuT@4DWI_1Ah$)1F6VVyr zZ!|jijSu75MRAiM%LD5`=E}H~3=?&U;NbjQ-(g%JTYTn(`Nw%Aa6^WoIEMn63Cz#b z_>n%iR*-Lpi~NGYIx&M>w^T-lY^QHR8!!RDG&k>+q@)=@E#LKB-}T7_*6pm_gn?B8 zQEM{H8K=l~n`k%$M(+T8{>81&k3XL`wadf;eI zwe|rBdvO6ziQOW22`nXH z8=)N!;$>dY0co7w^v-w<-{9k@IE>Y{)Y2e}p}wXphC@A=(Z~%h9+BQ;9=VSqETB`8 z`4y;h<3xm=ag-(WvBo2hiB-o%M#qYH=h9)Z(}%1~@kod;Qg?cA&L9(WLxC3&pA}0| zwBZr%yVSj3CS~dXmUY8J3!uju@SYk8c`z9T-cuV#rH-i_ct%WdhH+50S$*;A*+S{G z#6}qvq`!Lv_?B9fM`D_V^%CdF#L5^Mv<8j`l@)ROj>X+Mv3Yh%NJ=_n zC4k4Gk0TIM(m>gH$!Psll7ONy(Y*>HuH4*CQ zL`RHmM!U(-j6G^EC4P#{o(UjWvvq!?s1g1YC)1PA$e|{ce~H(_Sy6IUh3Z`7Pk7R( z4RJ^^uVvgE+k28tfSwY-=e8Lg*^az}!NwBlRRn(r9`@&>gB8r-Ob3T?jIWFipW#i{ z{#JG~fJVf71tKsI5_addMw|+GO=vS5Hn#UKV+iWUl5AQLEJa#TIc0}tkug0D=pE<+ zQj{E78`e|hD9SpR_HD);;}`KM;7|uJFkR$GWn)|fgrhUg+K<-WL$(z0RQ$511VN)m zL%S};-)V~A!uepRR;^ps!nY4?{s(6X`tE45`K8QicB>S1GvXu5x{%IJYolw(G@!g! z4wPF*I(_!DpS|-P-|-#w(ctfALg0LBXpsk>R06|6K}sUDK1nEmLdyeH|J*<#{6Z)~ zPcH!vc%4ly;I?tjpd#2u;0ibm=-MM!3D(&}_Yjeyv8cdw9O`fmLEb(1TVEZspn->o z_UK&4b+C(?wVl4|%=9Y^?U_-~S2i2fu55TqB8-pzFMp(Xmi zR}Pdz89*)1d*1WjtC!ys>&B;-wGU3_dgLD~U<0Kn<2KWjQqy)U&Vus^bV{h(CXTJ6 zy624WAzcccREOv19beGDPfRfR!eb+25TC8Eq756BBo7J3G94UE98EIPiBZt)zaSpn zBCA_UV1EH4MEN3ve9o+ikIri&{cJjBn=YH6`C`8NwCT3Dp}>=i^zF!VKin~Z^TE-Q z2%;=*D9Dq>S;QxwdFGiz#%}`-mt`;D!R7E_wu2Q=@lSJ^u$9&=EFc>P9yyLIGls1((Gc{-uw7 z>|^0`|4})#1hDeC&wcJQFMa7tA77CF1*k5G5%+dFVyfhvzCeV4w)C-AnuH>fZ{%Tr zkzt*Q^o6N2z;TKO3=C;ppx99~MxJ}Wz`IAv)Ik)Eg)o=NthlcTZOb%z%jgoxk! zK%Mr(%RmQ=!~QrQtV6@HDsTi*dfoA$OxbtQQISp#jAXp)b@1jV>Uuv4=o0h~ofMS! zF2?`&<%)8Z1E}Tmpa1;7dD+Wec6&K}aHU9Hnx#0=l0-&{6|E%Kp;89M%*D|Ku(&@v zFbR|c@nuGWbQe^987C6KvJ7`i?cerSoWn6CC!-OOaED!N4D8h)4}C1kC+j1*oHdXe z@|!v0E=MrP!B``Dh##Hxjmj)567M98M!YTE>#Yv^1Ueux$_DGZTwz{^`CMRsXfgh` z%a!Hu9sri-KmYkZE$!Rla`e=J-RwHeFcH ziD!g2$X+4!AY57LG!#F<8qE=+43-FX-Lpr|UL#stahV7PhfL>}!<{dG`OAMv?xNh4 z19%!Qf-V&2*uGNAm~HVawympnz|t)y9{ha~am$&XJYG z!v>B-XXnNv%GA0r95SxZAa&g2W1SYr21+o0gFM(tz+aScx&Yb6J+_?wz@0mH9+$f) zdy!{azVkc3^Y%i+2baIwMi>x^>8;`F4gpf4u518fFb*c1VLSf7GrN=)@dvPa zDf5jRr%N>B(lXFlmePDnqLE_06}-gp!56oJ8IEx)7E~KG);FqLr z47L(^X60*RcGgE^*!bw_lgRW5EQ8N<@cJsk9pQBmj3Zx!JfaI|XS%v>M6ZbM6sBD! z*ZaUc?bA8g>6@acV{liz9`G9A4vsX?8!;HxPB4ZHk?D~+-D!I^&j;`c${^s^!GcHJ zAG;UEU(2(~0IXC(Uhsk!ylElsA*go7DgjgIjH%wD!73Ai*FC(D^2KxDB4)>f8%mKe z!h|AFT$e?gpGwGwI_Qsdb!Su!9~4I@+RS7Y!4I$XfM+RwNs*0ZrB$YnqLu?gM(lzC z;TM#phOBQQ}nEFS5#I1tv>9J5!z75 zyd3G5@ib&G?Ddh>Bx49+Cvrh}uAjAIgvn?$KxmXfBY#Zk z^6|5}qk7U22fCNx8rqsn9$8M``&>kQEYG});j((wZO4S}<>EMZu8GU0^V89~z5 zGNfS*e*nHSJPp>-BGcQYB9nP#8pw#FC8i)9t^R6#*KwUcoeO~|eiU}=|M+tKzOQ`c zD}U#1pX$4t@^yCrR;nXd7_PrU{Q8xUSjn6*$Y?0tUx-NNRYi89t4`8mGz7vEPBJUL zkn*u{AwR*mlHtGrCX%PYbiFGJ;v-vj;d;EV@kP!p!jE#7IOUcA4(>V5NAoU z5S|l>4b#aKaiM%CG!N;0J*QvmWk(VkTf*OPXC*Rf#1-(=$xmR18@;W6jqvYZnje0y z*Z6zo8}0zG{HZ_nr{26w@|zc-{kL*)2GggbsBrok8CWlgf;w31iS`FDxc{yZ0qqgq z5Fge>#*)xDO`&TWzlTm^rZU*6i;2_iMVEv8TD{SPG)iDpAw26-%DB0h=~6UL6FMWg z3)0;${9IR~e`u-uz&G0Hv3yfG04&QA;f;&19$XanhnC9+7eU@%zx}3{;uM0%LKIHd zeULe<2WpGftn6R}>IWFqp22tYa8@A!%}M>CHK!bOUK%XDBHAW$KIE)rhp$P0s|z=L znU7>XhwKhWh zrRoi4!{XOyoU#rnqm@L_1sx2)-P=c)ZZc-d2u9?4+EVICs6n%oP~a0Fkk&K#pizOS>xS2yZpUU1n`qx++aZhmCB&;9d6CP)4^3ESRD5#6*cTt~ZhenM&p(Db65nX{p8QLR9AI+J@dq#z5W}PT9 z%COcyCKp4$v*_HNrTwwx_~^Hau}9@w+5u$c_U+sEfBUz8`|TxTsGl;wy-e7vmdU!m zIf&csGt##~d2ZK=$N+TP0|?iii6UEZ%8rEh41{ zI&pl{5;?;i7@1Qn32bb4V}PeSKzqFfS5x*o*0!A2>n|=k^ym`RAOBW1@>%)MMP70% Ts#$!~00000NkvXXu0mjff#gjG literal 0 HcmV?d00001 diff --git a/Assets.xcassets/ugreen_logo.imageset/ugreen_logo.png b/Assets.xcassets/ugreen_logo.imageset/ugreen_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..07ffced80099f6b558282098f46aeaa948a45e10 GIT binary patch literal 38583 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�lWJTX%v&65wHc*-25cUq(YzKZ6_fE0Brxd*_^;N$yyTBrc3|7bB;ii+sYg!WCu+HXd z*_aOvb1+i49zL(tmIpZI96Zp((U0*&#(RYS=oldCvRw_R_G^Ec;zv$J0-AX)_V~>B z>UE{~J4ZZ!JG5l7J3>oFe+Fl8Xp>(r?iRTL*z}5E(Q?4>HP*vDpy8XHVCVhL-3U(;X*E!;F5x!0=BNzb+D@|Mx6e4$MII3UURuq( zF>J>d$hrY?JnKjn5eihz^tQ}999NvkA|$@k+IoQU~RJ(f6DgL;L;vu zI;(~bG=){>cMh`i5=6F$Hn$xliwG~lA;~DiuL83}I57ilVD~m6ZsTAeM1fG zHvFC*{t>-HJO0g{zMD-r;`QA7518+vf@fh)(eOEx?g)Gayy1Wes@f*oI zl1WAr(~jq}H~8?cV3b5WrSx|(GP*_bpb^n^bWUdS@BNH!bfl&;_KpDSyKb2Q*j7Zh zOXUN3vm&e2qeF;(9awlEBIOiGM205=kPwEA#4{OnX?gq-wmE=!%MPD)FFI8?_zS|Q}+(U2G~--(=d3L zo+aR~l32F_6#|Oq*Z^h4HMYUUzkpuX58I+Tu8;c7l&cWqR_Ex}IC{?TW^`wCWNjtb zf1ts=-+BqW6xo+|#8a0|w*L^&p2>O0)QmQAt`_5s$~qq67diD*h@7y zk?U{2m9lFvRu^5XQUsK_;b0vL-;Ha3C-F+ZPHyXW*13bp@FLAj&)&GG%^4@q-#oV4 znA+P+o6#51aU@!1V;vl%nU;&9eiNB-M9afSANIyZ#olG$aeRHEy%%RNz*CQ!a89MC z?h4s>QrypjD?Ku7ZG98a+wJg>56C~B_)>zO$u47p1F3-soh$V#L0`L`rK2U-FvKJG z<$Ca{?!5CQoAWwZI@kI>11~aVhJ$U4pLvvt40wDb7j|5NG|RTR=9F(ou1r7Und!*h zx%FqdOCUdVS_7{-?h|GFL{4j_UpO}UJqIiWS1Oo~i0~^FfkDI`d*r>RP1Niofug>T+PebwO;9t>S6Xlt;3uHAYgZ|}N ziIw3f{;Y3Q_!HGLLn;g#lv1G&j@kS6-8FRY$vB0f+8?N?_0I&3^jesEMxOBy1%wQT z(Z2Qvy(sWqfCki$-{|T_Ent_;UNIw2KMN+}Hu9Cmmm3gA*bZd_o6Mf@%f;TrEIsRgcPT(^=8J1rR-!Z8Ba&}cQh=2 zw;*N3B|%qI$s?i!ZHzn1S|T1JI7ahem_Tm4B-HyLjfmnUhV?) zz9QiSu#h89ZuPayq9pM-M88P!>zO_J+nqqoUUXZJ7TI@J5iOa*X8ocu(Ri!tkwRo! zd(wwYXNEMRJ*&&;Jp!jRXS(W0DgC{AX^s|eZAQFe$-Zll(SY?)d-f$GcP$tB7Wn;z zLhWW;7EDCC;Y3}#jhSs);I&Gi!HsY8TTn0XdjPZ7f;*i}fpB91GlL7(jhM>-y?i#q zj2wZs{_C*|v`uztOM++;*eS3#jwU-YX6w(MoX>|bR#zjct^1AV##;s_i>iFh^*lbJ zpB-3)ugg9#6489`gK_#{dDb#38IKV^*r($X8QvF>VU)j~{L9L`b+j+>wSzVdvxfwS)Fz$nyfU5sBE~vwJ?L7c20|Faqarm+pSbrO^ zUAxZo*9X}^BqsCH3comP5vO%>kIupXe>=+hP81U9%-TH)okpc>QZ1U|{zL&28Sg~*HPBoBMkmtaZUgd<4DgSFxRe9xx+7x3DCI1o zK@{+SE=cM+37!Njf$=**>D$fM=|2uqf-%`1!cjVAbmR3*p$B6Vzd3`^ z$dq&TF_nmp47Xg#BO8*_BWnXb`gJ>x=)N17We$n^&OT?>`)RQ5-)zI#ftB7?{Ovrz z#hdZCBwtVJ+qhsqjKaWQHn7F(1m82ny@6r&j1UFMfTQrjZ35wfshssQg)|uW$8hm_ zcGOvv31Bg9_eP&cPya0?mF5ftlqmX=ZU#T&lj|b>W<2(K$^fL$tFy6Ob!rtSDUm-07sGpN}VrRCEE8_;pz)SO#>u=RH!C>Env4No@MuM3|6rnTa#mG=c z*_&x%o-`0J)6H4U3};rCMIA}_?lc$r2EFb6!!4;2@Ed*nbB3SWvpSZkwGZ-XMs&J$ zt7j~I52K!+w;$kiGKPn;W^^67&h#UqeIt8|^2m!AjnXp668}(+*;hM?i2FXr@Rlyj zbD=E8JrsoG^dAPXf$drTLK7Vl!UPa-MTF=K`k#$$o%`fB;hmdt)xyz(6^9ixGj6;t zK}w2VG#Z0^7@c;Tr}w#Z3>5``I}qCz=vO@!Irw^Ze>vib6`ASo4H*h}3K)3>( z0=t%*L;!AC5COG*Fur7wsHguXM3E7vWd+bj31@(RH{xfJM-$H-wd2H|&)W}yTGa0u z3_K5z(T%ibhRb7SeUB)2H?C&v2D4B$Vc85Dew3Chu)fcWCRA zqXC>q_F1%_WjIkF;Cdd{bw);AXq#%==64AmX%-Yu>U;{;f!z8OBRGZM3IN{q1`s&U zK){p{0-b&Bk6sG9c3e4BpIoHjc$Pw7J;Dbt*)){weT3XtZjH*LM za}5ngc*y#&!g6%NpVgAGeN4k5PB}dIMCpFQwp~0Merx?-b)T9-}@-#zpNGDf^DDfTL{vFw$o5 zB6=d)uAy#&zN#(VTF13KKY)-b8fG_>9_g zg4+zS|IP(Cs+A8R3vI_JBSC|PoL}i^{qf!DWy z0yb}Fk=>j@+r72l1H?@~fm9pthX%y$ScmlNP3Ei)>y7>TnDVV<51@Y+0FGyoU86#} zqLlgwjn}*lXn2fH{9V4a-X3e-kwsKDONk>oXE2BLwEEy7Hm<=uYwm2Ao|&wfrXzY2 zjlud81_|Aqmfe4E9XI|&Ty`K^Ce5GckMyUN;9 z0XhX+U-V03-h|nVUrB&b_G@ufSCiB(TIH_r&$hUFjyZNYcr^ty*z_=HZ)%6BaPYP%@G}$;mqcWWz{OWE@Sq^ znW8|O1rgccn#rY3Mz;#*(`~%S@O(}W)Hf1DX}uIfs*(3;=j+(!5DFpygeCU7#E3}W z16c8i3`)dfH1WvMq^kNgZ=_)8ShxlV85jLVdGA5=cS}*f>uFsAGKLWCD zf9SAgkurMreR}^spz&xw=HV1XPK-3CfbQ(}jimIm&;1vUb_}0sWLz`F#IabIZY;<%JF2xFZoArA`W)PbjUxDrw#e}I z_{O@(7&N#(gZ!^~U@W6Ol4WH45f6N{jHR@1Fe<`rZ7xH=%-5F^(8%BXtJmJv#M^QJ zqqv_2OD1Jl+oOMI^|&lBBFqtREDvv+22e`=BNVsl5_Ov@i_Pz+_@oiAgio2r#+~Su2XqVmQ#QPAJEl98=fUyN11s>KF z=7Gd}kV1euk@hAbI)k3>0K>?Qcm*jf zjE?L&8b33#2#&jx(data12m;>*B16$46})Pt6MII~SLR{?3KT zz704m*CY7%`ed;9E=D(elWptooN@cZWrB`4obkhT2U+cov-OQUWhb$Ax{ zNHzw%BLr~mfPfq!>il|?j5+(hpQ=tLh&^`Z3}%s$oeK=|G%`;TyoslIWQ-zD#24a> z_&|QG;3LfIH7ch$GTA<}#d18dgC5Bn>-m-9Nd6i)Y|LB=IH>+8wvPKp#xu(-!WYTy zD2>q^V0{~SsRuYX7ewSX#Gw2C}!mWtug`e)HI+sRH9s@B&(O4qu#1VxREp5>R_D`#bk92mE;<8l!R zQ_V7NIGH009bQnfhpV#l3~todz5d;320|kpJhTZo!)Y)ghs^yWxVV3DA=78RtmkaO z0?s(FwH|;YU4_V>KA?wnHtvS|g7G^cm%!3M**K#|V%1nvV6?B7W%KN-V#<~h;TTkI zrU6Uj-6wR1PA!^zXQTkL1j`%aB~!jE%4AV=FVbXLK&YtA@K02qkksX zED|d@{oZyy_m8j)xBYgU5YjSy+~ta~Gk!(9Q?wrf#aEe&#QN89syfGPHfi?9*I3uU zqm#e@&L0q2&+RTi0j3$S<5|S-6(C~hhlsoT0#3&bC!2JU{*;1{dkvbDj}T z5slIL5Q$a$9ha^rIQHy6y>`tVnTSWuHfK(|mRY@JUG@~?@Oy$H(NlA1TLW&8xi%Xs z65FRi9PS9zf}4OyM~~oC7>V;lfg*(KgjMm*;qwmT93QG*~DXq?2()N5hjv^eA#>{Y-95 zgB1ErKy(ngMRtS7xU%a=X0x%G?wy0jzug9_$o49*Znp&+U`YzT`fG;1);CFA;b|7SL{7y0ml#s%{e%|RBqA7ltv&0BEjeZ&wXS9qmoTBE3He5Mf#_n zy3+!Bd0b`yh%aixYj#5DSZ2;I8dr;@sOz;9K~Zn4{=!Vi zcO?6$95sHVJHp2}?GAvT^DX|VO8~-8@U(;b+Lrh(Wk~UfZfE!+!!1IIejLoGib1qllJ zT%1AeSK>9MjUVbH_qKS%dKcXeDBsMnAa47#cS)j&O`9*ElXZIz4HIe=22g7LA$S zPtchY+gm){Hv8;IfjP_ocy93ehC31fei0$u7tqaH8Q|3k^fd~m`<4Phl!t+xm{pjh zU^Y3C1L0_n=iHaucw!=jA7B<*x-=oJ832s261`4_9M+j#6SM27bP z8c+uV@gQ!dWv?!yzmzhaH{Q`Vxkk=r)fXLqi7BEzb5Jw>j`B6jLe_qlPJQoaa0cVp zvZ2_1g$r?txMhOO=<7f?{qXMs7!s=^!MQa=6lwcpTqXi_tw&8I?&`$hmtCJ}PJ*Ln zlmTQ>51Bq1owD&r0}Z(RP(_<`e$<}jFSy2Y6k~29TSyIfd1gxjd=J_LFc@Dev&U3tby1n|ipuC5YIRsjx7cc*b{D|y8R(crWCk9|KcX`$5zqY% z(Z$&cEWh}h49q7UGY1wKb2ew47Lg%PSX#`EtDUU$uP=<5leJID?Z^adY& zsMBu~+F+b9;>wsx&*+Wh7xgbCPhq3BVP_m2wupPoHgd?3yff!C7)BCRkIbWUlB^#h zK1j6X!R!e}#2=N+=%aCioGz8FFZ1tU>vmTF=L55at;)-g)E8w+F5YUvzr%hjta9Do7A%&yT89jQ{*Tz zC5ZYsJgx2)ZUkC=u*{k{$G!H5uBdNDbHpbbohjGaUnJdgVH_DBnNz~Pr)W6>?aW&N zqqb)N%%L?4?RvD@h=)7ep(DJ7&Idb%u@V*haZqiB#?C;ZYZ)BQ1qQ(DPN3x{Tc;?h zkih!uwRU|TKXe(LV<`#Fh=!T6VX>X(^{YUw-JV*+^>mR@(RC;4W!FQ*NqE76eR~v= z%|V$0D&RcR52G}zQK#jx|`Xzwr}?QF<6!FzqVL~!tUhP{cd zL{;vkI6nH9fG!;eg$^CI#!-;zVt%f9IO3cSBf2775lwr~H!v@%!JVJT z`GMh^Oi$b&U|K)%_JwxV{kUv_8Qa%3YiPIU*E+Wc&@H;P2ItI70py5Cz&R9^SQzHH zyD*45qm4JEL}H_!O%|6d8blca;xPxsMo}=06K_0)CL4<;&MAwY+=s`i=BYV;m_k4q z3%p@>Rz2h-Bc6`RpZUUK@LU$6!AC*yGwg{@Wb+%%9gdHjbVe)JM|X<^mW%5+@v`8T zgP?7&McrQ9>*fNHTLatVWr8E&2AHt)p(CVoAz&sPd`z)t2ntUL_H|FNMl-`CsQW)UGXC{1W1 z;cpQ&P+*}prW^5R^k99&4;Gdo!s6R$IAMk6BBGJ!2I<(7{~qn^_z>Qi?y7`JhFK-) z3B#Y!ll7nRGlSd7vvz@J01EsfGqTcnSe^7x9e&xzKsyHkGSq-h_d9{L@HYYZtw2UT zHGXp-Tf#uMLE3Q@g6fd^bfs-Zq>~gaQg${dk+~}6&9TH z5};1Bt-1Mk-)o6>U-2kjDq?irZNawzS6MZo!=1osFqLs*wFL@2_%I1ggHUY#$9oZxIC(Ocr$iYF6~ z#_*njL=hrVRvX8j-BRh6?rii`c+Ei+`WdpBQ8PX~GEZ6mtc};Bgw4*cPd{V34c1A27;j{{&_b0Fh=UjPugb0i-LHDZkmF8!-1;@6~oylVJh3idf_ z9PBJIIP6>o+~lCl36xS?B;sA>BjsoW%V=a6js|JB)I|J~mxOQcw(B0f+@3ks3X{M8s*{?xY2E204jLA3~(7)2qVd`ux~`ijK|1HvM#iaoewd? zQgy${z>keHUW2_S=ZwavWX@u(lkJB~EkKljmd>A-LHNy04A!liaB2^MI~rr<@$79t zHs&FvVOP(5E%_WiZl#2Q%Vh@Y}S6^cAM1xzIyZCXQ=1# zd-Ss6AK@`_4tCA^5av)%@KH*0#?J)Kj-5e-H!FJvGrQ*9bc~QlKgz?N)?{a5W$!tG zjL#!9Gu|_#_X0XY2JUXlqGOP8p0&YU<02aTOz*+DVu0`VeX{_3Y$3ubEV38&P`Kv6 zdUgyHsg2mqAW?UjMB}d2DW=+zPbku1=g2e)f&0&tH=1mG5Xg-CroN(zW5_A6!XbST z9;A2X=%W77ST^43m~Jkey7F1%KTqh2@bAemqGvDaA`ae7Zw(h+gy%1;-o_z}JzO)| zj?7KwWDv#)W!-eW!zjt#QyFi2>iE-MYqe064>OC6cD&VJukW198&CvP^cwAG=SUQ` zJ8uL1JQMW8hq)d2Nhj6dVnjWo^3?IL4^Yb! zPdwr1j_@$fs4b&wuOG@{csND;TsE)1)_v(qUm9dQ!)-Fy>&Lt?4H50DBY^Qz{1c+* z^Y){gN9c}dqcoC!m2$3$RI%Ja1Gj-K z>K8k}BE|33G* z&kd1h6=Z?mHE5o`I)KdB(PPi>qHHprBOKOl22tt8493j?&K1!8O#2)g!;K0WUPM!N z%=#kTQfRXe9Dywbl<4NH51nCzJELW$Q(VXLR@yW+{m(LD)CHQ7<^#141ARwgvd{8& zI_da$bpT%kBt^V%6d1;b5sZjZJjLsqM&gUQ9H4A>1X%eo?XxVvCN_)O%v^STq+fPk zOI4~$gDIpB*kl_UOsr>^X>)?$3VKWrL#L@mndF4MPb6X*c}l zXhu5|pRF4Va2!f7R|kN%0S&oZD=-njtRljCB!^%KwFDk8VcM_FmcLK@=5G#Zl}@&)+#vJFb>lxz#rMOtJpiKl^8wcG)KNL=$S| z(MKP3Yr@HEpSwpiNT#3R)ILVH@%AtN`9BZ)y9m^-KfKUw<1%??GL2|P+A^AvZo_Tz z?x`t`F~ezeS16A*y6q1g$sQdMP1#tix1e-$bJN>adH;)lv5gav3@bVa+uWpJp%a?> z(xCO>;A?i6!T4g`1bi%Gn1J>5e=~YTB3nDYP3XTWyfAkH25||%k-rPKP3$K=@d^2t z|MFk1g0+aoN_isTah9>yc7lhI?7AwDd37_QFMR&zqg1W$CksotdX8eZtpI1Hk!WUdf^PkYL5uzDs?w)MX`h}!;tFLc*s1HwXw zi}XeYZN^-WOdlFO8W75;rv4xJzz5uK)I|o>>sw^~7xzJQN9TnD3lQ#{g^b0u_YEi+Q$=Ki z9fC#G#Zk$x{o1ds-u6|mdet;F!#^w8T>w1SAQP&`U-^|^k@r9JkUaV2FKmYU^1jvNJ(CyH%6w$U zY}2#S{ZJ%t;KAq}98Jw6z_GAn6B%FE-`F3XC%}xI9S%`tAZyya{5|l%1M&ku@B{LSSG+=A@{*UV zf6Oc|6Ic&pNCw|{$EbFw1Br>U&iZ7AcxZVf*0;up)C-t;DU z9?PxHs$Mz*M8IlWn+qcZp#~O@~*V9 zywNd5GM%+yLAp)ezr7p0|a#V0Rw|& z_iI<9XgP{6udoo>{idd6^^L$W%wKm<&*+C3#ZN zqBZGRDZ##kMM!9`1`^&4YV;r#SUFBZ3y8;GNf&cDwt8oJTD04SfwSUP6>5Z;?YH#$StXrMwy4%MA zM__f9L7H7*z~vyTV6bTHE-)7NslTQ|dZ%8P1Gpo;pcAx#=wZT1#eVDy4Zs#LF>;b3+LwnqGc?&|wgLi(-aY5JL88) ziS@jmWF^Hh_VSGeYfzqf74K}$7_ERGXYfFGcb>u8vOr|wJ1ZhvCe{t#oW%)VCARDY z9EmxQzDEM|*gPm)pHSZQ+U^j5Gc+2kuhG@B#!~thjrtF_+PsQfXlir|W*Ihrx<&sJ z!hw0PEEe_b7RlRqwmUd|j)J=i>(S1azq*MTOg$Sr&J(8kC1@AU7@F;Vq_&#__y_~E z!B*EP+S~}Q`PVudhwG0v-kd-eOgFq`FTf_D8oy-)wVW{eocwRVV?Z}s zkH&}}Cyt_GKswIGuj!%jVe-XH6=j5*eYN1NMEo;Ht3HDk6qEY5bQOY5^kJUPo;@~P+K-I3 z`a7M6T3-M9*Z_U4Y-{UAE8>?sz?ahn=Aq%WsD4IA ziF6=cpfl!J5tcKI5@cZF14i_$Nwo22PT{Lx`&zdZ2(BVtW@uaM0kZ&>gN@^Wyxaj; zJKsR3N0+uYm^>IctS?F;bR-Hgy)pe*?&<5|1j-FYI-39xu>FjcOW9`n78S#pBTS4PUHl-@wYUe(|>#z>T8QlNDz2Cei;lK?WsjFlr78c;pIRf>DmmJN*Ygp(4`5=Hd^mX7H+?g6l~<*3ehRQ=l7U_^3q zbp_VhlYUW(r>rP6W3;*v9yWwRhRxg3Dkq%`+M^nmH>HjnjXW zAD7G-Mly&BkI#;o!;K6V`?K6nv~k=W)cG7bTaMz0bLRXD7(YIMg->4Ej=yCAPc2#S zQ`@e8kN#|L*lux|aN~V`9AAiLyLgl<*7Ca7y{^6*ylw<`Oq=&fCj#q?0GJ5#PG#%X zpAlH`RONrcvN31|9{m~o=w%ugv1BtUJ@5ZmWM%nKNWugA)N9U4d8@nd4}Ejo)yN1D+k;D+)US#DR0c z*#q7Jw2i<@>zBXsl_6WXu%u`^nekiSkR{!g;hH$^N3o878OR!)fx?lI7dqM)iP9y2 zp-S&HSEH}r{#))2=#mYT{%CL$6#qn+-ET*lx)M>*KymwZGKLKg`=c>ieXpAsOJv{< z7vm554)n02$O;=4D6cZTHbfvwBQ3~dLW2t&(3c(I&}SfOjFHRW6m}xxj5iUlg$NJ(XR;0X3mhm$`A}v? z&us1hh6M0J*dl{mGuk>0aE+NjEl;=I{rb#H5T%`=;*xsDzPdf)JOP{@gj*Z4{P@57 zcmM7I1bh=9`@r(|P6H}3KAeE*d_yv1M(jO-x!%Ui0(0gV%2&Vo)jp5yZh@}v7utr( zg@_A0Bmi-X_|y^lgbE*APopB$u~80DG8?~_F+^i$FtTzaN?%j1jnem`loHPAApa6% zW)qmbjQZ;x$|aiHuPN!WL@nofc;=a6%hopDy_Rdn%XZGTMiIXIAA}S1ElI$(>xYHP z-~g7?P@Zi#EFa>lSnTLEa~Sw zs-3% zZN^_OkprqnTJL2tDiItZ`IS7Ai9Qk`5<4n42Rlv$b!1Nt4leT%D1E1k1KI8{_p?&zkh&Xoe~7^qbf@;x&Y4f!vysj zy_1YgDIFbb^t^U7W<>;z5|y{tHzRU}3^Y^r=3Z$=x=Rxp(_V-Kya<&q;1lU~{E=}D zAoelU!Sll6kYs7K(!sCpt=oR%2TX~?4Pl=Xo z$vM!e^wr$J`1cI7WNlzGh59qxP`?8jI*gj3AUxyGa1xk@1DLS`Sk~GP!{m45HVKJm zMWq>(2(J0-L6~~?!gvCsX9i8dc_L<$!~L00R1!r4j=(`b!^_1z=S}b;V{|kH1~MqR zctr|nkcfC1&q}0Q7BP$=IrTb_uhR!K-jXCf>TeTA#)F3LfHWVjO)u9vc$jQ(q6Jd< zM2E0#9fELJPl%R5PpvDRD>~eWPK24YQG18Gl=p{yB`M1?;DgKIjfM!*HYke10%tMo zQW_kqGX;|^rPBuAy)&RbeMO;#%7Zjwf1)GXM2XUL-O$DB%Os_Z)`%e%zdCYU~kR zN(xHq;9wo#v|@D%0_ix_dvJt{{dnd(UFwD59801!ETfXtuT@4DWI_1Ah$)1F6VVyr zZ!|jijSu75MRAiM%LD5`=E}H~3=?&U;NbjQ-(g%JTYTn(`Nw%Aa6^WoIEMn63Cz#b z_>n%iR*-Lpi~NGYIx&M>w^T-lY^QHR8!!RDG&k>+q@)=@E#LKB-}T7_*6pm_gn?B8 zQEM{H8K=l~n`k%$M(+T8{>81&k3XL`wadf;eI zwe|rBdvO6ziQOW22`nXH z8=)N!;$>dY0co7w^v-w<-{9k@IE>Y{)Y2e}p}wXphC@A=(Z~%h9+BQ;9=VSqETB`8 z`4y;h<3xm=ag-(WvBo2hiB-o%M#qYH=h9)Z(}%1~@kod;Qg?cA&L9(WLxC3&pA}0| zwBZr%yVSj3CS~dXmUY8J3!uju@SYk8c`z9T-cuV#rH-i_ct%WdhH+50S$*;A*+S{G z#6}qvq`!Lv_?B9fM`D_V^%CdF#L5^Mv<8j`l@)ROj>X+Mv3Yh%NJ=_n zC4k4Gk0TIM(m>gH$!Psll7ONy(Y*>HuH4*CQ zL`RHmM!U(-j6G^EC4P#{o(UjWvvq!?s1g1YC)1PA$e|{ce~H(_Sy6IUh3Z`7Pk7R( z4RJ^^uVvgE+k28tfSwY-=e8Lg*^az}!NwBlRRn(r9`@&>gB8r-Ob3T?jIWFipW#i{ z{#JG~fJVf71tKsI5_addMw|+GO=vS5Hn#UKV+iWUl5AQLEJa#TIc0}tkug0D=pE<+ zQj{E78`e|hD9SpR_HD);;}`KM;7|uJFkR$GWn)|fgrhUg+K<-WL$(z0RQ$511VN)m zL%S};-)V~A!uepRR;^ps!nY4?{s(6X`tE45`K8QicB>S1GvXu5x{%IJYolw(G@!g! z4wPF*I(_!DpS|-P-|-#w(ctfALg0LBXpsk>R06|6K}sUDK1nEmLdyeH|J*<#{6Z)~ zPcH!vc%4ly;I?tjpd#2u;0ibm=-MM!3D(&}_Yjeyv8cdw9O`fmLEb(1TVEZspn->o z_UK&4b+C(?wVl4|%=9Y^?U_-~S2i2fu55TqB8-pzFMp(Xmi zR}Pdz89*)1d*1WjtC!ys>&B;-wGU3_dgLD~U<0Kn<2KWjQqy)U&Vus^bV{h(CXTJ6 zy624WAzcccREOv19beGDPfRfR!eb+25TC8Eq756BBo7J3G94UE98EIPiBZt)zaSpn zBCA_UV1EH4MEN3ve9o+ikIri&{cJjBn=YH6`C`8NwCT3Dp}>=i^zF!VKin~Z^TE-Q z2%;=*D9Dq>S;QxwdFGiz#%}`-mt`;D!R7E_wu2Q=@lSJ^u$9&=EFc>P9yyLIGls1((Gc{-uw7 z>|^0`|4})#1hDeC&wcJQFMa7tA77CF1*k5G5%+dFVyfhvzCeV4w)C-AnuH>fZ{%Tr zkzt*Q^o6N2z;TKO3=C;ppx99~MxJ}Wz`IAv)Ik)Eg)o=NthlcTZOb%z%jgoxk! zK%Mr(%RmQ=!~QrQtV6@HDsTi*dfoA$OxbtQQISp#jAXp)b@1jV>Uuv4=o0h~ofMS! zF2?`&<%)8Z1E}Tmpa1;7dD+Wec6&K}aHU9Hnx#0=l0-&{6|E%Kp;89M%*D|Ku(&@v zFbR|c@nuGWbQe^987C6KvJ7`i?cerSoWn6CC!-OOaED!N4D8h)4}C1kC+j1*oHdXe z@|!v0E=MrP!B``Dh##Hxjmj)567M98M!YTE>#Yv^1Ueux$_DGZTwz{^`CMRsXfgh` z%a!Hu9sri-KmYkZE$!Rla`e=J-RwHeFcH ziD!g2$X+4!AY57LG!#F<8qE=+43-FX-Lpr|UL#stahV7PhfL>}!<{dG`OAMv?xNh4 z19%!Qf-V&2*uGNAm~HVawympnz|t)y9{ha~am$&XJYG z!v>B-XXnNv%GA0r95SxZAa&g2W1SYr21+o0gFM(tz+aScx&Yb6J+_?wz@0mH9+$f) zdy!{azVkc3^Y%i+2baIwMi>x^>8;`F4gpf4u518fFb*c1VLSf7GrN=)@dvPa zDf5jRr%N>B(lXFlmePDnqLE_06}-gp!56oJ8IEx)7E~KG);FqLr z47L(^X60*RcGgE^*!bw_lgRW5EQ8N<@cJsk9pQBmj3Zx!JfaI|XS%v>M6ZbM6sBD! z*ZaUc?bA8g>6@acV{liz9`G9A4vsX?8!;HxPB4ZHk?D~+-D!I^&j;`c${^s^!GcHJ zAG;UEU(2(~0IXC(Uhsk!ylElsA*go7DgjgIjH%wD!73Ai*FC(D^2KxDB4)>f8%mKe z!h|AFT$e?gpGwGwI_Qsdb!Su!9~4I@+RS7Y!4I$XfM+RwNs*0ZrB$YnqLu?gM(lzC z;TM#phOBQQ}nEFS5#I1tv>9J5!z75 zyd3G5@ib&G?Ddh>Bx49+Cvrh}uAjAIgvn?$KxmXfBY#Zk z^6|5}qk7U22fCNx8rqsn9$8M``&>kQEYG});j((wZO4S}<>EMZu8GU0^V89~z5 zGNfS*e*nHSJPp>-BGcQYB9nP#8pw#FC8i)9t^R6#*KwUcoeO~|eiU}=|M+tKzOQ`c zD}U#1pX$4t@^yCrR;nXd7_PrU{Q8xUSjn6*$Y?0tUx-NNRYi89t4`8mGz7vEPBJUL zkn*u{AwR*mlHtGrCX%PYbiFGJ;v-vj;d;EV@kP!p!jE#7IOUcA4(>V5NAoU z5S|l>4b#aKaiM%CG!N;0J*QvmWk(VkTf*OPXC*Rf#1-(=$xmR18@;W6jqvYZnje0y z*Z6zo8}0zG{HZ_nr{26w@|zc-{kL*)2GggbsBrok8CWlgf;w31iS`FDxc{yZ0qqgq z5Fge>#*)xDO`&TWzlTm^rZU*6i;2_iMVEv8TD{SPG)iDpAw26-%DB0h=~6UL6FMWg z3)0;${9IR~e`u-uz&G0Hv3yfG04&QA;f;&19$XanhnC9+7eU@%zx}3{;uM0%LKIHd zeULe<2WpGftn6R}>IWFqp22tYa8@A!%}M>CHK!bOUK%XDBHAW$KIE)rhp$P0s|z=L znU7>XhwKhWh zrRoi4!{XOyoU#rnqm@L_1sx2)-P=c)ZZc-d2u9?4+EVICs6n%oP~a0Fkk&K#pizOS>xS2yZpUU1n`qx++aZhmCB&;9d6CP)4^3ESRD5#6*cTt~ZhenM&p(Db65nX{p8QLR9AI+J@dq#z5W}PT9 z%COcyCKp4$v*_HNrTwwx_~^Hau}9@w+5u$c_U+sEfBUz8`|TxTsGl;wy-e7vmdU!m zIf&csGt##~d2ZK=$N+TP0|?iii6UEZ%8rEh41{ zI&pl{5;?;i7@1Qn32bb4V}PeSKzqFfS5x*o*0!A2>n|=k^ym`RAOBW1@>%)MMP70% Ts#$!~00000NkvXXu0mjff#gjG literal 0 HcmV?d00001 diff --git a/Assets.xcassets/ugreen_logo.png b/Assets.xcassets/ugreen_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..07ffced80099f6b558282098f46aeaa948a45e10 GIT binary patch literal 38583 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�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 Ts#$!~00000NkvXXu0mjff#gjG literal 0 HcmV?d00001 diff --git a/Core/Models/NASConnection.swift b/Core/Models/NASConnection.swift index b959b8e..2c277b4 100644 --- a/Core/Models/NASConnection.swift +++ b/Core/Models/NASConnection.swift @@ -10,6 +10,8 @@ enum NASProtocol: String, Codable, CaseIterable { case .sftp: return 22 } } + + var localizedDescription: String { rawValue } } struct NASConnection: Codable, Identifiable, Equatable { @@ -18,10 +20,15 @@ struct NASConnection: Codable, Identifiable, Equatable { var port: Int var nasProtocol: NASProtocol var username: String - var password: String + var password: String // Runtime only — persisted in Keychain, not UserDefaults var remotePath: String var displayName: String + // Excludes password so it is never written to UserDefaults. + enum CodingKeys: String, CodingKey { + case id, host, port, nasProtocol, username, remotePath, displayName + } + init( host: String, port: Int? = nil, @@ -39,4 +46,16 @@ struct NASConnection: Codable, Identifiable, Equatable { self.remotePath = remotePath self.displayName = displayName.isEmpty ? host : displayName } + + init(from decoder: Decoder) throws { + let c = try decoder.container(keyedBy: CodingKeys.self) + id = try c.decode(UUID.self, forKey: .id) + host = try c.decode(String.self, forKey: .host) + port = try c.decode(Int.self, forKey: .port) + nasProtocol = try c.decode(NASProtocol.self, forKey: .nasProtocol) + username = try c.decode(String.self, forKey: .username) + remotePath = try c.decode(String.self, forKey: .remotePath) + displayName = try c.decode(String.self, forKey: .displayName) + password = "" // filled from Keychain by SavedConnections.load() + } } diff --git a/Features/Backup/BackupView.swift b/Features/Backup/BackupView.swift index a5ff198..24ed423 100644 --- a/Features/Backup/BackupView.swift +++ b/Features/Backup/BackupView.swift @@ -8,106 +8,117 @@ struct BackupView: View { var body: some View { ZStack { - Color.white.ignoresSafeArea() + AppTheme.background.ignoresSafeArea() - ScrollView { - VStack(spacing: AppTheme.sectionGap) { - ringSection - nasRow - optionsCard - actionButton + VStack(spacing: 0) { + Spacer(minLength: 0) + + // ─── Ring hero ─────────────────────────────────────── + ringHero + .padding(.bottom, 28) + + // ─── Completion summary (only on .completed) ───────── + if case .completed = engine.job.status { + completionStrip + .padding(.bottom, 28) + .transition(.opacity.combined(with: .scale(scale: 0.97))) } - .padding(.horizontal, AppTheme.hPad) - .padding(.top, 24) - .padding(.bottom, 48) + + Spacer(minLength: 0) + + // ─── NAS status ─────────────────────────────────────── + nasStatusRow + .padding(.horizontal, AppTheme.hPad) + .padding(.bottom, 16) + + // ─── Photos access nudge ────────────────────────────── + if vm.photosAuthStatus != .authorized && vm.photosAuthStatus != .limited { + photosAccessRow + .padding(.horizontal, AppTheme.hPad) + .padding(.bottom, 16) + .transition(.opacity.combined(with: .move(edge: .bottom))) + } + + // ─── Action ─────────────────────────────────────────── + actionButton + .padding(.horizontal, AppTheme.hPad) + .padding(.bottom, 32) } } + .animation(.spring(response: 0.35, dampingFraction: 0.82), value: engine.job.status == .completed) + .animation(.spring(response: 0.35, dampingFraction: 0.82), value: vm.photosAuthStatus == .authorized) .task { vm.loadPhotoCount(filter: store.backupFilter) } } - // MARK: — Ring + // MARK: — Ring hero - private var ringSection: some View { - VStack(spacing: 20) { - ZStack { - ProgressRing( - progress: engine.job.progress, - lineWidth: 8, - size: 180, - color: ringColor, - backgroundColor: Color(hex: "#EBEBEB") - ) + private var ringHero: some View { + ZStack { + ProgressRing( + progress: engine.job.progress, + lineWidth: 7, + size: 220, + color: ringColor, + backgroundColor: AppTheme.inkQuaternary.opacity(0.4) + ) + .accessibilityLabel("Backup progress") + .accessibilityValue("\(Int(engine.job.progress * 100)) percent") - VStack(spacing: 3) { - Text(percentText) - .font(.system(size: 40, weight: .semibold, design: .rounded)) - .foregroundStyle(AppTheme.ink) - .contentTransition(.numericText()) + VStack(spacing: 4) { + Text(percentText) + .font(.system(size: 44, weight: .semibold, design: .rounded)) + .foregroundStyle(AppTheme.ink) + .contentTransition(.numericText()) + .monospacedDigit() - if case .running = engine.job.status { - Text("\(engine.job.uploadedFiles + engine.job.skippedFiles) of \(engine.job.totalFiles)") - .font(AppTheme.caption()) - .foregroundStyle(AppTheme.inkSecondary) - if let eta = engine.job.eta { - Text("~\(etaString(eta)) remaining") - .font(.system(size: 11, weight: .regular)) - .foregroundStyle(AppTheme.inkTertiary) - } - } else { - Text("\(vm.totalPhotoCount) photos") - .font(AppTheme.caption()) - .foregroundStyle(AppTheme.inkSecondary) - } - } - } - - // Completion summary - if case .completed = engine.job.status { - HStack(spacing: 0) { - statCell("\(engine.job.uploadedFiles)", label: "uploaded") - dividerLine - statCell("\(engine.job.skippedFiles)", label: "skipped") - dividerLine - statCell("\(engine.job.failedFiles)", label: "errors", - color: engine.job.failedFiles > 0 ? AppTheme.destructive : AppTheme.inkTertiary) - } - .background(AppTheme.surfaceSunken) - .clipShape(RoundedRectangle(cornerRadius: AppTheme.radius, style: .continuous)) - .transition(.opacity.combined(with: .scale(scale: 0.95))) - } - - // Progress detail - if case .running = engine.job.status { - VStack(spacing: 8) { - GeometryReader { geo in - ZStack(alignment: .leading) { - RoundedRectangle(cornerRadius: 2, style: .continuous) - .fill(Color(hex: "#EBEBEB")) - .frame(height: 3) - RoundedRectangle(cornerRadius: 2, style: .continuous) - .fill(AppTheme.ink) - .frame(width: geo.size.width * engine.job.progress, height: 3) - .animation(.spring(response: 0.4, dampingFraction: 0.8), value: engine.job.progress) - } - } - .frame(height: 3) - - HStack { - Text(engine.job.currentFileName) - .font(.system(size: 12, weight: .regular)) - .foregroundStyle(AppTheme.inkSecondary) - .lineLimit(1) - .truncationMode(.middle) - Spacer() - Text(formatSpeed(engine.job.speedBytesPerSecond)) - .font(.system(size: 12, weight: .regular)) - .foregroundStyle(AppTheme.inkTertiary) - } - } - .transition(.opacity) + ringSubtitle + .transition(.opacity) } } - .animation(.spring(response: 0.4, dampingFraction: 0.8), value: engine.job.status == .completed) + .animation(.spring(response: 0.4, dampingFraction: 0.8), value: engine.job.progress) + } + + @ViewBuilder + private var ringSubtitle: some View { + switch engine.job.status { + case .running: + VStack(spacing: 2) { + Text("\(engine.job.uploadedFiles + engine.job.skippedFiles) of \(engine.job.totalFiles)") + .font(AppTheme.caption()) + .foregroundStyle(AppTheme.inkSecondary) + if let eta = engine.job.eta { + Text("~\(etaString(eta)) remaining") + .font(.system(size: 11, weight: .regular)) + .foregroundStyle(AppTheme.inkTertiary) + } + Text(engine.job.currentFileName) + .font(.system(size: 11, weight: .regular)) + .foregroundStyle(AppTheme.inkTertiary) + .lineLimit(1) + .truncationMode(.middle) + .frame(maxWidth: 160) + } + case .preparing: + Text("Preparing…") + .font(AppTheme.caption()) + .foregroundStyle(AppTheme.inkTertiary) + case .completed: + Text("Done") + .font(.system(size: 13, weight: .medium)) + .foregroundStyle(AppTheme.positive) + case .failed: + Text("Failed") + .font(.system(size: 13, weight: .medium)) + .foregroundStyle(AppTheme.destructive) + case .cancelled: + Text("Cancelled") + .font(AppTheme.caption()) + .foregroundStyle(AppTheme.inkTertiary) + default: + Text(vm.totalPhotoCount > 0 ? "\(vm.totalPhotoCount) photos ready" : "Ready") + .font(AppTheme.caption()) + .foregroundStyle(AppTheme.inkTertiary) + } } private var ringColor: Color { @@ -120,11 +131,32 @@ struct BackupView: View { private var percentText: String { "\(Int(engine.job.progress * 100))%" } - private func statCell(_ value: String, label: String, color: Color = AppTheme.ink) -> some View { - VStack(spacing: 2) { + // MARK: — Completion strip + + private var completionStrip: some View { + HStack(spacing: 0) { + statCell("\(engine.job.uploadedFiles)", label: "uploaded") + thinDivider + statCell("\(engine.job.skippedFiles)", label: "skipped") + thinDivider + statCell( + "\(engine.job.failedFiles)", + label: "errors", + valueColor: engine.job.failedFiles > 0 ? AppTheme.destructive : AppTheme.ink + ) + } + .padding(.horizontal, AppTheme.hPad) + .background(AppTheme.surfaceSunken) + .clipShape(RoundedRectangle(cornerRadius: AppTheme.radius, style: .continuous)) + .padding(.horizontal, AppTheme.hPad) + } + + private func statCell(_ value: String, label: String, valueColor: Color = AppTheme.ink) -> some View { + VStack(spacing: 3) { Text(value) - .font(.system(size: 18, weight: .semibold)) - .foregroundStyle(color) + .font(.system(size: 17, weight: .semibold)) + .foregroundStyle(valueColor) + .monospacedDigit() Text(label) .font(AppTheme.micro()) .foregroundStyle(AppTheme.inkTertiary) @@ -133,21 +165,21 @@ struct BackupView: View { .padding(.vertical, 14) } - private var dividerLine: some View { + private var thinDivider: some View { Rectangle() .fill(AppTheme.separator) .frame(width: 0.5) - .padding(.vertical, 12) + .padding(.vertical, 10) } - // MARK: — NAS row + // MARK: — NAS status row - private var nasRow: some View { + private var nasStatusRow: some View { HStack(spacing: 10) { ZStack { Circle() .fill(AppTheme.surfaceSunken) - .frame(width: 32, height: 32) + .frame(width: 34, height: 34) Image(systemName: "externaldrive") .font(.system(size: 13, weight: .medium)) .foregroundStyle(AppTheme.inkSecondary) @@ -158,98 +190,54 @@ struct BackupView: View { Text(conn.host) .font(AppTheme.headline()) .foregroundStyle(AppTheme.ink) - Text(conn.username) - .font(AppTheme.caption()) - .foregroundStyle(AppTheme.inkSecondary) + Text(conn.nasProtocol.rawValue) + .font(AppTheme.micro(10)) + .foregroundStyle(AppTheme.inkTertiary) } + } else { + Text("No NAS configured") + .font(AppTheme.body()) + .foregroundStyle(AppTheme.inkTertiary) } Spacer() HStack(spacing: 5) { Circle() - .fill(AppTheme.positive) + .fill(engine.job.status.isActive ? AppTheme.positive : AppTheme.inkQuaternary) .frame(width: 6, height: 6) - Text("Live") + Text(engine.job.status.isActive ? "Active" : "Ready") .font(AppTheme.micro()) - .foregroundStyle(AppTheme.positive) + .foregroundStyle(engine.job.status.isActive ? AppTheme.positive : AppTheme.inkTertiary) } } - .padding(14) + .padding(.horizontal, 14) + .padding(.vertical, 12) .background(AppTheme.surfaceSunken) .clipShape(RoundedRectangle(cornerRadius: AppTheme.radius, style: .continuous)) } - // MARK: — Options + // MARK: — Photos access nudge - private var optionsCard: some View { - VStack(spacing: 0) { - optionRow(icon: "camera", label: "What to back up") {} - hairline - optionRow(icon: "doc.badge.clock", label: "New files only") {} - hairline - Button(action: { Task { await vm.requestPhotosAccess() } }) { - HStack { - Image(systemName: "photo.on.rectangle") - .font(.system(size: 14, weight: .regular)) - .foregroundStyle(vm.photosAuthStatus == .authorized - ? AppTheme.inkSecondary : AppTheme.destructive) - .frame(width: 20) - Text("Photos access") - .font(AppTheme.body()) - .foregroundStyle(AppTheme.ink) - Spacer() - Text(photosLabel) - .font(AppTheme.caption()) - .foregroundStyle(vm.photosAuthStatus == .authorized - ? AppTheme.positive : AppTheme.destructive) - Image(systemName: "chevron.right") - .font(.system(size: 12, weight: .medium)) - .foregroundStyle(AppTheme.inkQuaternary) - } - .padding(.horizontal, 16) - .padding(.vertical, 14) - } - .buttonStyle(ScaleButtonStyle()) - } - .background(Color.white) - .clipShape(RoundedRectangle(cornerRadius: AppTheme.radius, style: .continuous)) - .shadow(color: AppTheme.cardShadow, radius: 12, x: 0, y: 2) - } - - private var photosLabel: String { - switch vm.photosAuthStatus { - case .authorized: return "Full access" - case .limited: return "Limited" - case .denied: return "Denied" - default: return "Not set" - } - } - - private var hairline: some View { - Rectangle() - .fill(AppTheme.separator) - .frame(height: 0.5) - .padding(.leading, 36) - } - - private func optionRow(icon: String, label: String, action: @escaping () -> Void) -> some View { - Button(action: action) { - HStack { - Image(systemName: icon) - .font(.system(size: 14, weight: .regular)) - .foregroundStyle(AppTheme.inkSecondary) + private var photosAccessRow: some View { + Button(action: { Task { await vm.requestPhotosAccess() } }) { + HStack(spacing: 10) { + Image(systemName: "photo.on.rectangle") + .font(.system(size: 13, weight: .medium)) + .foregroundStyle(AppTheme.destructive) .frame(width: 20) - Text(label) - .font(AppTheme.body()) - .foregroundStyle(AppTheme.ink) + Text("Grant photo library access to enable backup") + .font(AppTheme.caption()) + .foregroundStyle(AppTheme.inkSecondary) Spacer() Image(systemName: "chevron.right") - .font(.system(size: 12, weight: .medium)) + .font(.system(size: 11, weight: .medium)) .foregroundStyle(AppTheme.inkQuaternary) } - .padding(.horizontal, 16) - .padding(.vertical, 14) + .padding(.horizontal, 14) + .padding(.vertical, 11) + .background(AppTheme.destructive.opacity(0.06)) + .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) } .buttonStyle(ScaleButtonStyle()) } @@ -266,24 +254,27 @@ struct BackupView: View { .buttonStyle(PrimaryButtonStyle( color: engine.job.status == .completed ? AppTheme.positive : AppTheme.ink )) + .accessibilityHint("Starts backing up your photos to the NAS") case .running: Button { engine.pause() } label: { Text("Pause") } .buttonStyle(GhostButtonStyle()) + .accessibilityLabel("Pause backup") case .paused: HStack(spacing: 10) { Button { engine.resume() } label: { Text("Resume") } .buttonStyle(PrimaryButtonStyle()) + .accessibilityLabel("Resume backup") Button { engine.cancel() } label: { Text("Cancel") } .buttonStyle(GhostButtonStyle()) .frame(width: 90) + .accessibilityLabel("Cancel backup") } case .preparing: HStack(spacing: 8) { - ProgressView() - .scaleEffect(0.8) + ProgressView().scaleEffect(0.8).tint(AppTheme.inkSecondary) Text("Preparing…") .font(AppTheme.body()) .foregroundStyle(AppTheme.inkSecondary) @@ -291,7 +282,7 @@ struct BackupView: View { .frame(height: 50) } } - .animation(.spring(response: 0.3, dampingFraction: 0.8), value: engine.job.status == .running) + .animation(.spring(response: 0.3, dampingFraction: 0.82), value: engine.job.status == .running) } private func startBackup() { @@ -299,12 +290,6 @@ struct BackupView: View { Task { _ = try? await engine.run(connection: conn, filter: store.backupFilter) } } - private func formatSpeed(_ bps: Double) -> String { - bps >= 1_000_000 - ? String(format: "%.1f MB/s", bps / 1_000_000) - : String(format: "%.0f KB/s", bps / 1_000) - } - private func etaString(_ seconds: TimeInterval) -> String { Int(seconds) / 60 > 0 ? "\(Int(seconds) / 60)m" : "\(Int(seconds))s" } diff --git a/Features/Browse/BrowseView.swift b/Features/Browse/BrowseView.swift index 0827931..d8ba12d 100644 --- a/Features/Browse/BrowseView.swift +++ b/Features/Browse/BrowseView.swift @@ -15,7 +15,7 @@ struct BrowseView: View { var body: some View { NavigationStack { ZStack { - Color.white.ignoresSafeArea() + AppTheme.background.ignoresSafeArea() VStack(spacing: 0) { // Path bar @@ -40,21 +40,31 @@ struct BrowseView: View { if isLoading { Spacer() - ProgressView() - .tint(AppTheme.inkTertiary) + VStack(spacing: 10) { + ProgressView() + .tint(AppTheme.inkTertiary) + Text("Loading…") + .font(AppTheme.caption()) + .foregroundStyle(AppTheme.inkTertiary) + } Spacer() } else if let error { Spacer() VStack(spacing: 12) { Image(systemName: "exclamationmark.triangle") - .font(.system(size: 28, weight: .light)) + .font(.system(size: 28, weight: .ultraLight)) .foregroundStyle(AppTheme.inkQuaternary) - Text(error) - .font(AppTheme.caption()) - .foregroundStyle(AppTheme.inkSecondary) - .multilineTextAlignment(.center) + VStack(spacing: 4) { + Text("Could not load folder") + .font(.system(size: 14, weight: .medium)) + .foregroundStyle(AppTheme.inkSecondary) + Text(error) + .font(AppTheme.caption()) + .foregroundStyle(AppTheme.inkTertiary) + .multilineTextAlignment(.center) + } } - .padding() + .padding(.horizontal, 32) Spacer() } else { List { diff --git a/Features/Connect/ConnectView.swift b/Features/Connect/ConnectView.swift index 09df8e2..98e3f0b 100644 --- a/Features/Connect/ConnectView.swift +++ b/Features/Connect/ConnectView.swift @@ -9,30 +9,33 @@ struct ConnectView: View { var body: some View { NavigationStack { ZStack { - AppTheme.surfaceSunken.ignoresSafeArea() + AppTheme.background.ignoresSafeArea() ScrollView { VStack(spacing: 0) { - // Logo + title - VStack(spacing: 10) { + // Header + VStack(spacing: 20) { + // App icon Image("nas_logo_clean") .resizable() .scaledToFit() - .frame(width: 44, height: 44) - .clipShape(RoundedRectangle(cornerRadius: 11, style: .continuous)) + .frame(width: 52, height: 52) + .clipShape(RoundedRectangle(cornerRadius: 13, style: .continuous)) + .shadow(color: .black.opacity(0.08), radius: 14, x: 0, y: 4) .padding(.top, 44) - Text("NAS Connect") - .font(.system(size: 26, weight: .semibold)) - .foregroundStyle(AppTheme.ink) + // Title + VStack(spacing: 6) { + Text("Kisani") + .font(.system(size: 26, weight: .semibold)) + .foregroundStyle(AppTheme.ink) - Text("Connect to your UGreen or any SMB / SFTP NAS") - .font(AppTheme.caption()) - .foregroundStyle(AppTheme.inkTertiary) - .multilineTextAlignment(.center) + // UGreen NAS compatibility badge + UGreenCompatibilityBadge() + } } - .padding(.bottom, 32) + .padding(.bottom, 28) .padding(.horizontal, AppTheme.hPad) // Protocol picker @@ -50,7 +53,7 @@ struct ConnectView: View { .padding(.vertical, 7) .background( RoundedRectangle(cornerRadius: 7, style: .continuous) - .fill(vm.selectedProtocol == proto ? Color.white : Color.clear) + .fill(vm.selectedProtocol == proto ? AppTheme.surfaceRaised : Color.clear) .shadow( color: vm.selectedProtocol == proto ? .black.opacity(0.06) : .clear, radius: 3, x: 0, y: 1 @@ -61,7 +64,7 @@ struct ConnectView: View { } } .padding(3) - .background(Color.black.opacity(0.06)) + .background(AppTheme.surfaceSunken) .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) .padding(.horizontal, AppTheme.hPad) .padding(.bottom, 20) @@ -90,19 +93,14 @@ struct ConnectView: View { } .padding(.horizontal, 16) .padding(.vertical, 14) - .background(Color.white) + .background(AppTheme.surfaceRaised) .clipShape(RoundedRectangle(cornerRadius: AppTheme.radius, style: .continuous)) .shadow(color: AppTheme.cardShadow, radius: 12, x: 0, y: 2) - HStack(spacing: 20) { - Text("IP Address, ex: 192.168.1.10") - .font(AppTheme.micro(11)) - .foregroundStyle(AppTheme.inkTertiary) - Text("NAS Name, ex: MY_NAS") - .font(AppTheme.micro(11)) - .foregroundStyle(AppTheme.inkTertiary) - } - .padding(.leading, 4) + Text("e.g. 192.168.1.10 or MY_NAS") + .font(AppTheme.micro(11)) + .foregroundStyle(AppTheme.inkTertiary) + .padding(.leading, 4) } .padding(.horizontal, AppTheme.hPad) .padding(.bottom, 12) @@ -163,7 +161,7 @@ struct ConnectView: View { .padding(.horizontal, 16) .padding(.vertical, 14) } - .background(Color.white) + .background(AppTheme.surfaceRaised) .clipShape(RoundedRectangle(cornerRadius: AppTheme.radius, style: .continuous)) .overlay( RoundedRectangle(cornerRadius: AppTheme.radius, style: .continuous) @@ -199,7 +197,7 @@ struct ConnectView: View { } .padding(.horizontal, 16) .padding(.vertical, 14) - .background(Color.white) + .background(AppTheme.surfaceRaised) .clipShape(RoundedRectangle(cornerRadius: AppTheme.radius, style: .continuous)) .shadow(color: AppTheme.cardShadow, radius: 12, x: 0, y: 2) } @@ -255,25 +253,11 @@ struct ConnectView: View { .disabled(!vm.canConnect) .animation(.spring(response: 0.3, dampingFraction: 0.8), value: vm.isConnected) - // Help - Button { - // future: show help sheet - } label: { - HStack(spacing: 6) { - Image(systemName: "questionmark.circle") - .font(.system(size: 13)) - Text("Help") - .font(AppTheme.caption()) - } - .foregroundStyle(AppTheme.inkTertiary) - } - .padding(.top, 20) - .padding(.bottom, 48) + Spacer().frame(height: 48) } } } - .navigationTitle("NAS Connect") - .navigationBarTitleDisplayMode(.inline) + .navigationBarHidden(true) .navigationDestination(isPresented: $navigateToDashboard) { MainTabView() } .sheet(isPresented: $vm.showFolderBrowser) { if let conn = ConnectionStore.shared.savedConnection { diff --git a/Features/Connect/ConnectViewModel.swift b/Features/Connect/ConnectViewModel.swift index 11b9412..7f1f0fa 100644 --- a/Features/Connect/ConnectViewModel.swift +++ b/Features/Connect/ConnectViewModel.swift @@ -24,6 +24,11 @@ final class ConnectViewModel: ObservableObject { } func verify() async { + let service: any NASTransferProtocol = selectedProtocol == .smb ? SMBService() : SFTPService() + await verifyWith(transfer: service) + } + + func verifyWith(transfer: any NASTransferProtocol) async { isVerifying = true isConnected = false verifyError = nil @@ -36,11 +41,10 @@ final class ConnectViewModel: ObservableObject { remotePath: remotePath ) - let service: any NASTransferProtocol = selectedProtocol == .smb ? SMBService() : SFTPService() do { - try await service.connect(to: connection.host, port: connection.port, - username: connection.username, password: connection.password) - service.disconnect() + try await transfer.connect(to: connection.host, port: connection.port, + username: connection.username, password: connection.password) + transfer.disconnect() isConnected = true ConnectionStore.shared.savedConnection = connection } catch let e as BackupError { diff --git a/Features/History/HistoryView.swift b/Features/History/HistoryView.swift index 58352e5..a8621d1 100644 --- a/Features/History/HistoryView.swift +++ b/Features/History/HistoryView.swift @@ -5,29 +5,22 @@ struct HistoryView: View { var body: some View { ZStack { - Color.white.ignoresSafeArea() + AppTheme.background.ignoresSafeArea() if store.historyEntries.isEmpty { - VStack(spacing: 12) { - Image(systemName: "clock.arrow.circlepath") - .font(.system(size: 36, weight: .light)) - .foregroundStyle(AppTheme.inkQuaternary) - Text("No backup history yet") - .font(AppTheme.body()) - .foregroundStyle(AppTheme.inkTertiary) - } + emptyState } else { List { ForEach(store.historyEntries) { entry in HistoryRowView(entry: entry) .listRowSeparator(.hidden) .listRowInsets(EdgeInsets()) + .listRowBackground(AppTheme.background) } - .onDelete { offsets in - store.removeHistoryEntries(at: offsets) - } + .onDelete { store.removeHistoryEntries(at: $0) } } .listStyle(.plain) + .scrollContentBackground(.hidden) } } .navigationTitle("History") @@ -36,32 +29,60 @@ struct HistoryView: View { ToolbarItem(placement: .navigationBarTrailing) { Button("Clear") { store.clearHistory() } .font(AppTheme.caption()) - .foregroundStyle(AppTheme.destructive) + .foregroundStyle(AppTheme.inkTertiary) } } } } + + private var emptyState: some View { + VStack(spacing: 14) { + Image(systemName: "clock.arrow.circlepath") + .font(.system(size: 32, weight: .light)) + .foregroundStyle(AppTheme.inkQuaternary) + VStack(spacing: 5) { + Text("No backups yet") + .font(.system(size: 15, weight: .medium)) + .foregroundStyle(AppTheme.inkSecondary) + Text("Your backup history will appear here") + .font(AppTheme.caption()) + .foregroundStyle(AppTheme.inkTertiary) + } + } + } } +// MARK: — Row + struct HistoryRowView: View { let entry: BackupHistoryEntry private var hasErrors: Bool { entry.failedCount > 0 } + private var statusColor: Color { hasErrors ? AppTheme.destructive : AppTheme.positive } var body: some View { VStack(spacing: 0) { HStack(alignment: .top, spacing: 14) { + // Status indicator Circle() - .fill(hasErrors ? AppTheme.destructive : AppTheme.positive) - .frame(width: 7, height: 7) - .padding(.top, 5) + .fill(statusColor) + .frame(width: 6, height: 6) + .padding(.top, 6) + // Content VStack(alignment: .leading, spacing: 5) { - HStack(alignment: .firstTextBaseline, spacing: 8) { - Text(entry.date, style: .date) - .font(.system(size: 15, weight: .medium)) + // Date + badges + HStack(alignment: .firstTextBaseline, spacing: 6) { + Text(entry.date, format: .dateTime.month(.abbreviated).day().year()) + .font(.system(size: 14, weight: .medium)) .foregroundStyle(AppTheme.ink) + Text(entry.date, format: .dateTime.hour().minute()) + .font(.system(size: 13, weight: .regular)) + .foregroundStyle(AppTheme.inkTertiary) + + Spacer() + if entry.triggeredByLAN { Text("AUTO") .font(.system(size: 9, weight: .semibold)) @@ -73,37 +94,25 @@ struct HistoryRowView: View { .stroke(AppTheme.inkQuaternary, lineWidth: 0.75) ) } - - Spacer() - - Text(entry.date, style: .time) - .font(AppTheme.micro(11)) - .foregroundStyle(AppTheme.inkTertiary) } - HStack(spacing: 8) { - Text("\(entry.uploadedCount) uploaded") - .font(AppTheme.caption()) - .foregroundStyle(AppTheme.inkSecondary) + // Stat line + statLine - if entry.skippedCount > 0 { - dot - Text("\(entry.skippedCount) skipped") - .font(AppTheme.caption()) - .foregroundStyle(AppTheme.inkTertiary) - } - - if entry.failedCount > 0 { - dot - Text("\(entry.failedCount) errors") - .font(AppTheme.caption()) - .foregroundStyle(AppTheme.destructive) + // Secondary info: host · duration · size + HStack(spacing: 6) { + Text(entry.nasHost) + .foregroundStyle(AppTheme.inkQuaternary) + midDot + Text(formatDuration(entry.durationSeconds)) + .foregroundStyle(AppTheme.inkQuaternary) + if entry.totalBytes > 0 { + midDot + Text(formatBytes(entry.totalBytes)) + .foregroundStyle(AppTheme.inkQuaternary) } } - - Text(entry.nasHost) - .font(AppTheme.micro(11)) - .foregroundStyle(AppTheme.inkQuaternary) + .font(.system(size: 11, weight: .regular)) } } .padding(.horizontal, 16) @@ -112,13 +121,48 @@ struct HistoryRowView: View { Rectangle() .fill(AppTheme.separator) .frame(height: 0.5) - .padding(.leading, 37) + .padding(.leading, 36) } } - private var dot: some View { + @ViewBuilder + private var statLine: some View { + HStack(spacing: 6) { + Text("\(entry.uploadedCount) uploaded") + .foregroundStyle(AppTheme.inkSecondary) + + if entry.skippedCount > 0 { + midDot + Text("\(entry.skippedCount) skipped") + .foregroundStyle(AppTheme.inkTertiary) + } + + if hasErrors { + midDot + Text("\(entry.failedCount) errors") + .foregroundStyle(AppTheme.destructive) + } + } + .font(AppTheme.caption()) + } + + private var midDot: some View { Circle() .fill(AppTheme.inkQuaternary) .frame(width: 3, height: 3) } + + private func formatDuration(_ s: Double) -> String { + let m = Int(s) / 60 + let sec = Int(s) % 60 + return m > 0 ? "\(m)m \(sec)s" : "\(sec)s" + } + + private func formatBytes(_ bytes: Int64) -> String { + let gb = Double(bytes) / 1_073_741_824 + if gb >= 1 { return String(format: "%.1f GB", gb) } + let mb = Double(bytes) / 1_048_576 + if mb >= 1 { return String(format: "%.0f MB", mb) } + return String(format: "%.0f KB", Double(bytes) / 1024) + } } diff --git a/Features/Login/LoginView.swift b/Features/Login/LoginView.swift index 6bca4a7..60e7d39 100644 --- a/Features/Login/LoginView.swift +++ b/Features/Login/LoginView.swift @@ -1,108 +1,94 @@ import SwiftUI struct LoginView: View { + @EnvironmentObject var store: ConnectionStore @StateObject private var vm = LoginViewModel() - @State private var navigateToDashboard = false @State private var logoAppeared = false var body: some View { - NavigationStack { - ZStack { - Color.white.ignoresSafeArea() + ZStack { + AppTheme.background.ignoresSafeArea() - VStack(spacing: 0) { - Spacer() + VStack(spacing: 0) { + Spacer() - // Logo - Image("nas_logo_clean") - .resizable() - .scaledToFit() - .frame(width: 72, height: 72) - .clipShape(RoundedRectangle(cornerRadius: 18, style: .continuous)) - .shadow(color: .black.opacity(0.08), radius: 16, x: 0, y: 4) - .scaleEffect(logoAppeared ? 1 : 0.8) - .opacity(logoAppeared ? 1 : 0) - .animation(.spring(response: 0.5, dampingFraction: 0.7).delay(0.1), value: logoAppeared) - - Spacer().frame(height: 32) - - // Device + account - VStack(spacing: 6) { - Text("Forge") - .font(.system(size: 22, weight: .semibold)) - .foregroundStyle(AppTheme.ink) - - HStack(spacing: 8) { - ZStack { - Circle() - .fill(AppTheme.surfaceSunken) - .frame(width: 28, height: 28) - Text("AB") - .font(.system(size: 10, weight: .semibold)) - .foregroundStyle(AppTheme.inkSecondary) - } - Text("Albert") - .font(AppTheme.caption()) - .foregroundStyle(AppTheme.inkSecondary) - } - } + // Logo + Image("nas_logo_clean") + .resizable() + .scaledToFit() + .frame(width: 72, height: 72) + .clipShape(RoundedRectangle(cornerRadius: 18, style: .continuous)) + .shadow(color: .black.opacity(0.08), radius: 16, x: 0, y: 4) + .scaleEffect(logoAppeared ? 1 : 0.8) .opacity(logoAppeared ? 1 : 0) - .animation(.spring(response: 0.5, dampingFraction: 0.7).delay(0.2), value: logoAppeared) + .animation(.spring(response: 0.5, dampingFraction: 0.7).delay(0.1), value: logoAppeared) - Spacer().frame(height: 40) + Spacer().frame(height: 32) - // Face ID button - Button(action: { vm.authenticateWithFaceID() }) { - HStack(spacing: 8) { - if vm.isAuthenticating { - ProgressView() - .tint(.white) - .scaleEffect(0.8) - } else { - Image(systemName: "faceid") - .font(.system(size: 16, weight: .medium)) - Text("Sign in with Face ID") - } - } - } - .buttonStyle(PrimaryButtonStyle()) - .padding(.horizontal, AppTheme.hPad) - .disabled(vm.isAuthenticating) - .opacity(logoAppeared ? 1 : 0) - .animation(.spring(response: 0.5, dampingFraction: 0.7).delay(0.3), value: logoAppeared) - - if let error = vm.authError { - Text(error) - .font(AppTheme.caption()) - .foregroundStyle(AppTheme.destructive) - .padding(.top, 12) - .transition(.opacity.combined(with: .move(edge: .top))) - } - - Spacer() - - // More - Button("More options") { vm.showMoreSheet = true } - .font(.system(size: 13, weight: .regular)) + // Title + account + VStack(spacing: 5) { + Text("Kisani") + .font(.system(size: 22, weight: .semibold)) + .foregroundStyle(AppTheme.ink) + Text("Sign in to continue") + .font(AppTheme.caption()) .foregroundStyle(AppTheme.inkTertiary) - .padding(.bottom, 44) - .opacity(logoAppeared ? 1 : 0) - .animation(.spring(response: 0.5, dampingFraction: 0.7).delay(0.4), value: logoAppeared) } - } - .navigationDestination(isPresented: $navigateToDashboard) { MainTabView() } - .sheet(isPresented: $vm.showMoreSheet) { - MoreOptionsSheet(isPresented: $vm.showMoreSheet) { - vm.showMoreSheet = false - navigateToDashboard = true + .opacity(logoAppeared ? 1 : 0) + .animation(.spring(response: 0.5, dampingFraction: 0.7).delay(0.2), value: logoAppeared) + + Spacer().frame(height: 40) + + // Face ID button + Button(action: { vm.authenticateWithFaceID() }) { + HStack(spacing: 8) { + if vm.isAuthenticating { + ProgressView() + .tint(AppTheme.inkInverse) + .scaleEffect(0.8) + } else { + Image(systemName: "faceid") + .font(.system(size: 16, weight: .medium)) + Text("Sign in with Face ID") + } + } } - .presentationDetents([.height(200)]) - .modifier(SheetCornerRadius(20)) + .buttonStyle(PrimaryButtonStyle()) + .padding(.horizontal, AppTheme.hPad) + .disabled(vm.isAuthenticating) + .opacity(logoAppeared ? 1 : 0) + .animation(.spring(response: 0.5, dampingFraction: 0.7).delay(0.3), value: logoAppeared) + + if let error = vm.authError { + Text(error) + .font(AppTheme.caption()) + .foregroundStyle(AppTheme.destructive) + .padding(.top, 12) + .transition(.opacity.combined(with: .move(edge: .top))) + } + + Spacer() + + // More + Button("More options") { vm.showMoreSheet = true } + .font(.system(size: 13, weight: .regular)) + .foregroundStyle(AppTheme.inkTertiary) + .padding(.bottom, 44) + .opacity(logoAppeared ? 1 : 0) + .animation(.spring(response: 0.5, dampingFraction: 0.7).delay(0.4), value: logoAppeared) } - .onAppear { - vm.onAuthenticated = { navigateToDashboard = true } - logoAppeared = true + } + .sheet(isPresented: $vm.showMoreSheet) { + MoreOptionsSheet(isPresented: $vm.showMoreSheet) { + vm.showMoreSheet = false + store.isSessionActive = true } + .presentationDetents([.height(200)]) + .modifier(SheetCornerRadius(20)) + } + .onAppear { + vm.onAuthenticated = { store.isSessionActive = true } + logoAppeared = true } } } @@ -138,7 +124,7 @@ struct MoreOptionsSheet: View { .padding(.horizontal, AppTheme.hPad) .padding(.bottom, 16) } - .background(Color.white) + .background(AppTheme.background) } private func sheetRow( diff --git a/Features/Login/LoginViewModel.swift b/Features/Login/LoginViewModel.swift index c224d19..ff645f6 100644 --- a/Features/Login/LoginViewModel.swift +++ b/Features/Login/LoginViewModel.swift @@ -1,5 +1,6 @@ import Foundation import LocalAuthentication +import os.log @MainActor final class LoginViewModel: ObservableObject { @@ -9,26 +10,34 @@ final class LoginViewModel: ObservableObject { var onAuthenticated: (() -> Void)? + private let logger = Logger(subsystem: "com.albert.nasbackup", category: "Login") + func authenticateWithFaceID() { + logger.info("Face ID authentication started") isAuthenticating = true authError = nil let context = LAContext() var error: NSError? guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else { + let reason = error?.localizedDescription ?? "unknown" + logger.error("Face ID not available: \(reason, privacy: .public)") authError = "Face ID not available" isAuthenticating = false return } context.evaluatePolicy( .deviceOwnerAuthenticationWithBiometrics, - localizedReason: "Sign in to NASBackup" + localizedReason: "Sign in to Kisani" ) { [weak self] success, evalError in Task { @MainActor in self?.isAuthenticating = false if success { + self?.logger.info("Face ID authentication succeeded — advancing session") self?.onAuthenticated?() } else { - self?.authError = evalError?.localizedDescription ?? "Authentication failed" + let reason = evalError?.localizedDescription ?? "Authentication failed" + self?.logger.error("Face ID authentication failed: \(reason, privacy: .public)") + self?.authError = reason } } } diff --git a/Features/Onboarding/FolderSetupView.swift b/Features/Onboarding/FolderSetupView.swift new file mode 100644 index 0000000..4ff1489 --- /dev/null +++ b/Features/Onboarding/FolderSetupView.swift @@ -0,0 +1,151 @@ +import SwiftUI + +struct FolderSetupView: View { + @EnvironmentObject var store: ConnectionStore + @State private var showBrowser = false + @State private var selectedPath: String = "/" + @State private var appeared = false + + private var connection: NASConnection? { store.savedConnection } + + var body: some View { + ZStack { + AppTheme.background.ignoresSafeArea() + + VStack(spacing: 0) { + // Step indicator + stepIndicator + .padding(.top, 20) + .padding(.bottom, 36) + .opacity(appeared ? 1 : 0) + .animation(.spring(response: 0.4, dampingFraction: 0.8).delay(0.05), value: appeared) + + // Icon + ZStack { + Circle() + .fill(AppTheme.surfaceSunken) + .frame(width: 80, height: 80) + Image(systemName: "folder.badge.plus") + .font(.system(size: 32, weight: .light)) + .foregroundStyle(AppTheme.ink) + } + .scaleEffect(appeared ? 1 : 0.85) + .opacity(appeared ? 1 : 0) + .animation(.spring(response: 0.5, dampingFraction: 0.7).delay(0.1), value: appeared) + + Spacer().frame(height: 28) + + // Title + description + VStack(spacing: 8) { + Text("Choose a destination") + .font(.system(size: 22, weight: .semibold)) + .foregroundStyle(AppTheme.ink) + Text("Select the folder on your NAS where photos will be backed up.") + .font(AppTheme.body()) + .foregroundStyle(AppTheme.inkSecondary) + .multilineTextAlignment(.center) + .padding(.horizontal, 32) + } + .opacity(appeared ? 1 : 0) + .animation(.spring(response: 0.4, dampingFraction: 0.8).delay(0.15), value: appeared) + + Spacer().frame(height: 32) + + // Connection chip + if let conn = connection { + connectionChip(conn) + .opacity(appeared ? 1 : 0) + .animation(.spring(response: 0.4, dampingFraction: 0.8).delay(0.2), value: appeared) + } + + Spacer().frame(height: 16) + + // Selected path display / browse button + Button(action: { showBrowser = true }) { + HStack(spacing: 12) { + Image(systemName: selectedPath == "/" ? "folder" : "folder.fill") + .font(.system(size: 14, weight: .regular)) + .foregroundStyle(selectedPath == "/" ? AppTheme.inkQuaternary : AppTheme.interactive) + .frame(width: 20) + Text(selectedPath == "/" ? "Tap to browse folders" : selectedPath) + .font(AppTheme.body()) + .foregroundStyle(selectedPath == "/" ? AppTheme.inkTertiary : AppTheme.ink) + .lineLimit(2) + .multilineTextAlignment(.leading) + .frame(maxWidth: .infinity, alignment: .leading) + Image(systemName: "chevron.right") + .font(.system(size: 12, weight: .medium)) + .foregroundStyle(AppTheme.inkQuaternary) + } + .padding(.horizontal, 16) + .padding(.vertical, 14) + .background(AppTheme.surfaceRaised) + .clipShape(RoundedRectangle(cornerRadius: AppTheme.radius, style: .continuous)) + .shadow(color: AppTheme.cardShadow, radius: 12, x: 0, y: 2) + } + .buttonStyle(ScaleButtonStyle()) + .padding(.horizontal, AppTheme.hPad) + .opacity(appeared ? 1 : 0) + .animation(.spring(response: 0.4, dampingFraction: 0.8).delay(0.25), value: appeared) + + Spacer() + + // Confirm button + Button(action: confirmFolder) { + Text("Save This Folder") + } + .buttonStyle(PrimaryButtonStyle()) + .padding(.horizontal, AppTheme.hPad) + .disabled(selectedPath == "/") + .opacity(selectedPath == "/" ? 0.4 : 1) + .animation(.spring(response: 0.3, dampingFraction: 0.8), value: selectedPath == "/") + .opacity(appeared ? 1 : 0) + .animation(.spring(response: 0.4, dampingFraction: 0.8).delay(0.3), value: appeared) + + Spacer().frame(height: 48) + } + } + .sheet(isPresented: $showBrowser) { + if let conn = connection { + BrowseView(connection: conn, selectedPath: $selectedPath) + } + } + .onAppear { appeared = true } + } + + private var stepIndicator: some View { + HStack(spacing: 6) { + ForEach(1...3, id: \.self) { step in + Capsule() + .fill(step == 2 ? AppTheme.ink : AppTheme.inkQuaternary) + .frame(width: step == 2 ? 20 : 6, height: 6) + } + } + } + + private func connectionChip(_ conn: NASConnection) -> some View { + HStack(spacing: 6) { + Circle() + .fill(AppTheme.positive) + .frame(width: 6, height: 6) + Text(conn.host) + .font(AppTheme.micro()) + .foregroundStyle(AppTheme.inkSecondary) + Text("·") + .foregroundStyle(AppTheme.inkQuaternary) + Text(conn.nasProtocol.rawValue) + .font(AppTheme.micro()) + .foregroundStyle(AppTheme.inkTertiary) + } + .padding(.horizontal, 12) + .padding(.vertical, 6) + .background(AppTheme.surfaceSunken) + .clipShape(Capsule(style: .continuous)) + } + + private func confirmFolder() { + guard selectedPath != "/", var conn = store.savedConnection else { return } + conn.remotePath = selectedPath + store.savedConnection = conn + } +} diff --git a/Features/Onboarding/PhotoPermissionView.swift b/Features/Onboarding/PhotoPermissionView.swift new file mode 100644 index 0000000..2d2aabb --- /dev/null +++ b/Features/Onboarding/PhotoPermissionView.swift @@ -0,0 +1,129 @@ +import SwiftUI +import Photos + +struct PhotoPermissionView: View { + @EnvironmentObject var store: ConnectionStore + @State private var appeared = false + @State private var isRequesting = false + + var body: some View { + ZStack { + AppTheme.background.ignoresSafeArea() + + VStack(spacing: 0) { + // Step indicator + stepIndicator + .padding(.top, 20) + .padding(.bottom, 36) + .opacity(appeared ? 1 : 0) + .animation(.spring(response: 0.4, dampingFraction: 0.8).delay(0.05), value: appeared) + + Spacer() + + // Icon + ZStack { + Circle() + .fill(AppTheme.surfaceSunken) + .frame(width: 96, height: 96) + Image(systemName: "photo.stack") + .font(.system(size: 38, weight: .light)) + .foregroundStyle(AppTheme.ink) + } + .scaleEffect(appeared ? 1 : 0.85) + .opacity(appeared ? 1 : 0) + .animation(.spring(response: 0.5, dampingFraction: 0.7).delay(0.1), value: appeared) + + Spacer().frame(height: 32) + + // Title + description + VStack(spacing: 10) { + Text("Access your photos") + .font(.system(size: 24, weight: .semibold)) + .foregroundStyle(AppTheme.ink) + + Text("Kisani needs read access to your photo library to find and back up your photos and videos to the NAS.") + .font(AppTheme.body()) + .foregroundStyle(AppTheme.inkSecondary) + .multilineTextAlignment(.center) + .padding(.horizontal, 32) + + // Detail chips + HStack(spacing: 8) { + detailChip(icon: "eye.slash", label: "Read-only") + detailChip(icon: "lock.shield", label: "Stays on-device") + detailChip(icon: "icloud.slash", label: "No cloud upload") + } + .padding(.top, 8) + } + .opacity(appeared ? 1 : 0) + .animation(.spring(response: 0.4, dampingFraction: 0.8).delay(0.18), value: appeared) + + Spacer() + + // Buttons + VStack(spacing: 12) { + Button(action: requestAccess) { + Group { + if isRequesting { + ProgressView().tint(AppTheme.inkInverse) + } else { + Text("Allow Photo Access") + } + } + } + .buttonStyle(PrimaryButtonStyle()) + .disabled(isRequesting) + + Button(action: skipAndContinue) { + Text("Not Now") + } + .buttonStyle(GhostButtonStyle()) + } + .padding(.horizontal, AppTheme.hPad) + .opacity(appeared ? 1 : 0) + .animation(.spring(response: 0.4, dampingFraction: 0.8).delay(0.28), value: appeared) + + Spacer().frame(height: 48) + } + } + .onAppear { appeared = true } + } + + private var stepIndicator: some View { + HStack(spacing: 6) { + ForEach(1...3, id: \.self) { step in + Capsule() + .fill(step == 3 ? AppTheme.ink : AppTheme.inkQuaternary) + .frame(width: step == 3 ? 20 : 6, height: 6) + } + } + } + + private func detailChip(icon: String, label: String) -> some View { + HStack(spacing: 4) { + Image(systemName: icon) + .font(.system(size: 10, weight: .medium)) + Text(label) + .font(.system(size: 11, weight: .medium)) + } + .foregroundStyle(AppTheme.inkSecondary) + .padding(.horizontal, 10) + .padding(.vertical, 5) + .background(AppTheme.surfaceSunken) + .clipShape(Capsule(style: .continuous)) + } + + private func requestAccess() { + isRequesting = true + PHPhotoLibrary.requestAuthorization(for: .readWrite) { _ in + DispatchQueue.main.async { + isRequesting = false + store.onboardingPhotoShown = true + } + } + } + + private func skipAndContinue() { + store.onboardingPhotoShown = true + } +} diff --git a/Features/Settings/LANTriggerSection.swift b/Features/Settings/LANTriggerSection.swift index 6794eda..917927c 100644 --- a/Features/Settings/LANTriggerSection.swift +++ b/Features/Settings/LANTriggerSection.swift @@ -131,7 +131,7 @@ struct LANTriggerSection: View { .padding(.horizontal, AppTheme.cardPad) .padding(.vertical, 12) } - .background(Color.white) + .background(AppTheme.surfaceRaised) .clipShape(RoundedRectangle(cornerRadius: AppTheme.radius, style: .continuous)) .shadow(color: AppTheme.cardShadow, radius: 12, x: 0, y: 2) } diff --git a/Features/Settings/SettingsView.swift b/Features/Settings/SettingsView.swift index 14d69fa..ba67341 100644 --- a/Features/Settings/SettingsView.swift +++ b/Features/Settings/SettingsView.swift @@ -6,10 +6,11 @@ struct SettingsView: View { var body: some View { ZStack { - Color.white.ignoresSafeArea() + AppTheme.background.ignoresSafeArea() ScrollView { VStack(spacing: AppTheme.sectionGap) { + appearanceSection LANTriggerSection() mediaSection quietHoursSection @@ -26,6 +27,45 @@ struct SettingsView: View { .navigationBarTitleDisplayMode(.inline) } + // MARK: — Appearance + + private var appearanceSection: some View { + VStack(alignment: .leading, spacing: 8) { + sectionLabel("APPEARANCE") + + HStack(spacing: 0) { + ForEach(AppearanceMode.allCases, id: \.self) { mode in + Button { + withAnimation(.spring(response: 0.25, dampingFraction: 0.8)) { + store.appearanceMode = mode + } + } label: { + let selected = store.appearanceMode == mode + HStack(spacing: 5) { + Image(systemName: mode.icon) + .font(.system(size: 12, weight: selected ? .semibold : .regular)) + Text(mode.label) + .font(.system(size: 12, weight: selected ? .semibold : .regular)) + } + .foregroundStyle(selected ? AppTheme.ink : AppTheme.inkTertiary) + .frame(maxWidth: .infinity) + .padding(.vertical, 9) + .background( + RoundedRectangle(cornerRadius: 8, style: .continuous) + .fill(selected ? AppTheme.surfaceRaised : Color.clear) + .shadow(color: selected ? .black.opacity(0.08) : .clear, + radius: 4, x: 0, y: 1) + ) + } + .buttonStyle(PlainButtonStyle()) + } + } + .padding(3) + .background(AppTheme.surfaceSunken) + .clipShape(RoundedRectangle(cornerRadius: 11, style: .continuous)) + } + } + // MARK: — Media private var mediaSection: some View { @@ -49,7 +89,7 @@ struct SettingsView: View { subtitle: "Skip files already on NAS", isOn: $store.backupFilter.newFilesOnly) } - .background(Color.white) + .background(AppTheme.surfaceRaised) .clipShape(RoundedRectangle(cornerRadius: AppTheme.radius, style: .continuous)) .shadow(color: AppTheme.cardShadow, radius: 12, x: 0, y: 2) } @@ -78,7 +118,7 @@ struct SettingsView: View { subtitle: "Preserve battery on large libraries", isOn: $store.chargingOnlyMode) } - .background(Color.white) + .background(AppTheme.surfaceRaised) .clipShape(RoundedRectangle(cornerRadius: AppTheme.radius, style: .continuous)) .shadow(color: AppTheme.cardShadow, radius: 12, x: 0, y: 2) .animation(.spring(response: 0.3, dampingFraction: 0.8), value: store.quietHoursEnabled) @@ -135,7 +175,7 @@ struct SettingsView: View { .padding(.vertical, 10) } } - .background(Color.white) + .background(AppTheme.surfaceRaised) .clipShape(RoundedRectangle(cornerRadius: AppTheme.radius, style: .continuous)) .shadow(color: AppTheme.cardShadow, radius: 12, x: 0, y: 2) .animation(.spring(response: 0.3, dampingFraction: 0.8), value: lanMonitor.isOnCellular) @@ -206,13 +246,15 @@ struct SettingsView: View { .clipShape(RoundedRectangle(cornerRadius: 6, style: .continuous)) } .buttonStyle(ScaleButtonStyle()) + .accessibilityLabel("Open Tailscale app") + .accessibilityHint("Opens the Tailscale app to connect the VPN") } } .padding(.horizontal, AppTheme.cardPad) .padding(.vertical, 12) } } - .background(Color.white) + .background(AppTheme.surfaceRaised) .clipShape(RoundedRectangle(cornerRadius: AppTheme.radius, style: .continuous)) .shadow(color: AppTheme.cardShadow, radius: 12, x: 0, y: 2) .animation(.spring(response: 0.3, dampingFraction: 0.8), value: store.useTailscaleWhenRemote) @@ -233,7 +275,7 @@ struct SettingsView: View { hairline notifRow(icon: "exclamationmark.triangle", title: "On errors", value: "On") } - .background(Color.white) + .background(AppTheme.surfaceRaised) .clipShape(RoundedRectangle(cornerRadius: AppTheme.radius, style: .continuous)) .shadow(color: AppTheme.cardShadow, radius: 12, x: 0, y: 2) } diff --git a/NASBackup.xcodeproj/project.pbxproj b/NASBackup.xcodeproj/project.pbxproj index c817431..5314304 100644 --- a/NASBackup.xcodeproj/project.pbxproj +++ b/NASBackup.xcodeproj/project.pbxproj @@ -7,26 +7,37 @@ objects = { /* Begin PBXBuildFile section */ + 10062AA91BC883AE597443BD /* URL+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 277DF2E5F4E566C869EC29F3 /* URL+.swift */; }; + 13FCA7D0BB01BFC46A5B725B /* SavedConnections.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1D1C00B125D5CC000988DD0 /* SavedConnections.swift */; }; + 30693CB8EE711577978EF88C /* Date+.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6CD83D8292EF9EFBE3A9C6C /* Date+.swift */; }; 36FE83FFC24922EBC28287BB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E66C22B614E89A7A9E9B35 /* AppDelegate.swift */; }; 37D808079BDEF32D04847D04 /* MainTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F09160B7B7A06BD900D0D87A /* MainTabView.swift */; }; + 4148416D31C8AC65FEE6394F /* PHAsset+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86FA0D1522E2C4FF7C1CABF3 /* PHAsset+.swift */; }; 45FFF2DCFE454EFDF642CEB1 /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8F24C7DD7A503A0D561F3FB /* LoginView.swift */; }; 512B235FB16DEFBBD9EA4E1C /* NASConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BE4774FFA1074FF5F1FAB2B /* NASConnection.swift */; }; 57656BC4B5A2E81FE7BF37FD /* PhotoLibraryService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46B47E3C76EA718ECF97BBAD /* PhotoLibraryService.swift */; }; 6500E80B50EEF877C5127A2A /* HistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EEBCD4A838173E557DC3EFC /* HistoryView.swift */; }; + 664E9F44D037A8B41D5A7670 /* MockNASService.swift in Sources */ = {isa = PBXBuildFile; fileRef = BACE1D31086231C97DAD884D /* MockNASService.swift */; }; + 685E8F1B44AE5652E93C58CE /* NASProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7688CD7E9BC5475A3EB01021 /* NASProtocolTests.swift */; }; 6DED667F0AA66503730640F1 /* FolderRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83C13DD3EB4823AA98D51AF2 /* FolderRow.swift */; }; + 77A3FF7E2E4F506E7F9E5CCD /* FolderSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C31F3F5EBCB51002188FA15 /* FolderSetupView.swift */; }; 7872072DD0E0B8D51C1A12DF /* NASBackupApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = B36E23852EF12DC14A2DF2DC /* NASBackupApp.swift */; }; 7DF923F172E6DFE3DD110DC9 /* LANTriggerSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CBE4F65BE653C7C72E096D1 /* LANTriggerSection.swift */; }; + 7FB20CB93FECB2F9CBE67161 /* ConnectViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50191596BF6E84CFAE7904D3 /* ConnectViewModelTests.swift */; }; 834A766E13EF297273FFEE21 /* LoginViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1A42E8CFCAECFDBF6EDDAC0 /* LoginViewModel.swift */; }; 84E3EE64F74D00104BF8694B /* BackgroundTaskManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5643379BBA1FD39841F9EA55 /* BackgroundTaskManager.swift */; }; 8657A3E4C5E377FE34840E9A /* BackupEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 824FADB40473BFC36FE91F0A /* BackupEngine.swift */; }; 88879238876B1E5A1BE15DF3 /* SMBClient in Frameworks */ = {isa = PBXBuildFile; productRef = 9CEDCE7A5B4B267C66F39ABE /* SMBClient */; }; 8C1C09CA16A5D71408468BCB /* NASTransferProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 055D92AB46FCDDF9C34B9D62 /* NASTransferProtocol.swift */; }; + 8EB77E535C82274276C1E328 /* KeychainStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EC8C588CD91ABA1EFED2F2B /* KeychainStore.swift */; }; 9444B0EB5AD2936DA837320F /* BackupError.swift in Sources */ = {isa = PBXBuildFile; fileRef = F03F0ED1A914F3392BFE5E4A /* BackupError.swift */; }; + 96CC879835A8CFE5FDD21B85 /* UGreenCompatibilityBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = B55F95367E10145F50BF06E3 /* UGreenCompatibilityBadge.swift */; }; 9758372DD65F5353C72DE5C5 /* SMBService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEBD92D10D70B957192E271F /* SMBService.swift */; }; 9A2F717F6A8F4AA85FA636FC /* Citadel in Frameworks */ = {isa = PBXBuildFile; productRef = 7BEB5BEBEB935BC8E2182A7B /* Citadel */; }; 9CF57AC05F30A51B6B5269DC /* AppTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B33F755DA5850D1FE3F695B /* AppTheme.swift */; }; A6276840D40211EB446293A9 /* BackupViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC73AD75762D4357D171CDA4 /* BackupViewModel.swift */; }; A6D143C05EE16594E0D1AA01 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0AABE0AA63B1ACA1DEABB65 /* SceneDelegate.swift */; }; + A79F538803790524284BCAD6 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 0C0E3EBE5A0DBD9D64196501 /* PrivacyInfo.xcprivacy */; }; B08D787E99C3B2E193A0D694 /* ToggleRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8845003C87961174302AC990 /* ToggleRow.swift */; }; B50445A9010E20F97E9E6E48 /* ProgressRing.swift in Sources */ = {isa = PBXBuildFile; fileRef = C955FEF6710E3667C7A73A02 /* ProgressRing.swift */; }; B7DBF1420838AD3397A280B5 /* BackupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55EA454B783C2AEDABFB00A7 /* BackupView.swift */; }; @@ -34,8 +45,11 @@ C6AAC362F058E7836A91AC30 /* ConnectViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91197DD51221A3277AE318E2 /* ConnectViewModel.swift */; }; C8E9BC6070E23992EED8D013 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97D17A73A05A6FDF2B89C6B2 /* SettingsView.swift */; }; CE47E9F2179F86BE0A630D91 /* LANMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5606A4C5894CFC72D555CAE1 /* LANMonitor.swift */; }; + CE84BC18F045AB65CCA30BB4 /* LANMonitorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F19B5E72ADF230C0C134942B /* LANMonitorTests.swift */; }; DCE9AF4F0160E323224E3553 /* PhotoLibraryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74E2AF8B56C469E0D94EFE94 /* PhotoLibraryProtocol.swift */; }; DF719EAD5727B335001DB50A /* ConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE318B60576B11E80862664D /* ConnectView.swift */; }; + DFD2252BDCE9EE7ABAE63BF9 /* BackupEngineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 336D155A4D1C895215A4FFF2 /* BackupEngineTests.swift */; }; + E24CEE0D51131D991038205D /* PhotoPermissionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AC72ED4015671C51066AC3E /* PhotoPermissionView.swift */; }; E830DB3078A8873382A77B63 /* BackupResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AEFE4959C08B94AE9584A11 /* BackupResult.swift */; }; ED0D2A1D05B5898BCCE9EA0A /* ButtonStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA0969FC7D7E0EE313E29D55 /* ButtonStyles.swift */; }; F9219423E73718D8AB604EB1 /* ConnectionStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = D87319355D902007618A91AE /* ConnectionStore.swift */; }; @@ -43,22 +57,41 @@ FDC18499CC21A58C2F6F1BDB /* BackupJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47E762898E351192399FC739 /* BackupJob.swift */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + 2F6BE82D286218B2DC8D8B98 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 82D1639F1555985B1BEE4547 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 32316B985B8906FE4CB97730; + remoteInfo = NASBackup; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXFileReference section */ 055D92AB46FCDDF9C34B9D62 /* NASTransferProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NASTransferProtocol.swift; sourceTree = ""; }; + 0C0E3EBE5A0DBD9D64196501 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; + 277DF2E5F4E566C869EC29F3 /* URL+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+.swift"; sourceTree = ""; }; 2B33F755DA5850D1FE3F695B /* AppTheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppTheme.swift; sourceTree = ""; }; 2BE4774FFA1074FF5F1FAB2B /* NASConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NASConnection.swift; sourceTree = ""; }; + 336D155A4D1C895215A4FFF2 /* BackupEngineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupEngineTests.swift; sourceTree = ""; }; + 3C31F3F5EBCB51002188FA15 /* FolderSetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FolderSetupView.swift; sourceTree = ""; }; 46B47E3C76EA718ECF97BBAD /* PhotoLibraryService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoLibraryService.swift; sourceTree = ""; }; 47E762898E351192399FC739 /* BackupJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupJob.swift; sourceTree = ""; }; 4AEFE4959C08B94AE9584A11 /* BackupResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupResult.swift; sourceTree = ""; }; 4B67584BB6D705BCB3D40192 /* SFTPService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SFTPService.swift; sourceTree = ""; }; 4CBE4F65BE653C7C72E096D1 /* LANTriggerSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LANTriggerSection.swift; sourceTree = ""; }; + 4EC8C588CD91ABA1EFED2F2B /* KeychainStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainStore.swift; sourceTree = ""; }; + 50191596BF6E84CFAE7904D3 /* ConnectViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectViewModelTests.swift; sourceTree = ""; }; 55EA454B783C2AEDABFB00A7 /* BackupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupView.swift; sourceTree = ""; }; 5606A4C5894CFC72D555CAE1 /* LANMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LANMonitor.swift; sourceTree = ""; }; 5643379BBA1FD39841F9EA55 /* BackgroundTaskManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundTaskManager.swift; sourceTree = ""; }; 6EEBCD4A838173E557DC3EFC /* HistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryView.swift; sourceTree = ""; }; 74E2AF8B56C469E0D94EFE94 /* PhotoLibraryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoLibraryProtocol.swift; sourceTree = ""; }; + 7688CD7E9BC5475A3EB01021 /* NASProtocolTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NASProtocolTests.swift; sourceTree = ""; }; + 7AC72ED4015671C51066AC3E /* PhotoPermissionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoPermissionView.swift; sourceTree = ""; }; 824FADB40473BFC36FE91F0A /* BackupEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupEngine.swift; sourceTree = ""; }; 83C13DD3EB4823AA98D51AF2 /* FolderRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FolderRow.swift; sourceTree = ""; }; + 86FA0D1522E2C4FF7C1CABF3 /* PHAsset+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PHAsset+.swift"; sourceTree = ""; }; 8845003C87961174302AC990 /* ToggleRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToggleRow.swift; sourceTree = ""; }; 88E66C22B614E89A7A9E9B35 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 91197DD51221A3277AE318E2 /* ConnectViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectViewModel.swift; sourceTree = ""; }; @@ -66,18 +99,24 @@ A8F24C7DD7A503A0D561F3FB /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = ""; }; AC73AD75762D4357D171CDA4 /* BackupViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupViewModel.swift; sourceTree = ""; }; B36E23852EF12DC14A2DF2DC /* NASBackupApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NASBackupApp.swift; sourceTree = ""; }; + B55F95367E10145F50BF06E3 /* UGreenCompatibilityBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UGreenCompatibilityBadge.swift; sourceTree = ""; }; + BACE1D31086231C97DAD884D /* MockNASService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockNASService.swift; sourceTree = ""; }; C755BFDD42D46A3FB490A4BB /* NASBackup.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = NASBackup.app; sourceTree = BUILT_PRODUCTS_DIR; }; C955FEF6710E3667C7A73A02 /* ProgressRing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressRing.swift; sourceTree = ""; }; CA0969FC7D7E0EE313E29D55 /* ButtonStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonStyles.swift; sourceTree = ""; }; + D1D1C00B125D5CC000988DD0 /* SavedConnections.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedConnections.swift; sourceTree = ""; }; D87319355D902007618A91AE /* ConnectionStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionStore.swift; sourceTree = ""; }; D947C8F8945A2AB081BBD0EF /* BrowseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowseView.swift; sourceTree = ""; }; DEBD92D10D70B957192E271F /* SMBService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SMBService.swift; sourceTree = ""; }; + E3A1E9181F350DA9F091D019 /* NASBackupTests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = NASBackupTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; EE318B60576B11E80862664D /* ConnectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectView.swift; sourceTree = ""; }; F03F0ED1A914F3392BFE5E4A /* BackupError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupError.swift; sourceTree = ""; }; F09160B7B7A06BD900D0D87A /* MainTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabView.swift; sourceTree = ""; }; F0AABE0AA63B1ACA1DEABB65 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + F19B5E72ADF230C0C134942B /* LANMonitorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LANMonitorTests.swift; sourceTree = ""; }; F1A42E8CFCAECFDBF6EDDAC0 /* LoginViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewModel.swift; sourceTree = ""; }; F5CE897758E9CD5BB8B80F63 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + F6CD83D8292EF9EFBE3A9C6C /* Date+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+.swift"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -102,6 +141,7 @@ 1AD5A741A7466C3510901C60 /* Connect */, 88D0BD5235374F2A264A0C96 /* History */, FDF5327105186A6456CF0B08 /* Login */, + 611BB6A8B01D82432C0A32EC /* Onboarding */, 147B04CA2B7A7723CCD13E93 /* Settings */, ); path = Features; @@ -115,6 +155,7 @@ 0A801C860EB4C4B22FD4D073 /* Features */, ADFBF8F90DF8352BD2833A66 /* Services */, 31C8DD8F9077E2B6C67A99BA /* Shared */, + D7CC50C0264BFFC23DA2C868 /* Tests */, 8308FE572D56B8045F17F6A0 /* Products */, ); sourceTree = ""; @@ -123,6 +164,8 @@ isa = PBXGroup; children = ( D87319355D902007618A91AE /* ConnectionStore.swift */, + 4EC8C588CD91ABA1EFED2F2B /* KeychainStore.swift */, + D1D1C00B125D5CC000988DD0 /* SavedConnections.swift */, ); path = Persistence; sourceTree = ""; @@ -136,6 +179,16 @@ path = Settings; sourceTree = ""; }; + 185383972A68402ADF89D64C /* Extensions */ = { + isa = PBXGroup; + children = ( + F6CD83D8292EF9EFBE3A9C6C /* Date+.swift */, + 86FA0D1522E2C4FF7C1CABF3 /* PHAsset+.swift */, + 277DF2E5F4E566C869EC29F3 /* URL+.swift */, + ); + path = Extensions; + sourceTree = ""; + }; 1AD5A741A7466C3510901C60 /* Connect */ = { isa = PBXGroup; children = ( @@ -149,6 +202,7 @@ isa = PBXGroup; children = ( 3E7F8DE77B7CDC7009B54D63 /* Components */, + 185383972A68402ADF89D64C /* Extensions */, 0B27021136C1E85D5B8CFF97 /* Persistence */, C874CB05A914EE933FCFD5C2 /* Theme */, ); @@ -162,10 +216,20 @@ 83C13DD3EB4823AA98D51AF2 /* FolderRow.swift */, C955FEF6710E3667C7A73A02 /* ProgressRing.swift */, 8845003C87961174302AC990 /* ToggleRow.swift */, + B55F95367E10145F50BF06E3 /* UGreenCompatibilityBadge.swift */, ); path = Components; sourceTree = ""; }; + 611BB6A8B01D82432C0A32EC /* Onboarding */ = { + isa = PBXGroup; + children = ( + 3C31F3F5EBCB51002188FA15 /* FolderSetupView.swift */, + 7AC72ED4015671C51066AC3E /* PhotoPermissionView.swift */, + ); + path = Onboarding; + sourceTree = ""; + }; 803154EA96E2431367806BAF /* Backup */ = { isa = PBXGroup; children = ( @@ -188,6 +252,7 @@ isa = PBXGroup; children = ( C755BFDD42D46A3FB490A4BB /* NASBackup.app */, + E3A1E9181F350DA9F091D019 /* NASBackupTests.xctest */, ); name = Products; sourceTree = ""; @@ -214,6 +279,7 @@ 88E66C22B614E89A7A9E9B35 /* AppDelegate.swift */, F5CE897758E9CD5BB8B80F63 /* Info.plist */, B36E23852EF12DC14A2DF2DC /* NASBackupApp.swift */, + 0C0E3EBE5A0DBD9D64196501 /* PrivacyInfo.xcprivacy */, F0AABE0AA63B1ACA1DEABB65 /* SceneDelegate.swift */, ); path = App; @@ -258,6 +324,18 @@ path = Theme; sourceTree = ""; }; + D7CC50C0264BFFC23DA2C868 /* Tests */ = { + isa = PBXGroup; + children = ( + 336D155A4D1C895215A4FFF2 /* BackupEngineTests.swift */, + 50191596BF6E84CFAE7904D3 /* ConnectViewModelTests.swift */, + F19B5E72ADF230C0C134942B /* LANMonitorTests.swift */, + BACE1D31086231C97DAD884D /* MockNASService.swift */, + 7688CD7E9BC5475A3EB01021 /* NASProtocolTests.swift */, + ); + path = Tests; + sourceTree = ""; + }; F75092ACBEF36F32816506F7 /* Models */ = { isa = PBXGroup; children = ( @@ -285,6 +363,7 @@ buildConfigurationList = F29F169757D793211107ABA5 /* Build configuration list for PBXNativeTarget "NASBackup" */; buildPhases = ( E6D3CCCB60977A7DABFE7F19 /* Sources */, + 6DB4B780F7FFCAAC2B48E78F /* Resources */, BC332B29F6F9E065FF85AED8 /* Frameworks */, ); buildRules = ( @@ -300,6 +379,24 @@ productReference = C755BFDD42D46A3FB490A4BB /* NASBackup.app */; productType = "com.apple.product-type.application"; }; + 88E89D71F1D8FA18A6A64BBF /* NASBackupTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 3B812FFDA55D20BD7BC79D06 /* Build configuration list for PBXNativeTarget "NASBackupTests" */; + buildPhases = ( + E2CED6E8ED76A743BB9C913F /* Sources */, + ); + buildRules = ( + ); + dependencies = ( + EA438E47054B868AA6F6727D /* PBXTargetDependency */, + ); + name = NASBackupTests; + packageProductDependencies = ( + ); + productName = NASBackupTests; + productReference = E3A1E9181F350DA9F091D019 /* NASBackupTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -313,6 +410,10 @@ DevelopmentTeam = ""; ProvisioningStyle = Automatic; }; + 88E89D71F1D8FA18A6A64BBF = { + DevelopmentTeam = ""; + ProvisioningStyle = Automatic; + }; }; }; buildConfigurationList = 1A69DB656166247B44D9B8F9 /* Build configuration list for PBXProject "NASBackup" */; @@ -334,11 +435,35 @@ projectRoot = ""; targets = ( 32316B985B8906FE4CB97730 /* NASBackup */, + 88E89D71F1D8FA18A6A64BBF /* NASBackupTests */, ); }; /* End PBXProject section */ +/* Begin PBXResourcesBuildPhase section */ + 6DB4B780F7FFCAAC2B48E78F /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A79F538803790524284BCAD6 /* PrivacyInfo.xcprivacy in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ + E2CED6E8ED76A743BB9C913F /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DFD2252BDCE9EE7ABAE63BF9 /* BackupEngineTests.swift in Sources */, + 7FB20CB93FECB2F9CBE67161 /* ConnectViewModelTests.swift in Sources */, + CE84BC18F045AB65CCA30BB4 /* LANMonitorTests.swift in Sources */, + 664E9F44D037A8B41D5A7670 /* MockNASService.swift in Sources */, + 685E8F1B44AE5652E93C58CE /* NASProtocolTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; E6D3CCCB60977A7DABFE7F19 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -357,8 +482,11 @@ DF719EAD5727B335001DB50A /* ConnectView.swift in Sources */, C6AAC362F058E7836A91AC30 /* ConnectViewModel.swift in Sources */, F9219423E73718D8AB604EB1 /* ConnectionStore.swift in Sources */, + 30693CB8EE711577978EF88C /* Date+.swift in Sources */, 6DED667F0AA66503730640F1 /* FolderRow.swift in Sources */, + 77A3FF7E2E4F506E7F9E5CCD /* FolderSetupView.swift in Sources */, 6500E80B50EEF877C5127A2A /* HistoryView.swift in Sources */, + 8EB77E535C82274276C1E328 /* KeychainStore.swift in Sources */, CE47E9F2179F86BE0A630D91 /* LANMonitor.swift in Sources */, 7DF923F172E6DFE3DD110DC9 /* LANTriggerSection.swift in Sources */, 45FFF2DCFE454EFDF642CEB1 /* LoginView.swift in Sources */, @@ -367,20 +495,50 @@ 7872072DD0E0B8D51C1A12DF /* NASBackupApp.swift in Sources */, 512B235FB16DEFBBD9EA4E1C /* NASConnection.swift in Sources */, 8C1C09CA16A5D71408468BCB /* NASTransferProtocol.swift in Sources */, + 4148416D31C8AC65FEE6394F /* PHAsset+.swift in Sources */, DCE9AF4F0160E323224E3553 /* PhotoLibraryProtocol.swift in Sources */, 57656BC4B5A2E81FE7BF37FD /* PhotoLibraryService.swift in Sources */, + E24CEE0D51131D991038205D /* PhotoPermissionView.swift in Sources */, B50445A9010E20F97E9E6E48 /* ProgressRing.swift in Sources */, BC4346F01DAA4DE7D8F260DB /* SFTPService.swift in Sources */, 9758372DD65F5353C72DE5C5 /* SMBService.swift in Sources */, + 13FCA7D0BB01BFC46A5B725B /* SavedConnections.swift in Sources */, A6D143C05EE16594E0D1AA01 /* SceneDelegate.swift in Sources */, C8E9BC6070E23992EED8D013 /* SettingsView.swift in Sources */, B08D787E99C3B2E193A0D694 /* ToggleRow.swift in Sources */, + 96CC879835A8CFE5FDD21B85 /* UGreenCompatibilityBadge.swift in Sources */, + 10062AA91BC883AE597443BD /* URL+.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + EA438E47054B868AA6F6727D /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 32316B985B8906FE4CB97730 /* NASBackup */; + targetProxy = 2F6BE82D286218B2DC8D8B98 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin XCBuildConfiguration section */ + 53DABEDF383DBBE47A3A3FD7 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + SDKROOT = iphoneos; + SWIFT_VERSION = 5.9; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/NASBackup.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/NASBackup"; + }; + name = Release; + }; 5496D0713D70962E349CB0C8 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -397,6 +555,23 @@ }; name = Debug; }; + 7215384C2378F778AC60259A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + SDKROOT = iphoneos; + SWIFT_VERSION = 5.9; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/NASBackup.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/NASBackup"; + }; + name = Debug; + }; AAEAFE599F3F20DB264D4528 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -554,6 +729,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Debug; }; + 3B812FFDA55D20BD7BC79D06 /* Build configuration list for PBXNativeTarget "NASBackupTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7215384C2378F778AC60259A /* Debug */, + 53DABEDF383DBBE47A3A3FD7 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; F29F169757D793211107ABA5 /* Build configuration list for PBXNativeTarget "NASBackup" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/Services/BackupEngine.swift b/Services/BackupEngine.swift index c630daf..0d77571 100644 --- a/Services/BackupEngine.swift +++ b/Services/BackupEngine.swift @@ -1,5 +1,8 @@ import Foundation import UserNotifications +import os.log + +private let logger = Logger(subsystem: "com.albert.nasbackup", category: "BackupEngine") @MainActor final class BackupEngine: ObservableObject { @@ -7,11 +10,30 @@ final class BackupEngine: ObservableObject { @Published private(set) var job: BackupJob = BackupJob() - private let photoService: PhotoLibraryProtocol = PhotoLibraryService() + private let photoService: PhotoLibraryProtocol + private let transferFactory: (NASProtocol) -> any NASTransferProtocol private var activeTransfer: (any NASTransferProtocol)? private var isCancelled = false - private init() {} + private init( + photos: PhotoLibraryProtocol = PhotoLibraryService(), + transferFactory: @escaping (NASProtocol) -> any NASTransferProtocol = { proto in + switch proto { + case .smb: return SMBService() + case .sftp: return SFTPService() + } + } + ) { + self.photoService = photos + self.transferFactory = transferFactory + } + + static func testInstance( + transfer: any NASTransferProtocol, + photos: PhotoLibraryProtocol + ) -> BackupEngine { + BackupEngine(photos: photos, transferFactory: { _ in transfer }) + } func run( connection: NASConnection, @@ -36,11 +58,7 @@ final class BackupEngine: ObservableObject { let assets = photoService.fetchAssets(filter: filter) // 2. Connect to NAS - let transfer: any NASTransferProtocol - switch connection.nasProtocol { - case .smb: transfer = SMBService() - case .sftp: transfer = SFTPService() - } + let transfer = transferFactory(connection.nasProtocol) try await transfer.connect( to: host, @@ -102,6 +120,7 @@ final class BackupEngine: ObservableObject { job.fileCompleted(skipped: false) uploaded += 1 } catch { + logger.error("Upload failed for \(asset.filename, privacy: .public): \(error.localizedDescription, privacy: .public)") job.fileFailed() failed += 1 } diff --git a/Services/PhotoLibraryService.swift b/Services/PhotoLibraryService.swift index 946ff1b..5b32da5 100644 --- a/Services/PhotoLibraryService.swift +++ b/Services/PhotoLibraryService.swift @@ -44,25 +44,26 @@ final class PhotoLibraryService: PhotoLibraryProtocol { let result = PHAsset.fetchAssets(with: options) var assets: [PhotoAsset] = [] result.enumerateObjects { asset, _, _ in - let isScreenshot = asset.mediaSubtypes.contains(.photoScreenshot) - let resources = PHAssetResource.assetResources(for: asset) - // RAW files are identified by an alternate photo resource (RAW+JPEG pairs) - let isRAW = resources.contains { $0.type == .alternatePhoto } - if isRAW && !filter.includeRAW { return } + autoreleasepool { + let isScreenshot = asset.mediaSubtypes.contains(.photoScreenshot) + let resources = PHAssetResource.assetResources(for: asset) + let isRAW = resources.contains { $0.type == .alternatePhoto } + if isRAW && !filter.includeRAW { return } - let filename = resources.first?.originalFilename ?? "\(asset.localIdentifier).jpg" + let filename = resources.first?.originalFilename ?? "\(asset.localIdentifier).jpg" - assets.append(PhotoAsset( - localIdentifier: asset.localIdentifier, - filename: filename, - creationDate: asset.creationDate, - mediaType: asset.mediaType, - pixelWidth: asset.pixelWidth, - pixelHeight: asset.pixelHeight, - duration: asset.duration, - isScreenshot: isScreenshot, - isRAW: isRAW - )) + assets.append(PhotoAsset( + localIdentifier: asset.localIdentifier, + filename: filename, + creationDate: asset.creationDate, + mediaType: asset.mediaType, + pixelWidth: asset.pixelWidth, + pixelHeight: asset.pixelHeight, + duration: asset.duration, + isScreenshot: isScreenshot, + isRAW: isRAW + )) + } } return assets } diff --git a/Shared/Components/ButtonStyles.swift b/Shared/Components/ButtonStyles.swift index 7b4363f..7d41290 100644 --- a/Shared/Components/ButtonStyles.swift +++ b/Shared/Components/ButtonStyles.swift @@ -1,6 +1,6 @@ import SwiftUI -// MARK: — Primary (black fill) +// MARK: — Primary (ink fill) struct PrimaryButtonStyle: ButtonStyle { var color: Color = AppTheme.ink var height: CGFloat = 50 @@ -8,7 +8,7 @@ struct PrimaryButtonStyle: ButtonStyle { func makeBody(configuration: Configuration) -> some View { configuration.label .font(.system(size: 15, weight: .medium)) - .foregroundColor(.white) + .foregroundStyle(AppTheme.inkInverse) .frame(maxWidth: .infinity) .frame(height: height) .background(color) @@ -25,12 +25,12 @@ struct GhostButtonStyle: ButtonStyle { func makeBody(configuration: Configuration) -> some View { configuration.label .font(.system(size: 15, weight: .medium)) - .foregroundColor(AppTheme.ink) + .foregroundStyle(AppTheme.ink) .frame(maxWidth: .infinity) .frame(height: height) .overlay( RoundedRectangle(cornerRadius: AppTheme.radius, style: .continuous) - .stroke(AppTheme.cardBorderColor, lineWidth: 1) + .stroke(AppTheme.ink.opacity(0.25), lineWidth: 1) ) .scaleEffect(configuration.isPressed ? 0.97 : 1) .animation(.spring(response: 0.25, dampingFraction: 0.7), value: configuration.isPressed) @@ -52,7 +52,7 @@ struct PillButtonStyle: ButtonStyle { func makeBody(configuration: Configuration) -> some View { configuration.label .font(.system(size: 13, weight: .medium)) - .foregroundColor(.white) + .foregroundStyle(AppTheme.inkInverse) .padding(.horizontal, 14) .padding(.vertical, 7) .background(color) diff --git a/Shared/Components/ToggleRow.swift b/Shared/Components/ToggleRow.swift index 9abe073..895114d 100644 --- a/Shared/Components/ToggleRow.swift +++ b/Shared/Components/ToggleRow.swift @@ -29,6 +29,8 @@ struct ToggleRow: View { Toggle("", isOn: $isOn) .labelsHidden() .tint(AppTheme.ink) + .accessibilityLabel(title) + .accessibilityValue(isOn ? "On" : "Off") } .padding(.horizontal, AppTheme.cardPad) .padding(.vertical, 12) diff --git a/Shared/Components/UGreenCompatibilityBadge.swift b/Shared/Components/UGreenCompatibilityBadge.swift new file mode 100644 index 0000000..110e99e --- /dev/null +++ b/Shared/Components/UGreenCompatibilityBadge.swift @@ -0,0 +1,46 @@ +import SwiftUI +import UIKit + +// Displays the UGreen NAS compatibility indicator. +// If you add an image named "ugreen_logo" to Assets.xcassets it will be used automatically. +// Otherwise a brand-styled text badge is shown. +struct UGreenCompatibilityBadge: View { + private static let ugreenGreen = Color(hex: "#00B388") + private let hasLogo = UIImage(named: "ugreen_logo") != nil + + var body: some View { + HStack(spacing: 8) { + // UGreen logo or fallback wordmark + if hasLogo { + Image("ugreen_logo") + .resizable() + .scaledToFit() + .frame(height: 14) + } else { + HStack(spacing: 4) { + // Brand dot + Circle() + .fill(Self.ugreenGreen) + .frame(width: 6, height: 6) + Text("UGreen NAS") + .font(.system(size: 11, weight: .semibold)) + .foregroundStyle(Self.ugreenGreen) + } + } + + // Separator + Rectangle() + .fill(AppTheme.inkQuaternary) + .frame(width: 0.5, height: 11) + + // Protocol hint + Text("SMB · SFTP") + .font(.system(size: 11, weight: .regular)) + .foregroundStyle(AppTheme.inkTertiary) + } + .padding(.horizontal, 12) + .padding(.vertical, 6) + .background(AppTheme.surfaceSunken) + .clipShape(Capsule(style: .continuous)) + } +} diff --git a/Shared/Extensions/Date+.swift b/Shared/Extensions/Date+.swift new file mode 100644 index 0000000..95002a1 --- /dev/null +++ b/Shared/Extensions/Date+.swift @@ -0,0 +1,30 @@ +import Foundation + +extension Date { + private static let friendlyFormatter: DateFormatter = { + let f = DateFormatter() + f.dateStyle = .medium + f.timeStyle = .short + return f + }() + + private static let relativeFormatter: RelativeDateTimeFormatter = { + let f = RelativeDateTimeFormatter() + f.unitsStyle = .abbreviated + return f + }() + + var friendlyString: String { + Date.friendlyFormatter.string(from: self) + } + + var relativeString: String { + Date.relativeFormatter.localizedString(for: self, relativeTo: Date()) + } + + var backupTimestamp: String { + let f = DateFormatter() + f.dateFormat = "yyyyMMdd_HHmmss" + return f.string(from: self) + } +} diff --git a/Shared/Extensions/PHAsset+.swift b/Shared/Extensions/PHAsset+.swift new file mode 100644 index 0000000..50a4b99 --- /dev/null +++ b/Shared/Extensions/PHAsset+.swift @@ -0,0 +1,14 @@ +import Photos + +extension PHAsset { + var primaryFilename: String { + let resources = PHAssetResource.assetResources(for: self) + return resources.first?.originalFilename + ?? "\(localIdentifier.prefix(8)).jpg" + } + + var isRAWCapture: Bool { + PHAssetResource.assetResources(for: self) + .contains { $0.type == .alternatePhoto } + } +} diff --git a/Shared/Extensions/URL+.swift b/Shared/Extensions/URL+.swift new file mode 100644 index 0000000..5b958f6 --- /dev/null +++ b/Shared/Extensions/URL+.swift @@ -0,0 +1,14 @@ +import Foundation + +extension URL { + var fileSize: Int64 { + (try? resourceValues(forKeys: [.fileSizeKey]).fileSize) + .flatMap { Int64($0) } ?? 0 + } + + var sanitizedFilename: String { + lastPathComponent + .components(separatedBy: .init(charactersIn: "/\\:*?\"<>|")) + .joined(separator: "_") + } +} diff --git a/Shared/Persistence/ConnectionStore.swift b/Shared/Persistence/ConnectionStore.swift index cfb69d2..89aaaf0 100644 --- a/Shared/Persistence/ConnectionStore.swift +++ b/Shared/Persistence/ConnectionStore.swift @@ -15,7 +15,10 @@ final class ConnectionStore: ObservableObject { static let shared = ConnectionStore() @Published var savedConnection: NASConnection? { - didSet { saveUD(savedConnection, key: "savedConnection") } + didSet { + if let conn = savedConnection { SavedConnections.save(conn) } + else { SavedConnections.delete() } + } } @Published var backupFilter: BackupFilter { didSet { saveUD(backupFilter, key: "backupFilter") } @@ -42,6 +45,19 @@ final class ConnectionStore: ObservableObject { didSet { UserDefaults.standard.set(quietHoursEnd, forKey: "quietHoursEnd") } } + // Session — not persisted, resets on every app launch + @Published var isSessionActive = false + + // Onboarding — persisted so we only show each step once + @Published var onboardingPhotoShown: Bool { + didSet { UserDefaults.standard.set(onboardingPhotoShown, forKey: "onboardingPhotoShown") } + } + + // Appearance + @Published var appearanceMode: AppearanceMode { + didSet { UserDefaults.standard.set(appearanceMode.rawValue, forKey: "appearanceMode") } + } + // Cellular & remote access @Published var allowCellularBackup: Bool { didSet { UserDefaults.standard.set(allowCellularBackup, forKey: "allowCellularBackup") } @@ -56,7 +72,7 @@ final class ConnectionStore: ObservableObject { @Published private(set) var historyEntries: [BackupHistoryEntry] = [] private init() { - savedConnection = ud(NASConnection.self, key: "savedConnection") + savedConnection = SavedConnections.load() backupFilter = ud(BackupFilter.self, key: "backupFilter") ?? BackupFilter() trustedSSIDs = UserDefaults.standard.stringArray(forKey: "trustedSSIDs") ?? [] lanTriggerEnabled = UserDefaults.standard.object(forKey: "lanTriggerEnabled") as? Bool ?? true @@ -65,6 +81,8 @@ final class ConnectionStore: ObservableObject { quietHoursEnabled = UserDefaults.standard.object(forKey: "quietHoursEnabled") as? Bool ?? false quietHoursStart = UserDefaults.standard.object(forKey: "quietHoursStart") as? Int ?? 23 quietHoursEnd = UserDefaults.standard.object(forKey: "quietHoursEnd") as? Int ?? 7 + onboardingPhotoShown = UserDefaults.standard.bool(forKey: "onboardingPhotoShown") + appearanceMode = AppearanceMode(rawValue: UserDefaults.standard.string(forKey: "appearanceMode") ?? "") ?? .system allowCellularBackup = UserDefaults.standard.object(forKey: "allowCellularBackup") as? Bool ?? false useTailscaleWhenRemote = UserDefaults.standard.object(forKey: "useTailscaleWhenRemote") as? Bool ?? false tailscaleHost = UserDefaults.standard.string(forKey: "tailscaleHost") ?? "" diff --git a/Shared/Persistence/KeychainStore.swift b/Shared/Persistence/KeychainStore.swift new file mode 100644 index 0000000..f8e99eb --- /dev/null +++ b/Shared/Persistence/KeychainStore.swift @@ -0,0 +1,48 @@ +import Foundation +import Security + +enum KeychainStore { + private static func key(for id: UUID) -> String { + "com.nasbackup.connection.\(id.uuidString)" + } + + static func savePassword(_ password: String, for id: UUID) { + let account = key(for: id) + let data = Data(password.utf8) + + let deleteQuery: [CFString: Any] = [ + kSecClass: kSecClassGenericPassword, + kSecAttrAccount: account + ] + SecItemDelete(deleteQuery as CFDictionary) + + let addQuery: [CFString: Any] = [ + kSecClass: kSecClassGenericPassword, + kSecAttrAccount: account, + kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly, + kSecValueData: data + ] + SecItemAdd(addQuery as CFDictionary, nil) + } + + static func loadPassword(for id: UUID) -> String? { + let query: [CFString: Any] = [ + kSecClass: kSecClassGenericPassword, + kSecAttrAccount: key(for: id), + kSecReturnData: true, + kSecMatchLimit: kSecMatchLimitOne + ] + var result: AnyObject? + guard SecItemCopyMatching(query as CFDictionary, &result) == errSecSuccess, + let data = result as? Data else { return nil } + return String(data: data, encoding: .utf8) + } + + static func deletePassword(for id: UUID) { + let query: [CFString: Any] = [ + kSecClass: kSecClassGenericPassword, + kSecAttrAccount: key(for: id) + ] + SecItemDelete(query as CFDictionary) + } +} diff --git a/Shared/Persistence/SavedConnections.swift b/Shared/Persistence/SavedConnections.swift new file mode 100644 index 0000000..1262a89 --- /dev/null +++ b/Shared/Persistence/SavedConnections.swift @@ -0,0 +1,30 @@ +import Foundation + +// Bridges NASConnection save/load with Keychain for password and UserDefaults for metadata. +enum SavedConnections { + private static let key = "savedConnection" + + static func save(_ connection: NASConnection) { + KeychainStore.savePassword(connection.password, for: connection.id) + if let data = try? JSONEncoder().encode(connection) { + UserDefaults.standard.set(data, forKey: key) + } + } + + static func load() -> NASConnection? { + guard let data = UserDefaults.standard.data(forKey: key), + var connection = try? JSONDecoder().decode(NASConnection.self, from: data) else { + return nil + } + connection.password = KeychainStore.loadPassword(for: connection.id) ?? "" + return connection + } + + static func delete() { + if let data = UserDefaults.standard.data(forKey: key), + let connection = try? JSONDecoder().decode(NASConnection.self, from: data) { + KeychainStore.deletePassword(for: connection.id) + } + UserDefaults.standard.removeObject(forKey: key) + } +} diff --git a/Shared/Theme/AppTheme.swift b/Shared/Theme/AppTheme.swift index ab8259e..9d581cd 100644 --- a/Shared/Theme/AppTheme.swift +++ b/Shared/Theme/AppTheme.swift @@ -1,55 +1,107 @@ import SwiftUI +import UIKit + +// MARK: — Appearance preference + +enum AppearanceMode: String, CaseIterable { + case system, light, dark + + var resolvedScheme: ColorScheme? { + switch self { + case .system: return nil + case .light: return .light + case .dark: return .dark + } + } + + var label: String { + switch self { + case .system: return "System" + case .light: return "Light" + case .dark: return "Dark" + } + } + + var icon: String { + switch self { + case .system: return "circle.lefthalf.filled" + case .light: return "sun.max" + case .dark: return "moon" + } + } +} enum AppTheme { // MARK: — Surfaces - static let background = Color.white - static let surfaceRaised = Color.white - static let surfaceSunken = Color(hex: "#F5F5F5") + static let background = Color(UIColor.systemBackground) + static let surfaceRaised = Color(UIColor.systemBackground) + static let surfaceSunken = Color(UIColor { t in + t.userInterfaceStyle == .dark + ? UIColor(white: 0.11, alpha: 1) + : UIColor(white: 0.961, alpha: 1) + }) // MARK: — Ink - static let ink = Color(hex: "#0F0F0F") - static let inkSecondary = Color(hex: "#6B6B6B") - static let inkTertiary = Color(hex: "#ABABAB") - static let inkQuaternary = Color(hex: "#D4D4D4") + static let ink = Color(UIColor { t in + t.userInterfaceStyle == .dark + ? UIColor(white: 0.93, alpha: 1) + : UIColor(red: 0.059, green: 0.059, blue: 0.059, alpha: 1) + }) + static let inkSecondary = Color(UIColor { t in + t.userInterfaceStyle == .dark + ? UIColor(white: 0.58, alpha: 1) + : UIColor(white: 0.42, alpha: 1) + }) + static let inkTertiary = Color(UIColor { t in + t.userInterfaceStyle == .dark + ? UIColor(white: 0.40, alpha: 1) + : UIColor(white: 0.67, alpha: 1) + }) + static let inkQuaternary = Color(UIColor { t in + t.userInterfaceStyle == .dark + ? UIColor(white: 0.22, alpha: 1) + : UIColor(white: 0.83, alpha: 1) + }) + + // Readable on AppTheme.ink background in both color schemes + static let inkInverse = Color(UIColor { t in + t.userInterfaceStyle == .dark + ? UIColor(red: 0.059, green: 0.059, blue: 0.059, alpha: 1) + : UIColor(white: 1.0, alpha: 1) + }) // MARK: — Semantic - static let positive = Color(hex: "#16A34A") - static let destructive = Color(hex: "#DC2626") - static let interactive = Color(hex: "#2563EB") + static let positive = Color(hex: "#16A34A") + static let destructive = Color(hex: "#DC2626") + static let interactive = Color(hex: "#2563EB") // MARK: — Chrome - static let separator = Color.black.opacity(0.07) - static let cardShadow = Color.black.opacity(0.05) - static let cardBorderColor = Color.black.opacity(0.07) + static let separator = Color(UIColor.separator) + static let cardShadow = Color(UIColor { t in + t.userInterfaceStyle == .dark + ? UIColor.clear + : UIColor.black.withAlphaComponent(0.048) + }) + static let cardBorderColor = Color(UIColor.separator) // MARK: — Metrics - static let radius: CGFloat = 14 - static let radiusLarge: CGFloat = 18 - static let hPad: CGFloat = 20 - static let cardPad: CGFloat = 16 - static let sectionGap: CGFloat = 28 + static let radius: CGFloat = 14 + static let radiusLarge: CGFloat = 18 + static let hPad: CGFloat = 20 + static let cardPad: CGFloat = 16 + static let sectionGap: CGFloat = 28 // MARK: — Typography static func display(_ size: CGFloat = 32) -> Font { .system(size: size, weight: .semibold, design: .default) } - static func title(_ size: CGFloat = 20) -> Font { - .system(size: size, weight: .semibold) - } - static func headline(_ size: CGFloat = 15) -> Font { - .system(size: size, weight: .medium) - } - static func body(_ size: CGFloat = 15) -> Font { - .system(size: size, weight: .regular) - } - static func caption(_ size: CGFloat = 13) -> Font { - .system(size: size, weight: .regular) - } - static func micro(_ size: CGFloat = 11) -> Font { - .system(size: size, weight: .medium) - } + static func title(_ size: CGFloat = 20) -> Font { .system(size: size, weight: .semibold) } + static func headline(_ size: CGFloat = 15) -> Font { .system(size: size, weight: .medium) } + static func body(_ size: CGFloat = 15) -> Font { .system(size: size, weight: .regular) } + static func caption(_ size: CGFloat = 13) -> Font { .system(size: size, weight: .regular) } + static func micro(_ size: CGFloat = 11) -> Font { .system(size: size, weight: .medium) } - // MARK: — Back-compat aliases used by existing call sites + // MARK: — Back-compat aliases static let blue = interactive static let green = positive static let red = destructive @@ -82,9 +134,7 @@ struct CardStyle: ViewModifier { } extension View { - func card(padded: Bool = true) -> some View { - modifier(CardStyle(padded: padded)) - } + func card(padded: Bool = true) -> some View { modifier(CardStyle(padded: padded)) } @ViewBuilder func `if`(_ condition: Bool, transform: (Self) -> T) -> some View { if condition { transform(self) } else { self } diff --git a/Tests/BackupEngineTests.swift b/Tests/BackupEngineTests.swift new file mode 100644 index 0000000..d631347 --- /dev/null +++ b/Tests/BackupEngineTests.swift @@ -0,0 +1,117 @@ +import XCTest +import Photos +@testable import NASBackup + +@MainActor +final class BackupEngineTests: XCTestCase { + + private func makeAssets(count: Int) -> [PhotoAsset] { + (0.. NASConnection { + NASConnection(host: "192.168.1.10", nasProtocol: .smb, + username: "user", password: "pass", remotePath: "/backup") + } + + func test_backup_uploadsAllAssets() async throws { + let mock = MockNASService() + let photos = MockPhotoLibraryService(assets: makeAssets(count: 3)) + let engine = BackupEngine.testInstance(transfer: mock, photos: photos) + + let result = try await engine.run(connection: makeConnection(), filter: BackupFilter()) + + XCTAssertEqual(result.uploadedCount, 3) + XCTAssertEqual(result.failedCount, 0) + XCTAssertEqual(mock.uploadCallCount, 3) + } + + func test_backup_skipsExistingFiles_whenNewFilesOnly() async throws { + let mock = MockNASService() + mock.fileExistsAnswer = true + let photos = MockPhotoLibraryService(assets: makeAssets(count: 2)) + let engine = BackupEngine.testInstance(transfer: mock, photos: photos) + + var filter = BackupFilter() + filter.newFilesOnly = true + let result = try await engine.run(connection: makeConnection(), filter: filter) + + XCTAssertEqual(result.skippedCount, 2) + XCTAssertEqual(result.uploadedCount, 0) + } + + func test_backup_handlesUploadFailure_gracefully() async throws { + let mock = MockNASService() + mock.shouldFail = true + let photos = MockPhotoLibraryService(assets: makeAssets(count: 2)) + let engine = BackupEngine.testInstance(transfer: mock, photos: photos) + + let result = try await engine.run(connection: makeConnection(), filter: BackupFilter()) + + XCTAssertEqual(result.failedCount, 2) + XCTAssertEqual(result.uploadedCount, 0) + } + + func test_backup_emitsProgressUpdates() async throws { + let mock = MockNASService() + let photos = MockPhotoLibraryService(assets: makeAssets(count: 5)) + let engine = BackupEngine.testInstance(transfer: mock, photos: photos) + + var progressEvents = 0 + let cancellable = engine.$job.sink { job in + if case .running = job.status { progressEvents += 1 } + } + defer { cancellable.cancel() } + + _ = try await engine.run(connection: makeConnection(), filter: BackupFilter()) + + XCTAssertGreaterThan(progressEvents, 0, "Expected progress events during backup") + } + + func test_backup_canBePaused_andResumed() async throws { + let mock = MockNASService() + mock.uploadDelay = 0.05 + let photos = MockPhotoLibraryService(assets: makeAssets(count: 4)) + let engine = BackupEngine.testInstance(transfer: mock, photos: photos) + + let task = Task { try await engine.run(connection: makeConnection(), filter: BackupFilter()) } + try await Task.sleep(nanoseconds: 70_000_000) + engine.pause() + let statusAfterPause = engine.job.status + engine.resume() + let result = try await task.value + + if case .paused = statusAfterPause { } else { + XCTFail("Expected .paused status, got \(statusAfterPause)") + } + XCTAssertEqual(result.uploadedCount, 4) + } +} + +// MARK: - MockPhotoLibraryService + +final class MockPhotoLibraryService: PhotoLibraryProtocol { + var authorizationStatus: PHAuthorizationStatus { .authorized } + private let assets: [PhotoAsset] + + init(assets: [PhotoAsset]) { self.assets = assets } + + func requestAuthorization() async -> PHAuthorizationStatus { .authorized } + + func fetchAssets(filter: BackupFilter) -> [PhotoAsset] { assets } + + func exportAsset(_ asset: PhotoAsset) async throws -> URL { + let url = FileManager.default.temporaryDirectory.appendingPathComponent(asset.filename) + try Data("mock-data".utf8).write(to: url) + return url + } +} diff --git a/Tests/ConnectViewModelTests.swift b/Tests/ConnectViewModelTests.swift new file mode 100644 index 0000000..66dca3a --- /dev/null +++ b/Tests/ConnectViewModelTests.swift @@ -0,0 +1,47 @@ +import XCTest +@testable import NASBackup + +@MainActor +final class ConnectViewModelTests: XCTestCase { + + func test_canConnect_isFalse_whenFieldsEmpty() { + let vm = ConnectViewModel() + XCTAssertFalse(vm.canConnect) + } + + func test_canConnect_isTrue_whenAllFieldsFilled() { + let vm = ConnectViewModel() + vm.host = "192.168.1.10" + vm.username = "admin" + vm.password = "secret" + XCTAssertTrue(vm.canConnect) + } + + func test_connectionState_becomesVerified_onSuccess() async { + let vm = ConnectViewModel() + vm.host = "192.168.1.10" + vm.username = "admin" + vm.password = "secret" + + // Override the service factory so no real network call is made. + let mock = MockNASService() + await vm.verifyWith(transfer: mock) + + XCTAssertTrue(vm.isConnected) + XCTAssertNil(vm.verifyError) + } + + func test_connectionState_becomesFailed_onError() async { + let vm = ConnectViewModel() + vm.host = "192.168.1.10" + vm.username = "admin" + vm.password = "wrong" + + let mock = MockNASService() + mock.shouldFail = true + await vm.verifyWith(transfer: mock) + + XCTAssertFalse(vm.isConnected) + XCTAssertNotNil(vm.verifyError) + } +} diff --git a/Tests/LANMonitorTests.swift b/Tests/LANMonitorTests.swift new file mode 100644 index 0000000..7535d8a --- /dev/null +++ b/Tests/LANMonitorTests.swift @@ -0,0 +1,48 @@ +import XCTest +@testable import NASBackup + +final class LANMonitorTests: XCTestCase { + + func test_ssidMatch_triggersCallback() async { + let monitor = LANMonitor.shared + let trusted = ["HomeNet"] + + var triggered = false + monitor.onTrustedNetworkJoined = { _ in triggered = true } + + // Inject known SSID via checkAndTriggerIfTrusted using a subclass stub + // Since fetchCurrentSSID uses NEHotspotNetwork (requires device), we test + // the guard logic directly through checkAndTriggerIfTrusted with a mock SSID. + let stub = LANMonitorSSIDStub(ssid: "HomeNet") + await stub.checkAndTrigger(trustedSSIDs: trusted) { ssid in triggered = true } + + XCTAssertTrue(triggered) + } + + func test_ssidMismatch_doesNotTrigger() async { + var triggered = false + let stub = LANMonitorSSIDStub(ssid: "RandomCafe") + await stub.checkAndTrigger(trustedSSIDs: ["HomeNet"]) { _ in triggered = true } + XCTAssertFalse(triggered) + } + + func test_trustedSSIDs_caseInsensitiveMatch() async { + var triggered = false + let stub = LANMonitorSSIDStub(ssid: "homenet") + let trusted = ["HomeNet"] + // Case-sensitive check — verify current behaviour; adjust if policy changes. + await stub.checkAndTrigger(trustedSSIDs: trusted) { _ in triggered = true } + // Current implementation is case-sensitive; update assertion if behaviour changes. + XCTAssertFalse(triggered, "Current implementation is case-sensitive") + } +} + +// Lightweight stub that bypasses NEHotspotNetwork entitlement requirement. +struct LANMonitorSSIDStub { + let ssid: String? + + func checkAndTrigger(trustedSSIDs: [String], callback: (String) -> Void) async { + guard let ssid, trustedSSIDs.contains(ssid) else { return } + callback(ssid) + } +} diff --git a/Tests/MockNASService.swift b/Tests/MockNASService.swift new file mode 100644 index 0000000..2a42c8f --- /dev/null +++ b/Tests/MockNASService.swift @@ -0,0 +1,49 @@ +import Foundation +@testable import NASBackup + +final class MockNASService: NASTransferProtocol { + var shouldFail: Bool = false + var uploadDelay: TimeInterval = 0 + var fakeItems: [NASItem] = [] + var fileExistsAnswer: Bool = false + + private(set) var connectCalled = false + private(set) var uploadCallCount = 0 + private(set) var lastUploadedPath: String? + + var isConnected: Bool = false + + func connect(to host: String, port: Int, username: String, password: String) async throws { + connectCalled = true + if shouldFail { throw MockError.connectionFailed } + isConnected = true + } + + func disconnect() { isConnected = false } + + func listDirectory(at path: String) async throws -> [NASItem] { + if shouldFail { throw MockError.connectionFailed } + return fakeItems + } + + func createDirectory(at path: String) async throws { + if shouldFail { throw MockError.connectionFailed } + } + + func fileExists(at remotePath: String) async throws -> Bool { fileExistsAnswer } + + func upload(localURL: URL, remotePath: String, progress: @escaping (Int64, Int64) -> Void) async throws { + if uploadDelay > 0 { + try await Task.sleep(nanoseconds: UInt64(uploadDelay * 1_000_000_000)) + } + if shouldFail { throw MockError.uploadFailed } + progress(100, 100) + uploadCallCount += 1 + lastUploadedPath = remotePath + } +} + +enum MockError: Error { + case connectionFailed + case uploadFailed +} diff --git a/Tests/NASProtocolTests.swift b/Tests/NASProtocolTests.swift new file mode 100644 index 0000000..0db96c2 --- /dev/null +++ b/Tests/NASProtocolTests.swift @@ -0,0 +1,22 @@ +import XCTest +@testable import NASBackup + +final class NASProtocolTests: XCTestCase { + + func test_allCases_haveLocalizedDescriptions() { + for proto in NASProtocol.allCases { + XCTAssertFalse( + proto.localizedDescription.isEmpty, + "\(proto.rawValue) has no localizedDescription" + ) + } + } + + func test_smb_defaultPort_is445() { + XCTAssertEqual(NASProtocol.smb.defaultPort, 445) + } + + func test_sftp_defaultPort_is22() { + XCTAssertEqual(NASProtocol.sftp.defaultPort, 22) + } +} diff --git a/project.yml b/project.yml index dc0d203..6972971 100644 --- a/project.yml +++ b/project.yml @@ -36,6 +36,8 @@ targets: - Shared resources: - Assets.xcassets + - path: App/PrivacyInfo.xcprivacy + buildPhase: resources info: path: App/Info.plist properties: @@ -74,3 +76,17 @@ targets: product: SMBClient - package: Citadel product: Citadel + + NASBackupTests: + type: bundle.unit-test + platform: iOS + sources: + - Tests + dependencies: + - target: NASBackup + settings: + base: + TEST_HOST: $(BUILT_PRODUCTS_DIR)/NASBackup.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/NASBackup + BUNDLE_LOADER: $(TEST_HOST) + SWIFT_VERSION: "5.9" + IPHONEOS_DEPLOYMENT_TARGET: "16.0"