From 2c0372b421ff290b11b8c7c4b9f839a880127897 Mon Sep 17 00:00:00 2001 From: Twyco Date: Fri, 18 Sep 2026 09:03:28 +0200 Subject: [PATCH] feat(KAN-54): add local iOS and iPadOS Xcode builds --- .gitignore | 10 +- native/package.json | 8 +- native/src-tauri/gen/apple/.gitignore | 3 + .../AppIcon.appiconset/AppIcon-20x20@1x.png | Bin 0 -> 422 bytes .../AppIcon.appiconset/AppIcon-20x20@2x-1.png | Bin 0 -> 548 bytes .../AppIcon.appiconset/AppIcon-20x20@2x.png | Bin 0 -> 548 bytes .../AppIcon.appiconset/AppIcon-20x20@3x.png | Bin 0 -> 800 bytes .../AppIcon.appiconset/AppIcon-29x29@1x.png | Bin 0 -> 415 bytes .../AppIcon.appiconset/AppIcon-29x29@2x-1.png | Bin 0 -> 845 bytes .../AppIcon.appiconset/AppIcon-29x29@2x.png | Bin 0 -> 845 bytes .../AppIcon.appiconset/AppIcon-29x29@3x.png | Bin 0 -> 1110 bytes .../AppIcon.appiconset/AppIcon-40x40@1x.png | Bin 0 -> 548 bytes .../AppIcon.appiconset/AppIcon-40x40@2x-1.png | Bin 0 -> 1026 bytes .../AppIcon.appiconset/AppIcon-40x40@2x.png | Bin 0 -> 1026 bytes .../AppIcon.appiconset/AppIcon-40x40@3x.png | Bin 0 -> 1559 bytes .../AppIcon.appiconset/AppIcon-512@2x.png | Bin 0 -> 13961 bytes .../AppIcon.appiconset/AppIcon-60x60@2x.png | Bin 0 -> 1559 bytes .../AppIcon.appiconset/AppIcon-60x60@3x.png | Bin 0 -> 2118 bytes .../AppIcon.appiconset/AppIcon-76x76@1x.png | Bin 0 -> 955 bytes .../AppIcon.appiconset/AppIcon-76x76@2x.png | Bin 0 -> 1777 bytes .../AppIcon-83.5x83.5@2x.png | Bin 0 -> 1995 bytes .../AppIcon.appiconset/Contents.json | 116 +++ .../gen/apple/Assets.xcassets/Contents.json | 6 + .../gen/apple/LaunchScreen.storyboard | 30 + native/src-tauri/gen/apple/Podfile | 14 + .../kanbangit-native/WorkspacePolicy.swift | 58 ++ .../WorkspaceViewController.swift | 318 ++++++++ .../kanbangit-native/bindings/bindings.h | 7 + .../en.lproj/Localizable.strings | 23 + .../apple/Sources/kanbangit-native/main.mm | 36 + .../gen/apple/Tests/WorkspaceTests.swift | 118 +++ .../gen/apple/UITests/WorkspaceUITests.swift | 44 ++ .../project.pbxproj | 687 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/WorkspaceSettings.xcsettings | 10 + .../xcschemes/kanbangit-native_iOS.xcscheme | 144 ++++ .../gen/apple/kanbangit-native_iOS/Info.plist | 44 ++ .../kanbangit-native_iOS.entitlements | 5 + native/src-tauri/gen/apple/project.yml | 122 ++++ native/src-tauri/src/desktop.rs | 456 ++++++++++++ native/src-tauri/src/ios.rs | 14 + native/src-tauri/src/lib.rs | 466 +----------- native/src-tauri/tauri.ios.conf.json | 6 + 43 files changed, 2294 insertions(+), 458 deletions(-) create mode 100644 native/src-tauri/gen/apple/.gitignore create mode 100644 native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@1x.png create mode 100644 native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@2x-1.png create mode 100644 native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@2x.png create mode 100644 native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@3x.png create mode 100644 native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@1x.png create mode 100644 native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@2x-1.png create mode 100644 native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@2x.png create mode 100644 native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@3x.png create mode 100644 native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@1x.png create mode 100644 native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@2x-1.png create mode 100644 native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@2x.png create mode 100644 native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@3x.png create mode 100644 native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png create mode 100644 native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-60x60@2x.png create mode 100644 native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-60x60@3x.png create mode 100644 native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@1x.png create mode 100644 native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@2x.png create mode 100644 native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5x83.5@2x.png create mode 100644 native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 native/src-tauri/gen/apple/Assets.xcassets/Contents.json create mode 100644 native/src-tauri/gen/apple/LaunchScreen.storyboard create mode 100644 native/src-tauri/gen/apple/Podfile create mode 100644 native/src-tauri/gen/apple/Sources/kanbangit-native/WorkspacePolicy.swift create mode 100644 native/src-tauri/gen/apple/Sources/kanbangit-native/WorkspaceViewController.swift create mode 100644 native/src-tauri/gen/apple/Sources/kanbangit-native/bindings/bindings.h create mode 100644 native/src-tauri/gen/apple/Sources/kanbangit-native/en.lproj/Localizable.strings create mode 100644 native/src-tauri/gen/apple/Sources/kanbangit-native/main.mm create mode 100644 native/src-tauri/gen/apple/Tests/WorkspaceTests.swift create mode 100644 native/src-tauri/gen/apple/UITests/WorkspaceUITests.swift create mode 100644 native/src-tauri/gen/apple/kanbangit-native.xcodeproj/project.pbxproj create mode 100644 native/src-tauri/gen/apple/kanbangit-native.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 native/src-tauri/gen/apple/kanbangit-native.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 native/src-tauri/gen/apple/kanbangit-native.xcodeproj/xcshareddata/xcschemes/kanbangit-native_iOS.xcscheme create mode 100644 native/src-tauri/gen/apple/kanbangit-native_iOS/Info.plist create mode 100644 native/src-tauri/gen/apple/kanbangit-native_iOS/kanbangit-native_iOS.entitlements create mode 100644 native/src-tauri/gen/apple/project.yml create mode 100644 native/src-tauri/src/desktop.rs create mode 100644 native/src-tauri/src/ios.rs create mode 100644 native/src-tauri/tauri.ios.conf.json diff --git a/.gitignore b/.gitignore index 2219081..d5149e8 100644 --- a/.gitignore +++ b/.gitignore @@ -31,4 +31,12 @@ yarn-error.log /native/node_modules /native/src-tauri/target -/native/src-tauri/gen +/native/src-tauri/gen/* +!/native/src-tauri/gen/apple/ +/native/src-tauri/gen/apple/build/ +/native/src-tauri/gen/apple/Externals/ +/native/src-tauri/gen/apple/assets/ +/native/src-tauri/gen/apple/Pods/ +/native/src-tauri/gen/apple/ExportOptions.plist +/native/src-tauri/gen/apple/**/*.xcuserstate +/native/src-tauri/gen/apple/**/xcuserdata/ diff --git a/native/package.json b/native/package.json index e755dfb..2bbd856 100644 --- a/native/package.json +++ b/native/package.json @@ -9,7 +9,13 @@ "build:macos": "tauri build --debug --bundles app", "test": "node --test tests/*.test.js && cargo test --manifest-path src-tauri/Cargo.toml --locked", "check": "cargo fmt --manifest-path src-tauri/Cargo.toml --check && cargo clippy --manifest-path src-tauri/Cargo.toml --locked --all-targets --all-features -- -D warnings", - "smoke:macos": "cargo test --manifest-path src-tauri/Cargo.toml --locked --features smoke-test,custom-protocol --test macos_smoke" + "smoke:macos": "cargo test --manifest-path src-tauri/Cargo.toml --locked --features smoke-test,custom-protocol --test macos_smoke", + "dev:ios": "tauri ios dev", + "build:ios:simulator": "rm -rf src-tauri/gen/apple/build/arm64-sim/KanbanGit.app && tauri ios build --debug --target aarch64-sim --no-sign --ci", + "build:ios:device": "tauri ios build --debug --target aarch64 --export-method debugging --archive-only", + "open:ios": "tauri ios dev --open --no-dev-server", + "prepare:ios": "xcodegen generate --spec src-tauri/gen/apple/project.yml", + "test:ios": "xcodebuild -project src-tauri/gen/apple/kanbangit-native.xcodeproj -scheme kanbangit-native_iOS -configuration debug -destination \"platform=iOS Simulator,id=${IOS_SIMULATOR_ID:?Set IOS_SIMULATOR_ID to an installed simulator UUID}\" -parallel-testing-enabled NO CODE_SIGNING_ALLOWED=NO test" }, "devDependencies": { "@tauri-apps/cli": "2.11.4" diff --git a/native/src-tauri/gen/apple/.gitignore b/native/src-tauri/gen/apple/.gitignore new file mode 100644 index 0000000..6726e2f --- /dev/null +++ b/native/src-tauri/gen/apple/.gitignore @@ -0,0 +1,3 @@ +xcuserdata/ +build/ +Externals/ diff --git a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@1x.png b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..3ba6c85900771ce41d5fa553e61fc82c3e0e0db1 GIT binary patch literal 422 zcmV;X0a^ZuP)S6opTclGU z$^A6<0x+iMkj$$e_Voh?;%AuqWMfci@l7HEKY;W248{;H@>C;lm22?oy+|{W4u?hu z%EA<~YdQkgNB_1N`mlrR1~RJ}#H0l4+=OFxAs5o$1AnW3;GYDhcmle7Xlc$E4YB(g zdQ!lB@3IC#65%#%WJ+2D7)grEtfdI>YCG-x%ltc$v5uECn2r~Kp{6+90vj*x1`^g z1ZFb}Ch6C2eS4CTVUU40zdtg_u!}M3Ql~y$;iSm zhyf@q4r&L208tM7_u~)4+dX#}emwlZ@cZKriY#R1=V0KF7hn)}QDI=>V4;o!-|oJ{ z@cHr!s)Pv0gTF5U%>q^rqP`l`ao~?fAHcLikqw5g@3kFZ;FJ?!kc=@v5r4kq0>gKp zKBX!LjM#Y4cLZ)GRU8P5R$`)r5P+FMY*HB>nPk*~qYfN(;HU#f9XRU1Q3sAXaL@uK zVw>r(#wMizW(LemLV!`}Xxx87ie7KNLA547e0W1wf_hDUVOJG+)1NAxCSVrtfdI>YCG-x%ltc$v5uECn2r~Kp{6+90vj*x1`^g z1ZFb}Ch6C2eS4CTVUU40zdtg_u!}M3Ql~y$;iSm zhyf@q4r&L208tM7_u~)4+dX#}emwlZ@cZKriY#R1=V0KF7hn)}QDI=>V4;o!-|oJ{ z@cHr!s)Pv0gTF5U%>q^rqP`l`ao~?fAHcLikqw5g@3kFZ;FJ?!kc=@v5r4kq0>gKp zKBX!LjM#Y4cLZ)GRU8P5R$`)r5P+FMY*HB>nPk*~qYfN(;HU#f9XRU1Q3sAXaL@uK zVw>r(#wMizW(LemLV!`}Xxx87ie7KNLA547e0W1wf_hDUVOJG+)1NAxCSVrxgqR>2J>ZS)Qy~fQ;wp4TEwUf*h{d4k&dMYG zdks97j3_)}S)$qZ@a*Se4UAp+@mf>>&NG*a)WG3XM9Ec)ynD47Cz_3cw2+N)-?}Ud z*>{T6t#hrVRxWLT1P;oZ-=76XfiVPbHQPV(P}wR$Sv?x1TB^XG!Z!5y+zmfxCsTxd z`+Grj8Q+q8s1Kxm7xb#fJpm&m^Ku1pAJ%PcAUPbJv!vZsK`GjeYLTt5MJN92VYN>l$ z0$2XsoZiKP0n>9C7{{L(rnI`*6wI>q_M3hWR=}baaQzF)>R7Up`fHiJBxzbAFo6k7 zU;-1Ezyu~RfeB1t0uz|P1ST+n2~1%Az;fL`z_B7=SL&}NTP&(s0%H|{CBLlJdW2fZ z1)5YVSQVmCCBy6-EU{?UGPs9UBAk4EL$UIU>E{W^EwArMx5Txou^%q4B>}JP%IY5=Pv^+@aBgwq47XaM#Dv zu~~tqmPQMj47c&r2RB57Q}fDNIU{uI3kt_9>&8uYrh&d2B&IE!$Cc!`xVqE-sK9v^ eryhJ8o8>Q_-Zt`TWl|vk0000WLD-#LLV@-!>A^J|=@-4_7kbO*`rhctr zG|eprnGdoDWFI4PUuMFSb^mw2J9zjS_TSIkPl&f8329dp^002ov JPDHLkV1mwRwnG2_ literal 0 HcmV?d00001 diff --git a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@2x-1.png b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@2x-1.png new file mode 100644 index 0000000000000000000000000000000000000000..b9cd3140c29661928ef9fc8bd895834e0fa1b29c GIT binary patch literal 845 zcmV-T1G4;yP)1pcV^TAi%n_ffdUx=q?a=zW-$pOy2D8&FtId%{*R`1sX~fGZ6jp z7B+I9;6ot+sZs_8QC{09%s3_>=$?h^t^j!LqndWyNzkK;2&?G`gi}#Cs);a6auER* zy$i7934x0j^g-i=U3mEA8GIBIFwD9nOu&<=dob*Ocn7r=<0q%8uoP65;7s10*b3^ct6Skr9w7jkRy1uRe$IgJ!Mq_@L!jB%&L5DrF((jP z>rltLhsxC=-A8JX(&w)>F7BY0>$~W^6#6Kc z^kXQZSnz2GchHrRM4NgDmL1YBw{(ANNXrK6Jk5~JzIzVp4o*TrF8DOksncv@&Ez<7 z*m2xWHn@q6zKw{x_~wpexNI2fP&O{AD!!~N!Fxcm^p)$L&LnBo!_IhEU-4PkL zxvB3n3J5-l%m{w!qv?~Im&vt}Y3duQg6=tvgwD=_%OX&lzQ{IzIsgQF&=)-m)J7B8 zkWu;8T^bK@_`uS*2qByLQIC!GD^vG5no<~96sGzi0`=bu_MWd@UZx_l$bc-GhXA*Y*EB#64SNqaS%9hzy~{*v8gvHx4Lc*XkRZz58spc3c=M??|{!Q Xi=}3&9sjLE00000NkvXXu0mjfKU|EP literal 0 HcmV?d00001 diff --git a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@2x.png b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..b9cd3140c29661928ef9fc8bd895834e0fa1b29c GIT binary patch literal 845 zcmV-T1G4;yP)1pcV^TAi%n_ffdUx=q?a=zW-$pOy2D8&FtId%{*R`1sX~fGZ6jp z7B+I9;6ot+sZs_8QC{09%s3_>=$?h^t^j!LqndWyNzkK;2&?G`gi}#Cs);a6auER* zy$i7934x0j^g-i=U3mEA8GIBIFwD9nOu&<=dob*Ocn7r=<0q%8uoP65;7s10*b3^ct6Skr9w7jkRy1uRe$IgJ!Mq_@L!jB%&L5DrF((jP z>rltLhsxC=-A8JX(&w)>F7BY0>$~W^6#6Kc z^kXQZSnz2GchHrRM4NgDmL1YBw{(ANNXrK6Jk5~JzIzVp4o*TrF8DOksncv@&Ez<7 z*m2xWHn@q6zKw{x_~wpexNI2fP&O{AD!!~N!Fxcm^p)$L&LnBo!_IhEU-4PkL zxvB3n3J5-l%m{w!qv?~Im&vt}Y3duQg6=tvgwD=_%OX&lzQ{IzIsgQF&=)-m)J7B8 zkWu;8T^bK@_`uS*2qByLQIC!GD^vG5no<~96sGzi0`=bu_MWd@UZx_l$bc-GhXA*Y*EB#64SNqaS%9hzy~{*v8gvHx4Lc*XkRZz58spc3c=M??|{!Q Xi=}3&9sjLE00000NkvXXu0mjfKU|EP literal 0 HcmV?d00001 diff --git a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@3x.png b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..6b569414b72c84d6b9ec1e8986fb54a887c51f11 GIT binary patch literal 1110 zcmV-c1gZOpP)56dG3N@<}`=M2!4filx}#!lh?lct%3Y0mvP_uM--`RAAeqDZB_ zEss&p`~ZD&4NKP_mmGA^bt$t<`o3T*p{ipQbT++E^}Qw~xJ!NywM=!<%ZYAU+VId} z*9l7&)#X%DV_r37n$x0(+u`h`=AUhV8U}JxVGY%0S4IYxv$grxE9#i}0E!B#%f3R7 zPv24pm%&~idPEM-D9EW)Sy~ajF2A>{Ob7rQyO7oKSC>`F2iP4YLSiX zSBoS1r$i#tslJ<N7RBZ}ja+ojaFfa03DsiLPg)VjoWB*}+nQ}CgMc*$tXUp5tDouYkwXe9HK304Ik znrpWYEma3Eu;y^>RmJq@#phiaycXdUWaC~m-Y{R3PDqa1@b*+TDYKV0k;L)pVXoF? zL1U9L2Rqj@vZF*8OJiIBt55Q4?Wp1RlAKJs;t$|KwAL_7*QXtP*)t&sFj&}woeRF`k z$F6`Euh1GBT@OmJ*G#+EF$s`2q4GXvjTpk cjih_(KXQApVVUYEdjJ3c07*qoM6N<$f&>Kz_5c6? literal 0 HcmV?d00001 diff --git a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@1x.png b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..e3f34d5eb44d641d3bde798c7e319270d2603583 GIT binary patch literal 548 zcmV+<0^9wGP)tfdI>YCG-x%ltc$v5uECn2r~Kp{6+90vj*x1`^g z1ZFb}Ch6C2eS4CTVUU40zdtg_u!}M3Ql~y$;iSm zhyf@q4r&L208tM7_u~)4+dX#}emwlZ@cZKriY#R1=V0KF7hn)}QDI=>V4;o!-|oJ{ z@cHr!s)Pv0gTF5U%>q^rqP`l`ao~?fAHcLikqw5g@3kFZ;FJ?!kc=@v5r4kq0>gKp zKBX!LjM#Y4cLZ)GRU8P5R$`)r5P+FMY*HB>nPk*~qYfN(;HU#f9XRU1Q3sAXaL@uK zVw>r(#wMizW(LemLV!`}Xxx87ie7KNLA547e0W1wf_hDUVOJG+)1NAxCSVr0ZWrBIK!u-8}KVO6C;EV;< zqWkJ(A>t)#vE39nOZMUif)yH05rIH|}eTMhZNQyt9DY#%(kb(a>&dVtGo5+*Z?d;cR); zw=R zM*lIkT8H@}utLgH=4}Kiwx^fmTxOewUz?+3ef%S7NhRjo=X~21Vp2A$QeLX9rW43X z+o9tPHl*0cp(S%)>+z%`UDxOhH-_d&*WGEC8)#OzZQMrFP9ofuc$-3M&j@!(BNMtw zXL31@dE9MHztv0Go&QH;v5bL00D%T0UbZ)*?1@g;oz-Qv!-P)Qo!C~{0uwr6cVb^{ zCkpTAgx!f;t=4I=V;s~7J7ah9rQBeB7mR2mPWtb~Jj{r9UG`M1@jY=kZb{SJY_|n1Tzga?MaK wdl{7KPA^7(3dMF~fUj-&@{F%A)czgDA9Y-gOWZdwoB#j-07*qoM6N<$f;p?!BLDyZ literal 0 HcmV?d00001 diff --git a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@2x.png b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..51b1c2a0ec96d058b242ba09f0689bb9fa560ef7 GIT binary patch literal 1026 zcmV+d1pWJoP)0ZWrBIK!u-8}KVO6C;EV;< zqWkJ(A>t)#vE39nOZMUif)yH05rIH|}eTMhZNQyt9DY#%(kb(a>&dVtGo5+*Z?d;cR); zw=R zM*lIkT8H@}utLgH=4}Kiwx^fmTxOewUz?+3ef%S7NhRjo=X~21Vp2A$QeLX9rW43X z+o9tPHl*0cp(S%)>+z%`UDxOhH-_d&*WGEC8)#OzZQMrFP9ofuc$-3M&j@!(BNMtw zXL31@dE9MHztv0Go&QH;v5bL00D%T0UbZ)*?1@g;oz-Qv!-P)Qo!C~{0uwr6cVb^{ zCkpTAgx!f;t=4I=V;s~7J7ah9rQBeB7mR2mPWtb~Jj{r9UG`M1@jY=kZb{SJY_|n1Tzga?MaK wdl{7KPA^7(3dMF~fUj-&@{F%A)czgDA9Y-gOWZdwoB#j-07*qoM6N<$f;p?!BLDyZ literal 0 HcmV?d00001 diff --git a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@3x.png b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..30decc9810847f3ffbe6f2a75f833389e4b5d58c GIT binary patch literal 1559 zcmV+y2I%>TP)bz6JBl{n$5x$k~W*U-{H@Cab1v z?tRQ$yYc?L&x~D8PQj66%qXa^=GqT-cGWkUTkx=dQb)CHU)bHq%A{^8n5XyG52qD` z$=-|xrj*Q7ofgcg8OXtKU_yUJFdLtXco7*%nAn(2&ti42aUcg{8y-C^_-^nQti6&k zzByw!)#k%KY>Om>Z{zZrhB_DaB0WpWoS3{CKaBxo-4`$ES|Aa^`YUUi7L~MyM{eY0 zA?jB&gn^_!pN3$`fnW#*5DXv~)TrPmkD?S`*`lZ6xbfODDV>s$1f}9hQZ$vc2B-16 z5cFh)=T6kqVR0SK_M^t@niR}J9?vXM_=iAAKQ|eDQj*Wv z5!d&0D_+oh|5@HjzixQFm5yELG`=u3BbaCEqp{zOSt~pI1&;k0q!az;Dl(hZO3d6^ zk-H0zmB-9umtOE=&r(gv&6LbzmpVmrY7YOHea&@<6s9`%e@UrCk`g9`85cNQY*|{n zg#fcJUa79hNY`TQ?w3!NHC2~YaAxt5iTnIXYJ8{5av>5IpSSkSumAgdy$Is5-cHMz z#cYN(zTHL6j-!^Bq0rq$iriZ_jX^mDmo-Hvv!v!iz)!2p5*1Oo^L5DXv~KrjRY2nG-gAQ(U}fM5W@5DXv~Krnz{0Kou)0R%%ZfM5W@ z0D=Jo0|*8X3?LYS0R#gGru;n)WThI$R@VRcZq}=S8(37L-Ap;!2bneJD0lCqvI@@D z2_^LXqEwQNrW|5SR$?GUiawI1dr9QCb>qis|AuT|im2#z!u=4b1Pw!2l+|lWL8Q`?cGqLG(__=y_C3gJ`}EE~sj@L3Cy?DjfvENfv-$0Kou)0R#g}yA}5#myR&V3A5bM)n8lKi=~B(y zGhJ)Fh*@lN*kR_`#xsvT44m#$x?OFMY*X0X_4fCatPkC}H5Yw;<|75?(`3tY=Ud%3 zl)O(>^4{srxtmyzZ3a6tJ9bvh52@Q}sOt)%W)s*IJ2OMk1=-#>ANJ8z--sUBiD5=p zXHa&p99#L{JZc6hTA*>AjN&u^=236+x7yfT&2X zf+kT=K`8p_YzXxdE&Wi-F5D{r~J6zuaC8iGkbR5duH~`JlBnn9bC=3i5CE{ z`tTvW6990)CI{f*g8vaEWWxbaF+QyK(=Yz*W1Uvco#&HUr<3c&ZNFExii-2xZAs_f zTBDe*{K;UO^t^4@!_$t%V}_45-RnGUn0~|{{rPc=+BvS`lOFreS0r&t&2LhV#EDmf~D3iSQXTJE}7+}EwxjuYV1&X{hkEY zEJH`3x$S4d^IPhCO6wspKaNr zxz82>z+scsy}G5kMjwv~pTxYg!w-}n+4uyxY2o46mBdusp5TjH7Yrq@mmCWgoHXKp z-+Hu`0zmW)lLI_v1$GCV$ILCP_vKzZ?DnnY^&1KvL}wIkC6kMD?srdSk!y{FLfmFB zz~Q`DlnK@;ay{0unc^3lt-uVuu^z_*^mbCmGWu8CZiL$83jpA;+_8%3SpUAXWaTDJ z1uM%b2qG?MM7s@E3yg42&)-_D<^Zv5Lj`1Bl?>jb?Fw6QEvqBl69`Q`*MXqEMl} zyh@fSH*Lg#p4bJ+Dg#`F4rOAQ%_Wlg2M7RvgjURo)6F^Ei;msweoFG zYI-)|JggVF!Z(9lv&Q)l1^&eYM3>Jt0Dy)eU7VElygR)?m394%ATX=CPG&HmzPlSu zF0p*alS{P-fKmyi2x0|+;xsn^mr<9{F#v1^81@K(hX5jWXU_)t4$Zgj`Rb+!0CJ+E z^`(3$llxYgwlU~-uDH8p`b$?_>SU{YIHa`ef8@r*hZZv%%$ggV6giNu`&6u=>=($A z3X=X6s9vX6KJC+q)G6=p8G7!!i64_v^vZwghW~}B`Hc^siSh%Gvd!B>{ksk;^+SRH z9u!{f6w=LGP901ot2m(Y-L(OhU^W3=5MA(rJnycVM>|-|$pe;$G!~9IGpE!(F`yp3 z-&`<8RHn$gvtd-#jaWhq&6`#Djan26HSnnJz$TH#cbtOB+7qnQ+J zaN@z`5+=k7C?`9e(`1Y%S3bXa2KskzhTOH14|k==eOZu%V7YP;Nv6`wG(GRi;y4k9 zY$2xVWB_UZm|NT%NX=u9GQUiO&E7~;9DV-%j$YElYA#(6aUfun)i-H=!GI87X9DdkV_Nj3B1b(k#I9%4^sh>To$6)$A=Mj0c#GKYOk4 zA&R?8Q4oR*yKk?5IFgAG1Ov5GeTV4Y-KPzh&<&55i#h0%lT7Xs4yaBHR0rB>K>6gy zI5-Grhw0?IaJR~n?+&vdkGC)FLyJfhy8t2{8DR+qE+*_p}CnnRg_EzupeMD(+ zpN@li$hqrMnyAd{u;tsx8qlggNX@fiu7aE$HVcHza=(j3k_-D*W8_4DI1_)HTtj`He=SMAdh)OIvxa{RPO4IJ5t={S0JA zsL(2B%@@NYL#mB7L*zU~`+6-A7$pwmN(>jh`W9Q7nb|GUN>rmhQrz}) z%vc%O(2Z3NC*X25)X?S)8b$K*2P1vcBIOsrp}UV9Dv(;O1Gz=n`CsgteL3uH2pnQ| z5LuoKNqq=5JL>5AVBjg1fJ>7VNmxSl+1oIW4Ew5MY?6%VMyB#h(^xpS-?QbkS@&To?E0tVh{po8q3hOv*`t-3*v|c+1uI{eb_4p`z2KXlSF$ z3&*uwh;G%u*qTOW&8z6Zv;u{f3K=1L+&Ik&!IY^Kd8`Sg{KgyY0o+GW`3;XtuyYbY z8T+sV^NH>+s2%3b0|LnwFP1fbn~Eu`_D7IFu|+#8l>vmNG&kkzY!5rercSsDT>d#(9ebHNn!vims7 zOz)&6Uu!go&Q|n?4o;SYAj>fOh1aLDQP?3YwwcCJGOrb{?1iwDurq2ieg#pRz=^O!+hFDchXhjr za+`{#TE0KVMw=HcQr#UD#DmZI9>)@P2|Ob#p({tqbg_7Qmx_v&#S)nM*yF-BfPxi$ z_{^&59%3p1&S-ufWZSnp#a5n_i@{ql;yywnL9RO*$RJyX9xUZ+s+$E9&Os2>{Vts4 z=lc_upiXxiou-=qfZB{!HP=?lu33EH%FLGdRw&K zpiF~C+N>SlIGW?u;ARH=`>Uv@*?bJUuY|kRsB8tWrtNL6aQngC&}KBxuYJ=xH1OxV zA#KXxG&veve+G1bvlgwmA9d_!y<*EZh9UPpSux4;OKx&3RIJRP3KGd47Ho6FJvCzs zxy|@%;29N=N_b1e4Q;;yGY70?} ze`b6SFNl)>zPBa|SW*yjWawvw`Fb@D_r=O+2I?Z?cHqtohkPekQi|p%V59?Z$-@cd z6eIyqx!V>(^U7FvFx|F(Tx`WBvl==JM6_`D&h`7zz(JgpO5cRR{?LBU33keT+#)fd zqp9ZE1Kb~#W1)SBX8>8n``^0DGy`T%0w$L@@)QRk0{$9M+_O=HAFKf|XA#Y@Wc9DU zUC^IjFbL!y9mQ<#UaklJMBAz%*eB@Mg|=BQdoHuqs=_WTi3?ZT^nC+U^A)H!5zh{- z>3&0CUk6&XNcF2x6v2Ql4r2juuBiyE0MW@Ekx&f8T~>~O)^R|9{i?dDmU^&%#Td2$ zK9xsN&GQtv?~SHC(0i|n36=!MB=$(dUUU$+Z^4d%-3CB1o401ro*@uU(8qK~nXsC3 zl6g_j>G@s*r+y?|@VF$^)Oz092UdP?CvkFqvcbL>wqOOAIks^;c^|CNKy(hA3+Va> z2(oCs1ya^y39QPL^Cw(*sN-8Q^dz|=Cq(Wqb`7G7tDr)#l5$ntC zr)!LEY(&?_s#dt`SfDs~OIRqvdJ%0S=+D15Yp!*EUulPKhZNu7Aq`!A01ulE4GnZ1 z`txT^HZ6gT^_R`R$B`|P02EPncs66y>c4LOv4wvO{TIi*Uu*ij6Vx>&n|5sfUeHjz z-^W4o!jmWD8S-7`*j!)X8>~%3NNVaP1J^SP_jED~iA?(zA3wdKj4iR>3U}|4kbH@O z=hU-~(mHD1(oZ6(8Bx{DjPM=&ydeF~rMYDidAqi_+F*UQb&F85G;g7ZN6I;udbJ)v z03iS2R0enYTzSb7D7^F1SaE}Ow&bDB_$!#7pwp4v+ZpF3L)R4v=#LoCFv$LbKzx?) zeG9qhVt((g1~DtYEMXjS)v*G@d={_S!s~lnI4J5}@{$y_xTPZ$M7+=DH6uB`7KAB+ zT-O0S7=Q(e^+K#vV)=)iOjrvx%3)QR4K6 zhc{DdF$6OVu*{6_#e82{cs+lTfk6ghTvwYhDYU2F{$%dU>jC9n=6XLNsm`(w+C<>s zx${el_QvaCQm^FH-6r?pp~jWx$@4~^7#||xL4cr^X2YtMojhRbAx~)EV6zWR29Ia2 znFk}auAF=~2?)z+NHVG3lXdR^Ye zLQDOVlnmV+&Z__i=$@@6!vh)I5muG<=anBo^B-~OWH4dr&cpYJo$QPZ2(L=-Lka(8~tKXEOEr=KvFOKEu(^@vlz~F#1 z<(u-G{K>4nctJSJZ@1*Lyql!}C1K9lMg#S>77Gye3!&2u*7I-y>W(BpcIzJQBz?Xm zyt0DIHaLLjT6AUVXuzNtzpW-dUp^T-hXHYCN1{x^r6ChS%%rgtB=0`51pi77GN|z{7%cxGtti`N}Msk#Auoi%;N&oG5QI zL-yOk8dY#MM3NBr9(oD?z@34bnlriyq)WtPLUO~iq+4WD2AjSbv_2aeooF<2h0GqsY`#W>a@CZr39W}ei z4g6Ig?<(qHgK+@rw^K}>Z^Fu4J9+KB#mp4>NYK96t}e0Wq?@IoF7<=Nug7m}Q&pJN zZRpcxDzI|CuokJbWM;fTLw$0xjy&zPrm~t9Yt-nPwl!~Z*VX!?h6ghxxbDAyyQl;` ztGUVP(TZ=*=tM-FZDaCwro?Dq@DkXYSg_-j7u7^I()#W@(vmQ?an*9xx``&`k2eIm z2CiW_fDNhet%=643vuyv=DFo3^V;s<$j*1joUCEQEw>J_RJZZ^P!mF+{#te!(=_6+~t{{b36KZ z&NF_hxycHr0due5cXR#&s=TD>Kc00|Dk6 zleOQejSNqH(v+)X#c-Q~p(Y*uK89nt28_8%?E=D$ezr7Meg!$m?$8ruHM#v(^Kr*k zN(Q9>NmD3=+1Yw#z6K5A@g`-pUouv@nvbn;Bl{P9kvWOscz6+*@~+m-bICfepdqCG zqLd6sI7qu$bACPL=ml<(aF{U+mML?A+DHVYQ>PHiQ<(icf-u$1{S>U!zd4{@J_<@B z=B(l?!jL+2&X*oZK0mdsJs>DYU>z{;)0kDppmW3ASG4m42A-YYtyUv$_Q_<#lOwyI0O4#pqgmSydDulgDg4)MCXPzL9ML)%metX@cV|FTi`XZ8s;@P-=#qr=mk#(3gnyv z=feCbqX=FP^M73nva8_fj~^?z2E*}ITOaTY4x;C$f@dLnZ3O|vZ-*efpoyb#7p8u@ zG2!hI-O!hk{wr`-YxEl!z9WS-8=si^=@OK2W1FaWn)37`SkZmnMU8KkM61E>=az9l z9ms*VFrSsE@hzv(L$sh=`L7e`{Q}7)M?eV3ONW>~nLNr??<^l7K9OF_yuWifNgqCW z%t^$PWVOm6(SDBHxyCU4)OgKSKnXUW-p){7)&Ekn9NM%ya_R##Hn=R6uVHiD~XeraE)mSUjraqYjIygS*{ByGr|GpNSwiz%tr^zRLbaqO>fk(?~>9`|P=gl2+-q-x1?Z)#y zbn!4_alOvBZX6fh+@+1xK<@M!C|492K79ZHS%Bp7cK^KAW{F4|KMIRt*LtGsUE(L! zPwWq9U!W^M#QS8NyOl@Lu?M>cWfGAD$zP9c5ID4xV>h+1>)IN^aBWCQ zVqI{#MWB8jOA4NwBz?NNEQsOJ1uq2^&y3v|dzGphyMd)+ae{{se{@Y^#C3s#j~Ct? zD(+zh*ULcH)}&@nX4Pne%F`WKeQLWkYyO6<0U|8EfmjiB$Bl@?B`t=R>Rhn8 zzx0!+$!zmI(>k4(u5~=I7fumxcMX~SP;eFV@hp|t(V*le1aF<+&9L^d210R|o(;ez zDV;G5K$0OeMXBVAqz0fAtb)nQe6ICQ+y5Fo1RLVD?xxub!HB&5Ndpi_Ws~qozyVSo z#jeNTB}#EXFv@=^d6WeaHc(~^N8udkJ`GQ|_!ltkBlKT0tS=Zy!)%tB1c<#L#IikO ziut8I6CSsD+*mL_lv-B0UEqZH(UWGGfZ;V~;NjSyUc3w52ZP;@#j+qP_F=uxt-AKp z?q1sw7N4&A^a;X=Kg4z(DzThhDu_ zEt~LE6ab3E#)1RCT_fatF6l_-1~uVxN6%i_4Dp2E1J!iJ>f??jM+HFXRr|;Vg`}PE zl?#UY32A+bX%;ug{wXjPK3PHJRl8TZjts)($)jk6*;K%Rhi{>p0N}?~xS@*z>To}< za6otd4%Tqn8xj$o`tRr?52co~b)murk~CA|1;u5^TqftD(ZS$R>n=HHt4IPp!~V)# z4f_^_bpfmep!9Pe^*Rgk&xV|pe4C`)6qm1U==H(tIavwZ|JAuuNcm(F7YBIkL#1a_ z69>m%bKziudfo)ZIDnjIZAWQ)VE$8*hy-|R8woO5bb(PnCKfC^v3kp?eK4qa^Vvv> zg=Dz}2S7I5r)NwCn=-=Sz3vQ|L;d+Y_oeLeQ=9lfl`-w65p{l8LyD&az{7H6@7C_o zmif}nkZt>9ayCdR=Y$_pfZj1NY$J)Xg)ABIO!qsg+baVmdMUEpK~!1 zrxvT~6AP#a#4#s1hIms8+J7P<@V;kX7S|y+p|Z$XOyfnw=-RQ3!oXo&i^wDG5F?8T z0n3}k7F7E5i?cw!Woc5|<0L!*3;mEj=Ei%7e1hkY1ug-;CFP~8(3?DoLa4Vc1N4It z7`yjZk-XkGWh}rC_}oJ8J`%?SU>eL`L579y?wRg6l(5@%b?z=``}3{^s&T>)x$yp+ zX^=WU54yHL=z@NjH^CH1^F+WepQaBJ13x9Zc6vP4!2^(f3zNJiDWcjoia_gxNuU>% z+)HGXB@M~ME@e;+X<@mXpz1(i?qPDH_WPH3MG?*?QO%gTH2|+f{bViIHB)c6V7LV5 zlfz5A`-0N$8AJS`{IS*MXJ8gvQ@z#r-?jM|s%G({cqFv|T%z|wK{P&x8Ql@voFC=cDn%M2G zWn^Z?y--{SJJET27fU!%w72Z1TSB#uNnEkX#AH|N1sMp%TT@S$ef_t`4P-Z#lK4&u zrGS!s+#)WT7BwA$J;K1QVWM|Ps@GRzv==%sK%WQhgN^(2(eNnkRUOQc!unUJ!h19T z5b>;$p&QDwKtZb}5R{^TWdOYm+iGubZvL!}w z5q-QOyvKC2O8kgdG)ua21IemAxy(esymP$AzcG%?Pm4UkL?XK%u59}*V ziT!yJdHBmnvU`J1qHy5tKKW;rp0`iEHf!v1zF2M5a`9|?yWtU`gEzjY9E;Gy3n}ha zTKifI;@Juld$Sr(sX8~3tUj)DkQ85f}a3)A=m~n72~4h#aQyI z4~A&hm@DY2ugNJnAofU zs2_s0*x5mR0+xC|7izKj*vOt=>FQv0hlK{A{gkRx9~Y8$f(util7wMjxb;XIsjOE= zJRzS4ooxgh-pq zHRbqo*rb$fm~T(}Ar!y!Q^ry902be-Y=^#)1L5k#eEM@v%D32SKd7)TPsZkS4cAe^ zk{+BxF^s{^ij3Xl36y4kKl(^azyzPUN79s91b2(Sd7WTQ6*jO(9LM zLavk6{s8o`Xylk4RZDPr6hPhZk--e=13mQ+#To)Q7imfbH?SX3~HX>OL3O97p5DXp+N8K2UZK@&Q@6PfCS6h z^j{;J;fM*?kH#DvubO80tOq?mXx73JD^PQ5+>0L_^t5>`8e+vs(W4%!;XkIvCXk59 z5BNAFCkt!?-cVAXplEpYmvlEq+p99BGMB(Rf>A9REZ|M}*vGBEsQ-x%3F|ViSu?p% z36)I2sXVC86kEPUQeU{w*6lM!thP^V7iiQ9hcWp9(%%`CakQ%#+9{>VUMR-BRZj<3 za3bds$XU?EiW!&h-@ykyv0c~hKTqxI-`%x<#jBBg$Uj(c&7NDC*cDDS!6-JUu3I@M z<-s<|T;}{_%o~kUU@I|X@;CKo{N}As@*(k$0-=@h=yHW@I20e0YRcMI1P-k!Uxy}} z`Qy{^|pfabz%R$ z=<^R_1D$Lwy4RfWJtkU~%So6iWLq=j@9P@CcGkfXZCAF;Q_)IZAM36tfIG|6A@^YR zqpLS?y_4_xv1WY9%w;Fw3Us>RaW(uV+xBmoG-B=@B`-%_#9a;#F~8YfaDSfO!t{9o zAF<#CIH0Wluzj&)-R(|7D38lDKxTg*w##_3f{H${L@$QHJ#g8iMSsrha#B4B#>I4D8w7u@!ZMzryE zlh=;Bct$c39_HWzba&dYLZXwF+=0cr{2cr!wB|U3f+IaEj)PX*CCtHMa0o7M9{$dC zY!j};=DU0>4LET_T>jwGmhJaYWZ@GZ3wL%5jL$wFm_FNvE)E3Y`Hy7r#FhFGjAAEQ zZl*OB1qRps67;c<7x6jNvUN-|0>c#Ib-;N^_lwtFu&*9%PLFlRg3-ZnmVL>Wuh z4>DQ93Z#dlW9GFhy7cLAY9YD;B2}|5Sus|#%zyzBIOMmTWUO<6osgEahH&cBw6W*F z;gYvsZ;7w?+)3zULU!7V2QBHuE4$G|Q8R!=7O1)rbNu8fxyXyRB`2;+T-)WCcS%Ya z@_t-lz&fEX*EpE4VgX$JmfKIz`-veM8Y|PBL8!wV%Av#BbXd!Nd>#`zk?I&h)mW^g zA4&=&+Lp)(hxs-z`A&naj&CkKWXETXGhN|hBl(0dj6^M{9}8|Yl7h%Cx0M`@R}dfZ zA$^pguKi{69_(;BJd4pf$t9P6v4OnmL$TIz4=p@IZg@M&Jh8Bw)^c2~LiKxJ-IjZ8 zwdHK*@71Db)SSMWlWrdC_OW{9)-R1(zlLf;!46kr_85)r|K$PaT>4JXxSn4pbn4*| z$O}AJ$2|FdcdrjIT>-?P03Mdr3GicuW|pS`W7L;;y5C`p`)iIvQ5f#Pi)^}ygw0}Y z)9+~0jaOM|clO0BKU`uq&wAMPv^k%3%rQ!CK{Q!R8+2Sd;A!Nah|U1r_YxaIA_H8uj5uZhlNyL$5C$!(B|a2@8fzprzL zkX0h13?|7LwZ%_MG!308B9us2rByIny92pWm!JQ=D7)6t#$_ zg}xIUIfD%z6i_JN3<3Dal3`ZmOdm)Sv>|4QVT9kNR;}TF+VGQ&fpJ0+) z#8>$bhc}@KgIujR=hZg-z6ySG!Hb+w_wv|Gnq7%2xRQS0037~&Z=|StOz(RZtH!t! zl3$j^K}Wo84wV6qcYnYhOJoEr(1UbLP=(Wdy+^;d(U-KpLrxP5Ftu+uB5|D?;E6obTxcI-lw+11-?u!Y`=X{ONfgVR@&?&j}jd zjN(|+71nk+Nh|lHA%??qM(Qm)Yl)y2N}c3dXjHVwq_vh*hW1iG>`fj^rrlxm(BfQ0 zFp@La!h}`EE>!ObEb^)>*u`<#pYrVeH1eB&a<|X7+o`aI@3RPQwYcNJc&po)099sB zc=GuitEC&yAS3*#zz!XaR~o-A%fh{;Q*!F#OKY=D(74?S^BRh@r!Guh@R-k+Gl4mz z^VZ}N+-9tNu27-!iQ}fQ9jMoR&x`dK);7qUYfM-@s`Tc-`ZNVCb{CLnI63m~^*V||% z3?IZ0ni5}@3f?$}eFD=8uRa?~-qU3rFZw(PF9RHsKefd6T8lMd+!*yzVl8~6_1PQY z)riB0Nr-ZIxowM9hphF*SN&2$SphOKAi~m+hhChg+f9`x4Oc;ZDrx#O^8US60Vy6= zsbZnRKyR%d58ZdTvwvq;Nj81Y1|99mA&A#7o=U&_P~X;+wa{O(BJibHt>P9Ik(p1U z1l>^Tu9YS3;<7|x-i yAkA3%krN1&X1~Z}kw?d^wd071B}>8$EX;UJ-;ifKhZTP)bz6JBl{n$5x$k~W*U-{H@Cab1v z?tRQ$yYc?L&x~D8PQj66%qXa^=GqT-cGWkUTkx=dQb)CHU)bHq%A{^8n5XyG52qD` z$=-|xrj*Q7ofgcg8OXtKU_yUJFdLtXco7*%nAn(2&ti42aUcg{8y-C^_-^nQti6&k zzByw!)#k%KY>Om>Z{zZrhB_DaB0WpWoS3{CKaBxo-4`$ES|Aa^`YUUi7L~MyM{eY0 zA?jB&gn^_!pN3$`fnW#*5DXv~)TrPmkD?S`*`lZ6xbfODDV>s$1f}9hQZ$vc2B-16 z5cFh)=T6kqVR0SK_M^t@niR}J9?vXM_=iAAKQ|eDQj*Wv z5!d&0D_+oh|5@HjzixQFm5yELG`=u3BbaCEqp{zOSt~pI1&;k0q!az;Dl(hZO3d6^ zk-H0zmB-9umtOE=&r(gv&6LbzmpVmrY7YOHea&@<6s9`%e@UrCk`g9`85cNQY*|{n zg#fcJUa79hNY`TQ?w3!NHC2~YaAxt5iTnIXYJ8{5av>5IpSSkSumAgdy$Is5-cHMz z#cYN(zTHL6j-!^Bq0rq$iriZ_jX^mDmo-Hvv!v!iz)!2p5*1Oo^L5DXv~KrjRY2nG-gAQ(U}fM5W@5DXv~Krnz{0Kou)0R%%ZfM5W@ z0D=Jo0|*8X3?LYS0R#gGru;n)WThI$R@VRcZq}=S8(37L-Ap;!2bneJD0lCqvI@@D z2_^LXqEwQNrW|5SR$?GUiawI1dr9QCb>qis|AuT|im2#z!u=4b1Pw!2l+|lWL8Q`?cGqLG(__=y_C3gJ`}EE~sj@L3Cy?DjfvENfv-$0Kou)0R#g}yA}5#myR&V3A5bM)n8lKi=~B(y zGhJ)Fh*@lN*kR_`#xsvT44m#$x?OFMY*X0X_4fCatPkC}H5Yw;<|75?(`3tY=Ud%3 zl)O(>^4{srxtmyzZ3a6tJ9bvh52@Q}sOt)%W)s*IJ2OMk1=-#>ANJ8z--sUBiD5=p zX475 zr)ZW%SZR~9kzgPPw00@rq~t>6y&>)PXtgCT6OR9_cubwcdG}yg8e$~btv*&azp?>; z#3w4^+V!@pg&b)3-yX7hq2${Q>tD$9C#Kh8&bo~}G)buBX*T~wiOp%@e?%AT5`7qL zx`!S~r`7zT=!5x`XSR4TQ6`|T=)%D%s}#3^{+`0L$>i?_tKGcvb5>`gL2N*v3Cy+>7@=?IxMnp7>c*CfqwQ7h*(#O0C((b zhH&5FHRuKX0wq-pe>$Acn>j7-T+=SSFUX|4tiUHQ6uM`1^2Xcfs^WE;o_EGpea^wL z?P=KNYZ*@CFdyH~h8WlSnCBslG8{7R*t~P^(9*hWnam8oN-K%uN>3K4EMSP23ecJ5 z>|)?ab0-{4fhY`j#SjsI2(|b@limk97tv=J!dA#^PmUDju1JZ?j{8^r|4D&YTIhSkk|=5Wk_`=iRZrR%OS4~;lovf# zgw+`7FP*A$l{i;*&v|NpZ?)T{i)ZwV*?<1W@<6L?z+K;TtUoAwhJc^u05Y-Hy`(iSd!D#TGAp zj0Bd^l}2-1izf!`4W&8>wu7qq3DxHR7Qxr99`0$dxkJk(DBwS!WXZT4^wOL>hGyei z_gq?oJ+m$PPpjgPk6`m9Ll@2iL~+nSY2&J8qY+@LT-Q&EVWa{d6&gKw|u zXp49D@}Lt9?AjGt@$(=xZ|XbKp--B?kT5JHJ*q)xGhCo7_-EEPC^D&XoAPJqX`6Dh zgBuj&PunKnl>gyBZU6TFwEh3rL0bQP9Z1+d`uz7#-%FeW9_V>f$2OFbaK>+atwc!A zc}O?GcB7&PT5c*%z!`%^)3XvoGJNmn<5nEx-FG=K(2VNB;Ew!YrH|~P(^HY2%er@C zFs{7%R^W-j{W`!@i9=B~CEBvCIBll9v_AV$O6xSE zLl$LxG-##ra&OC(A-NelhJT(66905D2bF~mxnDC&7zw5YNA?Ye_o_d~jMTBfY92v0 zbNT|ThC1CSz~+1f?ZuN?m1o`)e~}ua0^itb(Xo4DbpGn|5Mq;>d-gDgo3~m z8llYh`!7dpDiBla`|^R)J~%6P(-1Xf#nD>#zM^hgQ6sTrPw+W_W9%!boh^A@Ep|?$RtZCwds> z)OA2x*V~%}N)n29qL-9Zor}nITm!@yb|p2oZxGfQTD5K#>X*m!*6Z4;@U0)kInWS6 zS`)u#eOZ8(?14U|pNViAN1#2wirXT_dj^37YQ&s`>}Lz{i4=dMu1BD@6CR(rQta9T zWFPLs2EubrR2A1b8qO)K2Uxd2_Fj?Q8xnD@<=bsFJFseyE{E;)1HnUfz8ihJyrr2| z*Q>50)d;&gD+`-?(8#0w$$f-g-BV+N7si?S;+1VBMX}+Fj?J2-a#eB`hTOrL!}#h2 zjdNX`{TcpNiQ|Y@k;wTHfm2UGyCg*Q8IuS$oI~lg9xi*<^F&!qC#Ii3E0e6P*>`4; zgwwEW!Ld1yQ@X9;*zq)7MDXYOE^KpRM)gv6d3qa%`fg^vGeK7@vW{EG;S3Jpjqr~* zN|eT(cDz15!DbEEgL%!bzJROJX}1|Bp_-R5vqU(4`=yLp1L+v#y1?Sm&1HgFGz1Zj lhOovC|9$`f literal 0 HcmV?d00001 diff --git a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@1x.png b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..cdd488d7b485b8543e2d91f855a673bc5479e466 GIT binary patch literal 955 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/native/src-tauri/gen/apple/Podfile b/native/src-tauri/gen/apple/Podfile new file mode 100644 index 0000000..a517b3c --- /dev/null +++ b/native/src-tauri/gen/apple/Podfile @@ -0,0 +1,14 @@ +platform :ios, '17.0' + +target 'kanbangit-native_iOS' do + # Pods for kanbangit-native_iOS +end + +# Inherit the deployment target from the Podfile. +post_install do |installer| + installer.pods_project.targets.each do |target| + target.build_configurations.each do |config| + config.build_settings.delete 'IPHONEOS_DEPLOYMENT_TARGET' + end + end +end diff --git a/native/src-tauri/gen/apple/Sources/kanbangit-native/WorkspacePolicy.swift b/native/src-tauri/gen/apple/Sources/kanbangit-native/WorkspacePolicy.swift new file mode 100644 index 0000000..730d682 --- /dev/null +++ b/native/src-tauri/gen/apple/Sources/kanbangit-native/WorkspacePolicy.swift @@ -0,0 +1,58 @@ +import Foundation + +struct WorkspaceServer: Codable, Equatable { + let url: URL + let profile: UUID + + static func normalize(_ input: String) -> URL? { + let input = input.trimmingCharacters(in: .whitespacesAndNewlines) + guard input.lowercased().hasPrefix("https://"), + !input.contains("\\"), + input.rangeOfCharacter(from: .controlCharacters) == nil, + var components = URLComponents(string: input), + components.scheme?.lowercased() == "https", + let host = components.host, !host.isEmpty, + components.user == nil, components.password == nil, + components.query == nil, components.fragment == nil, + components.port.map({ (1...65535).contains($0) }) ?? true else { return nil } + components.scheme = "https" + components.host = host.lowercased() + if components.port == 443 { components.port = nil } + if !components.path.hasSuffix("/") { components.path += "/" } + return components.url + } + + func allows(_ destination: URL) -> Bool { + destination.scheme?.lowercased() == "https" + && destination.host?.lowercased() == url.host?.lowercased() + && (destination.port ?? 443) == (url.port ?? 443) + && destination.user == nil && destination.password == nil + } + + static func selecting(_ url: URL, current: WorkspaceServer?) -> WorkspaceServer { + current?.url == url ? current! : WorkspaceServer(url: url, profile: UUID()) + } +} + +struct WorkspaceConnection { + private(set) var revision = 0 + private(set) var ready = false + private(set) var connecting = false + + mutating func begin() -> Int { + revision += 1 + connecting = true + return revision + } + + mutating func finish() { + connecting = false + ready = true + } + + mutating func fail(_ expectedRevision: Int) -> Bool { + guard expectedRevision == revision, connecting else { return false } + connecting = false + return true + } +} diff --git a/native/src-tauri/gen/apple/Sources/kanbangit-native/WorkspaceViewController.swift b/native/src-tauri/gen/apple/Sources/kanbangit-native/WorkspaceViewController.swift new file mode 100644 index 0000000..e8d90a1 --- /dev/null +++ b/native/src-tauri/gen/apple/Sources/kanbangit-native/WorkspaceViewController.swift @@ -0,0 +1,318 @@ +import UIKit +import WebKit + +@_cdecl("install_ios_workspace") +func installIOSWorkspace() { + DispatchQueue.main.async { + guard let root = UIApplication.shared.connectedScenes + .compactMap({ $0 as? UIWindowScene }).flatMap(\.windows) + .first(where: { $0.isKeyWindow })?.rootViewController + ?? UIApplication.shared.windows.first?.rootViewController else { return } + guard !root.children.contains(where: { $0 is WorkspaceViewController }) else { return } + let controller = WorkspaceViewController() + root.view.subviews.forEach { $0.isHidden = true } + root.addChild(controller) + root.view.addSubview(controller.view) + controller.view.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + controller.view.topAnchor.constraint(equalTo: root.view.topAnchor), + controller.view.bottomAnchor.constraint(equalTo: root.view.bottomAnchor), + controller.view.leadingAnchor.constraint(equalTo: root.view.leadingAnchor), + controller.view.trailingAnchor.constraint(equalTo: root.view.trailingAnchor), + ]) + controller.didMove(toParent: root) + } +} + +final class WorkspaceViewController: UIViewController, WKNavigationDelegate, WKUIDelegate, UITextFieldDelegate { + private let defaults: UserDefaults + private let serverKey = "ios.activeServer" + private(set) var server: WorkspaceServer? + private(set) var workspace: WKWebView? + private var connection = WorkspaceConnection() + private var pendingNavigation: WKNavigation? + private var timeout: DispatchWorkItem? + private let launcher = UIScrollView() + private let stack = UIStackView() + private let address = UITextField() + private let status = UILabel() + private let connectButton = UIButton(type: .system) + private let resumeButton = UIButton(type: .system) + private let toolbar = UIToolbar() + private var backButton: UIBarButtonItem! + private var appearance: UISegmentedControl! + + init(defaults: UserDefaults = .standard) { + self.defaults = defaults + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + private func text(_ key: String) -> String { + NSLocalizedString(key, comment: "KanbanGit iOS workspace") + } + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .systemBackground + view.tintColor = UIColor(red: 0, green: 0.43, blue: 0.18, alpha: 1) + configureLauncher() + configureToolbar() + if let data = defaults.data(forKey: serverKey) { + if let saved = try? JSONDecoder().decode(WorkspaceServer.self, from: data), + WorkspaceServer.normalize(saved.url.absoluteString) == saved.url { + server = saved + } else { + status.text = text("storageError") + } + } + address.text = server?.url.absoluteString ?? "https://kanbangit.de/" + showSettings() + if let server { open(server.url) } + } + + private func label(_ key: String, style: UIFont.TextStyle = .body) -> UILabel { + let label = UILabel() + label.text = text(key) + label.font = .preferredFont(forTextStyle: style) + label.adjustsFontForContentSizeCategory = true + label.numberOfLines = 0 + return label + } + + private func configureLauncher() { + launcher.accessibilityIdentifier = "serverSettings" + launcher.keyboardDismissMode = .interactive + launcher.alwaysBounceVertical = true + view.addSubview(launcher) + launcher.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + launcher.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + launcher.bottomAnchor.constraint(equalTo: view.keyboardLayoutGuide.topAnchor), + launcher.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + launcher.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), + ]) + stack.axis = .vertical + stack.spacing = 18 + stack.translatesAutoresizingMaskIntoConstraints = false + launcher.addSubview(stack) + let width = stack.widthAnchor.constraint(equalTo: launcher.frameLayoutGuide.widthAnchor, constant: -40) + width.priority = .defaultHigh + NSLayoutConstraint.activate([ + stack.topAnchor.constraint(equalTo: launcher.contentLayoutGuide.topAnchor, constant: 32), + stack.bottomAnchor.constraint(equalTo: launcher.contentLayoutGuide.bottomAnchor, constant: -32), + stack.centerXAnchor.constraint(equalTo: launcher.frameLayoutGuide.centerXAnchor), + stack.leadingAnchor.constraint(greaterThanOrEqualTo: launcher.contentLayoutGuide.leadingAnchor, constant: 20), + stack.trailingAnchor.constraint(lessThanOrEqualTo: launcher.contentLayoutGuide.trailingAnchor, constant: -20), + stack.widthAnchor.constraint(lessThanOrEqualToConstant: 480), width, + ]) + stack.addArrangedSubview(label("appName", style: .headline)) + stack.addArrangedSubview(label("title", style: .largeTitle)) + stack.addArrangedSubview(label("description")) + stack.addArrangedSubview(label("serverLabel", style: .headline)) + address.borderStyle = .roundedRect + address.keyboardType = .URL + address.textContentType = .URL + address.autocorrectionType = .no + address.autocapitalizationType = .none + address.clearButtonMode = .whileEditing + address.returnKeyType = .go + address.delegate = self + address.accessibilityLabel = text("serverLabel") + address.accessibilityIdentifier = "serverAddress" + address.heightAnchor.constraint(greaterThanOrEqualToConstant: 48).isActive = true + stack.addArrangedSubview(address) + stack.addArrangedSubview(label("serverHelp", style: .footnote)) + connectButton.configuration = .filled() + connectButton.setTitle(text("openWorkspace"), for: .normal) + connectButton.accessibilityIdentifier = "openWorkspace" + connectButton.addTarget(self, action: #selector(connect), for: .touchUpInside) + stack.addArrangedSubview(connectButton) + status.numberOfLines = 0 + status.accessibilityIdentifier = "connectionStatus" + status.font = .preferredFont(forTextStyle: .body) + status.adjustsFontForContentSizeCategory = true + stack.addArrangedSubview(status) + resumeButton.setTitle(text("resume"), for: .normal) + resumeButton.accessibilityIdentifier = "resumeWorkspace" + resumeButton.addTarget(self, action: #selector(showWorkspace), for: .touchUpInside) + stack.addArrangedSubview(resumeButton) + stack.addArrangedSubview(label("appearance", style: .headline)) + appearance = UISegmentedControl(items: [text("system"), text("light"), text("dark")]) + appearance.selectedSegmentIndex = min(2, max(0, defaults.integer(forKey: "ios.appearance"))) + appearance.accessibilityIdentifier = "appearance" + appearance.addTarget(self, action: #selector(changeAppearance), for: .valueChanged) + stack.addArrangedSubview(appearance) + [connectButton, resumeButton, appearance].forEach { + $0.heightAnchor.constraint(greaterThanOrEqualToConstant: 44).isActive = true + } + changeAppearance() + } + + private func configureToolbar() { + toolbar.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(toolbar) + backButton = UIBarButtonItem(title: text("back"), style: .plain, target: self, action: #selector(goBack)) + backButton.accessibilityIdentifier = "workspaceBack" + let settings = UIBarButtonItem(title: text("serverSettings"), style: .plain, target: self, action: #selector(showSettings)) + settings.accessibilityIdentifier = "showServerSettings" + toolbar.items = [backButton, UIBarButtonItem(systemItem: .flexibleSpace), settings] + NSLayoutConstraint.activate([ + toolbar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + toolbar.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + toolbar.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), + ]) + } + + @objc private func changeAppearance() { + defaults.set(appearance.selectedSegmentIndex, forKey: "ios.appearance") + overrideUserInterfaceStyle = [.unspecified, .light, .dark][appearance.selectedSegmentIndex] + } + + func textFieldShouldReturn(_ textField: UITextField) -> Bool { connect(); return true } + + @objc private func connect() { + guard let url = WorkspaceServer.normalize(address.text ?? "") else { + status.text = text("invalidServer") + UIAccessibility.post(notification: .announcement, argument: status.text) + return + } + if let server, server.url != url { + let alert = UIAlertController(title: text("confirmSwitch"), message: text("switchWarning"), preferredStyle: .alert) + alert.addAction(UIAlertAction(title: text("cancel"), style: .cancel)) + alert.addAction(UIAlertAction(title: text("confirmSwitch"), style: .destructive) { [weak self] _ in self?.open(url) }) + present(alert, animated: true) + } else { open(url) } + } + + func open(_ url: URL) { + if server?.url == url, connection.ready { showWorkspace(); return } + let selected = WorkspaceServer.selecting(url, current: server) + guard let data = try? JSONEncoder().encode(selected) else { status.text = text("storageError"); return } + defaults.set(data, forKey: serverKey) + timeout?.cancel() + workspace?.stopLoading() + workspace?.navigationDelegate = nil + workspace?.removeFromSuperview() + connection = WorkspaceConnection() + server = selected + let configuration = WKWebViewConfiguration() + configuration.websiteDataStore = WKWebsiteDataStore(forIdentifier: selected.profile) + // Remote content receives a plain WKWebView, with no native message handlers or Tauri bridge. + let webview = WKWebView(frame: .zero, configuration: configuration) + webview.navigationDelegate = self + webview.uiDelegate = self + webview.allowsBackForwardNavigationGestures = true + webview.accessibilityIdentifier = "workspace" + webview.translatesAutoresizingMaskIntoConstraints = false + webview.isHidden = true + view.insertSubview(webview, belowSubview: launcher) + NSLayoutConstraint.activate([ + webview.topAnchor.constraint(equalTo: toolbar.bottomAnchor), + webview.bottomAnchor.constraint(equalTo: view.keyboardLayoutGuide.topAnchor), + webview.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + webview.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), + ]) + workspace = webview + view.endEditing(true) + address.text = url.absoluteString + beginNavigation() + pendingNavigation = webview.load(URLRequest(url: url)) + } + + private func beginNavigation() { + timeout?.cancel() + let revision = connection.begin() + status.text = text("connecting") + connectButton.isEnabled = false + resumeButton.isHidden = !connection.ready + let work = DispatchWorkItem { [weak self] in self?.fail(revision) } + timeout = work + DispatchQueue.main.asyncAfter(deadline: .now() + 15, execute: work) + } + + private func fail(_ revision: Int) { + guard connection.fail(revision) else { return } + timeout?.cancel() + status.text = text("connectionError") + connectButton.isEnabled = true + connectButton.setTitle(text("retry"), for: .normal) + showSettings() + } + + @objc private func showSettings() { + view.endEditing(true) + launcher.isHidden = false + toolbar.isHidden = true + workspace?.isHidden = true + resumeButton.isHidden = !connection.ready + } + + @objc private func showWorkspace() { + guard connection.ready else { return } + view.endEditing(true) + launcher.isHidden = true + toolbar.isHidden = false + workspace?.isHidden = false + backButton.isEnabled = workspace?.canGoBack == true + } + + @objc private func goBack() { if workspace?.canGoBack == true { workspace?.goBack() } } + + func webView(_ webView: WKWebView, decidePolicyFor action: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { + guard webView === workspace, let url = action.request.url, + server?.allows(url) == true else { decisionHandler(.cancel); return } + if action.targetFrame == nil { + decisionHandler(.cancel) + webView.load(action.request) + } else { decisionHandler(.allow) } + } + + func webView(_ webView: WKWebView, decidePolicyFor response: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) { + guard webView === workspace, let url = response.response.url, + server?.allows(url) == true else { decisionHandler(.cancel); return } + decisionHandler(.allow) + } + + func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { + guard webView === workspace else { return } + pendingNavigation = navigation + beginNavigation() + } + + func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + guard webView === workspace, navigation === pendingNavigation else { return } + let firstLoad = !connection.ready + timeout?.cancel() + connection.finish() + connectButton.isEnabled = true + connectButton.setTitle(text("openWorkspace"), for: .normal) + status.text = text("connected") + resumeButton.isHidden = false + backButton.isEnabled = webView.canGoBack + if firstLoad { showWorkspace() } + } + + func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { + navigationFailed(webView, navigation, error) + } + + func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { + navigationFailed(webView, navigation, error) + } + + private func navigationFailed(_ webView: WKWebView, _ navigation: WKNavigation?, _ error: Error) { + guard webView === workspace, navigation === pendingNavigation, + (error as NSError).code != NSURLErrorCancelled else { return } + fail(connection.revision) + } + + func webViewWebContentProcessDidTerminate(_ webView: WKWebView) { + guard webView === workspace else { return } + connection = WorkspaceConnection() + status.text = text("processTerminated") + connectButton.isEnabled = true + showSettings() + } +} diff --git a/native/src-tauri/gen/apple/Sources/kanbangit-native/bindings/bindings.h b/native/src-tauri/gen/apple/Sources/kanbangit-native/bindings/bindings.h new file mode 100644 index 0000000..6c2bd87 --- /dev/null +++ b/native/src-tauri/gen/apple/Sources/kanbangit-native/bindings/bindings.h @@ -0,0 +1,7 @@ +#pragma once + +namespace ffi { + extern "C" { + void start_app(); + } +} diff --git a/native/src-tauri/gen/apple/Sources/kanbangit-native/en.lproj/Localizable.strings b/native/src-tauri/gen/apple/Sources/kanbangit-native/en.lproj/Localizable.strings new file mode 100644 index 0000000..ddac212 --- /dev/null +++ b/native/src-tauri/gen/apple/Sources/kanbangit-native/en.lproj/Localizable.strings @@ -0,0 +1,23 @@ +"appName" = "KanbanGit"; +"title" = "Choose your workspace."; +"description" = "Connect to your KanbanGit installation over HTTPS."; +"serverLabel" = "Server address"; +"serverHelp" = "Use the installation address without login details, a query or a fragment."; +"openWorkspace" = "Open workspace"; +"retry" = "Try again"; +"resume" = "Return to workspace"; +"switchWarning" = "Switching servers closes the current workspace and discards unsaved inputs. You will sign in separately on the new server."; +"confirmSwitch" = "Switch server"; +"cancel" = "Cancel"; +"connecting" = "Connecting to your server…"; +"connected" = "Your workspace is open. Use Server settings to change servers."; +"connectionError" = "The server did not finish loading. Check your connection, server address and HTTPS certificate, then try again. An already open workspace and its inputs are kept."; +"invalidServer" = "Enter a valid HTTPS installation address without login details, a query or a fragment."; +"storageError" = "The saved server setting could not be read. Enter your server address again."; +"processTerminated" = "iOS closed the workspace to free memory. Open it again to continue. Unsaved inputs may have been lost."; +"appearance" = "Appearance"; +"system" = "System"; +"light" = "Light"; +"dark" = "Dark"; +"back" = "Back"; +"serverSettings" = "Server settings"; diff --git a/native/src-tauri/gen/apple/Sources/kanbangit-native/main.mm b/native/src-tauri/gen/apple/Sources/kanbangit-native/main.mm new file mode 100644 index 0000000..e0f0e74 --- /dev/null +++ b/native/src-tauri/gen/apple/Sources/kanbangit-native/main.mm @@ -0,0 +1,36 @@ +#include "bindings/bindings.h" +#import +#include +#include + +extern "C" void install_ios_workspace(void); + +int main(int argc, char * argv[]) { + @autoreleasepool { + // Xcode mirrors os_log to stderr. Tauri 2.11 redirects stderr back to os_log, + // which can fill its pipe and block UIKit/XCTest. Keep Xcode's original streams. + const bool debuggerLogging = getenv("OS_ACTIVITY_DT_MODE") != nullptr + || getenv("XCTestConfigurationFilePath") != nullptr; + __block int originalOut = debuggerLogging ? dup(STDOUT_FILENO) : -1; + __block int originalError = debuggerLogging ? dup(STDERR_FILENO) : -1; + [[NSNotificationCenter defaultCenter] + addObserverForName:UIApplicationDidBecomeActiveNotification + object:nil + queue:NSOperationQueue.mainQueue + usingBlock:^(NSNotification *notification) { + if (originalOut >= 0) { + dup2(originalOut, STDOUT_FILENO); + close(originalOut); + originalOut = -1; + } + if (originalError >= 0) { + dup2(originalError, STDERR_FILENO); + close(originalError); + originalError = -1; + } + install_ios_workspace(); + }]; + ffi::start_app(); + return 0; + } +} diff --git a/native/src-tauri/gen/apple/Tests/WorkspaceTests.swift b/native/src-tauri/gen/apple/Tests/WorkspaceTests.swift new file mode 100644 index 0000000..95d8602 --- /dev/null +++ b/native/src-tauri/gen/apple/Tests/WorkspaceTests.swift @@ -0,0 +1,118 @@ +import XCTest +import WebKit + +final class WorkspaceTests: XCTestCase { + func testServerValidationAndOriginBoundary() throws { + let url = try XCTUnwrap(WorkspaceServer.normalize(" HTTPS://Example.com:443/kanban ")) + XCTAssertEqual(url.absoluteString, "https://example.com/kanban/") + let server = WorkspaceServer(url: url, profile: UUID()) + for value in ["http://example.com/", "https://user:secret@example.com/", "https://example.com/?token=1", "https://example.com/#x", "https://exa\nmple.com", "https://example.com\\evil", "file:///etc/passwd", "javascript:alert(1)", "https://example.com:99999/"] { + XCTAssertNil(WorkspaceServer.normalize(value), value) + } + for value in ["https://example.com/login", "https://example.com/two-factor-challenge", "https://example.com/projects/1?ticket=54#details"] { + XCTAssertTrue(server.allows(try XCTUnwrap(URL(string: value))), value) + } + for value in ["http://example.com/", "https://example.com:8443/", "https://evil.example.com/", "https://user@example.com/", "file:///tmp/index.html", "tauri://localhost/action/open"] { + XCTAssertFalse(server.allows(try XCTUnwrap(URL(string: value))), value) + } + } + + func testServerChangesNeverReusePreviousProfile() throws { + let firstURL = try XCTUnwrap(WorkspaceServer.normalize("https://example.com/")) + let otherURL = try XCTUnwrap(WorkspaceServer.normalize("https://example.com/other/")) + let first = WorkspaceServer.selecting(firstURL, current: nil) + XCTAssertEqual(WorkspaceServer.selecting(firstURL, current: first), first) + let second = WorkspaceServer.selecting(otherURL, current: first) + XCTAssertNotEqual(second.profile, first.profile) + XCTAssertNotEqual(WorkspaceServer.selecting(firstURL, current: second).profile, first.profile) + XCTAssertEqual(try JSONDecoder().decode(WorkspaceServer.self, from: JSONEncoder().encode(first)), first) + } + + func testTimeoutDoesNotDiscardLoadedWorkspaceOrAffectLaterNavigation() { + var state = WorkspaceConnection() + let first = state.begin() + let second = state.begin() + XCTAssertFalse(state.fail(first)) + state.finish() + XCTAssertFalse(state.fail(second)) + let third = state.begin() + XCTAssertTrue(state.fail(third)) + XCTAssertTrue(state.ready) + XCTAssertFalse(state.fail(third)) + } + + @MainActor + func testWKWebViewPersistsCookiesAcrossViewsAndIsolatesServerChanges() async throws { + let profile = UUID() + let first = WKWebViewConfiguration() + first.websiteDataStore = WKWebsiteDataStore(forIdentifier: profile) + let cookie = try XCTUnwrap(HTTPCookie(properties: [ + .domain: "example.com", .path: "/", .name: "laravel_session", .value: "test-session", + .secure: "TRUE", .expires: Date().addingTimeInterval(3600), + ])) + await first.websiteDataStore.httpCookieStore.setCookie(cookie) + let restored = WKWebViewConfiguration() + restored.websiteDataStore = WKWebsiteDataStore(forIdentifier: profile) + let restoredCookies = await restored.websiteDataStore.httpCookieStore.allCookies() + XCTAssertTrue(restoredCookies.contains { $0.name == "laravel_session" && $0.value == "test-session" }) + let other = WKWebsiteDataStore(forIdentifier: UUID()) + let otherCookies = await other.httpCookieStore.allCookies() + XCTAssertTrue(otherCookies.isEmpty) + await restored.websiteDataStore.httpCookieStore.deleteCookie(cookie) + let loggedOut = await first.websiteDataStore.httpCookieStore.allCookies() + XCTAssertFalse(loggedOut.contains { $0.name == "laravel_session" }) + } + + @MainActor + func testControllerRestoresServerAndCreatesIsolatedUnprivilegedWebView() throws { + let name = "KAN54-tests-\(UUID())" + let defaults = try XCTUnwrap(UserDefaults(suiteName: name)) + defer { defaults.removePersistentDomain(forName: name) } + let controller = WorkspaceViewController(defaults: defaults) + controller.loadViewIfNeeded() + let url = try XCTUnwrap(WorkspaceServer.normalize("https://example.invalid/")) + controller.open(url) + let server = try XCTUnwrap(controller.server) + XCTAssertEqual(controller.workspace?.configuration.websiteDataStore.identifier, server.profile) + XCTAssertTrue(controller.workspace?.configuration.userContentController.userScripts.isEmpty == true) + let restored = WorkspaceViewController(defaults: defaults) + restored.loadViewIfNeeded() + XCTAssertEqual(restored.server, server) + let other = try XCTUnwrap(WorkspaceServer.normalize("https://other.invalid/")) + restored.open(other) + XCTAssertNotEqual(restored.server?.profile, server.profile) + } + @MainActor + func testConnectionFailureKeepsLoadedFormAndAllowsResume() async throws { + let name = "KAN54-form-\(UUID())" + let defaults = try XCTUnwrap(UserDefaults(suiteName: name)) + defer { defaults.removePersistentDomain(forName: name) } + let controller = WorkspaceViewController(defaults: defaults) + controller.loadViewIfNeeded() + let url = try XCTUnwrap(WorkspaceServer.normalize("https://example.invalid/")) + controller.open(url) + let webview = try XCTUnwrap(controller.workspace) + webview.stopLoading() + let navigation = try XCTUnwrap(webview.loadHTMLString("", baseURL: url)) + var loaded = false + for _ in 0..<50 { + if let value = try? await webview.evaluateJavaScript("document.getElementById('draft')?.value"), + value as? String == "unsaved ticket" { loaded = true; break } + try await Task.sleep(for: .milliseconds(100)) + } + XCTAssertTrue(loaded) + controller.webView(webview, didFinish: navigation) + controller.webView(webview, didStartProvisionalNavigation: navigation) + controller.webView(webview, didFailProvisionalNavigation: navigation, + withError: NSError(domain: NSURLErrorDomain, code: NSURLErrorNotConnectedToInternet)) + XCTAssertTrue(webview.isHidden) + controller.open(url) + XCTAssertTrue(controller.workspace === webview) + XCTAssertFalse(webview.isHidden) + let value = try await webview.evaluateJavaScript("document.getElementById('draft').value") + XCTAssertEqual(value as? String, "unsaved ticket") + let bridge = try await webview.evaluateJavaScript("typeof window.__TAURI_INTERNALS__") + XCTAssertEqual(bridge as? String, "undefined") + } + +} diff --git a/native/src-tauri/gen/apple/UITests/WorkspaceUITests.swift b/native/src-tauri/gen/apple/UITests/WorkspaceUITests.swift new file mode 100644 index 0000000..ace0c39 --- /dev/null +++ b/native/src-tauri/gen/apple/UITests/WorkspaceUITests.swift @@ -0,0 +1,44 @@ +import XCTest + +final class WorkspaceUITests: XCTestCase { + @MainActor + private func reveal(_ element: XCUIElement, in scrollView: XCUIElement) { + for _ in 0..<8 { + if scrollView.frame.contains(CGPoint(x: element.frame.midX, y: element.frame.midY)), element.isHittable { return } + let start = scrollView.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)) + let direction: CGFloat = element.frame.midY > scrollView.frame.midY ? 0.2 : 0.8 + let end = scrollView.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: direction)) + start.press(forDuration: 0.05, thenDragTo: end) + } + XCTAssertTrue(element.isHittable) + } + + @MainActor + func testLauncherKeyboardValidationRotationAndAppearance() { + continueAfterFailure = false + XCUIDevice.shared.orientation = .portrait + let app = XCUIApplication() + app.launch() + let settings = app.buttons["showServerSettings"] + if settings.waitForExistence(timeout: 3) { settings.tap() } + let address = app.textFields["serverAddress"] + XCTAssertTrue(address.waitForExistence(timeout: 15)) + address.tap() + let clear = address.buttons.firstMatch + if clear.exists { clear.tap() } + address.typeText("http://invalid.example") + app.buttons["openWorkspace"].tap() + XCTAssertTrue(app.staticTexts["connectionStatus"].label.contains("valid HTTPS")) + XCUIDevice.shared.orientation = .landscapeLeft + let launcher = app.scrollViews["serverSettings"] + reveal(address, in: launcher) + let open = app.buttons["openWorkspace"] + reveal(open, in: launcher) + let dark = app.segmentedControls["appearance"].buttons["Dark"] + reveal(dark, in: launcher) + dark.tap() + XCTAssertTrue(dark.isSelected) + app.segmentedControls["appearance"].buttons["System"].tap() + XCUIDevice.shared.orientation = .portrait + } +} diff --git a/native/src-tauri/gen/apple/kanbangit-native.xcodeproj/project.pbxproj b/native/src-tauri/gen/apple/kanbangit-native.xcodeproj/project.pbxproj new file mode 100644 index 0000000..03f3633 --- /dev/null +++ b/native/src-tauri/gen/apple/kanbangit-native.xcodeproj/project.pbxproj @@ -0,0 +1,687 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 009F8FEC2765B2F19DC12CA8 /* WorkspaceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08DF9B2F499E1EB89EB39BC5 /* WorkspaceTests.swift */; }; + 0FAB3CC7BB466BFF871C0C30 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 31630170C358EA8657E34854 /* Assets.xcassets */; }; + 205D0CBA33F7D3C59AAD5300 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 63685B1189795CAEA85F7AB7 /* QuartzCore.framework */; }; + 333E7761EA87AE29CE04F1AC /* main.mm in Sources */ = {isa = PBXBuildFile; fileRef = F46963153D20DB21D97843E9 /* main.mm */; }; + 34C189B72A1B9EBE33D700D1 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F63BC1BDE65F5AEBE5B13519 /* LaunchScreen.storyboard */; }; + 5E764E27CA85A3746845F417 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8243D0C3821DAC731FECF105 /* UIKit.framework */; }; + 6514B0DB3A5CCCDF7385AC18 /* WorkspaceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE63F98563975EF4FF2A10FB /* WorkspaceViewController.swift */; }; + 70EBA05ACBA5402617A3371C /* WorkspacePolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 554AB059B4228085924F4959 /* WorkspacePolicy.swift */; }; + 7245AD23F39C4620BAA3AA92 /* WorkspaceUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED199829CC1B1598F58EDC9B /* WorkspaceUITests.swift */; }; + 73954B7AADE8C37663BEF288 /* Metal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CA65E67046326EA008175045 /* Metal.framework */; }; + 98D5B23C9BE2F62C23DF397A /* WorkspacePolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 554AB059B4228085924F4959 /* WorkspacePolicy.swift */; }; + A46344B6DF1B3A0F2798B751 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = C8C02BC0E757897278EB954A /* Localizable.strings */; }; + B939982700E800053A9C7663 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE3B71153677F722CAC6DCF7 /* Security.framework */; }; + C362C102D4950A81D89BB8C8 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 92EBA86D28BBFD1C822CC6F6 /* WebKit.framework */; }; + C44BDCDEA454DBD106D51C9C /* WorkspaceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE63F98563975EF4FF2A10FB /* WorkspaceViewController.swift */; }; + CD64D99EBD87E6FFC0C9001C /* MetalKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D581BB90601D7A89F8F188E2 /* MetalKit.framework */; }; + D8772A5E508092D6F831058A /* libapp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 91991DFDB0C064B06DF854ED /* libapp.a */; }; + EB37EAF992C1A156795CAEC0 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0CF713E7BFE3F6B5BAA3550E /* CoreGraphics.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 8665A25D07C5F1A295164797 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = AA6AD2A91A9E86D319D86D59 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 721CF221CEDDC28FF2C5EBD5; + remoteInfo = "kanbangit-native_iOS"; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 01E10FB645809173AF1D4C2D /* bindings.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = bindings.h; sourceTree = ""; }; + 05A753935B6CB0316DA56D96 /* desktop.rs */ = {isa = PBXFileReference; path = desktop.rs; sourceTree = ""; }; + 08DF9B2F499E1EB89EB39BC5 /* WorkspaceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceTests.swift; sourceTree = ""; }; + 0CF713E7BFE3F6B5BAA3550E /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; + 23960010246A2A72E9940D3F /* kanbangit-native_iOS.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = "kanbangit-native_iOS.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 2B96972F58CC158AB3DFF94A /* lib.rs */ = {isa = PBXFileReference; path = lib.rs; sourceTree = ""; }; + 31630170C358EA8657E34854 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 4C76059D31C50B198779A5F7 /* en.lproj */ = {isa = PBXFileReference; path = en.lproj; sourceTree = ""; }; + 554AB059B4228085924F4959 /* WorkspacePolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspacePolicy.swift; sourceTree = ""; }; + 63685B1189795CAEA85F7AB7 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; + 7440C7F0426E6B3977DD1D94 /* main.rs */ = {isa = PBXFileReference; path = main.rs; sourceTree = ""; }; + 8243D0C3821DAC731FECF105 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; + 91991DFDB0C064B06DF854ED /* libapp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libapp.a; sourceTree = ""; }; + 92EBA86D28BBFD1C822CC6F6 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; }; + 946BA5CC3FD90AD90D101A36 /* kanbangit-native_iOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "kanbangit-native_iOS.entitlements"; sourceTree = ""; }; + 9CAD0AE17A7966154CA29001 /* WorkspaceUITests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = WorkspaceUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + A372EBDD353022BDB3E78972 /* WorkspaceTests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = WorkspaceTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + AEBAD9ABE758897C50BC1F2E /* locale.rs */ = {isa = PBXFileReference; path = locale.rs; sourceTree = ""; }; + CA65E67046326EA008175045 /* Metal.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Metal.framework; path = System/Library/Frameworks/Metal.framework; sourceTree = SDKROOT; }; + CAF08AD60D2F29A879CDAEBB /* ios.rs */ = {isa = PBXFileReference; path = ios.rs; sourceTree = ""; }; + D13B8D854BEB7757AB95231B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + D581BB90601D7A89F8F188E2 /* MetalKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MetalKit.framework; path = System/Library/Frameworks/MetalKit.framework; sourceTree = SDKROOT; }; + DE3B71153677F722CAC6DCF7 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; + E8BA104C81071623E1FF367D /* server.rs */ = {isa = PBXFileReference; path = server.rs; sourceTree = ""; }; + ED199829CC1B1598F58EDC9B /* WorkspaceUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceUITests.swift; sourceTree = ""; }; + ED4469441C3E0F37B1123572 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; + EE63F98563975EF4FF2A10FB /* WorkspaceViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceViewController.swift; sourceTree = ""; }; + F46963153D20DB21D97843E9 /* main.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = main.mm; sourceTree = ""; }; + F63BC1BDE65F5AEBE5B13519 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 32E70C778E91CB2E8CD86715 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D8772A5E508092D6F831058A /* libapp.a in Frameworks */, + EB37EAF992C1A156795CAEC0 /* CoreGraphics.framework in Frameworks */, + 73954B7AADE8C37663BEF288 /* Metal.framework in Frameworks */, + CD64D99EBD87E6FFC0C9001C /* MetalKit.framework in Frameworks */, + 205D0CBA33F7D3C59AAD5300 /* QuartzCore.framework in Frameworks */, + B939982700E800053A9C7663 /* Security.framework in Frameworks */, + 5E764E27CA85A3746845F417 /* UIKit.framework in Frameworks */, + C362C102D4950A81D89BB8C8 /* WebKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 140A7DB7E2461E64B1FBFA70 /* Tests */ = { + isa = PBXGroup; + children = ( + 08DF9B2F499E1EB89EB39BC5 /* WorkspaceTests.swift */, + ); + path = Tests; + sourceTree = ""; + }; + 157AF88201BDE4D99AA2E14C = { + isa = PBXGroup; + children = ( + 31630170C358EA8657E34854 /* Assets.xcassets */, + F63BC1BDE65F5AEBE5B13519 /* LaunchScreen.storyboard */, + 4058655E6F37D054E25D1F37 /* kanbangit-native_iOS */, + 8606F3EE5A736385061692EF /* Sources */, + 335B1262A9019C70C7F887A5 /* src */, + 140A7DB7E2461E64B1FBFA70 /* Tests */, + 3C9F31BB065F210267ECA390 /* UITests */, + 3028C3786B437115B6486F1C /* Frameworks */, + 59D2DE9FD94E6D2D64CB0117 /* Products */, + ); + sourceTree = ""; + }; + 2A11A26F0082A5075CD710C6 /* bindings */ = { + isa = PBXGroup; + children = ( + 01E10FB645809173AF1D4C2D /* bindings.h */, + ); + path = bindings; + sourceTree = ""; + }; + 3028C3786B437115B6486F1C /* Frameworks */ = { + isa = PBXGroup; + children = ( + 0CF713E7BFE3F6B5BAA3550E /* CoreGraphics.framework */, + 91991DFDB0C064B06DF854ED /* libapp.a */, + CA65E67046326EA008175045 /* Metal.framework */, + D581BB90601D7A89F8F188E2 /* MetalKit.framework */, + 63685B1189795CAEA85F7AB7 /* QuartzCore.framework */, + DE3B71153677F722CAC6DCF7 /* Security.framework */, + 8243D0C3821DAC731FECF105 /* UIKit.framework */, + 92EBA86D28BBFD1C822CC6F6 /* WebKit.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 335B1262A9019C70C7F887A5 /* src */ = { + isa = PBXGroup; + children = ( + 05A753935B6CB0316DA56D96 /* desktop.rs */, + CAF08AD60D2F29A879CDAEBB /* ios.rs */, + 2B96972F58CC158AB3DFF94A /* lib.rs */, + AEBAD9ABE758897C50BC1F2E /* locale.rs */, + 7440C7F0426E6B3977DD1D94 /* main.rs */, + E8BA104C81071623E1FF367D /* server.rs */, + ); + name = src; + path = ../../src; + sourceTree = ""; + }; + 3C9F31BB065F210267ECA390 /* UITests */ = { + isa = PBXGroup; + children = ( + ED199829CC1B1598F58EDC9B /* WorkspaceUITests.swift */, + ); + path = UITests; + sourceTree = ""; + }; + 4058655E6F37D054E25D1F37 /* kanbangit-native_iOS */ = { + isa = PBXGroup; + children = ( + D13B8D854BEB7757AB95231B /* Info.plist */, + 946BA5CC3FD90AD90D101A36 /* kanbangit-native_iOS.entitlements */, + ); + path = "kanbangit-native_iOS"; + sourceTree = ""; + }; + 59D2DE9FD94E6D2D64CB0117 /* Products */ = { + isa = PBXGroup; + children = ( + 23960010246A2A72E9940D3F /* kanbangit-native_iOS.app */, + A372EBDD353022BDB3E78972 /* WorkspaceTests.xctest */, + 9CAD0AE17A7966154CA29001 /* WorkspaceUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 8606F3EE5A736385061692EF /* Sources */ = { + isa = PBXGroup; + children = ( + 89AA023B8C28CB860513C685 /* kanbangit-native */, + ); + path = Sources; + sourceTree = ""; + }; + 89AA023B8C28CB860513C685 /* kanbangit-native */ = { + isa = PBXGroup; + children = ( + 4C76059D31C50B198779A5F7 /* en.lproj */, + F46963153D20DB21D97843E9 /* main.mm */, + 554AB059B4228085924F4959 /* WorkspacePolicy.swift */, + EE63F98563975EF4FF2A10FB /* WorkspaceViewController.swift */, + 2A11A26F0082A5075CD710C6 /* bindings */, + C8C02BC0E757897278EB954A /* Localizable.strings */, + ); + path = "kanbangit-native"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 721CF221CEDDC28FF2C5EBD5 /* kanbangit-native_iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = F0C7EA9610FB69A5EA27B950 /* Build configuration list for PBXNativeTarget "kanbangit-native_iOS" */; + buildPhases = ( + C74F06A7ECCC2C6C79C74A2D /* Build Rust Code */, + F39C45598E4345705F3820FA /* Sources */, + 4A4C7FC1B57785DDD6B301FB /* Resources */, + 32E70C778E91CB2E8CD86715 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "kanbangit-native_iOS"; + packageProductDependencies = ( + ); + productName = "kanbangit-native_iOS"; + productReference = 23960010246A2A72E9940D3F /* kanbangit-native_iOS.app */; + productType = "com.apple.product-type.application"; + }; + 988CFF304CB81C8F3A4D6612 /* WorkspaceUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = E6F418AEF58E5F3D739C68F1 /* Build configuration list for PBXNativeTarget "WorkspaceUITests" */; + buildPhases = ( + 0258B26407CD2D8C61079F84 /* Sources */, + ); + buildRules = ( + ); + dependencies = ( + 5658F3B2915DC3A46531EDCE /* PBXTargetDependency */, + ); + name = WorkspaceUITests; + packageProductDependencies = ( + ); + productName = WorkspaceUITests; + productReference = 9CAD0AE17A7966154CA29001 /* WorkspaceUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; + CBC4561F8018D37DD49A0598 /* WorkspaceTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 26F675D59EF454AE0D263F35 /* Build configuration list for PBXNativeTarget "WorkspaceTests" */; + buildPhases = ( + 2A0467365B852D44D1A9FD5B /* Sources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = WorkspaceTests; + packageProductDependencies = ( + ); + productName = WorkspaceTests; + productReference = A372EBDD353022BDB3E78972 /* WorkspaceTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + AA6AD2A91A9E86D319D86D59 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1430; + TargetAttributes = { + 721CF221CEDDC28FF2C5EBD5 = { + DevelopmentTeam = ""; + ProvisioningStyle = Automatic; + }; + 988CFF304CB81C8F3A4D6612 = { + TestTargetID = 721CF221CEDDC28FF2C5EBD5; + }; + }; + }; + buildConfigurationList = A9CA551DD3F3EBCCF8C41CC1 /* Build configuration list for PBXProject "kanbangit-native" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + Base, + en, + ); + mainGroup = 157AF88201BDE4D99AA2E14C; + minimizedProjectReferenceProxies = 1; + preferredProjectObjectVersion = 77; + productRefGroup = 59D2DE9FD94E6D2D64CB0117 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 721CF221CEDDC28FF2C5EBD5 /* kanbangit-native_iOS */, + CBC4561F8018D37DD49A0598 /* WorkspaceTests */, + 988CFF304CB81C8F3A4D6612 /* WorkspaceUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 4A4C7FC1B57785DDD6B301FB /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0FAB3CC7BB466BFF871C0C30 /* Assets.xcassets in Resources */, + 34C189B72A1B9EBE33D700D1 /* LaunchScreen.storyboard in Resources */, + A46344B6DF1B3A0F2798B751 /* Localizable.strings in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + C74F06A7ECCC2C6C79C74A2D /* Build Rust Code */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Build Rust Code"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(SRCROOT)/Externals/x86_64/${CONFIGURATION}/libapp.a", + "$(SRCROOT)/Externals/arm64/${CONFIGURATION}/libapp.a", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "export PATH=\"$(dirname \"$(rustup which cargo)\"):$PATH\"\nexport RUSTC=\"$(rustup which rustc)\"\ncd \"$SRCROOT/../../..\"\n./node_modules/.bin/tauri ios xcode-script -v --platform ${PLATFORM_DISPLAY_NAME:?} --sdk-root ${SDKROOT:?} --framework-search-paths \"${FRAMEWORK_SEARCH_PATHS:?}\" --header-search-paths \"${HEADER_SEARCH_PATHS:?}\" --gcc-preprocessor-definitions \"${GCC_PREPROCESSOR_DEFINITIONS:-}\" --configuration ${CONFIGURATION:?} ${FORCE_COLOR} ${ARCHS:?}\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 0258B26407CD2D8C61079F84 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7245AD23F39C4620BAA3AA92 /* WorkspaceUITests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2A0467365B852D44D1A9FD5B /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 98D5B23C9BE2F62C23DF397A /* WorkspacePolicy.swift in Sources */, + 009F8FEC2765B2F19DC12CA8 /* WorkspaceTests.swift in Sources */, + 6514B0DB3A5CCCDF7385AC18 /* WorkspaceViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F39C45598E4345705F3820FA /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 70EBA05ACBA5402617A3371C /* WorkspacePolicy.swift in Sources */, + C44BDCDEA454DBD106D51C9C /* WorkspaceViewController.swift in Sources */, + 333E7761EA87AE29CE04F1AC /* main.mm in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 5658F3B2915DC3A46531EDCE /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 721CF221CEDDC28FF2C5EBD5 /* kanbangit-native_iOS */; + targetProxy = 8665A25D07C5F1A295164797 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + C8C02BC0E757897278EB954A /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + ED4469441C3E0F37B1123572 /* en */, + ); + name = Localizable.strings; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 0C46162024E99BEFF872CABF /* release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ARCHS = ( + arm64, + ); + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = "kanbangit-native_iOS/kanbangit-native_iOS.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ""; + ENABLE_BITCODE = NO; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + "EXCLUDED_ARCHS[sdk=iphoneos*]" = x86_64; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "\".\"", + ); + INFOPLIST_FILE = "kanbangit-native_iOS/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + "LIBRARY_SEARCH_PATHS[arch=arm64]" = "$(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)"; + "LIBRARY_SEARCH_PATHS[arch=x86_64]" = "$(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)"; + PRODUCT_BUNDLE_IDENTIFIER = de.kanbangit.desktop; + PRODUCT_MODULE_NAME = KanbanGitNative; + PRODUCT_NAME = "KanbanGit"; + SDKROOT = iphoneos; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALID_ARCHS = arm64; + }; + name = release; + }; + 390A27695F28BF3AA9A049C9 /* debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "DEBUG=1", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = debug; + }; + 5110A47C3B60906E18EE09D2 /* debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + GENERATE_INFOPLIST_FILE = YES; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = de.kanbangit.uitests; + SDKROOT = iphoneos; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = "kanbangit-native_iOS"; + }; + name = debug; + }; + C81211E40BBB158A79AEBE3D /* release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + }; + name = release; + }; + D41150B2CAE4512D04C26759 /* debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ARCHS = ( + arm64, + ); + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = "kanbangit-native_iOS/kanbangit-native_iOS.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ""; + ENABLE_BITCODE = NO; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + "EXCLUDED_ARCHS[sdk=iphoneos*]" = x86_64; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "\".\"", + ); + INFOPLIST_FILE = "kanbangit-native_iOS/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + "LIBRARY_SEARCH_PATHS[arch=arm64]" = "$(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)"; + "LIBRARY_SEARCH_PATHS[arch=x86_64]" = "$(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)"; + PRODUCT_BUNDLE_IDENTIFIER = de.kanbangit.desktop; + PRODUCT_MODULE_NAME = KanbanGitNative; + PRODUCT_NAME = "KanbanGit"; + SDKROOT = iphoneos; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALID_ARCHS = arm64; + }; + name = debug; + }; + EF104E75BD04AFEF735BAA52 /* release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + GENERATE_INFOPLIST_FILE = YES; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = de.kanbangit.uitests; + SDKROOT = iphoneos; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = "kanbangit-native_iOS"; + }; + name = release; + }; + FB6E824120F9ED3CFE30E713 /* debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + GENERATE_INFOPLIST_FILE = YES; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = de.kanbangit.tests; + SDKROOT = iphoneos; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = debug; + }; + FF1553DD6765EEA42FF50CDD /* release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + GENERATE_INFOPLIST_FILE = YES; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = de.kanbangit.tests; + SDKROOT = iphoneos; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 26F675D59EF454AE0D263F35 /* Build configuration list for PBXNativeTarget "WorkspaceTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + FB6E824120F9ED3CFE30E713 /* debug */, + FF1553DD6765EEA42FF50CDD /* release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = debug; + }; + A9CA551DD3F3EBCCF8C41CC1 /* Build configuration list for PBXProject "kanbangit-native" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 390A27695F28BF3AA9A049C9 /* debug */, + C81211E40BBB158A79AEBE3D /* release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = debug; + }; + E6F418AEF58E5F3D739C68F1 /* Build configuration list for PBXNativeTarget "WorkspaceUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 5110A47C3B60906E18EE09D2 /* debug */, + EF104E75BD04AFEF735BAA52 /* release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = debug; + }; + F0C7EA9610FB69A5EA27B950 /* Build configuration list for PBXNativeTarget "kanbangit-native_iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D41150B2CAE4512D04C26759 /* debug */, + 0C46162024E99BEFF872CABF /* release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = debug; + }; +/* End XCConfigurationList section */ + }; + rootObject = AA6AD2A91A9E86D319D86D59 /* Project object */; +} diff --git a/native/src-tauri/gen/apple/kanbangit-native.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/native/src-tauri/gen/apple/kanbangit-native.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/native/src-tauri/gen/apple/kanbangit-native.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/native/src-tauri/gen/apple/kanbangit-native.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/native/src-tauri/gen/apple/kanbangit-native.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..ac90d5a --- /dev/null +++ b/native/src-tauri/gen/apple/kanbangit-native.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,10 @@ + + + + + BuildSystemType + Original + DisableBuildSystemDeprecationDiagnostic + + + diff --git a/native/src-tauri/gen/apple/kanbangit-native.xcodeproj/xcshareddata/xcschemes/kanbangit-native_iOS.xcscheme b/native/src-tauri/gen/apple/kanbangit-native.xcodeproj/xcshareddata/xcschemes/kanbangit-native_iOS.xcscheme new file mode 100644 index 0000000..470d0ab --- /dev/null +++ b/native/src-tauri/gen/apple/kanbangit-native.xcodeproj/xcshareddata/xcschemes/kanbangit-native_iOS.xcscheme @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/native/src-tauri/gen/apple/kanbangit-native_iOS/Info.plist b/native/src-tauri/gen/apple/kanbangit-native_iOS/Info.plist new file mode 100644 index 0000000..e0b1335 --- /dev/null +++ b/native/src-tauri/gen/apple/kanbangit-native_iOS/Info.plist @@ -0,0 +1,44 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 0.1.0 + CFBundleVersion + 0.1.0 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + arm64 + metal + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + \ No newline at end of file diff --git a/native/src-tauri/gen/apple/kanbangit-native_iOS/kanbangit-native_iOS.entitlements b/native/src-tauri/gen/apple/kanbangit-native_iOS/kanbangit-native_iOS.entitlements new file mode 100644 index 0000000..0c67376 --- /dev/null +++ b/native/src-tauri/gen/apple/kanbangit-native_iOS/kanbangit-native_iOS.entitlements @@ -0,0 +1,5 @@ + + + + + diff --git a/native/src-tauri/gen/apple/project.yml b/native/src-tauri/gen/apple/project.yml new file mode 100644 index 0000000..155ef24 --- /dev/null +++ b/native/src-tauri/gen/apple/project.yml @@ -0,0 +1,122 @@ +name: kanbangit-native +options: + bundleIdPrefix: de.kanbangit.desktop + deploymentTarget: + iOS: 17.0 +fileGroups: [../../src] +configs: + debug: debug + release: release +settingGroups: + app: + base: + PRODUCT_NAME: KanbanGit + SWIFT_VERSION: "5.0" + PRODUCT_MODULE_NAME: KanbanGitNative + TARGETED_DEVICE_FAMILY: "1,2" + CODE_SIGN_STYLE: Automatic + DEVELOPMENT_TEAM: "" + ENABLE_USER_SCRIPT_SANDBOXING: false + PRODUCT_BUNDLE_IDENTIFIER: de.kanbangit.desktop +targetTemplates: + app: + type: application + sources: + - path: Sources + scheme: + environmentVariables: + RUST_BACKTRACE: full + RUST_LOG: info + settings: + groups: [app] +targets: + kanbangit-native_iOS: + type: application + platform: iOS + sources: + - path: Sources + - path: Assets.xcassets + - path: kanbangit-native_iOS + - path: LaunchScreen.storyboard + info: + path: kanbangit-native_iOS/Info.plist + properties: + LSRequiresIPhoneOS: true + UILaunchStoryboardName: LaunchScreen + UIRequiredDeviceCapabilities: [arm64, metal] + UISupportedInterfaceOrientations: + - UIInterfaceOrientationPortrait + - UIInterfaceOrientationLandscapeLeft + - UIInterfaceOrientationLandscapeRight + UISupportedInterfaceOrientations~ipad: + - UIInterfaceOrientationPortrait + - UIInterfaceOrientationPortraitUpsideDown + - UIInterfaceOrientationLandscapeLeft + - UIInterfaceOrientationLandscapeRight + CFBundleShortVersionString: 0.1.0 + CFBundleVersion: "0.1.0" + entitlements: + path: kanbangit-native_iOS/kanbangit-native_iOS.entitlements + scheme: + environmentVariables: + RUST_BACKTRACE: full + RUST_LOG: info + testTargets: + - WorkspaceTests + - WorkspaceUITests + settings: + base: + ENABLE_BITCODE: false + ARCHS: [arm64] + VALID_ARCHS: arm64 + LIBRARY_SEARCH_PATHS[arch=x86_64]: $(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME) + LIBRARY_SEARCH_PATHS[arch=arm64]: $(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME) + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES: true + EXCLUDED_ARCHS[sdk=iphoneos*]: x86_64 + groups: [app] + dependencies: + - framework: libapp.a + embed: false + - sdk: CoreGraphics.framework + - sdk: Metal.framework + - sdk: MetalKit.framework + - sdk: QuartzCore.framework + - sdk: Security.framework + - sdk: UIKit.framework + - sdk: WebKit.framework + preBuildScripts: + - script: | + export PATH="$(dirname "$(rustup which cargo)"):$PATH" + export RUSTC="$(rustup which rustc)" + cd "$SRCROOT/../../.." + ./node_modules/.bin/tauri ios xcode-script -v --platform ${PLATFORM_DISPLAY_NAME:?} --sdk-root ${SDKROOT:?} --framework-search-paths "${FRAMEWORK_SEARCH_PATHS:?}" --header-search-paths "${HEADER_SEARCH_PATHS:?}" --gcc-preprocessor-definitions "${GCC_PREPROCESSOR_DEFINITIONS:-}" --configuration ${CONFIGURATION:?} ${FORCE_COLOR} ${ARCHS:?} + name: Build Rust Code + basedOnDependencyAnalysis: false + outputFiles: + - $(SRCROOT)/Externals/x86_64/${CONFIGURATION}/libapp.a + - $(SRCROOT)/Externals/arm64/${CONFIGURATION}/libapp.a + WorkspaceTests: + type: bundle.unit-test + platform: iOS + sources: + - path: Tests + - path: Sources/kanbangit-native/WorkspacePolicy.swift + - path: Sources/kanbangit-native/WorkspaceViewController.swift + - path: Sources/kanbangit-native/en.lproj + settings: + base: + GENERATE_INFOPLIST_FILE: YES + PRODUCT_BUNDLE_IDENTIFIER: de.kanbangit.tests + SWIFT_VERSION: "5.0" + WorkspaceUITests: + type: bundle.ui-testing + platform: iOS + sources: [UITests] + dependencies: + - target: kanbangit-native_iOS + settings: + base: + GENERATE_INFOPLIST_FILE: YES + PRODUCT_BUNDLE_IDENTIFIER: de.kanbangit.uitests + SWIFT_VERSION: "5.0" + TEST_TARGET_NAME: kanbangit-native_iOS diff --git a/native/src-tauri/src/desktop.rs b/native/src-tauri/src/desktop.rs new file mode 100644 index 0000000..ac50ba1 --- /dev/null +++ b/native/src-tauri/src/desktop.rs @@ -0,0 +1,456 @@ +use crate::locale::en; +use crate::server::{validate_url, Server, DEFAULT_SERVER}; +use std::{path::PathBuf, sync::Mutex, time::Duration}; +use tauri::{ + menu::{MenuBuilder, SubmenuBuilder}, + webview::{NewWindowResponse, PageLoadEvent}, + AppHandle, Manager, Url, WebviewUrl, WebviewWindowBuilder, WindowEvent, +}; + +#[derive(Default)] +struct Connection { + server: Option, + label: Option, + revision: u64, + ready: bool, + phase: &'static str, +} + +impl Connection { + fn begin_navigation(&mut self) -> u64 { + self.revision += 1; + self.phase = "connecting"; + self.revision + } + + fn timeout(&mut self, revision: u64) -> bool { + if self.revision != revision || self.phase != "connecting" { + return false; + } + self.phase = "connectionError"; + true + } +} + +fn allows_server_navigation(server: &Url, destination: &Url) -> bool { + destination.scheme() == "https" + && destination.origin() == server.origin() + && destination.username().is_empty() + && destination.password().is_none() +} + +fn allows_launcher_navigation(destination: &Url) -> bool { + destination.scheme() == "tauri" + && destination.host_str() == Some("localhost") + && destination.port().is_none() + && destination.username().is_empty() + && destination.password().is_none() +} + +fn configuration_path(app: &AppHandle) -> tauri::Result { + Ok(app.path().app_config_dir()?.join("active-server")) +} + +fn update_launcher(app: &AppHandle) { + let state = app.state::>(); + let state = state.lock().unwrap(); + let mut message = Url::parse("tauri://localhost/").unwrap(); + message + .query_pairs_mut() + .append_pair( + "server", + state + .server + .as_ref() + .map_or(DEFAULT_SERVER, |server| server.url.as_str()), + ) + .append_pair("phase", state.phase) + .append_pair("ready", if state.ready { "true" } else { "false" }); + if let Some(window) = app.get_webview_window("launcher") { + let _ = window.eval(format!( + "window.updateLauncher?.('{}')", + message.query().unwrap() + )); + } +} + +fn show_launcher(app: &AppHandle) { + update_launcher(app); + if let Some(window) = app.get_webview_window("launcher") { + let _ = window.show(); + let _ = window.set_focus(); + } +} + +fn begin_navigation(app: &AppHandle, label: &str) { + let state = app.state::>(); + let revision = { + let mut state = state.lock().unwrap(); + if state.label.as_deref() != Some(label) { + return; + } + state.begin_navigation() + }; + update_launcher(app); + let handle = app.clone(); + std::thread::spawn(move || { + std::thread::sleep(Duration::from_secs(15)); + let state = handle.state::>(); + let timed_out = state.lock().unwrap().timeout(revision); + if timed_out { + show_launcher(&handle); + } + }); +} + +fn open_workspace(app: &AppHandle, input: &str) -> Result<(), &'static str> { + let url = validate_url(input)?; + let state = app.state::>(); + let (server, old_label, label) = { + let mut state = state.lock().unwrap(); + let same_server = state + .server + .as_ref() + .is_some_and(|server| server.url == url); + if same_server && state.ready { + if let Some(window) = state + .label + .as_ref() + .and_then(|label| app.get_webview_window(label)) + { + window.show().map_err(|_| "connectionError")?; + window.set_focus().map_err(|_| "connectionError")?; + return Ok(()); + } + } + let server = if same_server { + state.server.clone().unwrap() + } else { + Server::new(url).map_err(|_| "storageError")? + }; + server + .save(&configuration_path(app).map_err(|_| "storageError")?) + .map_err(|_| "storageError")?; + let old_label = state.label.take(); + state.server = Some(server.clone()); + state.ready = false; + state.revision += 1; + let label = format!("workspace-{}", state.revision); + state.label = Some(label.clone()); + (server, old_label, label) + }; + if let Some(window) = old_label.and_then(|label| app.get_webview_window(&label)) { + window.close().map_err(|_| "connectionError")?; + } + begin_navigation(app, &label); + let handle = app.clone(); + let navigation_label = label.clone(); + let server_url = server.url.clone(); + let window = WebviewWindowBuilder::new(app, &label, WebviewUrl::External(server.url)) + .title(en::APP_NAME) + .inner_size(1280.0, 850.0) + .min_inner_size(375.0, 600.0) + .center() + .visible(false) + .data_store_identifier(server.profile) + .on_navigation(move |destination| { + if !allows_server_navigation(&server_url, destination) { + return false; + } + begin_navigation(&handle, &navigation_label); + true + }) + .on_new_window(|_, _| NewWindowResponse::Deny) + .on_page_load(|window, payload| { + if payload.event() != PageLoadEvent::Finished { + return; + } + let app = window.app_handle(); + let state = app.state::>(); + let should_show = { + let mut state = state.lock().unwrap(); + if state.label.as_deref() != Some(window.label()) { + return; + } + let first_load = !state.ready; + state.ready = true; + state.phase = "connected"; + first_load + }; + update_launcher(app); + if should_show { + let _ = window.show(); + let _ = window.set_focus(); + if let Some(launcher) = app.get_webview_window("launcher") { + let _ = launcher.hide(); + } + } + }) + .build() + .map_err(|_| "connectionError")?; + let handle = app.clone(); + window.on_window_event(move |event| { + if matches!(event, WindowEvent::Destroyed) { + let state = handle.state::>(); + let current = { + let mut state = state.lock().unwrap(); + if state.label.as_deref() == Some(&label) { + state.label = None; + state.ready = false; + state.revision += 1; + state.phase = "idle"; + true + } else { + false + } + }; + if current { + show_launcher(&handle); + } + } + }); + Ok(()) +} + +fn handle_launcher_action(app: &AppHandle, destination: &Url) { + let result = match destination.path() { + "/action/open" => { + let input = destination + .query_pairs() + .find(|(key, _)| key == "server") + .map(|(_, value)| value.into_owned()) + .unwrap_or_default(); + open_workspace(app, &input) + } + "/action/resume" => { + let label = app + .state::>() + .lock() + .unwrap() + .label + .clone(); + if let Some(window) = label.and_then(|label| app.get_webview_window(&label)) { + let _ = window.show(); + let _ = window.set_focus(); + } + Ok(()) + } + _ => Ok(()), + }; + if let Err(error) = result { + app.state::>().lock().unwrap().phase = error; + show_launcher(app); + } +} + +pub fn context() -> tauri::Context { + tauri::generate_context!() +} + +pub fn builder() -> tauri::Builder { + tauri::Builder::default().setup(|app| { + let (server, phase) = match Server::load(&configuration_path(app.handle())?) { + Ok(server) => (server, "idle"), + Err(_) => (None, "storageError"), + }; + app.manage(Mutex::new(Connection { + server, + phase, + ..Default::default() + })); + let menu = MenuBuilder::new(app) + .item( + &SubmenuBuilder::new(app, en::APP_NAME) + .text("server-settings", en::SERVER_SETTINGS) + .separator() + .quit() + .build()?, + ) + .item( + &SubmenuBuilder::new(app, en::EDIT) + .undo() + .redo() + .separator() + .cut() + .copy() + .paste() + .select_all() + .build()?, + ) + .build()?; + app.set_menu(menu)?; + app.on_menu_event(|app, event| { + if event.id().as_ref() == "server-settings" { + show_launcher(app); + } + }); + let handle = app.handle().clone(); + let launcher = + WebviewWindowBuilder::new(app, "launcher", WebviewUrl::App("index.html".into())) + .title(en::APP_NAME) + .inner_size(560.0, 720.0) + .min_inner_size(375.0, 640.0) + .center() + .on_navigation(move |destination| { + if !allows_launcher_navigation(destination) { + return false; + } + if destination.path().starts_with("/action/") { + let handle = handle.clone(); + let destination = destination.clone(); + let _ = handle.clone().run_on_main_thread(move || { + handle_launcher_action(&handle, &destination) + }); + return false; + } + matches!(destination.path(), "" | "/" | "/index.html") + }) + .on_new_window(|_, _| NewWindowResponse::Deny) + .on_page_load(|window, payload| { + if payload.event() == PageLoadEvent::Finished { + let app = window.app_handle().clone(); + update_launcher(&app); + std::thread::spawn(move || { + let handle = app.clone(); + let _ = app.run_on_main_thread(move || { + let server = handle + .state::>() + .lock() + .unwrap() + .server + .clone(); + if let Some(server) = server { + if let Err(error) = open_workspace(&handle, server.url.as_str()) + { + handle.state::>().lock().unwrap().phase = + error; + show_launcher(&handle); + } + } + }); + }); + } + }) + .build()?; + let window = launcher.clone(); + launcher.on_window_event(move |event| { + if let WindowEvent::CloseRequested { api, .. } = event { + api.prevent_close(); + let _ = window.hide(); + } + }); + Ok(()) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn navigation_is_limited_to_the_selected_https_origin() { + let server = validate_url("https://example.com:8443/app/").unwrap(); + for path in [ + "/login", + "/two-factor-challenge", + "/projects/1?ticket=52#details", + ] { + assert!(allows_server_navigation( + &server, + &server.join(path).unwrap() + )); + } + for destination in [ + "http://example.com:8443/", + "https://example.com/", + "https://other.example.com:8443/", + "https://user:secret@example.com:8443/", + "tauri://localhost/action/open?server=https://evil.test", + "file:///etc/passwd", + "data:text/html,hello", + "javascript:alert(1)", + ] { + assert!(!allows_server_navigation( + &server, + &Url::parse(destination).unwrap() + )); + } + } + + #[test] + fn launcher_actions_are_restricted_to_the_bundled_origin() { + assert!(allows_launcher_navigation( + &Url::parse("tauri://localhost/action/open").unwrap() + )); + for input in [ + DEFAULT_SERVER, + "https://localhost/", + "tauri://other/index.html", + "tauri://user@localhost/index.html", + ] { + assert!(!allows_launcher_navigation(&Url::parse(input).unwrap())); + } + } + + #[test] + fn stale_timeouts_do_not_replace_newer_connections_or_loaded_forms() { + let mut state = Connection::default(); + let first = state.begin_navigation(); + let second = state.begin_navigation(); + assert!(!state.timeout(first)); + state.ready = true; + assert!(state.timeout(second)); + assert!(state.ready); + assert!(!state.timeout(second)); + let third = state.begin_navigation(); + state.phase = "connected"; + assert!(!state.timeout(third)); + } + + #[test] + fn remote_pages_cannot_invoke_native_commands() { + let app = tauri::test::mock_builder().build(context()).unwrap(); + let window = WebviewWindowBuilder::new( + &app, + "workspace-1", + WebviewUrl::External(Url::parse(DEFAULT_SERVER).unwrap()), + ) + .build() + .unwrap(); + for command in [ + "plugin:window|close", + "plugin:webview|create_webview_window", + "plugin:path|resolve_directory", + ] { + let response = tauri::test::get_ipc_response( + &window, + tauri::webview::InvokeRequest { + cmd: command.into(), + callback: tauri::ipc::CallbackFn(0), + error: tauri::ipc::CallbackFn(1), + url: Url::parse(DEFAULT_SERVER).unwrap(), + body: tauri::ipc::InvokeBody::default(), + headers: Default::default(), + invoke_key: tauri::test::INVOKE_KEY.into(), + }, + ); + assert!(response + .unwrap_err() + .as_str() + .unwrap() + .contains("not allowed")); + } + } + + #[test] + fn packaged_app_exposes_no_native_capabilities_or_global_api() { + let config: serde_json::Value = + serde_json::from_str(include_str!("../tauri.conf.json")).unwrap(); + assert_eq!(config["bundle"]["macOS"]["minimumSystemVersion"], "14.0"); + assert_eq!(config["app"]["withGlobalTauri"], false); + assert_eq!( + config["app"]["security"]["capabilities"], + serde_json::json!([]) + ); + } +} diff --git a/native/src-tauri/src/ios.rs b/native/src-tauri/src/ios.rs new file mode 100644 index 0000000..591731b --- /dev/null +++ b/native/src-tauri/src/ios.rs @@ -0,0 +1,14 @@ +use tauri::{WebviewUrl, WebviewWindowBuilder}; + +pub fn context() -> tauri::Context { + tauri::generate_context!() +} + +pub fn builder() -> tauri::Builder { + tauri::Builder::default().setup(|app| { + WebviewWindowBuilder::new(app, "host", WebviewUrl::App("index.html".into())) + .on_navigation(|_| false) + .build()?; + Ok(()) + }) +} diff --git a/native/src-tauri/src/lib.rs b/native/src-tauri/src/lib.rs index ec24cda..34cd168 100644 --- a/native/src-tauri/src/lib.rs +++ b/native/src-tauri/src/lib.rs @@ -1,464 +1,18 @@ +#[cfg(not(target_os = "ios"))] +mod desktop; +#[cfg(target_os = "ios")] +mod ios; +#[cfg(not(target_os = "ios"))] mod locale; +#[cfg(not(target_os = "ios"))] mod server; -use locale::en; -use server::{validate_url, Server, DEFAULT_SERVER}; -use std::{path::PathBuf, sync::Mutex, time::Duration}; -use tauri::{ - menu::{MenuBuilder, SubmenuBuilder}, - webview::{NewWindowResponse, PageLoadEvent}, - AppHandle, Manager, Url, WebviewUrl, WebviewWindowBuilder, WindowEvent, -}; - -#[derive(Default)] -struct Connection { - server: Option, - label: Option, - revision: u64, - ready: bool, - phase: &'static str, -} - -impl Connection { - fn begin_navigation(&mut self) -> u64 { - self.revision += 1; - self.phase = "connecting"; - self.revision - } - - fn timeout(&mut self, revision: u64) -> bool { - if self.revision != revision || self.phase != "connecting" { - return false; - } - self.phase = "connectionError"; - true - } -} - -fn allows_server_navigation(server: &Url, destination: &Url) -> bool { - destination.scheme() == "https" - && destination.origin() == server.origin() - && destination.username().is_empty() - && destination.password().is_none() -} - -fn allows_launcher_navigation(destination: &Url) -> bool { - destination.scheme() == "tauri" - && destination.host_str() == Some("localhost") - && destination.port().is_none() - && destination.username().is_empty() - && destination.password().is_none() -} - -fn configuration_path(app: &AppHandle) -> tauri::Result { - Ok(app.path().app_config_dir()?.join("active-server")) -} - -fn update_launcher(app: &AppHandle) { - let state = app.state::>(); - let state = state.lock().unwrap(); - let mut message = Url::parse("tauri://localhost/").unwrap(); - message - .query_pairs_mut() - .append_pair( - "server", - state - .server - .as_ref() - .map_or(DEFAULT_SERVER, |server| server.url.as_str()), - ) - .append_pair("phase", state.phase) - .append_pair("ready", if state.ready { "true" } else { "false" }); - if let Some(window) = app.get_webview_window("launcher") { - let _ = window.eval(format!( - "window.updateLauncher?.('{}')", - message.query().unwrap() - )); - } -} - -fn show_launcher(app: &AppHandle) { - update_launcher(app); - if let Some(window) = app.get_webview_window("launcher") { - let _ = window.show(); - let _ = window.set_focus(); - } -} - -fn begin_navigation(app: &AppHandle, label: &str) { - let state = app.state::>(); - let revision = { - let mut state = state.lock().unwrap(); - if state.label.as_deref() != Some(label) { - return; - } - state.begin_navigation() - }; - update_launcher(app); - let handle = app.clone(); - std::thread::spawn(move || { - std::thread::sleep(Duration::from_secs(15)); - let state = handle.state::>(); - let timed_out = state.lock().unwrap().timeout(revision); - if timed_out { - show_launcher(&handle); - } - }); -} - -fn open_workspace(app: &AppHandle, input: &str) -> Result<(), &'static str> { - let url = validate_url(input)?; - let state = app.state::>(); - let (server, old_label, label) = { - let mut state = state.lock().unwrap(); - let same_server = state - .server - .as_ref() - .is_some_and(|server| server.url == url); - if same_server && state.ready { - if let Some(window) = state - .label - .as_ref() - .and_then(|label| app.get_webview_window(label)) - { - window.show().map_err(|_| "connectionError")?; - window.set_focus().map_err(|_| "connectionError")?; - return Ok(()); - } - } - let server = if same_server { - state.server.clone().unwrap() - } else { - Server::new(url).map_err(|_| "storageError")? - }; - server - .save(&configuration_path(app).map_err(|_| "storageError")?) - .map_err(|_| "storageError")?; - let old_label = state.label.take(); - state.server = Some(server.clone()); - state.ready = false; - state.revision += 1; - let label = format!("workspace-{}", state.revision); - state.label = Some(label.clone()); - (server, old_label, label) - }; - if let Some(window) = old_label.and_then(|label| app.get_webview_window(&label)) { - window.close().map_err(|_| "connectionError")?; - } - begin_navigation(app, &label); - let handle = app.clone(); - let navigation_label = label.clone(); - let server_url = server.url.clone(); - let window = WebviewWindowBuilder::new(app, &label, WebviewUrl::External(server.url)) - .title(en::APP_NAME) - .inner_size(1280.0, 850.0) - .min_inner_size(375.0, 600.0) - .center() - .visible(false) - .data_store_identifier(server.profile) - .on_navigation(move |destination| { - if !allows_server_navigation(&server_url, destination) { - return false; - } - begin_navigation(&handle, &navigation_label); - true - }) - .on_new_window(|_, _| NewWindowResponse::Deny) - .on_page_load(|window, payload| { - if payload.event() != PageLoadEvent::Finished { - return; - } - let app = window.app_handle(); - let state = app.state::>(); - let should_show = { - let mut state = state.lock().unwrap(); - if state.label.as_deref() != Some(window.label()) { - return; - } - let first_load = !state.ready; - state.ready = true; - state.phase = "connected"; - first_load - }; - update_launcher(app); - if should_show { - let _ = window.show(); - let _ = window.set_focus(); - if let Some(launcher) = app.get_webview_window("launcher") { - let _ = launcher.hide(); - } - } - }) - .build() - .map_err(|_| "connectionError")?; - let handle = app.clone(); - window.on_window_event(move |event| { - if matches!(event, WindowEvent::Destroyed) { - let state = handle.state::>(); - let current = { - let mut state = state.lock().unwrap(); - if state.label.as_deref() == Some(&label) { - state.label = None; - state.ready = false; - state.revision += 1; - state.phase = "idle"; - true - } else { - false - } - }; - if current { - show_launcher(&handle); - } - } - }); - Ok(()) -} - -fn handle_launcher_action(app: &AppHandle, destination: &Url) { - let result = match destination.path() { - "/action/open" => { - let input = destination - .query_pairs() - .find(|(key, _)| key == "server") - .map(|(_, value)| value.into_owned()) - .unwrap_or_default(); - open_workspace(app, &input) - } - "/action/resume" => { - let label = app - .state::>() - .lock() - .unwrap() - .label - .clone(); - if let Some(window) = label.and_then(|label| app.get_webview_window(&label)) { - let _ = window.show(); - let _ = window.set_focus(); - } - Ok(()) - } - _ => Ok(()), - }; - if let Err(error) = result { - app.state::>().lock().unwrap().phase = error; - show_launcher(app); - } -} - -pub fn context() -> tauri::Context { - tauri::generate_context!() -} - -pub fn builder() -> tauri::Builder { - tauri::Builder::default().setup(|app| { - let (server, phase) = match Server::load(&configuration_path(app.handle())?) { - Ok(server) => (server, "idle"), - Err(_) => (None, "storageError"), - }; - app.manage(Mutex::new(Connection { - server, - phase, - ..Default::default() - })); - let menu = MenuBuilder::new(app) - .item( - &SubmenuBuilder::new(app, en::APP_NAME) - .text("server-settings", en::SERVER_SETTINGS) - .separator() - .quit() - .build()?, - ) - .item( - &SubmenuBuilder::new(app, en::EDIT) - .undo() - .redo() - .separator() - .cut() - .copy() - .paste() - .select_all() - .build()?, - ) - .build()?; - app.set_menu(menu)?; - app.on_menu_event(|app, event| { - if event.id().as_ref() == "server-settings" { - show_launcher(app); - } - }); - let handle = app.handle().clone(); - let launcher = - WebviewWindowBuilder::new(app, "launcher", WebviewUrl::App("index.html".into())) - .title(en::APP_NAME) - .inner_size(560.0, 720.0) - .min_inner_size(375.0, 640.0) - .center() - .on_navigation(move |destination| { - if !allows_launcher_navigation(destination) { - return false; - } - if destination.path().starts_with("/action/") { - let handle = handle.clone(); - let destination = destination.clone(); - let _ = handle.clone().run_on_main_thread(move || { - handle_launcher_action(&handle, &destination) - }); - return false; - } - matches!(destination.path(), "" | "/" | "/index.html") - }) - .on_new_window(|_, _| NewWindowResponse::Deny) - .on_page_load(|window, payload| { - if payload.event() == PageLoadEvent::Finished { - let app = window.app_handle().clone(); - update_launcher(&app); - std::thread::spawn(move || { - let handle = app.clone(); - let _ = app.run_on_main_thread(move || { - let server = handle - .state::>() - .lock() - .unwrap() - .server - .clone(); - if let Some(server) = server { - if let Err(error) = open_workspace(&handle, server.url.as_str()) - { - handle.state::>().lock().unwrap().phase = - error; - show_launcher(&handle); - } - } - }); - }); - } - }) - .build()?; - let window = launcher.clone(); - launcher.on_window_event(move |event| { - if let WindowEvent::CloseRequested { api, .. } = event { - api.prevent_close(); - let _ = window.hide(); - } - }); - Ok(()) - }) -} +#[cfg(not(target_os = "ios"))] +pub use desktop::{builder, context}; +#[cfg(target_os = "ios")] +pub use ios::{builder, context}; #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { builder().run(context()).expect("Could not start KanbanGit"); } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn navigation_is_limited_to_the_selected_https_origin() { - let server = validate_url("https://example.com:8443/app/").unwrap(); - for path in [ - "/login", - "/two-factor-challenge", - "/projects/1?ticket=52#details", - ] { - assert!(allows_server_navigation( - &server, - &server.join(path).unwrap() - )); - } - for destination in [ - "http://example.com:8443/", - "https://example.com/", - "https://other.example.com:8443/", - "https://user:secret@example.com:8443/", - "tauri://localhost/action/open?server=https://evil.test", - "file:///etc/passwd", - "data:text/html,hello", - "javascript:alert(1)", - ] { - assert!(!allows_server_navigation( - &server, - &Url::parse(destination).unwrap() - )); - } - } - - #[test] - fn launcher_actions_are_restricted_to_the_bundled_origin() { - assert!(allows_launcher_navigation( - &Url::parse("tauri://localhost/action/open").unwrap() - )); - for input in [ - DEFAULT_SERVER, - "https://localhost/", - "tauri://other/index.html", - "tauri://user@localhost/index.html", - ] { - assert!(!allows_launcher_navigation(&Url::parse(input).unwrap())); - } - } - - #[test] - fn stale_timeouts_do_not_replace_newer_connections_or_loaded_forms() { - let mut state = Connection::default(); - let first = state.begin_navigation(); - let second = state.begin_navigation(); - assert!(!state.timeout(first)); - state.ready = true; - assert!(state.timeout(second)); - assert!(state.ready); - assert!(!state.timeout(second)); - let third = state.begin_navigation(); - state.phase = "connected"; - assert!(!state.timeout(third)); - } - - #[test] - fn remote_pages_cannot_invoke_native_commands() { - let app = tauri::test::mock_builder().build(context()).unwrap(); - let window = WebviewWindowBuilder::new( - &app, - "workspace-1", - WebviewUrl::External(Url::parse(DEFAULT_SERVER).unwrap()), - ) - .build() - .unwrap(); - for command in [ - "plugin:window|close", - "plugin:webview|create_webview_window", - "plugin:path|resolve_directory", - ] { - let response = tauri::test::get_ipc_response( - &window, - tauri::webview::InvokeRequest { - cmd: command.into(), - callback: tauri::ipc::CallbackFn(0), - error: tauri::ipc::CallbackFn(1), - url: Url::parse(DEFAULT_SERVER).unwrap(), - body: tauri::ipc::InvokeBody::default(), - headers: Default::default(), - invoke_key: tauri::test::INVOKE_KEY.into(), - }, - ); - assert!(response - .unwrap_err() - .as_str() - .unwrap() - .contains("not allowed")); - } - } - - #[test] - fn packaged_app_exposes_no_native_capabilities_or_global_api() { - let config: serde_json::Value = - serde_json::from_str(include_str!("../tauri.conf.json")).unwrap(); - assert_eq!(config["bundle"]["macOS"]["minimumSystemVersion"], "14.0"); - assert_eq!(config["app"]["withGlobalTauri"], false); - assert_eq!( - config["app"]["security"]["capabilities"], - serde_json::json!([]) - ); - } -} diff --git a/native/src-tauri/tauri.ios.conf.json b/native/src-tauri/tauri.ios.conf.json new file mode 100644 index 0000000..6722463 --- /dev/null +++ b/native/src-tauri/tauri.ios.conf.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "bundle": { + "iOS": { "minimumSystemVersion": "17.0" } + } +}