├── .github └── workflows │ └── build.yml ├── .gitignore ├── .gitmodules ├── LICENSE ├── OpenParsec.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ └── OpenParsec.xcscheme ├── OpenParsec ├── ActivityIndicator.swift ├── AppDelegate.swift ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── icon-1024.png │ │ ├── icon-20.png │ │ ├── icon-20@2x.png │ │ ├── icon-20@3x.png │ │ ├── icon-29.png │ │ ├── icon-29@2x.png │ │ ├── icon-29@3x.png │ │ ├── icon-40.png │ │ ├── icon-40@2x.png │ │ ├── icon-40@3x.png │ │ ├── icon-60@2x.png │ │ ├── icon-60@3x.png │ │ ├── icon-76.png │ │ ├── icon-76@2x.png │ │ └── icon-83.5@2x.png │ ├── BackgroundButton.colorset │ │ └── Contents.json │ ├── BackgroundCard.colorset │ │ └── Contents.json │ ├── BackgroundField.colorset │ │ └── Contents.json │ ├── BackgroundGray.colorset │ │ └── Contents.json │ ├── BackgroundPrompt.colorset │ │ └── Contents.json │ ├── BackgroundTab.colorset │ │ └── Contents.json │ ├── Contents.json │ ├── Foreground.colorset │ │ └── Contents.json │ ├── ForegroundInactive.colorset │ │ └── Contents.json │ ├── IconTransparent.imageset │ │ ├── Contents.json │ │ └── icon_transparent.png │ ├── LogoShadow.imageset │ │ ├── Contents.json │ │ └── logo_shadow.png │ ├── Shading.colorset │ │ └── Contents.json │ └── SymbolExit.symbolset │ │ ├── Contents.json │ │ └── rectangle.portrait.and.arrow.right.svg ├── Base.lproj │ └── LaunchScreen.storyboard ├── CParsec.swift ├── ContentView.swift ├── ExUI.swift ├── GameController.swift ├── Info.plist ├── KeyBoardTest.swift ├── KeyCodeTranslators.swift ├── LoginView.swift ├── MainView.swift ├── NetworkHandler.swift ├── OpenParsec-Bridging-Header.h ├── ParsecGLKRenderer.swift ├── ParsecGLKViewController.swift ├── ParsecMetalRenderer.swift ├── ParsecMetalViewController.swift ├── ParsecSDKBridge.swift ├── ParsecUserData.swift ├── ParsecView.swift ├── ParsecViewController.swift ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── SceneDelegate.swift ├── SettingsHandler.swift ├── SettingsView.swift ├── Shared.swift ├── TouchHandlingView.swift ├── UIViewControllerWrapper.swift ├── URLImage.swift ├── ViewContainerPatch.swift ├── audio.c └── audio.h ├── README.md └── altstore.json /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Xcode - Build 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | inputs: 8 | release__nightly: 9 | description: Create a nightly release 10 | type: boolean 11 | required: false 12 | 13 | jobs: 14 | build: 15 | name: Build using xcodebuild command 16 | runs-on: macos-latest 17 | env: 18 | scheme: OpenParsec 19 | archive_path: archive 20 | outputs: 21 | scheme: ${{ steps.scheme.outputs.scheme }} 22 | archive_path: ${{ env.archive_path }} 23 | 24 | steps: 25 | - uses: maxim-lobanov/setup-xcode@v1 26 | with: 27 | xcode-version: '15.3' 28 | - name: Checkout 29 | uses: actions/checkout@v3 30 | with: 31 | submodules: recursive 32 | - name: Set Scheme 33 | id: scheme 34 | run: | 35 | if [ $scheme = default ] 36 | then 37 | scheme_list=$(xcodebuild -list -json | tr -d "\n") 38 | scheme=$(echo $scheme_list | ruby -e "require 'json'; puts JSON.parse(STDIN.gets)['project']['targets'][0]") 39 | echo Using default scheme: $scheme 40 | else 41 | echo Using configured scheme: $scheme 42 | fi 43 | echo "scheme=$scheme" >> $GITHUB_OUTPUT 44 | - name: Set filetype_parameter 45 | id: filetype_parameter 46 | run: | 47 | filetype_parameter=`ls -A | grep -i \\.xcworkspace\$ && echo workspace || echo project` 48 | echo "filetype_parameter=$filetype_parameter" >> $GITHUB_OUTPUT 49 | - name: Set file_to_build 50 | id: file_to_build 51 | run: | 52 | file_to_build=`ls -A | grep -i \\.xcworkspace\$ || ls -A | grep -i \\.xcodeproj\$` 53 | file_to_build=`echo $file_to_build | awk '{$1=$1;print}'` 54 | echo "file_to_build=$file_to_build" >> $GITHUB_OUTPUT 55 | - name: Build 56 | env: 57 | scheme: ${{ steps.scheme.outputs.scheme }} 58 | filetype_parameter: ${{ steps.filetype_parameter.outputs.filetype_parameter }} 59 | file_to_build: ${{ steps.file_to_build.outputs.file_to_build }} 60 | run: xcodebuild clean build -scheme "$scheme" -"$filetype_parameter" "$file_to_build" -sdk iphoneos -arch arm64 -configuration Release CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO | xcpretty && exit ${PIPESTATUS[0]} 61 | - name: Analyze 62 | env: 63 | scheme: ${{ steps.scheme.outputs.scheme }} 64 | filetype_parameter: ${{ steps.filetype_parameter.outputs.filetype_parameter }} 65 | file_to_build: ${{ steps.file_to_build.outputs.file_to_build }} 66 | run: xcodebuild analyze -scheme "$scheme" -"$filetype_parameter" "$file_to_build" -sdk iphoneos -arch arm64 -configuration Release CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO | xcpretty && exit ${PIPESTATUS[0]} 67 | - name: Archive 68 | env: 69 | scheme: ${{ steps.scheme.outputs.scheme }} 70 | filetype_parameter: ${{ steps.filetype_parameter.outputs.filetype_parameter }} 71 | file_to_build: ${{ steps.file_to_build.outputs.file_to_build }} 72 | run: xcodebuild archive -archivePath "$archive_path" -scheme "$scheme" -"$filetype_parameter" "$file_to_build" -sdk iphoneos -arch arm64 -configuration Release CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO | xcpretty && exit ${PIPESTATUS[0]} 73 | - name: Tar Build Artifact 74 | run: tar -cvf "$archive_path.xcarchive.tar" "$archive_path.xcarchive" 75 | - name: Upload a Build Artifact 76 | uses: actions/upload-artifact@v3 77 | with: 78 | name: ${{ env.archive_path }}.xcarchive.tar 79 | path: ${{ env.archive_path }}.xcarchive.tar 80 | 81 | package: 82 | name: Create fake-signed ipa 83 | runs-on: ubuntu-latest 84 | needs: [build] 85 | env: 86 | scheme: ${{ needs.build.outputs.scheme }} 87 | archive_path: ${{ needs.build.outputs.archive_path }} 88 | outputs: 89 | artifact: ${{ env.scheme }}.ipa 90 | 91 | steps: 92 | - name: Download a Build Artifact 93 | uses: actions/download-artifact@v3 94 | with: 95 | name: ${{ env.archive_path }}.xcarchive.tar 96 | - name: Extract Build Artifact 97 | run: tar -xf "$archive_path.xcarchive.tar" 98 | - name: Install ldid 99 | run: | 100 | if [ `uname -s` = "Linux" ]; then 101 | curl -sSL -o /usr/local/bin/ldid "${{ github.server_url }}/ProcursusTeam/ldid/releases/latest/download/ldid_linux_`uname -m`" 102 | chmod +x /usr/local/bin/ldid 103 | elif [ `uname -s` = "Darwin" ]; then 104 | brew install ldid 105 | else 106 | exit 1 107 | fi 108 | - name: Fakesign 109 | run: | 110 | find "$archive_path.xcarchive/Products/Applications/$scheme.app" -type d -path '*/Frameworks/*.framework' -exec ldid -S \{\} \; 111 | ldid -S "$archive_path.xcarchive/Products/Applications/$scheme.app" 112 | - name: Create IPA 113 | run: | 114 | mv "$archive_path.xcarchive/Products/Applications" Payload 115 | zip -r "$scheme.ipa" "Payload" -x "._*" -x ".DS_Store" -x "__MACOSX" 116 | - name: Upload a Build Artifact 117 | uses: actions/upload-artifact@v3 118 | with: 119 | name: ${{ env.scheme }}.ipa 120 | path: ${{ env.scheme }}.ipa 121 | 122 | release__nightly: 123 | name: Nightly Release 124 | permissions: 125 | contents: write 126 | if: inputs.release__nightly || github.event_name == 'push' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch) 127 | runs-on: ubuntu-latest 128 | needs: [package] 129 | concurrency: 130 | group: release__nightly 131 | cancel-in-progress: true 132 | 133 | steps: 134 | - name: Download a Build Artifact 135 | uses: actions/download-artifact@v3 136 | with: 137 | name: ${{ needs.package.outputs.artifact }} 138 | - name: Nightly Release 139 | uses: andelf/nightly-release@v1 140 | env: 141 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 142 | with: 143 | body: | 144 | This is a nightly release [created automatically with GitHub Actions workflow](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}/attempts/${{ github.run_attempt }}). 145 | files: | 146 | ${{ needs.package.outputs.artifact }} 147 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # General 2 | .DS_Store 3 | .AppleDouble 4 | .LSOverride 5 | 6 | # Icon must end with two \r 7 | Icon 8 | 9 | 10 | # Thumbnails 11 | ._* 12 | 13 | # Files that might appear in the root of a volume 14 | .DocumentRevisions-V100 15 | .fseventsd 16 | .Spotlight-V100 17 | .TemporaryItems 18 | .Trashes 19 | .VolumeIcon.icns 20 | .com.apple.timemachine.donotpresent 21 | 22 | # Directories potentially created on remote AFP share 23 | .AppleDB 24 | .AppleDesktop 25 | Network Trash Folder 26 | Temporary Items 27 | .apdisk 28 | 29 | ## User settings 30 | xcuserdata/ 31 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "Frameworks/ParsecSDK.framework"] 2 | path = Frameworks/ParsecSDK.framework 3 | url = https://github.com/AngelDTF/parsec-sdk.git 4 | branch = sdk/ios/ParsecSDK.framework 5 | shallow = true 6 | -------------------------------------------------------------------------------- /OpenParsec.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 170E0A772BEF5AFF00B0EA32 /* ParsecViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 170E0A762BEF5AFF00B0EA32 /* ParsecViewController.swift */; }; 11 | 17374D412CB8E35D00EDCEE7 /* KeyCodeTranslators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17374D402CB8E35500EDCEE7 /* KeyCodeTranslators.swift */; }; 12 | 17840D1D2CB639530097374B /* ParsecUserData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17840D1C2CB6394D0097374B /* ParsecUserData.swift */; }; 13 | 17CD1E032BF07BC3003D2102 /* ViewContainerPatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17CD1E022BF07BC3003D2102 /* ViewContainerPatch.swift */; }; 14 | 17EA35F72BF712A8001BE0DF /* ParsecSDKBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17EA35F62BF712A8001BE0DF /* ParsecSDKBridge.swift */; }; 15 | 17EEB91C2BEE62BA00502A3A /* KeyBoardTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17EEB91B2BEE62BA00502A3A /* KeyBoardTest.swift */; }; 16 | 271D14F9292E90EB00D7F1D6 /* ParsecView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 271D14F8292E90EB00D7F1D6 /* ParsecView.swift */; }; 17 | 271D14FC292EAA3600D7F1D6 /* ParsecGLKRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 271D14FA292EAA3600D7F1D6 /* ParsecGLKRenderer.swift */; }; 18 | 271D14FD292EAA3600D7F1D6 /* ParsecGLKViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 271D14FB292EAA3600D7F1D6 /* ParsecGLKViewController.swift */; }; 19 | 274A69CF29EA07FA00F595A5 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 274A69CE29EA07FA00F595A5 /* SettingsView.swift */; }; 20 | 27899AC7292FE2A9001ACA33 /* audio.c in Sources */ = {isa = PBXBuildFile; fileRef = 27899AC5292FE2A9001ACA33 /* audio.c */; }; 21 | 27A267352B1AEAB700F34C63 /* SettingsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27A267342B1AEAB700F34C63 /* SettingsHandler.swift */; }; 22 | 27A923E029E8E53000F54BDA /* TouchHandlingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27A923DF29E8E53000F54BDA /* TouchHandlingView.swift */; }; 23 | 27A923E229E8FEE900F54BDA /* UIViewControllerWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27A923E129E8FEE900F54BDA /* UIViewControllerWrapper.swift */; }; 24 | 27AC751829EA339B00E8CAF7 /* URLImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27AC751729EA339B00E8CAF7 /* URLImage.swift */; }; 25 | 27AD36602B19731800C8A607 /* ExUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27AD365F2B19731800C8A607 /* ExUI.swift */; }; 26 | 27B23A222B1B979C00B52F14 /* ParsecMetalRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27B23A212B1B979C00B52F14 /* ParsecMetalRenderer.swift */; }; 27 | 27B23A242B1B98EF00B52F14 /* ParsecMetalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27B23A232B1B98EF00B52F14 /* ParsecMetalViewController.swift */; }; 28 | 27B8D564292DC7A000A324AD /* CParsec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27B8D563292DC7A000A324AD /* CParsec.swift */; }; 29 | 27B8D571292DCA5B00A324AD /* ParsecSDK.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27B8D56F292DCA5800A324AD /* ParsecSDK.framework */; settings = {ATTRIBUTES = (Required, ); }; }; 30 | 27B8D572292DCA5B00A324AD /* ParsecSDK.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 27B8D56F292DCA5800A324AD /* ParsecSDK.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 31 | 27E61A92292965FC00FF6563 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E61A91292965FC00FF6563 /* AppDelegate.swift */; }; 32 | 27E61A94292965FC00FF6563 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E61A93292965FC00FF6563 /* SceneDelegate.swift */; }; 33 | 27E61A96292965FC00FF6563 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E61A95292965FC00FF6563 /* ContentView.swift */; }; 34 | 27E61A98292965FD00FF6563 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 27E61A97292965FD00FF6563 /* Assets.xcassets */; }; 35 | 27E61A9B292965FD00FF6563 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 27E61A9A292965FD00FF6563 /* Preview Assets.xcassets */; }; 36 | 27E61A9E292965FD00FF6563 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 27E61A9C292965FD00FF6563 /* LaunchScreen.storyboard */; }; 37 | 27E61AA62929817700FF6563 /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E61AA52929817700FF6563 /* LoginView.swift */; }; 38 | 27E61AA8292994B500FF6563 /* ActivityIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E61AA7292994B500FF6563 /* ActivityIndicator.swift */; }; 39 | 27E61AAA2929B92200FF6563 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E61AA92929B92200FF6563 /* MainView.swift */; }; 40 | 27ED36FF292D4F9800B1BE3D /* NetworkHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27ED36FE292D4F9800B1BE3D /* NetworkHandler.swift */; }; 41 | 84480EBE2ADC4FDA007DE5F1 /* GameController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84480EBD2ADC4FDA007DE5F1 /* GameController.swift */; }; 42 | FC16A7AD29A97BDA00BB70A7 /* Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC16A7AC29A97BDA00BB70A7 /* Shared.swift */; }; 43 | /* End PBXBuildFile section */ 44 | 45 | /* Begin PBXCopyFilesBuildPhase section */ 46 | 27B8D569292DC81500A324AD /* Embed Frameworks */ = { 47 | isa = PBXCopyFilesBuildPhase; 48 | buildActionMask = 2147483647; 49 | dstPath = ""; 50 | dstSubfolderSpec = 10; 51 | files = ( 52 | 27B8D572292DCA5B00A324AD /* ParsecSDK.framework in Embed Frameworks */, 53 | ); 54 | name = "Embed Frameworks"; 55 | runOnlyForDeploymentPostprocessing = 0; 56 | }; 57 | /* End PBXCopyFilesBuildPhase section */ 58 | 59 | /* Begin PBXFileReference section */ 60 | 170E0A762BEF5AFF00B0EA32 /* ParsecViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParsecViewController.swift; sourceTree = ""; }; 61 | 17374D402CB8E35500EDCEE7 /* KeyCodeTranslators.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyCodeTranslators.swift; sourceTree = ""; }; 62 | 17840D1C2CB6394D0097374B /* ParsecUserData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParsecUserData.swift; sourceTree = ""; }; 63 | 17CD1E022BF07BC3003D2102 /* ViewContainerPatch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewContainerPatch.swift; sourceTree = ""; }; 64 | 17EA35F62BF712A8001BE0DF /* ParsecSDKBridge.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParsecSDKBridge.swift; sourceTree = ""; }; 65 | 17EEB91B2BEE62BA00502A3A /* KeyBoardTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyBoardTest.swift; sourceTree = ""; }; 66 | 271D14F8292E90EB00D7F1D6 /* ParsecView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParsecView.swift; sourceTree = ""; }; 67 | 271D14FA292EAA3600D7F1D6 /* ParsecGLKRenderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParsecGLKRenderer.swift; sourceTree = ""; }; 68 | 271D14FB292EAA3600D7F1D6 /* ParsecGLKViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParsecGLKViewController.swift; sourceTree = ""; }; 69 | 274A69CE29EA07FA00F595A5 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; 70 | 27899AC4292FE2A8001ACA33 /* OpenParsec-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "OpenParsec-Bridging-Header.h"; sourceTree = ""; }; 71 | 27899AC5292FE2A9001ACA33 /* audio.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = audio.c; sourceTree = ""; }; 72 | 27899AC6292FE2A9001ACA33 /* audio.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = audio.h; sourceTree = ""; }; 73 | 27A267342B1AEAB700F34C63 /* SettingsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsHandler.swift; sourceTree = ""; }; 74 | 27A923DF29E8E53000F54BDA /* TouchHandlingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TouchHandlingView.swift; sourceTree = ""; }; 75 | 27A923E129E8FEE900F54BDA /* UIViewControllerWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewControllerWrapper.swift; sourceTree = ""; }; 76 | 27AC751729EA339B00E8CAF7 /* URLImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLImage.swift; sourceTree = ""; }; 77 | 27AD365F2B19731800C8A607 /* ExUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExUI.swift; sourceTree = ""; }; 78 | 27B23A212B1B979C00B52F14 /* ParsecMetalRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParsecMetalRenderer.swift; sourceTree = ""; }; 79 | 27B23A232B1B98EF00B52F14 /* ParsecMetalViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParsecMetalViewController.swift; sourceTree = ""; }; 80 | 27B8D563292DC7A000A324AD /* CParsec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CParsec.swift; sourceTree = ""; }; 81 | 27B8D56F292DCA5800A324AD /* ParsecSDK.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ParsecSDK.framework; path = Frameworks/ParsecSDK.framework; sourceTree = SOURCE_ROOT; }; 82 | 27E61A8E292965FC00FF6563 /* OpenParsec.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = OpenParsec.app; sourceTree = BUILT_PRODUCTS_DIR; }; 83 | 27E61A91292965FC00FF6563 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 84 | 27E61A93292965FC00FF6563 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 85 | 27E61A95292965FC00FF6563 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 86 | 27E61A97292965FD00FF6563 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 87 | 27E61A9A292965FD00FF6563 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 88 | 27E61A9D292965FD00FF6563 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 89 | 27E61A9F292965FD00FF6563 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 90 | 27E61AA52929817700FF6563 /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = ""; }; 91 | 27E61AA7292994B500FF6563 /* ActivityIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicator.swift; sourceTree = ""; }; 92 | 27E61AA92929B92200FF6563 /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = ""; }; 93 | 27ED36FE292D4F9800B1BE3D /* NetworkHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkHandler.swift; sourceTree = ""; }; 94 | 84480EBD2ADC4FDA007DE5F1 /* GameController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GameController.swift; sourceTree = ""; }; 95 | FC16A7AC29A97BDA00BB70A7 /* Shared.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shared.swift; sourceTree = ""; }; 96 | /* End PBXFileReference section */ 97 | 98 | /* Begin PBXFrameworksBuildPhase section */ 99 | 27E61A8B292965FC00FF6563 /* Frameworks */ = { 100 | isa = PBXFrameworksBuildPhase; 101 | buildActionMask = 2147483647; 102 | files = ( 103 | 27B8D571292DCA5B00A324AD /* ParsecSDK.framework in Frameworks */, 104 | ); 105 | runOnlyForDeploymentPostprocessing = 0; 106 | }; 107 | /* End PBXFrameworksBuildPhase section */ 108 | 109 | /* Begin PBXGroup section */ 110 | 27B8D56A292DC98B00A324AD /* Frameworks */ = { 111 | isa = PBXGroup; 112 | children = ( 113 | 27B8D56F292DCA5800A324AD /* ParsecSDK.framework */, 114 | ); 115 | path = Frameworks; 116 | sourceTree = ""; 117 | }; 118 | 27E61A85292965FC00FF6563 = { 119 | isa = PBXGroup; 120 | children = ( 121 | 27B8D56A292DC98B00A324AD /* Frameworks */, 122 | 27E61A90292965FC00FF6563 /* OpenParsec */, 123 | 27E61A8F292965FC00FF6563 /* Products */, 124 | ); 125 | sourceTree = ""; 126 | usesTabs = 1; 127 | }; 128 | 27E61A8F292965FC00FF6563 /* Products */ = { 129 | isa = PBXGroup; 130 | children = ( 131 | 27E61A8E292965FC00FF6563 /* OpenParsec.app */, 132 | ); 133 | name = Products; 134 | sourceTree = ""; 135 | }; 136 | 27E61A90292965FC00FF6563 /* OpenParsec */ = { 137 | isa = PBXGroup; 138 | children = ( 139 | 17374D402CB8E35500EDCEE7 /* KeyCodeTranslators.swift */, 140 | 17840D1C2CB6394D0097374B /* ParsecUserData.swift */, 141 | 27E61AA7292994B500FF6563 /* ActivityIndicator.swift */, 142 | 27E61A91292965FC00FF6563 /* AppDelegate.swift */, 143 | 27E61A97292965FD00FF6563 /* Assets.xcassets */, 144 | 27899AC5292FE2A9001ACA33 /* audio.c */, 145 | 27899AC6292FE2A9001ACA33 /* audio.h */, 146 | 27E61A95292965FC00FF6563 /* ContentView.swift */, 147 | 27B8D563292DC7A000A324AD /* CParsec.swift */, 148 | 27AD365F2B19731800C8A607 /* ExUI.swift */, 149 | 84480EBD2ADC4FDA007DE5F1 /* GameController.swift */, 150 | 27E61A9F292965FD00FF6563 /* Info.plist */, 151 | 17EEB91B2BEE62BA00502A3A /* KeyBoardTest.swift */, 152 | 27E61A9C292965FD00FF6563 /* LaunchScreen.storyboard */, 153 | 27E61AA52929817700FF6563 /* LoginView.swift */, 154 | 27E61AA92929B92200FF6563 /* MainView.swift */, 155 | 27ED36FE292D4F9800B1BE3D /* NetworkHandler.swift */, 156 | 27899AC4292FE2A8001ACA33 /* OpenParsec-Bridging-Header.h */, 157 | 271D14FA292EAA3600D7F1D6 /* ParsecGLKRenderer.swift */, 158 | 271D14FB292EAA3600D7F1D6 /* ParsecGLKViewController.swift */, 159 | 27B23A212B1B979C00B52F14 /* ParsecMetalRenderer.swift */, 160 | 27B23A232B1B98EF00B52F14 /* ParsecMetalViewController.swift */, 161 | 17EA35F62BF712A8001BE0DF /* ParsecSDKBridge.swift */, 162 | 271D14F8292E90EB00D7F1D6 /* ParsecView.swift */, 163 | 170E0A762BEF5AFF00B0EA32 /* ParsecViewController.swift */, 164 | 27E61A99292965FD00FF6563 /* Preview Content */, 165 | 27E61A93292965FC00FF6563 /* SceneDelegate.swift */, 166 | 27A267342B1AEAB700F34C63 /* SettingsHandler.swift */, 167 | 274A69CE29EA07FA00F595A5 /* SettingsView.swift */, 168 | FC16A7AC29A97BDA00BB70A7 /* Shared.swift */, 169 | 27A923DF29E8E53000F54BDA /* TouchHandlingView.swift */, 170 | 27A923E129E8FEE900F54BDA /* UIViewControllerWrapper.swift */, 171 | 27AC751729EA339B00E8CAF7 /* URLImage.swift */, 172 | 17CD1E022BF07BC3003D2102 /* ViewContainerPatch.swift */, 173 | ); 174 | path = OpenParsec; 175 | sourceTree = ""; 176 | }; 177 | 27E61A99292965FD00FF6563 /* Preview Content */ = { 178 | isa = PBXGroup; 179 | children = ( 180 | 27E61A9A292965FD00FF6563 /* Preview Assets.xcassets */, 181 | ); 182 | path = "Preview Content"; 183 | sourceTree = ""; 184 | }; 185 | /* End PBXGroup section */ 186 | 187 | /* Begin PBXNativeTarget section */ 188 | 27E61A8D292965FC00FF6563 /* OpenParsec */ = { 189 | isa = PBXNativeTarget; 190 | buildConfigurationList = 27E61AA2292965FD00FF6563 /* Build configuration list for PBXNativeTarget "OpenParsec" */; 191 | buildPhases = ( 192 | 27E61A8A292965FC00FF6563 /* Sources */, 193 | 27E61A8B292965FC00FF6563 /* Frameworks */, 194 | 27E61A8C292965FC00FF6563 /* Resources */, 195 | 27B8D569292DC81500A324AD /* Embed Frameworks */, 196 | ); 197 | buildRules = ( 198 | ); 199 | dependencies = ( 200 | ); 201 | name = OpenParsec; 202 | packageProductDependencies = ( 203 | ); 204 | productName = OpenParsec; 205 | productReference = 27E61A8E292965FC00FF6563 /* OpenParsec.app */; 206 | productType = "com.apple.product-type.application"; 207 | }; 208 | /* End PBXNativeTarget section */ 209 | 210 | /* Begin PBXProject section */ 211 | 27E61A86292965FC00FF6563 /* Project object */ = { 212 | isa = PBXProject; 213 | attributes = { 214 | LastSwiftUpdateCheck = 1250; 215 | LastUpgradeCheck = 1250; 216 | TargetAttributes = { 217 | 27E61A8D292965FC00FF6563 = { 218 | CreatedOnToolsVersion = 12.5; 219 | LastSwiftMigration = 1250; 220 | }; 221 | }; 222 | }; 223 | buildConfigurationList = 27E61A89292965FC00FF6563 /* Build configuration list for PBXProject "OpenParsec" */; 224 | compatibilityVersion = "Xcode 9.3"; 225 | developmentRegion = en; 226 | hasScannedForEncodings = 0; 227 | knownRegions = ( 228 | en, 229 | Base, 230 | ); 231 | mainGroup = 27E61A85292965FC00FF6563; 232 | packageReferences = ( 233 | ); 234 | productRefGroup = 27E61A8F292965FC00FF6563 /* Products */; 235 | projectDirPath = ""; 236 | projectRoot = ""; 237 | targets = ( 238 | 27E61A8D292965FC00FF6563 /* OpenParsec */, 239 | ); 240 | }; 241 | /* End PBXProject section */ 242 | 243 | /* Begin PBXResourcesBuildPhase section */ 244 | 27E61A8C292965FC00FF6563 /* Resources */ = { 245 | isa = PBXResourcesBuildPhase; 246 | buildActionMask = 2147483647; 247 | files = ( 248 | 27E61A9E292965FD00FF6563 /* LaunchScreen.storyboard in Resources */, 249 | 27E61A9B292965FD00FF6563 /* Preview Assets.xcassets in Resources */, 250 | 27E61A98292965FD00FF6563 /* Assets.xcassets in Resources */, 251 | ); 252 | runOnlyForDeploymentPostprocessing = 0; 253 | }; 254 | /* End PBXResourcesBuildPhase section */ 255 | 256 | /* Begin PBXSourcesBuildPhase section */ 257 | 27E61A8A292965FC00FF6563 /* Sources */ = { 258 | isa = PBXSourcesBuildPhase; 259 | buildActionMask = 2147483647; 260 | files = ( 261 | FC16A7AD29A97BDA00BB70A7 /* Shared.swift in Sources */, 262 | 84480EBE2ADC4FDA007DE5F1 /* GameController.swift in Sources */, 263 | 27AC751829EA339B00E8CAF7 /* URLImage.swift in Sources */, 264 | 27B23A242B1B98EF00B52F14 /* ParsecMetalViewController.swift in Sources */, 265 | 27A923E029E8E53000F54BDA /* TouchHandlingView.swift in Sources */, 266 | 27E61A92292965FC00FF6563 /* AppDelegate.swift in Sources */, 267 | 27E61AAA2929B92200FF6563 /* MainView.swift in Sources */, 268 | 271D14FD292EAA3600D7F1D6 /* ParsecGLKViewController.swift in Sources */, 269 | 274A69CF29EA07FA00F595A5 /* SettingsView.swift in Sources */, 270 | 17CD1E032BF07BC3003D2102 /* ViewContainerPatch.swift in Sources */, 271 | 27E61AA8292994B500FF6563 /* ActivityIndicator.swift in Sources */, 272 | 271D14FC292EAA3600D7F1D6 /* ParsecGLKRenderer.swift in Sources */, 273 | 27E61A94292965FC00FF6563 /* SceneDelegate.swift in Sources */, 274 | 27A267352B1AEAB700F34C63 /* SettingsHandler.swift in Sources */, 275 | 27AD36602B19731800C8A607 /* ExUI.swift in Sources */, 276 | 27899AC7292FE2A9001ACA33 /* audio.c in Sources */, 277 | 27E61AA62929817700FF6563 /* LoginView.swift in Sources */, 278 | 27B8D564292DC7A000A324AD /* CParsec.swift in Sources */, 279 | 27A923E229E8FEE900F54BDA /* UIViewControllerWrapper.swift in Sources */, 280 | 170E0A772BEF5AFF00B0EA32 /* ParsecViewController.swift in Sources */, 281 | 27B23A222B1B979C00B52F14 /* ParsecMetalRenderer.swift in Sources */, 282 | 17840D1D2CB639530097374B /* ParsecUserData.swift in Sources */, 283 | 27E61A96292965FC00FF6563 /* ContentView.swift in Sources */, 284 | 17EA35F72BF712A8001BE0DF /* ParsecSDKBridge.swift in Sources */, 285 | 27ED36FF292D4F9800B1BE3D /* NetworkHandler.swift in Sources */, 286 | 17EEB91C2BEE62BA00502A3A /* KeyBoardTest.swift in Sources */, 287 | 17374D412CB8E35D00EDCEE7 /* KeyCodeTranslators.swift in Sources */, 288 | 271D14F9292E90EB00D7F1D6 /* ParsecView.swift in Sources */, 289 | ); 290 | runOnlyForDeploymentPostprocessing = 0; 291 | }; 292 | /* End PBXSourcesBuildPhase section */ 293 | 294 | /* Begin PBXVariantGroup section */ 295 | 27E61A9C292965FD00FF6563 /* LaunchScreen.storyboard */ = { 296 | isa = PBXVariantGroup; 297 | children = ( 298 | 27E61A9D292965FD00FF6563 /* Base */, 299 | ); 300 | name = LaunchScreen.storyboard; 301 | sourceTree = ""; 302 | }; 303 | /* End PBXVariantGroup section */ 304 | 305 | /* Begin XCBuildConfiguration section */ 306 | 27E61AA0292965FD00FF6563 /* Debug */ = { 307 | isa = XCBuildConfiguration; 308 | buildSettings = { 309 | ALWAYS_SEARCH_USER_PATHS = NO; 310 | CLANG_ANALYZER_NONNULL = YES; 311 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 312 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 313 | CLANG_CXX_LIBRARY = "libc++"; 314 | CLANG_ENABLE_MODULES = YES; 315 | CLANG_ENABLE_OBJC_ARC = YES; 316 | CLANG_ENABLE_OBJC_WEAK = YES; 317 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 318 | CLANG_WARN_BOOL_CONVERSION = YES; 319 | CLANG_WARN_COMMA = YES; 320 | CLANG_WARN_CONSTANT_CONVERSION = YES; 321 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 322 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 323 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 324 | CLANG_WARN_EMPTY_BODY = YES; 325 | CLANG_WARN_ENUM_CONVERSION = YES; 326 | CLANG_WARN_INFINITE_RECURSION = YES; 327 | CLANG_WARN_INT_CONVERSION = YES; 328 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 329 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 330 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 331 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 332 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 333 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 334 | CLANG_WARN_STRICT_PROTOTYPES = YES; 335 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 336 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 337 | CLANG_WARN_UNREACHABLE_CODE = YES; 338 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 339 | COPY_PHASE_STRIP = NO; 340 | DEBUG_INFORMATION_FORMAT = dwarf; 341 | ENABLE_STRICT_OBJC_MSGSEND = YES; 342 | ENABLE_TESTABILITY = YES; 343 | GCC_C_LANGUAGE_STANDARD = gnu11; 344 | GCC_DYNAMIC_NO_PIC = NO; 345 | GCC_NO_COMMON_BLOCKS = YES; 346 | GCC_OPTIMIZATION_LEVEL = 0; 347 | GCC_PREPROCESSOR_DEFINITIONS = ( 348 | "DEBUG=1", 349 | GLES_SILENCE_DEPRECATION, 350 | "$(inherited)", 351 | ); 352 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 353 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 354 | GCC_WARN_UNDECLARED_SELECTOR = YES; 355 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 356 | GCC_WARN_UNUSED_FUNCTION = YES; 357 | GCC_WARN_UNUSED_VARIABLE = YES; 358 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 359 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 360 | MTL_FAST_MATH = YES; 361 | ONLY_ACTIVE_ARCH = YES; 362 | SDKROOT = iphoneos; 363 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 364 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 365 | }; 366 | name = Debug; 367 | }; 368 | 27E61AA1292965FD00FF6563 /* Release */ = { 369 | isa = XCBuildConfiguration; 370 | buildSettings = { 371 | ALWAYS_SEARCH_USER_PATHS = NO; 372 | CLANG_ANALYZER_NONNULL = YES; 373 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 374 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 375 | CLANG_CXX_LIBRARY = "libc++"; 376 | CLANG_ENABLE_MODULES = YES; 377 | CLANG_ENABLE_OBJC_ARC = YES; 378 | CLANG_ENABLE_OBJC_WEAK = YES; 379 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 380 | CLANG_WARN_BOOL_CONVERSION = YES; 381 | CLANG_WARN_COMMA = YES; 382 | CLANG_WARN_CONSTANT_CONVERSION = YES; 383 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 384 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 385 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 386 | CLANG_WARN_EMPTY_BODY = YES; 387 | CLANG_WARN_ENUM_CONVERSION = YES; 388 | CLANG_WARN_INFINITE_RECURSION = YES; 389 | CLANG_WARN_INT_CONVERSION = YES; 390 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 391 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 392 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 393 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 394 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 395 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 396 | CLANG_WARN_STRICT_PROTOTYPES = YES; 397 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 398 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 399 | CLANG_WARN_UNREACHABLE_CODE = YES; 400 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 401 | COPY_PHASE_STRIP = NO; 402 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 403 | ENABLE_NS_ASSERTIONS = NO; 404 | ENABLE_STRICT_OBJC_MSGSEND = YES; 405 | GCC_C_LANGUAGE_STANDARD = gnu11; 406 | GCC_NO_COMMON_BLOCKS = YES; 407 | GCC_PREPROCESSOR_DEFINITIONS = GLES_SILENCE_DEPRECATION; 408 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 409 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 410 | GCC_WARN_UNDECLARED_SELECTOR = YES; 411 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 412 | GCC_WARN_UNUSED_FUNCTION = YES; 413 | GCC_WARN_UNUSED_VARIABLE = YES; 414 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 415 | MTL_ENABLE_DEBUG_INFO = NO; 416 | MTL_FAST_MATH = YES; 417 | SDKROOT = iphoneos; 418 | SWIFT_COMPILATION_MODE = wholemodule; 419 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 420 | VALIDATE_PRODUCT = YES; 421 | }; 422 | name = Release; 423 | }; 424 | 27E61AA3292965FD00FF6563 /* Debug */ = { 425 | isa = XCBuildConfiguration; 426 | buildSettings = { 427 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 428 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 429 | CLANG_ENABLE_MODULES = YES; 430 | CODE_SIGNING_ALLOWED = YES; 431 | CODE_SIGN_IDENTITY = "Apple Development"; 432 | CODE_SIGN_STYLE = Automatic; 433 | DEVELOPMENT_ASSET_PATHS = "\"OpenParsec/Preview Content\""; 434 | DEVELOPMENT_TEAM = 3C3YHUUV75; 435 | ENABLE_PREVIEWS = YES; 436 | "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64; 437 | INFOPLIST_FILE = OpenParsec/Info.plist; 438 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 439 | LD_RUNPATH_SEARCH_PATHS = ( 440 | "$(inherited)", 441 | "@executable_path/Frameworks", 442 | ); 443 | PRODUCT_BUNDLE_IDENTIFIER = com.aigch.OpenParsec1; 444 | PRODUCT_NAME = "$(TARGET_NAME)"; 445 | PROVISIONING_PROFILE_SPECIFIER = ""; 446 | SWIFT_OBJC_BRIDGING_HEADER = "OpenParsec/OpenParsec-Bridging-Header.h"; 447 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 448 | SWIFT_VERSION = 5.0; 449 | SYSTEM_FRAMEWORK_SEARCH_PATHS = $PROJECT_DIR/Frameworks; 450 | TARGETED_DEVICE_FAMILY = "1,2"; 451 | VALIDATE_WORKSPACE = YES; 452 | }; 453 | name = Debug; 454 | }; 455 | 27E61AA4292965FD00FF6563 /* Release */ = { 456 | isa = XCBuildConfiguration; 457 | buildSettings = { 458 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 459 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 460 | CLANG_ENABLE_MODULES = YES; 461 | CODE_SIGNING_ALLOWED = YES; 462 | CODE_SIGN_IDENTITY = "Apple Development"; 463 | CODE_SIGN_STYLE = Automatic; 464 | DEVELOPMENT_ASSET_PATHS = "\"OpenParsec/Preview Content\""; 465 | DEVELOPMENT_TEAM = 3C3YHUUV75; 466 | ENABLE_PREVIEWS = YES; 467 | "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64; 468 | INFOPLIST_FILE = OpenParsec/Info.plist; 469 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 470 | LD_RUNPATH_SEARCH_PATHS = ( 471 | "$(inherited)", 472 | "@executable_path/Frameworks", 473 | ); 474 | PRODUCT_BUNDLE_IDENTIFIER = com.aigch.OpenParsec1; 475 | PRODUCT_NAME = "$(TARGET_NAME)"; 476 | PROVISIONING_PROFILE_SPECIFIER = ""; 477 | SWIFT_OBJC_BRIDGING_HEADER = "OpenParsec/OpenParsec-Bridging-Header.h"; 478 | SWIFT_VERSION = 5.0; 479 | SYSTEM_FRAMEWORK_SEARCH_PATHS = $PROJECT_DIR/Frameworks; 480 | TARGETED_DEVICE_FAMILY = "1,2"; 481 | VALIDATE_WORKSPACE = YES; 482 | }; 483 | name = Release; 484 | }; 485 | /* End XCBuildConfiguration section */ 486 | 487 | /* Begin XCConfigurationList section */ 488 | 27E61A89292965FC00FF6563 /* Build configuration list for PBXProject "OpenParsec" */ = { 489 | isa = XCConfigurationList; 490 | buildConfigurations = ( 491 | 27E61AA0292965FD00FF6563 /* Debug */, 492 | 27E61AA1292965FD00FF6563 /* Release */, 493 | ); 494 | defaultConfigurationIsVisible = 0; 495 | defaultConfigurationName = Release; 496 | }; 497 | 27E61AA2292965FD00FF6563 /* Build configuration list for PBXNativeTarget "OpenParsec" */ = { 498 | isa = XCConfigurationList; 499 | buildConfigurations = ( 500 | 27E61AA3292965FD00FF6563 /* Debug */, 501 | 27E61AA4292965FD00FF6563 /* Release */, 502 | ); 503 | defaultConfigurationIsVisible = 0; 504 | defaultConfigurationName = Release; 505 | }; 506 | /* End XCConfigurationList section */ 507 | }; 508 | rootObject = 27E61A86292965FC00FF6563 /* Project object */; 509 | } 510 | -------------------------------------------------------------------------------- /OpenParsec.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /OpenParsec.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /OpenParsec.xcodeproj/xcshareddata/xcschemes/OpenParsec.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /OpenParsec/ActivityIndicator.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import SwiftUI 3 | 4 | struct ActivityIndicator: UIViewRepresentable 5 | { 6 | @Binding var isAnimating:Bool 7 | let style:UIActivityIndicatorView.Style 8 | var tint:UIColor 9 | 10 | func makeUIView(context:UIViewRepresentableContext) -> UIActivityIndicatorView 11 | { 12 | let uiView:UIActivityIndicatorView = UIActivityIndicatorView(style:style) 13 | uiView.color = tint; 14 | return uiView 15 | } 16 | 17 | func updateUIView(_ uiView:UIActivityIndicatorView, context:UIViewRepresentableContext) 18 | { 19 | isAnimating ? uiView.startAnimating() : uiView.stopAnimating() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /OpenParsec/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | @main 4 | class AppDelegate:UIResponder, UIApplicationDelegate 5 | { 6 | func application(_ application:UIApplication, didFinishLaunchingWithOptions launchOptions:[UIApplication.LaunchOptionsKey: Any]?) -> Bool 7 | { 8 | // Override point for customization after application launch. 9 | UTMViewControllerPatches.patchAll() 10 | return true 11 | } 12 | 13 | func application(_ application:UIApplication, configurationForConnecting connectingSceneSession:UISceneSession, options:UIScene.ConnectionOptions) -> UISceneConfiguration 14 | { 15 | // Called when a new scene session is being created. 16 | // Use this method to select a configuration to create the new scene with. 17 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 18 | } 19 | 20 | func application(_ application:UIApplication, didDiscardSceneSessions sceneSessions:Set) 21 | { 22 | // Called when the user discards a scene session. 23 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 24 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 25 | } 26 | 27 | func applicationWillTerminate(_ application: UIApplication) 28 | { 29 | CParsec.destroy() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /OpenParsec/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "1.000", 9 | "green" : "0.250", 10 | "red" : "0.625" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /OpenParsec/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon-20@2x.png", 5 | "idiom" : "iphone", 6 | "scale" : "2x", 7 | "size" : "20x20" 8 | }, 9 | { 10 | "filename" : "icon-20@3x.png", 11 | "idiom" : "iphone", 12 | "scale" : "3x", 13 | "size" : "20x20" 14 | }, 15 | { 16 | "filename" : "icon-29@2x.png", 17 | "idiom" : "iphone", 18 | "scale" : "2x", 19 | "size" : "29x29" 20 | }, 21 | { 22 | "filename" : "icon-29@3x.png", 23 | "idiom" : "iphone", 24 | "scale" : "3x", 25 | "size" : "29x29" 26 | }, 27 | { 28 | "filename" : "icon-40@2x.png", 29 | "idiom" : "iphone", 30 | "scale" : "2x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "filename" : "icon-40@3x.png", 35 | "idiom" : "iphone", 36 | "scale" : "3x", 37 | "size" : "40x40" 38 | }, 39 | { 40 | "filename" : "icon-60@2x.png", 41 | "idiom" : "iphone", 42 | "scale" : "2x", 43 | "size" : "60x60" 44 | }, 45 | { 46 | "filename" : "icon-60@3x.png", 47 | "idiom" : "iphone", 48 | "scale" : "3x", 49 | "size" : "60x60" 50 | }, 51 | { 52 | "filename" : "icon-20.png", 53 | "idiom" : "ipad", 54 | "scale" : "1x", 55 | "size" : "20x20" 56 | }, 57 | { 58 | "filename" : "icon-20@2x.png", 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "20x20" 62 | }, 63 | { 64 | "filename" : "icon-29.png", 65 | "idiom" : "ipad", 66 | "scale" : "1x", 67 | "size" : "29x29" 68 | }, 69 | { 70 | "filename" : "icon-29@2x.png", 71 | "idiom" : "ipad", 72 | "scale" : "2x", 73 | "size" : "29x29" 74 | }, 75 | { 76 | "filename" : "icon-40.png", 77 | "idiom" : "ipad", 78 | "scale" : "1x", 79 | "size" : "40x40" 80 | }, 81 | { 82 | "filename" : "icon-40@2x.png", 83 | "idiom" : "ipad", 84 | "scale" : "2x", 85 | "size" : "40x40" 86 | }, 87 | { 88 | "filename" : "icon-76.png", 89 | "idiom" : "ipad", 90 | "scale" : "1x", 91 | "size" : "76x76" 92 | }, 93 | { 94 | "filename" : "icon-76@2x.png", 95 | "idiom" : "ipad", 96 | "scale" : "2x", 97 | "size" : "76x76" 98 | }, 99 | { 100 | "filename" : "icon-83.5@2x.png", 101 | "idiom" : "ipad", 102 | "scale" : "2x", 103 | "size" : "83.5x83.5" 104 | }, 105 | { 106 | "filename" : "icon-1024.png", 107 | "idiom" : "ios-marketing", 108 | "scale" : "1x", 109 | "size" : "1024x1024" 110 | } 111 | ], 112 | "info" : { 113 | "author" : "xcode", 114 | "version" : 1 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /OpenParsec/Assets.xcassets/AppIcon.appiconset/icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugeBlack/OpenParsec/8a3cdde5fe028ff83ad70333acd931d62c536072/OpenParsec/Assets.xcassets/AppIcon.appiconset/icon-1024.png -------------------------------------------------------------------------------- /OpenParsec/Assets.xcassets/AppIcon.appiconset/icon-20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugeBlack/OpenParsec/8a3cdde5fe028ff83ad70333acd931d62c536072/OpenParsec/Assets.xcassets/AppIcon.appiconset/icon-20.png -------------------------------------------------------------------------------- /OpenParsec/Assets.xcassets/AppIcon.appiconset/icon-20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugeBlack/OpenParsec/8a3cdde5fe028ff83ad70333acd931d62c536072/OpenParsec/Assets.xcassets/AppIcon.appiconset/icon-20@2x.png -------------------------------------------------------------------------------- /OpenParsec/Assets.xcassets/AppIcon.appiconset/icon-20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugeBlack/OpenParsec/8a3cdde5fe028ff83ad70333acd931d62c536072/OpenParsec/Assets.xcassets/AppIcon.appiconset/icon-20@3x.png -------------------------------------------------------------------------------- /OpenParsec/Assets.xcassets/AppIcon.appiconset/icon-29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugeBlack/OpenParsec/8a3cdde5fe028ff83ad70333acd931d62c536072/OpenParsec/Assets.xcassets/AppIcon.appiconset/icon-29.png -------------------------------------------------------------------------------- /OpenParsec/Assets.xcassets/AppIcon.appiconset/icon-29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugeBlack/OpenParsec/8a3cdde5fe028ff83ad70333acd931d62c536072/OpenParsec/Assets.xcassets/AppIcon.appiconset/icon-29@2x.png -------------------------------------------------------------------------------- /OpenParsec/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugeBlack/OpenParsec/8a3cdde5fe028ff83ad70333acd931d62c536072/OpenParsec/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png -------------------------------------------------------------------------------- /OpenParsec/Assets.xcassets/AppIcon.appiconset/icon-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugeBlack/OpenParsec/8a3cdde5fe028ff83ad70333acd931d62c536072/OpenParsec/Assets.xcassets/AppIcon.appiconset/icon-40.png -------------------------------------------------------------------------------- /OpenParsec/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugeBlack/OpenParsec/8a3cdde5fe028ff83ad70333acd931d62c536072/OpenParsec/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png -------------------------------------------------------------------------------- /OpenParsec/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugeBlack/OpenParsec/8a3cdde5fe028ff83ad70333acd931d62c536072/OpenParsec/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png -------------------------------------------------------------------------------- /OpenParsec/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugeBlack/OpenParsec/8a3cdde5fe028ff83ad70333acd931d62c536072/OpenParsec/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png -------------------------------------------------------------------------------- /OpenParsec/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugeBlack/OpenParsec/8a3cdde5fe028ff83ad70333acd931d62c536072/OpenParsec/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png -------------------------------------------------------------------------------- /OpenParsec/Assets.xcassets/AppIcon.appiconset/icon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugeBlack/OpenParsec/8a3cdde5fe028ff83ad70333acd931d62c536072/OpenParsec/Assets.xcassets/AppIcon.appiconset/icon-76.png -------------------------------------------------------------------------------- /OpenParsec/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugeBlack/OpenParsec/8a3cdde5fe028ff83ad70333acd931d62c536072/OpenParsec/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png -------------------------------------------------------------------------------- /OpenParsec/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugeBlack/OpenParsec/8a3cdde5fe028ff83ad70333acd931d62c536072/OpenParsec/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png -------------------------------------------------------------------------------- /OpenParsec/Assets.xcassets/BackgroundButton.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "0.300", 8 | "blue" : "0.525", 9 | "green" : "0.500", 10 | "red" : "0.500" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /OpenParsec/Assets.xcassets/BackgroundCard.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.205", 9 | "green" : "0.200", 10 | "red" : "0.200" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /OpenParsec/Assets.xcassets/BackgroundField.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "0.500", 8 | "blue" : "0.215", 9 | "green" : "0.200", 10 | "red" : "0.200" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /OpenParsec/Assets.xcassets/BackgroundGray.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.110", 9 | "green" : "0.100", 10 | "red" : "0.100" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /OpenParsec/Assets.xcassets/BackgroundPrompt.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.255", 9 | "green" : "0.250", 10 | "red" : "0.250" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /OpenParsec/Assets.xcassets/BackgroundTab.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.129", 9 | "green" : "0.122", 10 | "red" : "0.122" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /OpenParsec/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /OpenParsec/Assets.xcassets/Foreground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "1.000", 9 | "green" : "1.000", 10 | "red" : "1.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /OpenParsec/Assets.xcassets/ForegroundInactive.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "0.750", 8 | "blue" : "1.000", 9 | "green" : "1.000", 10 | "red" : "1.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /OpenParsec/Assets.xcassets/IconTransparent.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon_transparent.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /OpenParsec/Assets.xcassets/IconTransparent.imageset/icon_transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugeBlack/OpenParsec/8a3cdde5fe028ff83ad70333acd931d62c536072/OpenParsec/Assets.xcassets/IconTransparent.imageset/icon_transparent.png -------------------------------------------------------------------------------- /OpenParsec/Assets.xcassets/LogoShadow.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "logo_shadow.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /OpenParsec/Assets.xcassets/LogoShadow.imageset/logo_shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugeBlack/OpenParsec/8a3cdde5fe028ff83ad70333acd931d62c536072/OpenParsec/Assets.xcassets/LogoShadow.imageset/logo_shadow.png -------------------------------------------------------------------------------- /OpenParsec/Assets.xcassets/Shading.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "0.500", 8 | "blue" : "0.000", 9 | "green" : "0.000", 10 | "red" : "0.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /OpenParsec/Assets.xcassets/SymbolExit.symbolset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "symbols" : [ 7 | { 8 | "filename" : "rectangle.portrait.and.arrow.right.svg", 9 | "idiom" : "universal" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /OpenParsec/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /OpenParsec/CParsec.swift: -------------------------------------------------------------------------------- 1 | import ParsecSDK 2 | import SwiftUI 3 | import CoreGraphics 4 | import GLKit 5 | 6 | class ParsecResolution : Hashable { 7 | var width : Int 8 | var height : Int 9 | var desc : String 10 | private init(width: Int, height: Int, desc: String) { 11 | self.height = height 12 | self.width = width 13 | self.desc = desc 14 | } 15 | 16 | static func == (lhs: ParsecResolution, rhs: ParsecResolution) -> Bool { 17 | ObjectIdentifier(lhs) == ObjectIdentifier(rhs) 18 | } 19 | 20 | func hash(into hasher: inout Hasher) { 21 | hasher.combine(ObjectIdentifier(self)) 22 | } 23 | 24 | static var resolutions = [ 25 | ParsecResolution(width: 0, height: 0, desc: "Host Resolution"), 26 | ParsecResolution(width: 3480, height: 2160, desc: "Client Resolution"), // will be modified during connection 27 | ParsecResolution(width: 3480, height: 2160, desc: "3840x2160 (16:9)"), 28 | ParsecResolution(width: 3480, height: 1600, desc: "3840x1600 (21:9)"), 29 | ParsecResolution(width: 3440, height: 1440, desc: "3440x1440 (21:9)"), 30 | ParsecResolution(width: 2560, height: 1600, desc: "2560x1600 (16:10)"), 31 | ParsecResolution(width: 2560, height: 1440, desc: "2560x1440 (16:9)"), 32 | ParsecResolution(width: 2560, height: 1080, desc: "2560x1080 (21:9)"), 33 | ParsecResolution(width: 1920, height: 1200, desc: "1920x1200 (16:10)"), 34 | ParsecResolution(width: 1920, height: 1080, desc: "1920x1080 (16:9)"), 35 | ParsecResolution(width: 1680, height: 1050, desc: "1680x1050 (16:10)"), 36 | ParsecResolution(width: 1600, height: 1200, desc: "1600x1200 (4:3)"), 37 | ParsecResolution(width: 1366, height: 768, desc: "1366x768 (16:9)"), 38 | ParsecResolution(width: 1280, height: 1024, desc: "1280x1024 (5:4)"), 39 | ParsecResolution(width: 1280, height: 800, desc: "1280x800 (16:10)"), 40 | ParsecResolution(width: 1280, height: 720, desc: "1280x720 (16:9)"), 41 | ParsecResolution(width: 1024, height: 768, desc: "1024x768 (4:3)"), 42 | ] 43 | static var bitrates = [3, 5, 7, 10, 15, 20, 25, 30, 35, 40, 45, 50] 44 | } 45 | 46 | 47 | struct MouseInfo { 48 | var pngCursor:Bool = false 49 | var mouseX:Int32 = 1 50 | var mouseY:Int32 = 1 51 | var cursorWidth = 0 52 | var cursorHeight = 0 53 | var cursorHotX = 0 54 | var cursorHotY = 0 55 | var cursorImg: CGImage? 56 | var cursorHidden = false 57 | var mousePositionRelative = false 58 | } 59 | 60 | protocol ParsecService { 61 | var clientWidth: Float { get } 62 | var clientHeight: Float { get } 63 | var hostWidth: Float { get } 64 | var hostHeight: Float { get } 65 | var mouseInfo: MouseInfo { get } 66 | 67 | func connect(_ peerID: String) -> ParsecStatus 68 | func disconnect() 69 | func getStatus() -> ParsecStatus 70 | func getStatusEx(_ pcs: inout ParsecClientStatus) -> ParsecStatus 71 | func setFrame(_ width: CGFloat, _ height: CGFloat, _ scale: CGFloat) 72 | func renderGLFrame(timeout: UInt32) 73 | func setMuted(_ muted: Bool) 74 | func applyConfig() 75 | func sendMouseMessage(_ button: ParsecMouseButton, _ x: Int32, _ y: Int32, _ pressed: Bool) 76 | func sendMouseClickMessage(_ button: ParsecMouseButton, _ pressed: Bool) 77 | func sendMouseDelta(_ dx: Int32, _ dy: Int32) 78 | func sendMousePosition(_ x: Int32, _ y: Int32) 79 | func sendKeyboardMessage(event: KeyBoardKeyEvent) 80 | func sendVirtualKeyboardInput(text: String) 81 | func sendVirtualKeyboardInput(text: String, isOn: Bool) 82 | func sendGameControllerButtonMessage(controllerId: UInt32, _ button: ParsecGamepadButton, pressed: Bool) 83 | func sendGameControllerAxisMessage(controllerId: UInt32, _ button: ParsecGamepadAxis, _ value: Int16) 84 | func sendGameControllerUnplugMessage(controllerId: UInt32) 85 | func sendWheelMsg(x: Int32, y: Int32) 86 | func sendUserData(type: ParsecUserDataType, message: Data) 87 | func updateHostVideoConfig() 88 | } 89 | 90 | class CParsec 91 | { 92 | public static var hostWidth:Float { 93 | return parsecImpl.hostWidth 94 | } 95 | public static var hostHeight:Float { 96 | return parsecImpl.hostHeight 97 | } 98 | 99 | public static var clientWidth:Float { 100 | return parsecImpl.clientWidth 101 | } 102 | public static var clientHeight:Float { 103 | return parsecImpl.clientHeight 104 | } 105 | 106 | public static var mouseInfo: MouseInfo { 107 | return parsecImpl.mouseInfo 108 | } 109 | 110 | 111 | 112 | static var parsecImpl: ParsecService! 113 | 114 | static func initialize() 115 | { 116 | parsecImpl = ParsecSDKBridge() 117 | } 118 | 119 | static func destroy() 120 | { 121 | 122 | } 123 | 124 | static func connect(_ peerID:String) -> ParsecStatus 125 | { 126 | parsecImpl.connect(peerID) 127 | } 128 | 129 | static func disconnect() 130 | { 131 | parsecImpl.disconnect() 132 | } 133 | 134 | static func getStatus() -> ParsecStatus 135 | { 136 | return parsecImpl.getStatus() 137 | } 138 | 139 | static func getStatusEx(_ pcs:inout ParsecClientStatus) -> ParsecStatus 140 | { 141 | return parsecImpl.getStatusEx(&pcs) 142 | } 143 | 144 | static func setFrame(_ width:CGFloat, _ height:CGFloat, _ scale:CGFloat ) 145 | { 146 | parsecImpl.setFrame(width, height, scale) 147 | } 148 | 149 | static func renderGLFrame(timeout:UInt32 = 16) // timeout in ms, 16 == 60 FPS, 8 == 120 FPS, etc. 150 | { 151 | parsecImpl.renderGLFrame(timeout: timeout) 152 | } 153 | 154 | static func setMuted(_ muted:Bool) 155 | { 156 | parsecImpl.setMuted(muted) 157 | } 158 | 159 | static func applyConfig() 160 | { 161 | parsecImpl.applyConfig() 162 | } 163 | 164 | static func updateHostVideoConfig() { 165 | parsecImpl.updateHostVideoConfig() 166 | } 167 | 168 | static func sendMouseMessage(_ button:ParsecMouseButton, _ x:Int32, _ y:Int32, _ pressed:Bool) 169 | { 170 | parsecImpl.sendMouseMessage(button, x, y, pressed) 171 | } 172 | 173 | static func sendMouseClickMessage(_ button:ParsecMouseButton, _ pressed:Bool) { 174 | parsecImpl.sendMouseClickMessage(button, pressed) 175 | } 176 | 177 | static func sendMouseDelta(_ dx: Int32, _ dy: Int32) { 178 | parsecImpl.sendMouseDelta(dx, dy) 179 | } 180 | 181 | static func sendMousePosition(_ x:Int32, _ y:Int32) 182 | { 183 | parsecImpl.sendMousePosition(x, y) 184 | } 185 | 186 | static func sendKeyboardMessage(event:KeyBoardKeyEvent) 187 | { 188 | parsecImpl.sendKeyboardMessage(event: event) 189 | } 190 | 191 | static func sendVirtualKeyboardInput(text: String) { 192 | parsecImpl.sendVirtualKeyboardInput(text: text) 193 | } 194 | 195 | static func sendVirtualKeyboardInput(text: String, isOn: Bool) { 196 | parsecImpl.sendVirtualKeyboardInput(text: text, isOn: isOn) 197 | } 198 | 199 | static func sendGameControllerButtonMessage(controllerId:UInt32, _ button:ParsecGamepadButton, pressed:Bool) 200 | { 201 | parsecImpl.sendGameControllerButtonMessage(controllerId: controllerId, button, pressed: pressed) 202 | } 203 | 204 | 205 | static func sendGameControllerAxisMessage(controllerId:UInt32, _ button:ParsecGamepadAxis, _ value: Int16) 206 | { 207 | parsecImpl.sendGameControllerAxisMessage(controllerId: controllerId, button, value) 208 | } 209 | 210 | static func sendGameControllerUnplugMessage(controllerId:UInt32) 211 | { 212 | parsecImpl.sendGameControllerUnplugMessage(controllerId: controllerId) 213 | } 214 | 215 | static func sendWheelMsg(x: Int32, y: Int32) { 216 | parsecImpl.sendWheelMsg(x: x, y: y) 217 | } 218 | 219 | static func sendUserData(type: ParsecUserDataType, message: Data) { 220 | parsecImpl.sendUserData(type: type, message: message) 221 | } 222 | 223 | static func getImpl() -> ParsecService { 224 | return parsecImpl 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /OpenParsec/ContentView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | enum ViewType 4 | { 5 | case login 6 | case main 7 | case parsec 8 | case test 9 | } 10 | 11 | struct ContentView:View 12 | { 13 | @State var curView:ViewType = .login 14 | 15 | let defaultTransition = AnyTransition.move(edge:.trailing) 16 | 17 | var body:some View 18 | { 19 | ZStack() 20 | { 21 | switch curView 22 | { 23 | case .login: 24 | LoginView(self) 25 | case .main: 26 | MainView(self) 27 | .transition(defaultTransition) 28 | case .parsec: 29 | ParsecView(self) 30 | case .test: 31 | TestView(self) 32 | } 33 | } 34 | .onAppear(perform:initApp) 35 | .background(Rectangle().fill(Color.black).edgesIgnoringSafeArea(.all)) 36 | } 37 | 38 | func initApp() 39 | { 40 | 41 | // Load prefs 42 | SettingsHandler.load() 43 | 44 | // Check to see if we have old session data 45 | if let data = loadFromKeychain(key: GLBDataModel.shared.SessionKeyChainKey) 46 | { 47 | let decoder = JSONDecoder() 48 | 49 | print("Retrieved data from keychain: \(data).\nTrying to restore session.") 50 | NetworkHandler.clinfo = try? decoder.decode(ClientInfo.self, from:data) 51 | if NetworkHandler.clinfo != nil 52 | { 53 | curView = .main 54 | print("Session restored and moved to the main page.") 55 | } 56 | else 57 | { 58 | print("Unable to restore session, falling back to login page.") 59 | } 60 | } 61 | 62 | print("Initialized") 63 | } 64 | 65 | func loadFromKeychain(key: String) -> Data? 66 | { 67 | let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword, kSecAttrAccount as String: key, kSecReturnData as String: kCFBooleanTrue!, kSecMatchLimit as String: kSecMatchLimitOne] 68 | var item: CFTypeRef? 69 | let status = SecItemCopyMatching(query as CFDictionary, &item) 70 | guard status == errSecSuccess else 71 | { 72 | if status != errSecItemNotFound 73 | { 74 | print("Error loading from keychain: \(status)") 75 | } 76 | return nil 77 | } 78 | guard let data = item as? Data else 79 | { 80 | return nil 81 | } 82 | return data 83 | } 84 | 85 | public func setView(_ t:ViewType) 86 | { 87 | withAnimation(.easeInOut) { curView = t } 88 | } 89 | } 90 | 91 | struct ContentView_Previews:PreviewProvider 92 | { 93 | static var previews:some View 94 | { 95 | ContentView() 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /OpenParsec/ExUI.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | /** 4 | * Category Title 5 | * 6 | * Displays a title before the start of a list. 7 | */ 8 | struct CatTitle:View 9 | { 10 | var text:String 11 | 12 | init(_ text:String) 13 | { 14 | self.text = text 15 | } 16 | 17 | var body:some View 18 | { 19 | HStack() 20 | { 21 | Text(text) 22 | Spacer() 23 | } 24 | .padding(.horizontal) 25 | .padding(.vertical, 4) 26 | } 27 | } 28 | 29 | /** 30 | * Category List 31 | * 32 | * Displays a list of items. Acts as a container. 33 | * Use of `CatItem`s is recommended. 34 | */ 35 | struct CatList:View 36 | { 37 | var content:() -> Content 38 | 39 | init(@ViewBuilder content:@escaping () -> Content) 40 | { 41 | self.content = content 42 | } 43 | 44 | var body:some View 45 | { 46 | VStack(content:content) 47 | .padding(.vertical, 10) 48 | .background(Rectangle().fill(Color("BackgroundTab")).cornerRadius(10)) 49 | .padding(.horizontal) 50 | } 51 | } 52 | 53 | /** 54 | * Category Item 55 | * 56 | * Displays a new item with a label in a list. 57 | * Should be used within `CatList`s. 58 | */ 59 | struct CatItem:View 60 | { 61 | var title:String 62 | var content:() -> Content 63 | 64 | init(_ title:String, @ViewBuilder content:@escaping () -> Content) 65 | { 66 | self.title = title 67 | self.content = content 68 | } 69 | 70 | var body:some View 71 | { 72 | HStack() 73 | { 74 | Text(title) 75 | .lineLimit(1) 76 | Spacer() 77 | content() 78 | } 79 | .padding(.horizontal) 80 | } 81 | } 82 | 83 | /** 84 | * Choice 85 | * 86 | * Used to depict a single choice with a given label and value. 87 | */ 88 | struct Choice 89 | { 90 | var label:String 91 | var value:T 92 | 93 | init(_ label:String, _ value:T) 94 | { 95 | self.label = label; 96 | self.value = value; 97 | } 98 | } 99 | 100 | /** 101 | * Segmented Picker 102 | * 103 | * Displays a segmented picker with a given list of choices. 104 | */ 105 | struct SegmentPicker:View 106 | { 107 | var selection:Binding 108 | var options:[Choice] 109 | 110 | init(selection:Binding, options:[Choice]) 111 | { 112 | self.selection = selection 113 | self.options = options 114 | } 115 | 116 | var body:some View 117 | { 118 | Picker("", selection:selection) 119 | { 120 | ForEach(options.indices, id:\.self) 121 | { i in 122 | Text(options[i].label).tag(options[i].value) 123 | } 124 | } 125 | .pickerStyle(.segmented) 126 | } 127 | } 128 | 129 | /** 130 | * Multiple Choice Picker 131 | * 132 | * Displays a button that opens a menu to pick from a given list of choices. 133 | */ 134 | struct MultiPicker:View 135 | { 136 | var selection:Binding 137 | var options:[Choice] 138 | 139 | @State var showChoices:Bool = false 140 | @State var valueText:String = "Choose..." 141 | 142 | init(selection:Binding, options:[Choice]) 143 | { 144 | self.selection = selection 145 | self.options = options 146 | } 147 | 148 | var body:some View 149 | { 150 | if #available(iOS 15, *) 151 | { 152 | Picker("", selection:selection) 153 | { 154 | ForEach(options.indices, id:\.self) 155 | { i in 156 | Text(options[i].label).tag(options[i].value) 157 | } 158 | } 159 | .pickerStyle(.menu) 160 | } 161 | else 162 | { 163 | // Dumb workaround for older iOS versions. -Angel 164 | Button(action:{showChoices.toggle()}) 165 | { 166 | HStack() 167 | { 168 | Text(valueText) 169 | .multilineTextAlignment(.center) 170 | .padding(.trailing, -4) 171 | Image(systemName:"chevron.up.chevron.down") 172 | .font(.system(size:12)) 173 | } 174 | .foregroundColor(Color("AccentColor")) 175 | } 176 | .actionSheet(isPresented:$showChoices) 177 | { 178 | genActionSheet() 179 | } 180 | .onAppear 181 | { 182 | for option in options 183 | { 184 | if option.value == selection.wrappedValue 185 | { 186 | valueText = option.label 187 | break 188 | } 189 | } 190 | } 191 | } 192 | } 193 | 194 | func genActionSheet() -> ActionSheet 195 | { 196 | let buttons = options.enumerated().map 197 | { i, option in 198 | Alert.Button.default(Text(option.value == selection.wrappedValue ? " \(option.label) ✓" : option.label), action:{select(option)}) 199 | } 200 | return ActionSheet(title:Text("Pick your preference:"), buttons:buttons + [Alert.Button.cancel()]) 201 | } 202 | 203 | func select(_ option:Choice) 204 | { 205 | valueText = option.label 206 | selection.wrappedValue = option.value 207 | } 208 | } 209 | 210 | struct ExUI_Previews:PreviewProvider 211 | { 212 | @State static var value1 = false 213 | @State static var value2 = true 214 | 215 | static var previews:some View 216 | { 217 | VStack() 218 | { 219 | CatTitle("Category Title") 220 | CatList() 221 | { 222 | CatItem("Category Item") 223 | { 224 | Toggle("", isOn:.constant(true)) 225 | .frame(width:80) 226 | } 227 | } 228 | CatTitle("Other Controls") 229 | CatList() 230 | { 231 | CatItem("Segmented Picker") 232 | { 233 | SegmentPicker(selection:$value1, options: 234 | [ 235 | Choice("False", false), 236 | Choice("True", true) 237 | ]) 238 | .frame(width:180) 239 | } 240 | CatItem("Multiple Choice Picker") 241 | { 242 | MultiPicker(selection:$value2, options: 243 | [ 244 | Choice("False", false), 245 | Choice("True", true) 246 | ]) 247 | } 248 | } 249 | Spacer() 250 | } 251 | .background(Rectangle().fill(Color("BackgroundGray")).edgesIgnoringSafeArea(.all)) 252 | .foregroundColor(Color("Foreground")) 253 | .colorScheme(appScheme) 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /OpenParsec/GameController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import GameController 3 | import ParsecSDK 4 | 5 | class GamepadController { 6 | 7 | private let maximumControllerCount: Int = 1 8 | private(set) var controllers = Set() 9 | private(set) var mice = Set() 10 | //private var panRecognizer: UIPanGestureRecognizer! 11 | weak var delegate: InputManagerDelegate? 12 | 13 | let viewController: UIViewController 14 | 15 | init(viewController: UIViewController ) { 16 | self.viewController = viewController 17 | } 18 | 19 | public func viewDidLoad() { 20 | 21 | NotificationCenter.default.addObserver(self, 22 | selector: #selector(self.didConnectController), 23 | name: NSNotification.Name.GCControllerDidConnect, 24 | object: nil) 25 | NotificationCenter.default.addObserver(self, 26 | selector: #selector(self.didDisconnectController), 27 | name: NSNotification.Name.GCControllerDidDisconnect, 28 | object: nil) 29 | 30 | NotificationCenter.default.addObserver(self, 31 | selector: #selector(self.didMouseConnectController), 32 | name: NSNotification.Name.GCMouseDidConnect, 33 | object: nil) 34 | NotificationCenter.default.addObserver(self, 35 | selector: #selector(self.didMouseDisconnectController), 36 | name: NSNotification.Name.GCMouseDidDisconnect, 37 | object: nil) 38 | 39 | GCController.startWirelessControllerDiscovery {} 40 | self.registerControllerHandler() 41 | self.registerMouseHandler() 42 | 43 | } 44 | 45 | func registerControllerHandler() 46 | { 47 | for controller in GCController.controllers() { 48 | controllers.insert(controller) 49 | if controllers.count > 1 { break } 50 | 51 | delegate?.inputManager(self, didConnect: controller) 52 | 53 | controller.extendedGamepad?.dpad.left.pressedChangedHandler = { (button, value, pressed) in self.buttonChangedHandler(GAMEPAD_BUTTON_DPAD_LEFT, pressed) } 54 | controller.extendedGamepad?.dpad.right.pressedChangedHandler = { (button, value, pressed) in self.buttonChangedHandler(GAMEPAD_BUTTON_DPAD_RIGHT, pressed) } 55 | controller.extendedGamepad?.dpad.up.pressedChangedHandler = { (button, value, pressed) in self.buttonChangedHandler(GAMEPAD_BUTTON_DPAD_UP, pressed) } 56 | controller.extendedGamepad?.dpad.down.pressedChangedHandler = { (button, value, pressed) in self.buttonChangedHandler(GAMEPAD_BUTTON_DPAD_DOWN, pressed) } 57 | 58 | // buttonA is labeled "X" (blue) on PS4 controller 59 | controller.extendedGamepad?.buttonA.pressedChangedHandler = { (button, value, pressed) in self.buttonChangedHandler(GAMEPAD_BUTTON_A, pressed) } 60 | // buttonB is labeled "circle" (red) on PS4 controller 61 | controller.extendedGamepad?.buttonB.pressedChangedHandler = { (button, value, pressed) in self.buttonChangedHandler(GAMEPAD_BUTTON_B, pressed) } 62 | // buttonX is labeled "square" (pink) on PS4 controller 63 | controller.extendedGamepad?.buttonX.pressedChangedHandler = { (button, value, pressed) in self.buttonChangedHandler(GAMEPAD_BUTTON_X, pressed) } 64 | // buttonY is labeled "triangle" (green) on PS4 controller 65 | controller.extendedGamepad?.buttonY.pressedChangedHandler = { (button, value, pressed) in self.buttonChangedHandler(GAMEPAD_BUTTON_Y, pressed) } 66 | 67 | // buttonOptions is labeled "SHARE" on PS4 controller 68 | controller.extendedGamepad?.buttonOptions?.pressedChangedHandler = { (button, value, pressed) in self.buttonChangedHandler(GAMEPAD_BUTTON_BACK, pressed) } 69 | // buttonMenu is labeled "OPTIONS" on PS4 controller 70 | controller.extendedGamepad?.buttonMenu.pressedChangedHandler = { (button, value, pressed) in self.buttonChangedHandler(GAMEPAD_BUTTON_START, pressed) } 71 | 72 | controller.extendedGamepad?.leftShoulder.pressedChangedHandler = { (button, value, pressed) in self.buttonChangedHandler(GAMEPAD_BUTTON_LSHOULDER, pressed) } 73 | controller.extendedGamepad?.rightShoulder.pressedChangedHandler = { (button, value, pressed) in self.buttonChangedHandler(GAMEPAD_BUTTON_RSHOULDER, pressed) } 74 | 75 | //controller.extendedGamepad?.leftTrigger.PressedChangedHandler = { (button, value, pressed) in self.triggerButtonChangedHandler(GAMEPAD_AXIS_TRIGGERL, pressed) } 76 | controller.extendedGamepad?.leftTrigger.valueChangedHandler = { (button, value, pressed) in self.triggerChangedHandler(GAMEPAD_AXIS_TRIGGERL, value, pressed) } 77 | //controller.extendedGamepad?.rightTrigger.pressedChangedHandler = { (button, value, pressed) in self.triggerButtonChangedHandler(GAMEPAD_AXIS_TRIGGERR, pressed) } 78 | controller.extendedGamepad?.rightTrigger.valueChangedHandler = { (button, value, pressed) in self.triggerChangedHandler(GAMEPAD_AXIS_TRIGGERR, value, pressed) } 79 | 80 | controller.extendedGamepad?.leftThumbstick.valueChangedHandler = { (button, xvalue, yvalue) in self.thumbLstickChangedHandler(xvalue, yvalue) } 81 | controller.extendedGamepad?.rightThumbstick.valueChangedHandler = { (button, xvalue, yvalue) in self.thumbRstickChangedHandler(xvalue, yvalue) } 82 | 83 | controller.extendedGamepad?.leftThumbstickButton?.pressedChangedHandler = { (button, value, pressed) in self.buttonChangedHandler(GAMEPAD_BUTTON_LSTICK, pressed) } 84 | controller.extendedGamepad?.rightThumbstickButton?.pressedChangedHandler = { (button, value, pressed) in self.buttonChangedHandler(GAMEPAD_BUTTON_RSTICK, pressed) } 85 | } 86 | 87 | 88 | 89 | } 90 | 91 | func registerMouseHandler() { 92 | for mouse in GCMouse.mice() { 93 | mice.insert(mouse) 94 | mouse.mouseInput?.leftButton.pressedChangedHandler = {(input: GCControllerButtonInput, v: Float, pressed: Bool) in 95 | CParsec.sendMouseClickMessage(MOUSE_L, pressed) 96 | } 97 | mouse.mouseInput?.rightButton?.pressedChangedHandler = {(input: GCControllerButtonInput, v: Float, pressed: Bool) in 98 | CParsec.sendMouseClickMessage(MOUSE_R, pressed) 99 | } 100 | mouse.mouseInput?.middleButton?.pressedChangedHandler = {(input: GCControllerButtonInput, v: Float, pressed: Bool) in 101 | CParsec.sendMouseClickMessage(MOUSE_MIDDLE, pressed) 102 | } 103 | mouse.mouseInput?.mouseMovedHandler={(input: GCMouseInput, v: Float, v2: Float) in 104 | CParsec.sendMouseDelta(Int32(v/1.25 * SettingsHandler.mouseSensitivity), Int32(-v2/1.25 * SettingsHandler.mouseSensitivity)) 105 | } 106 | mouse.mouseInput?.scroll.yAxis.valueChangedHandler = {(axis: GCControllerAxisInput, value: Float) in 107 | CParsec.sendWheelMsg(x: Int32(value), y: 0) 108 | } 109 | mouse.mouseInput?.scroll.xAxis.valueChangedHandler = {(axis: GCControllerAxisInput, value: Float) in 110 | CParsec.sendWheelMsg(x: 0, y: Int32(value)) 111 | } 112 | } 113 | } 114 | 115 | @objc func didMouseConnectController(_ notification: Notification) { 116 | self.registerMouseHandler() 117 | } 118 | 119 | @objc func didMouseDisconnectController(_ notification: Notification) { 120 | let mouse = notification.object as! GCMouse 121 | mice.remove(mouse) 122 | } 123 | 124 | 125 | 126 | @objc func didConnectController(_ notification: Notification) { 127 | 128 | //guard controllers.count < maximumControllerCount else { return } 129 | //let controller = notification.object as! GCController 130 | self.registerControllerHandler() 131 | } 132 | 133 | @objc func didDisconnectController(_ notification: Notification) { 134 | 135 | let controller = notification.object as! GCController 136 | controllers.remove(controller) 137 | 138 | delegate?.inputManager(self, didDisconnect: controller) 139 | CParsec.sendGameControllerUnplugMessage(controllerId:1) 140 | } 141 | 142 | func ButtonFloatToParsecInt(_ value: Float) -> Int16 143 | { 144 | let newval: Float = (65535.0*value-1.0)/2.0 145 | return Int16(newval) 146 | } 147 | 148 | func buttonChangedHandler(_ button: ParsecGamepadButton, _ pressed: Bool) { 149 | CParsec.sendGameControllerButtonMessage(controllerId:1, button, pressed:pressed) 150 | } 151 | 152 | //func triggerButtonChangedHandler(_ button: ParsecGamepadAxis, _ pressed: Bool) { 153 | //CParsec.sendGameControllerTriggerButtonMessage(controllerId:1, button, pressed) 154 | //} 155 | 156 | func triggerChangedHandler(_ button:ParsecGamepadAxis, _ value: Float, _ pressed: Bool) { 157 | CParsec.sendGameControllerAxisMessage(controllerId:1, button, ButtonFloatToParsecInt(value)) 158 | } 159 | 160 | 161 | func thumbLstickChangedHandler(_ xvalue: Float, _ yvalue: Float) { 162 | CParsec.sendGameControllerAxisMessage(controllerId:1, GAMEPAD_AXIS_LX, ButtonFloatToParsecInt(xvalue)) 163 | CParsec.sendGameControllerAxisMessage(controllerId:1, GAMEPAD_AXIS_LY, ButtonFloatToParsecInt(-yvalue)) 164 | 165 | } 166 | 167 | func thumbRstickChangedHandler(_ xvalue: Float, _ yvalue: Float) { 168 | CParsec.sendGameControllerAxisMessage(controllerId:1, GAMEPAD_AXIS_RX, ButtonFloatToParsecInt(xvalue)) 169 | CParsec.sendGameControllerAxisMessage(controllerId:1, GAMEPAD_AXIS_RY, ButtonFloatToParsecInt(-yvalue)) 170 | } 171 | 172 | } 173 | 174 | protocol InputManagerDelegate: AnyObject { 175 | func inputManager(_ manager: GamepadController, didConnect controller: GCController) 176 | func inputManager(_ manager: GamepadController, didDisconnect controller: GCController) 177 | } 178 | -------------------------------------------------------------------------------- /OpenParsec/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | GCSupportedGameControllers 22 | 23 | 24 | ProfileName 25 | DirectionalGamepad 26 | 27 | 28 | ProfileName 29 | ExtendedGamepad 30 | 31 | 32 | ProfileName 33 | MicroGamepad 34 | 35 | 36 | GCSupportsControllerUserInteraction 37 | 38 | LSApplicationCategoryType 39 | 40 | LSRequiresIPhoneOS 41 | 42 | UIApplicationSceneManifest 43 | 44 | UIApplicationSupportsMultipleScenes 45 | 46 | UISceneConfigurations 47 | 48 | UIWindowSceneSessionRoleApplication 49 | 50 | 51 | UISceneConfigurationName 52 | Default Configuration 53 | UISceneDelegateClassName 54 | $(PRODUCT_MODULE_NAME).SceneDelegate 55 | 56 | 57 | 58 | 59 | UIApplicationSupportsIndirectInputEvents 60 | 61 | UILaunchStoryboardName 62 | LaunchScreen 63 | UIRequiredDeviceCapabilities 64 | 65 | armv7 66 | 67 | UIRequiresFullScreen 68 | 69 | UIStatusBarStyle 70 | UIStatusBarStyleLightContent 71 | UISupportedInterfaceOrientations 72 | 73 | UIInterfaceOrientationPortrait 74 | UIInterfaceOrientationLandscapeLeft 75 | UIInterfaceOrientationLandscapeRight 76 | 77 | UISupportedInterfaceOrientations~ipad 78 | 79 | UIInterfaceOrientationPortrait 80 | UIInterfaceOrientationPortraitUpsideDown 81 | UIInterfaceOrientationLandscapeLeft 82 | UIInterfaceOrientationLandscapeRight 83 | 84 | UIViewControllerBasedStatusBarAppearance 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /OpenParsec/KeyBoardTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyBoardTest.swift 3 | // OpenParsec 4 | // 5 | // Created by s s on 2024/5/10. 6 | // 7 | 8 | import Foundation 9 | import ParsecSDK 10 | import UIKit 11 | import SwiftUI 12 | import GameController 13 | 14 | struct TestView : View { 15 | var controller:ContentView? 16 | 17 | 18 | init(_ controller:ContentView?) 19 | { 20 | self.controller = controller 21 | } 22 | 23 | var body:some View 24 | { 25 | 26 | UIViewControllerWrapper(KeyboardTestController()) 27 | .ignoresSafeArea(.all, edges: .all) 28 | } 29 | } 30 | 31 | class KeyboardTestController:UIViewController 32 | { 33 | var id = 0 34 | 35 | 36 | init() { 37 | 38 | super.init(nibName: nil, bundle: nil) 39 | } 40 | 41 | required init?(coder: NSCoder) { 42 | fatalError("init(coder:) has not been implemented") 43 | } 44 | 45 | override var prefersPointerLocked: Bool { 46 | return true 47 | } 48 | 49 | override func pressesBegan(_ presses: Set, with event: UIPressesEvent?) { 50 | self.id += 1 51 | print("KEY EVENT!\(self.id)") 52 | } 53 | 54 | // Must be placed in viewDidAppear since parent do not exist in viewDidLoad! 55 | @objc override func viewWillAppear(_ animated: Bool) { 56 | if let parent = parent { 57 | parent.setChildForHomeIndicatorAutoHidden(self) 58 | parent.setChildViewControllerForPointerLock(self) 59 | } 60 | setNeedsUpdateOfPrefersPointerLocked() 61 | } 62 | 63 | 64 | @objc override func viewDidLoad() { 65 | 66 | 67 | let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:))) 68 | 69 | let startTime = CFAbsoluteTimeGetCurrent() 70 | let endTime = CFAbsoluteTimeGetCurrent() 71 | 72 | print("代码执行时长:\((endTime - startTime)*1000) 毫秒") 73 | setNeedsUpdateOfPrefersPointerLocked() 74 | 75 | // Add the gesture recognizer to your view 76 | view.addGestureRecognizer(panGesture) 77 | 78 | } 79 | 80 | 81 | 82 | @objc func handlePan(_ gesture: UIPanGestureRecognizer){ 83 | print("PAN!") 84 | setNeedsUpdateOfPrefersPointerLocked() 85 | 86 | } 87 | 88 | } 89 | 90 | 91 | -------------------------------------------------------------------------------- /OpenParsec/KeyCodeTranslators.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import ParsecSDK 3 | 4 | class KeyCodeTranslators { 5 | static func uiKeyCodeToInt(key: UIKeyboardHIDUsage) -> Int { 6 | switch key { 7 | case .keyboardErrorRollOver: 8 | return 1 9 | case .keyboardPOSTFail: 10 | return 2 11 | case .keyboardErrorUndefined: 12 | return 3 13 | case .keyboardA: 14 | return 4 15 | case .keyboardB: 16 | return 5 17 | case .keyboardC: 18 | return 6 19 | case .keyboardD: 20 | return 7 21 | case .keyboardE: 22 | return 8 23 | case .keyboardF: 24 | return 9 25 | case .keyboardG: 26 | return 10 27 | case .keyboardH: 28 | return 11 29 | case .keyboardI: 30 | return 12 31 | case .keyboardJ: 32 | return 13 33 | case .keyboardK: 34 | return 14 35 | case .keyboardL: 36 | return 15 37 | case .keyboardM: 38 | return 16 39 | case .keyboardN: 40 | return 17 41 | case .keyboardO: 42 | return 18 43 | case .keyboardP: 44 | return 19 45 | case .keyboardQ: 46 | return 20 47 | case .keyboardR: 48 | return 21 49 | case .keyboardS: 50 | return 22 51 | case .keyboardT: 52 | return 23 53 | case .keyboardU: 54 | return 24 55 | case .keyboardV: 56 | return 25 57 | case .keyboardW: 58 | return 26 59 | case .keyboardX: 60 | return 27 61 | case .keyboardY: 62 | return 28 63 | case .keyboardZ: 64 | return 29 65 | case .keyboard1: 66 | return 30 67 | case .keyboard2: 68 | return 31 69 | case .keyboard3: 70 | return 32 71 | case .keyboard4: 72 | return 33 73 | case .keyboard5: 74 | return 34 75 | case .keyboard6: 76 | return 35 77 | case .keyboard7: 78 | return 36 79 | case .keyboard8: 80 | return 37 81 | case .keyboard9: 82 | return 38 83 | case .keyboard0: 84 | return 39 85 | case .keyboardReturnOrEnter: 86 | return 40 87 | case .keyboardEscape: 88 | return 41 89 | case .keyboardDeleteOrBackspace: 90 | return 42 91 | case .keyboardTab: 92 | return 43 93 | case .keyboardSpacebar: 94 | return 44 95 | case .keyboardHyphen: 96 | return 45 97 | case .keyboardEqualSign: 98 | return 46 99 | case .keyboardOpenBracket: 100 | return 47 101 | case .keyboardCloseBracket: 102 | return 48 103 | case .keyboardBackslash: 104 | return 49 105 | case .keyboardNonUSPound: 106 | return 50 107 | case .keyboardSemicolon: 108 | return 51 109 | case .keyboardQuote: 110 | return 52 111 | case .keyboardGraveAccentAndTilde: 112 | return 53 113 | case .keyboardComma: 114 | return 54 115 | case .keyboardPeriod: 116 | return 55 117 | case .keyboardSlash: 118 | return 56 119 | case .keyboardCapsLock: 120 | return 57 121 | case .keyboardF1: 122 | return 58 123 | case .keyboardF2: 124 | return 59 125 | case .keyboardF3: 126 | return 60 127 | case .keyboardF4: 128 | return 61 129 | case .keyboardF5: 130 | return 62 131 | case .keyboardF6: 132 | return 63 133 | case .keyboardF7: 134 | return 64 135 | case .keyboardF8: 136 | return 65 137 | case .keyboardF9: 138 | return 66 139 | case .keyboardF10: 140 | return 67 141 | case .keyboardF11: 142 | return 68 143 | case .keyboardF12: 144 | return 69 145 | case .keyboardPrintScreen: 146 | return 70 147 | case .keyboardScrollLock: 148 | return 71 149 | case .keyboardPause: 150 | return 72 151 | case .keyboardInsert: 152 | return 73 153 | case .keyboardHome: 154 | return 74 155 | case .keyboardPageUp: 156 | return 75 157 | case .keyboardDeleteForward: 158 | return 76 159 | case .keyboardEnd: 160 | return 77 161 | case .keyboardPageDown: 162 | return 78 163 | case .keyboardRightArrow: 164 | return 79 165 | case .keyboardLeftArrow: 166 | return 80 167 | case .keyboardDownArrow: 168 | return 81 169 | case .keyboardUpArrow: 170 | return 82 171 | case .keypadNumLock: 172 | return 83 173 | case .keypadSlash: 174 | return 84 175 | case .keypadAsterisk: 176 | return 85 177 | case .keypadHyphen: 178 | return 86 179 | case .keypadPlus: 180 | return 87 181 | case .keypadEnter: 182 | return 88 183 | case .keypad1: 184 | return 89 185 | case .keypad2: 186 | return 90 187 | case .keypad3: 188 | return 91 189 | case .keypad4: 190 | return 92 191 | case .keypad5: 192 | return 93 193 | case .keypad6: 194 | return 94 195 | case .keypad7: 196 | return 95 197 | case .keypad8: 198 | return 96 199 | case .keypad9: 200 | return 97 201 | case .keypad0: 202 | return 98 203 | case .keypadPeriod: 204 | return 99 205 | case .keyboardNonUSBackslash: 206 | return 100 207 | case .keyboardApplication: 208 | return 101 209 | case .keyboardPower: 210 | return 102 211 | case .keypadEqualSign: 212 | return 103 213 | case .keyboardF13: 214 | return 104 215 | case .keyboardF14: 216 | return 105 217 | case .keyboardF15: 218 | return 106 219 | case .keyboardF16: 220 | return 107 221 | case .keyboardF17: 222 | return 108 223 | case .keyboardF18: 224 | return 109 225 | case .keyboardF19: 226 | return 110 227 | case .keyboardF20: 228 | return 111 229 | case .keyboardF21: 230 | return 112 231 | case .keyboardF22: 232 | return 113 233 | case .keyboardF23: 234 | return 114 235 | case .keyboardF24: 236 | return 115 237 | case .keyboardExecute: 238 | return 116 239 | case .keyboardHelp: 240 | return 117 241 | case .keyboardMenu: 242 | return 118 243 | case .keyboardSelect: 244 | return 119 245 | case .keyboardStop: 246 | return 120 247 | case .keyboardAgain: 248 | return 121 249 | case .keyboardUndo: 250 | return 122 251 | case .keyboardCut: 252 | return 123 253 | case .keyboardCopy: 254 | return 124 255 | case .keyboardPaste: 256 | return 125 257 | case .keyboardFind: 258 | return 126 259 | case .keyboardMute: 260 | return 127 261 | case .keyboardVolumeUp: 262 | return 128 263 | case .keyboardVolumeDown: 264 | return 129 265 | case .keyboardLockingCapsLock: 266 | return 130 267 | case .keyboardLockingNumLock: 268 | return 131 269 | case .keyboardLockingScrollLock: 270 | return 132 271 | case .keypadComma: 272 | return 133 273 | case .keypadEqualSignAS400: 274 | return 134 275 | case .keyboardInternational1: 276 | return 135 277 | case .keyboardInternational2: 278 | return 136 279 | case .keyboardInternational3: 280 | return 137 281 | case .keyboardInternational4: 282 | return 138 283 | case .keyboardInternational5: 284 | return 139 285 | case .keyboardInternational6: 286 | return 140 287 | case .keyboardInternational7: 288 | return 141 289 | case .keyboardInternational8: 290 | return 142 291 | case .keyboardInternational9: 292 | return 143 293 | case .keyboardLANG1: 294 | return 144 295 | case .keyboardLANG2: 296 | return 145 297 | case .keyboardLANG3: 298 | return 146 299 | case .keyboardLANG4: 300 | return 147 301 | case .keyboardLANG5: 302 | return 148 303 | case .keyboardLANG6: 304 | return 149 305 | case .keyboardLANG7: 306 | return 150 307 | case .keyboardLANG8: 308 | return 151 309 | case .keyboardLANG9: 310 | return 152 311 | case .keyboardAlternateErase: 312 | return 153 313 | case .keyboardSysReqOrAttention: 314 | return 154 315 | case .keyboardCancel: 316 | return 155 317 | case .keyboardClear: 318 | return 156 319 | case .keyboardPrior: 320 | return 157 321 | case .keyboardReturn: 322 | return 158 323 | case .keyboardSeparator: 324 | return 159 325 | case .keyboardOut: 326 | return 160 327 | case .keyboardOper: 328 | return 161 329 | case .keyboardClearOrAgain: 330 | return 162 331 | case .keyboardCrSelOrProps: 332 | return 163 333 | case .keyboardExSel: 334 | return 164 335 | case .keyboardLeftControl: 336 | return 224 337 | case .keyboardLeftShift: 338 | return 225 339 | case .keyboardLeftAlt: 340 | return 226 341 | case .keyboardLeftGUI: 342 | return 227 343 | case .keyboardRightControl: 344 | return 228 345 | case .keyboardRightShift: 346 | return 229 347 | case .keyboardRightAlt: 348 | return 230 349 | case .keyboardRightGUI: 350 | return 231 351 | case .keyboard_Reserved: 352 | return 65535 353 | case .keyboardHangul: 354 | return 144 355 | case .keyboardHanja: 356 | return 145 357 | case .keyboardKanaSwitch: 358 | return 144 359 | case .keyboardAlphanumericSwitch: 360 | return 145 361 | case .keyboardKatakana: 362 | return 146 363 | case .keyboardHiragana: 364 | return 147 365 | case .keyboardZenkakuHankakuKanji: 366 | return 148 367 | default: 368 | return 0 369 | } 370 | } 371 | 372 | static func parsecKeyCodeTranslator(_ str:String) -> ParsecKeycode? 373 | { 374 | switch str 375 | { 376 | case "A": return ParsecKeycode(4) 377 | case "B": return ParsecKeycode(5) 378 | case "C": return ParsecKeycode(6) 379 | case "D": return ParsecKeycode(7) 380 | case "E": return ParsecKeycode(8) 381 | case "F": return ParsecKeycode(9) 382 | case "G": return ParsecKeycode(10) 383 | case "H": return ParsecKeycode(11) 384 | case "I": return ParsecKeycode(12) 385 | case "J": return ParsecKeycode(13) 386 | case "K": return ParsecKeycode(14) 387 | case "L": return ParsecKeycode(15) 388 | case "M": return ParsecKeycode(16) 389 | case "N": return ParsecKeycode(17) 390 | case "O": return ParsecKeycode(18) 391 | case "P": return ParsecKeycode(19) 392 | case "Q": return ParsecKeycode(20) 393 | case "R": return ParsecKeycode(21) 394 | case "S": return ParsecKeycode(22) 395 | case "T": return ParsecKeycode(23) 396 | case "U": return ParsecKeycode(24) 397 | case "V": return ParsecKeycode(25) 398 | case "W": return ParsecKeycode(26) 399 | case "X": return ParsecKeycode(27) 400 | case "Y": return ParsecKeycode(28) 401 | case "Z": return ParsecKeycode(29) 402 | case "1": return ParsecKeycode(30) 403 | case "2": return ParsecKeycode(31) 404 | case "3": return ParsecKeycode(32) 405 | case "4": return ParsecKeycode(33) 406 | case "5": return ParsecKeycode(34) 407 | case "6": return ParsecKeycode(35) 408 | case "7": return ParsecKeycode(36) 409 | case "8": return ParsecKeycode(37) 410 | case "9": return ParsecKeycode(38) 411 | case "0": return ParsecKeycode(39) 412 | case "ENTER": return ParsecKeycode(40) 413 | case "UIKeyInputEscape": return ParsecKeycode(41) // ESCAPE with re-factored 414 | case "BACKSPACE": return ParsecKeycode(42) 415 | case "TAB": return ParsecKeycode(43) 416 | case "SPACE": return ParsecKeycode(44) 417 | case "MINUS": return ParsecKeycode(45) 418 | case "EQUALS": return ParsecKeycode(46) 419 | case "LBRACKET": return ParsecKeycode(47) 420 | case "RBRACKET": return ParsecKeycode(48) 421 | case "BACKSLASH": return ParsecKeycode(49) 422 | case "SEMICOLON": return ParsecKeycode(51) 423 | case "APOSTROPHE": return ParsecKeycode(52) 424 | case "BACKTICK": return ParsecKeycode(53) 425 | case "COMMA": return ParsecKeycode(54) 426 | case "PERIOD": return ParsecKeycode(55) 427 | case "SLASH": return ParsecKeycode(56) 428 | case "CAPSLOCK": return ParsecKeycode(57) 429 | case "F1": return ParsecKeycode(58) 430 | case "F2": return ParsecKeycode(59) 431 | case "F3": return ParsecKeycode(60) 432 | case "F4": return ParsecKeycode(61) 433 | case "F5": return ParsecKeycode(62) 434 | case "F6": return ParsecKeycode(63) 435 | case "F7": return ParsecKeycode(64) 436 | case "F8": return ParsecKeycode(65) 437 | case "F9": return ParsecKeycode(66) 438 | case "F10": return ParsecKeycode(67) 439 | case "F11": return ParsecKeycode(68) 440 | case "F12": return ParsecKeycode(69) 441 | case "PRINTSCREEN": return ParsecKeycode(70) 442 | case "SCROLLLOCK": return ParsecKeycode(71) 443 | case "PAUSE": return ParsecKeycode(72) 444 | case "INSERT": return ParsecKeycode(73) 445 | case "HOME": return ParsecKeycode(74) 446 | case "PAGEUP": return ParsecKeycode(75) 447 | case "DELETE": return ParsecKeycode(76) 448 | case "END": return ParsecKeycode(77) 449 | case "PAGEDOWN": return ParsecKeycode(78) 450 | case "RIGHT": return ParsecKeycode(79) 451 | case "LEFT": return ParsecKeycode(80) 452 | case "DOWN": return ParsecKeycode(81) 453 | case "UP": return ParsecKeycode(82) 454 | case "NUMLOCK": return ParsecKeycode(83) 455 | case "KP_DIVIDE": return ParsecKeycode(84) 456 | case "KP_MULTIPLY": return ParsecKeycode(85) 457 | case "KP_MINUS": return ParsecKeycode(86) 458 | case "KP_PLUS": return ParsecKeycode(87) 459 | case "KP_ENTER": return ParsecKeycode(88) 460 | case "KP_1": return ParsecKeycode(89) 461 | case "KP_2": return ParsecKeycode(90) 462 | case "KP_3": return ParsecKeycode(91) 463 | case "KP_4": return ParsecKeycode(92) 464 | case "KP_5": return ParsecKeycode(93) 465 | case "KP_6": return ParsecKeycode(94) 466 | case "KP_7": return ParsecKeycode(95) 467 | case "KP_8": return ParsecKeycode(96) 468 | case "KP_9": return ParsecKeycode(97) 469 | case "KP_0": return ParsecKeycode(98) 470 | case "KP_PERIOD": return ParsecKeycode(99) 471 | case "APPLICATION": return ParsecKeycode(101) 472 | case "F13": return ParsecKeycode(104) 473 | case "F14": return ParsecKeycode(105) 474 | case "F15": return ParsecKeycode(106) 475 | case "F16": return ParsecKeycode(107) 476 | case "F17": return ParsecKeycode(108) 477 | case "F18": return ParsecKeycode(109) 478 | case "F19": return ParsecKeycode(110) 479 | case "MENU": return ParsecKeycode(118) 480 | case "MUTE": return ParsecKeycode(127) 481 | case "VOLUMEUP": return ParsecKeycode(128) 482 | case "VOLUMEDOWN": return ParsecKeycode(129) 483 | case "CONTROL": return ParsecKeycode(224) 484 | case "SHIFT": return ParsecKeycode(225) 485 | case "LALT": return ParsecKeycode(226) 486 | case "LGUI": return ParsecKeycode(227) 487 | case "RCTRL": return ParsecKeycode(228) 488 | case "RSHIFT": return ParsecKeycode(229) 489 | case "RALT": return ParsecKeycode(230) 490 | case "RGUI": return ParsecKeycode(231) 491 | case "AUDIONEXT": return ParsecKeycode(258) 492 | case "AUDIOPREV": return ParsecKeycode(259) 493 | case "AUDIOSTOP": return ParsecKeycode(260) 494 | case "AUDIOPLAY": return ParsecKeycode(261) 495 | case "AUDIOMUTE": return ParsecKeycode(262) 496 | case "MEDIASELECT": return ParsecKeycode(263) 497 | 498 | default: return nil 499 | } 500 | } 501 | 502 | static func getParsecKeycode(for key: String) -> (ParsecKeycode: Int, keyMod: Bool) { 503 | var keyMod = false 504 | var parsecKeycode: Int = 0 505 | 506 | switch key { 507 | // Non-shifted characters 508 | case "-": 509 | parsecKeycode = 45 510 | case "=": 511 | parsecKeycode = 46 512 | case "[": 513 | parsecKeycode = 47 514 | case "]": 515 | parsecKeycode = 48 516 | case "\\": 517 | parsecKeycode = 49 518 | case ";": 519 | parsecKeycode = 51 520 | case "’": 521 | parsecKeycode = 52 522 | case "'": 523 | parsecKeycode = 52 524 | case "`": 525 | parsecKeycode = 53 526 | case ",": 527 | parsecKeycode = 54 528 | case ".": 529 | parsecKeycode = 55 530 | case "/": 531 | parsecKeycode = 56 532 | 533 | // Shifted characters 534 | case "_": 535 | parsecKeycode = 45 536 | keyMod = true 537 | case "+": 538 | parsecKeycode = 46 539 | keyMod = true 540 | case "{": 541 | parsecKeycode = 47 542 | keyMod = true 543 | case "}": 544 | parsecKeycode = 48 545 | keyMod = true 546 | case "|": 547 | parsecKeycode = 49 548 | keyMod = true 549 | case ":": 550 | parsecKeycode = 51 551 | keyMod = true 552 | case "\"": 553 | parsecKeycode = 52 554 | keyMod = true 555 | case "”": 556 | parsecKeycode = 52 557 | keyMod = true 558 | case "~": 559 | parsecKeycode = 53 560 | keyMod = true 561 | case "<": 562 | parsecKeycode = 54 563 | keyMod = true 564 | case ">": 565 | parsecKeycode = 55 566 | keyMod = true 567 | case "?": 568 | parsecKeycode = 56 569 | keyMod = true 570 | case "!": 571 | parsecKeycode = 30 572 | keyMod = true 573 | case "@": 574 | parsecKeycode = 31 575 | keyMod = true 576 | case "#": 577 | parsecKeycode = 32 578 | keyMod = true 579 | case "$": 580 | parsecKeycode = 33 581 | keyMod = true 582 | case "%": 583 | parsecKeycode = 34 584 | keyMod = true 585 | case "^": 586 | parsecKeycode = 35 587 | keyMod = true 588 | case "&": 589 | parsecKeycode = 36 590 | keyMod = true 591 | case "*": 592 | parsecKeycode = 37 593 | keyMod = true 594 | case "(": 595 | parsecKeycode = 38 596 | keyMod = true 597 | case ")": 598 | parsecKeycode = 39 599 | keyMod = true 600 | 601 | default: 602 | parsecKeycode = -1 // Unknown key 603 | } 604 | 605 | return (parsecKeycode, keyMod) 606 | } 607 | 608 | } 609 | -------------------------------------------------------------------------------- /OpenParsec/LoginView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import os 3 | 4 | struct LoginView:View 5 | { 6 | var controller:ContentView? 7 | 8 | @State var inputEmail:String = "" 9 | @State var inputPassword:String = "" 10 | @State var inputTFA:String = "" 11 | @State var isTFAOn:Bool = false 12 | @State private var presentTFAAlert = false 13 | @State var isLoading:Bool = false 14 | @State var showAlert:Bool = false 15 | @State var alertText:String = "" 16 | 17 | init(_ controller:ContentView?) 18 | { 19 | self.controller = controller 20 | } 21 | 22 | var body:some View 23 | { 24 | ZStack() 25 | { 26 | // Background 27 | Rectangle() 28 | .fill(Color("BackgroundGray")) 29 | .edgesIgnoringSafeArea(.all) 30 | 31 | // Login controls 32 | VStack(spacing:8) 33 | { 34 | HStack(spacing:2) 35 | { 36 | Image("IconTransparent") 37 | .resizable() 38 | .aspectRatio(contentMode: .fit) 39 | Image("LogoShadow") 40 | .resizable() 41 | .aspectRatio(contentMode: .fit) 42 | .padding([.top, .bottom, .trailing]) 43 | } 44 | .frame(height:80) 45 | TextField("Email", text:$inputEmail) 46 | .padding() 47 | .background(Rectangle().fill(Color("BackgroundField"))) 48 | .cornerRadius(8) 49 | .disableAutocorrection(true) 50 | .autocapitalization(/*@START_MENU_TOKEN@*/.none/*@END_MENU_TOKEN@*/) 51 | .keyboardType(.emailAddress) 52 | .textContentType(.emailAddress) 53 | SecureField("Password", text:$inputPassword) 54 | .padding() 55 | .background(Rectangle().fill(Color("BackgroundField"))) 56 | .cornerRadius(8) 57 | .disableAutocorrection(true) 58 | .autocapitalization(/*@START_MENU_TOKEN@*/.none/*@END_MENU_TOKEN@*/) 59 | .textContentType(.password) 60 | Button(action:{authenticate()}) 61 | { 62 | ZStack() 63 | { 64 | Rectangle() 65 | .fill(Color("AccentColor")) 66 | .cornerRadius(8) 67 | Text("Login") 68 | .foregroundColor(.white) 69 | } 70 | .frame(height:54) 71 | } 72 | } 73 | 74 | .padding() 75 | .frame(maxWidth:400) 76 | .disabled(isLoading) // Disable when loading 77 | 78 | // Loading elements 79 | if isLoading || presentTFAAlert 80 | { 81 | ZStack() 82 | { 83 | Rectangle() // Darken background 84 | .fill(Color.black) 85 | .opacity(0.5) 86 | .edgesIgnoringSafeArea(.all) 87 | VStack() 88 | { 89 | if isLoading 90 | { 91 | ActivityIndicator(isAnimating:$isLoading, style:.large, tint:.white) 92 | .padding() 93 | Text("Loading...") 94 | .multilineTextAlignment(.center) 95 | } 96 | else if presentTFAAlert 97 | { 98 | Text("Please enter your 2FA code from your authenticator app") 99 | .multilineTextAlignment(.center) 100 | SecureField("2FA Code", text:$inputTFA) 101 | .padding() 102 | .background(Rectangle().fill(Color("BackgroundField"))) 103 | .foregroundColor(Color("Foreground")) 104 | .cornerRadius(8) 105 | .disableAutocorrection(true) 106 | .autocapitalization(/*@START_MENU_TOKEN@*/.none/*@END_MENU_TOKEN@*/) 107 | .textContentType(.oneTimeCode) 108 | HStack() 109 | { 110 | Button(action:{presentTFAAlert = false}) 111 | { 112 | ZStack() 113 | { 114 | Rectangle() 115 | .fill(Color("BackgroundButton")) 116 | .cornerRadius(8) 117 | Text("Cancel") 118 | .foregroundColor(Color("Foreground")) 119 | } 120 | .frame(height:54) 121 | } 122 | Button(action:{authenticate(inputTFA)}) 123 | { 124 | ZStack() 125 | { 126 | Rectangle() 127 | .fill(Color("AccentColor")) 128 | .cornerRadius(8) 129 | Text("Enter") 130 | .foregroundColor(.white) 131 | } 132 | .frame(height:54) 133 | } 134 | } 135 | } 136 | } 137 | .padding() 138 | .background(Rectangle().fill(Color("BackgroundPrompt"))) 139 | .cornerRadius(8) 140 | .padding() 141 | } 142 | } 143 | } 144 | .foregroundColor(Color("Foreground")) 145 | .alert(isPresented:$showAlert) 146 | { 147 | Alert(title:Text("Login Failed"), message: Text(alertText)) 148 | } 149 | } 150 | 151 | func saveToKeychain(data: Data, key: String) 152 | { 153 | let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword, kSecAttrAccount as String: key, kSecValueData as String: data] 154 | let status = SecItemAdd(query as CFDictionary, nil) 155 | guard status == errSecSuccess else 156 | { 157 | print("Error saving to Keychain: \(status)") 158 | return 159 | } 160 | print("Data saved to Keychain.") 161 | } 162 | 163 | func authenticate(_ tfa:String? = "") 164 | { 165 | #if DEBUG 166 | if inputEmail == "test@example.com" // skip authentication (DEBUG ONLY) 167 | { 168 | if let c = controller 169 | { 170 | c.setView(.main) 171 | } 172 | return 173 | } 174 | #endif 175 | 176 | withAnimation { isLoading = true } 177 | 178 | let apiURL = URL(string:"https://kessel-api.parsec.app/v1/auth")! 179 | 180 | var request = URLRequest(url:apiURL) 181 | request.httpMethod = "POST"; 182 | request.setValue("application/json", forHTTPHeaderField:"Content-Type") 183 | request.setValue("parsec/150-93b Windows/11 libmatoya/4.0", forHTTPHeaderField: "User-Agent") 184 | request.httpBody = try? JSONSerialization.data(withJSONObject: 185 | [ 186 | "email":inputEmail, 187 | "password":inputPassword, 188 | "tfa": tfa 189 | ], options:[]) 190 | 191 | let task = URLSession.shared.dataTask(with:request) 192 | { (data, response, error) in 193 | isLoading = false 194 | if let data = data 195 | { 196 | let statusCode:Int = (response as! HTTPURLResponse).statusCode 197 | let decoder = JSONDecoder() 198 | 199 | print("Login Information:") 200 | print(statusCode) 201 | print(String(data:data, encoding:.utf8)!) 202 | 203 | if statusCode == 201 // 201 Created 204 | { 205 | // store it and recover it from the next app opening, so people won't swear 206 | NetworkHandler.clinfo = try? decoder.decode(ClientInfo.self, from:data) 207 | 208 | saveToKeychain(data: data, key: GLBDataModel.shared.SessionKeyChainKey) 209 | 210 | if let c = controller 211 | { 212 | print("*** Login succeeded! ***") 213 | c.setView(.main) 214 | } 215 | } 216 | else if statusCode >= 400 // 4XX client errors 217 | { 218 | let info:ErrorInfo = try! decoder.decode(ErrorInfo.self, from:data) 219 | 220 | do 221 | { 222 | let json = try JSONSerialization.jsonObject(with: data, options: []) 223 | if let dict = json as? [String: Any], let isTFARequired = dict["tfa_required"] as? Bool { 224 | print("Code output:") 225 | print(dict) 226 | if isTFARequired 227 | { 228 | presentTFAAlert = true 229 | } 230 | else 231 | { 232 | alertText = "Error: \(info)" 233 | showAlert = true 234 | } 235 | } else { 236 | alertText = info.error 237 | showAlert = true 238 | } 239 | } 240 | catch 241 | { 242 | print("Error on trying JSON Serialization on error data!") 243 | } 244 | } 245 | } 246 | } 247 | task.resume() 248 | } 249 | } 250 | 251 | struct LoginView_Previews:PreviewProvider 252 | { 253 | static var previews:some View 254 | { 255 | LoginView(nil) 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /OpenParsec/MainView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import ParsecSDK 3 | 4 | struct MainView:View 5 | { 6 | var controller:ContentView? 7 | 8 | @State private var page:Page = .hosts 9 | 10 | // Host page vars 11 | @State var hostCountStr:String = "0 hosts" 12 | @State var refreshTime:String = "Last refreshed at 1/1/1970 12:00 AM" 13 | 14 | @State var hosts:Array = [] 15 | 16 | // Friend page vars 17 | @State var friendCountStr:String = "0 friends" 18 | 19 | @State var userInfo:IdentifiableUserInfo? = nil 20 | @State var friends:Array = [] 21 | 22 | // Global vars 23 | @State var showBaseAlert:Bool = false 24 | @State var baseAlertText:String = "" 25 | 26 | @State var showLogoutAlert:Bool = false 27 | 28 | @State var isConnecting:Bool = false 29 | @State var connectingToName:String = "" 30 | @State var pollTimer:Timer? 31 | 32 | @State var isRefreshing:Bool = false 33 | 34 | @State var inSettings:Bool = false 35 | 36 | var busy:Bool 37 | { 38 | isConnecting || isRefreshing || inSettings 39 | } 40 | 41 | init(_ controller:ContentView?) 42 | { 43 | self.controller = controller 44 | } 45 | 46 | var body:some View 47 | { 48 | ZStack() 49 | { 50 | // Background 51 | Rectangle() 52 | .fill(Color("BackgroundTab")) 53 | .edgesIgnoringSafeArea(.all) 54 | Rectangle() 55 | .fill(Color("BackgroundGray")) 56 | .padding(.vertical, 52) 57 | 58 | // Main controls 59 | VStack() 60 | { 61 | // Navigation controls 62 | HStack() 63 | { 64 | Button(action:{ showLogoutAlert = true }, label:{ Image("SymbolExit").scaleEffect(x:-1) }) 65 | .padding() 66 | .alert(isPresented:$showLogoutAlert) 67 | { 68 | Alert(title:Text("Are you sure you want to logout?"), primaryButton:.destructive(Text("Logout"), action:logout), secondaryButton:.cancel(Text("Cancel"))) 69 | } 70 | // Button(action: { 71 | // if let c = controller 72 | // { 73 | // c.setView(.test) 74 | // 75 | // } 76 | // 77 | // }, label: { 78 | // Text("Show TestView") 79 | // }) 80 | 81 | Spacer() 82 | HStack() 83 | { 84 | if page == .hosts 85 | { 86 | // Probably not the best solution for equal spacing, but I don't know how to do math properly in SwiftUI. Please send me an issue if you have a better solution. 87 | Image(systemName:"arrow.clockwise") 88 | .padding(4) 89 | .opacity(0) 90 | 91 | Text(hostCountStr) 92 | .multilineTextAlignment(.center) 93 | .foregroundColor(Color("Foreground")) 94 | .font(.system(size:20, weight:.medium)) 95 | Button(action:refreshHosts, label:{ Image(systemName:"arrow.clockwise") }) 96 | .padding(4) 97 | } 98 | else if page == .friends 99 | { 100 | Text(friendCountStr) 101 | .multilineTextAlignment(.center) 102 | .foregroundColor(Color("Foreground")) 103 | .font(.system(size:20, weight:.medium)) 104 | } 105 | } 106 | Spacer() 107 | Button(action:{ inSettings = true }, label:{ Image(systemName:"gear") }) 108 | .padding() 109 | } 110 | .foregroundColor(Color("AccentColor")) 111 | .background(Color("BackgroundTab") 112 | .frame(height:52) 113 | .shadow(color:Color("Shading"), radius:4, y:6) 114 | .mask(Rectangle().frame(height:80).offset(y:50)) 115 | ) 116 | .zIndex(1) 117 | 118 | ZStack() 119 | { 120 | // Hosts page 121 | ScrollView(.vertical) 122 | { 123 | VStack() 124 | { 125 | Text(refreshTime) 126 | .multilineTextAlignment(.center) 127 | .opacity(0.5) 128 | ForEach(hosts) 129 | { i in 130 | ZStack() 131 | { 132 | VStack() 133 | { 134 | URLImage(url:URL(string:"https://parsecusercontent.com/cors-resize-image/w=64,h=64,fit=crop,background=white,q=90,f=jpeg/avatars/\(String(i.user.id))/avatar"), 135 | output: 136 | { 137 | $0 138 | .resizable() 139 | .aspectRatio(contentMode:.fit) 140 | .frame(width:64, height:64) 141 | .cornerRadius(8) 142 | }, 143 | placeholder: 144 | { 145 | Image("IconTransparent") 146 | .resizable() 147 | .aspectRatio(contentMode:.fit) 148 | .frame(width:64, height:64) 149 | .background(Rectangle().fill(Color("BackgroundPrompt"))) 150 | .cornerRadius(8) 151 | }) 152 | Text(i.hostname) 153 | .font(.system(size:20, weight:.medium)) 154 | .multilineTextAlignment(.center) 155 | Text("\(i.user.name)#\(String(i.user.id))") 156 | .font(.system(size:16, weight:.medium)) 157 | .multilineTextAlignment(.center) 158 | .opacity(0.5) 159 | Button(action:{ connectTo(i) }) 160 | { 161 | ZStack() 162 | { 163 | Rectangle() 164 | .fill(Color("AccentColor")) 165 | .cornerRadius(8) 166 | Text("Connect") 167 | .foregroundColor(.white) 168 | .padding(8) 169 | } 170 | .frame(maxWidth:100) 171 | } 172 | } 173 | 174 | if i.connections > 0 175 | { 176 | VStack() 177 | { 178 | HStack() 179 | { 180 | Image(systemName:"person.fill") 181 | Text(String(i.connections)) 182 | .font(.system(size:16, weight:.medium)) 183 | Spacer() 184 | } 185 | Spacer() 186 | } 187 | } 188 | } 189 | .padding() 190 | .frame(maxWidth:400) 191 | .background(Rectangle().fill(Color("BackgroundCard"))) 192 | .cornerRadius(8) 193 | } 194 | } 195 | .padding() 196 | } 197 | .zIndex(page == .hosts ? 0 : -1) 198 | .disabled(page != .hosts) 199 | .opacity(page == .hosts ? 1 : 0) 200 | 201 | // Friends page 202 | ScrollView(.vertical) 203 | { 204 | VStack() 205 | { 206 | if let user = userInfo 207 | { 208 | Text("You") 209 | .multilineTextAlignment(.center) 210 | .opacity(0.5) 211 | HStack() 212 | { 213 | URLImage(url:URL(string:"https://parsecusercontent.com/cors-resize-image/w=48,h=48,fit=crop,background=white,q=90,f=jpeg/avatars/\(String(user.id))/avatar"), 214 | output: 215 | { 216 | $0 217 | .resizable() 218 | .aspectRatio(contentMode:.fit) 219 | .frame(width:48, height:48) 220 | .cornerRadius(6) 221 | }, 222 | placeholder: 223 | { 224 | Image("IconTransparent") 225 | .resizable() 226 | .aspectRatio(contentMode:.fit) 227 | .frame(width:48, height:48) 228 | .background(Rectangle().fill(Color("BackgroundPrompt"))) 229 | .cornerRadius(6) 230 | }) 231 | Text("\(user.username)#\(String(user.id))") 232 | .font(.system(size:16, weight:.medium)) 233 | .multilineTextAlignment(.center) 234 | Spacer() 235 | } 236 | .padding(8) 237 | .background(Color("BackgroundCard")) 238 | .cornerRadius(12) 239 | } 240 | if friends.count > 0 241 | { 242 | Text("Friends") 243 | .multilineTextAlignment(.center) 244 | .opacity(0.5) 245 | ForEach(friends) 246 | { i in 247 | HStack() 248 | { 249 | URLImage(url:URL(string:"https://parsecusercontent.com/cors-resize-image/w=48,h=48,fit=crop,background=white,q=90,f=jpeg/avatars/\(String(i.id))/avatar"), 250 | output: 251 | { 252 | $0 253 | .resizable() 254 | .aspectRatio(contentMode:.fit) 255 | .frame(width:48, height:48) 256 | .cornerRadius(6) 257 | }, 258 | placeholder: 259 | { 260 | Image("IconTransparent") 261 | .resizable() 262 | .aspectRatio(contentMode:.fit) 263 | .frame(width:48, height:48) 264 | .background(Rectangle().fill(Color("BackgroundPrompt"))) 265 | .cornerRadius(6) 266 | }) 267 | Text("\(i.username)#\(String(i.id))") 268 | .font(.system(size:16, weight:.medium)) 269 | .multilineTextAlignment(.center) 270 | Spacer() 271 | // Button(action:{ }, label:{ Image(systemName:"ellipsis.circle.fill") }) 272 | // .font(.system(size:20)) 273 | // .foregroundColor(Color("AccentColor")) 274 | // .padding(8) 275 | } 276 | .padding(8) 277 | .background(Color("BackgroundCard")) 278 | .cornerRadius(12) 279 | } 280 | } 281 | } 282 | .padding() 283 | } 284 | .zIndex(page == .friends ? 0 : -1) 285 | .disabled(page != .friends) 286 | .opacity(page == .friends ? 1 : 0) 287 | } 288 | .padding(.top, -8) 289 | .frame(maxWidth:.infinity) 290 | .alert(isPresented:$showBaseAlert) 291 | { 292 | Alert(title:Text(baseAlertText)) 293 | } 294 | 295 | // Page controls 296 | HStack() 297 | { 298 | Spacer() 299 | Button(action:{ page = .hosts }, label: 300 | { 301 | VStack() 302 | { 303 | Image(systemName:"desktopcomputer") 304 | Text("Hosts") 305 | } 306 | }) 307 | .foregroundColor(Color(page == .hosts ? "AccentColor" : "ForegroundInactive")) 308 | .disabled(page == .hosts) 309 | Spacer() 310 | Button(action:{ page = .friends }, label: 311 | { 312 | VStack() 313 | { 314 | Image(systemName:"person.2.fill") 315 | Text("Friends") 316 | } 317 | }) 318 | .foregroundColor(Color(page == .friends ? "AccentColor" : "ForegroundInactive")) 319 | .disabled(page == .friends) 320 | Spacer() 321 | } 322 | .padding([.leading, .bottom, .trailing], 4) 323 | .background(Color("BackgroundTab") 324 | .padding(.top, -8) 325 | .shadow(color:Color("Shading"), radius:4, y:-2) 326 | .mask(Rectangle().frame(height:80).offset(y:-50)) 327 | ) 328 | .zIndex(1) 329 | } 330 | .onAppear(perform:initView) 331 | .disabled(busy) // disable view if busy 332 | 333 | // Settings screen 334 | SettingsView(visible:$inSettings) 335 | 336 | // Loading elements 337 | if isConnecting 338 | { 339 | ZStack() 340 | { 341 | Rectangle() // Darken background 342 | .fill(Color.black) 343 | .opacity(0.5) 344 | .edgesIgnoringSafeArea(.all) 345 | VStack() 346 | { 347 | ActivityIndicator(isAnimating:$isConnecting, style:.large, tint:.white) 348 | .padding() 349 | Text("Requesting connection to \(connectingToName)...") 350 | .multilineTextAlignment(.center) 351 | Button(action:cancelConnection) 352 | { 353 | ZStack() 354 | { 355 | Rectangle() 356 | .fill(Color("BackgroundButton")) 357 | .cornerRadius(8) 358 | Text("Cancel") 359 | .foregroundColor(.red) 360 | } 361 | } 362 | .frame(maxWidth:100, maxHeight:48) 363 | } 364 | .padding() 365 | .background(Rectangle().fill(Color("BackgroundPrompt"))) 366 | .cornerRadius(8) 367 | .padding() 368 | } 369 | } 370 | if isRefreshing 371 | { 372 | ZStack() 373 | { 374 | Rectangle() // Darken background 375 | .fill(Color.black) 376 | .opacity(0.5) 377 | .edgesIgnoringSafeArea(.all) 378 | VStack() 379 | { 380 | ActivityIndicator(isAnimating:$isRefreshing, style:.large, tint:.white) 381 | .padding() 382 | Text("Refreshing hosts...") 383 | .multilineTextAlignment(.center) 384 | } 385 | .padding() 386 | .background(Rectangle().fill(Color("BackgroundPrompt"))) 387 | .cornerRadius(8) 388 | .padding() 389 | } 390 | } 391 | } 392 | .foregroundColor(Color("Foreground")) 393 | } 394 | 395 | func initView() 396 | { 397 | refreshHosts() 398 | refreshSelf() 399 | refreshFriends() 400 | } 401 | 402 | func refreshHosts() 403 | { 404 | withAnimation 405 | { 406 | isRefreshing = true 407 | 408 | let clinfo = NetworkHandler.clinfo 409 | if clinfo == nil 410 | { 411 | isRefreshing = false; 412 | baseAlertText = "Error gathering hosts: Invalid session" 413 | showBaseAlert = true 414 | return 415 | } 416 | 417 | let apiURL = URL(string:"https://kessel-api.parsec.app/v2/hosts?mode=desktop&public=false")! 418 | 419 | var request = URLRequest(url:apiURL) 420 | request.httpMethod = "GET" 421 | request.setValue("application/json", forHTTPHeaderField:"Content-Type") 422 | request.setValue("Bearer \(clinfo!.session_id)", forHTTPHeaderField:"Authorization") 423 | request.setValue("parsec/150-93b Windows/11 libmatoya/4.0", forHTTPHeaderField: "User-Agent") 424 | 425 | let task = URLSession.shared.dataTask(with:request) 426 | { (data, response, error) in 427 | if let data = data 428 | { 429 | let statusCode:Int = (response as! HTTPURLResponse).statusCode 430 | let decoder = JSONDecoder() 431 | 432 | if statusCode == 200 // 200 OK 433 | { 434 | let info:HostInfoList = try! decoder.decode(HostInfoList.self, from:data) 435 | hosts.removeAll() 436 | if let datas = info.data 437 | { 438 | datas.forEach 439 | { h in 440 | hosts.append(IdentifiableHostInfo(id:h.peer_id, hostname:h.name, user:h.user, connections:h.players)) 441 | } 442 | } 443 | 444 | var grammar:String = "hosts" 445 | if hosts.count == 1 446 | { 447 | grammar = "host" 448 | } 449 | 450 | hostCountStr = "\(hosts.count) \(grammar)" 451 | 452 | let formatter = DateFormatter() 453 | formatter.dateFormat = "M/d/yyyy h:mm a" 454 | refreshTime = "Last refreshed at \(formatter.string(from:Date()))" 455 | } 456 | else if statusCode == 403 // 403 Forbidden 457 | { 458 | let info:ErrorInfo = try! decoder.decode(ErrorInfo.self, from:data) 459 | 460 | baseAlertText = "Error gathering hosts: \(info.error)" 461 | showBaseAlert = true 462 | } 463 | } 464 | 465 | isRefreshing = false 466 | } 467 | task.resume() 468 | } 469 | } 470 | 471 | func refreshSelf() 472 | { 473 | withAnimation 474 | { 475 | let clinfo = NetworkHandler.clinfo 476 | if clinfo == nil 477 | { 478 | return 479 | } 480 | 481 | let apiURL = URL(string:"https://kessel-api.parsec.app/me")! 482 | 483 | var request = URLRequest(url:apiURL) 484 | request.httpMethod = "GET" 485 | request.setValue("application/json", forHTTPHeaderField:"Content-Type") 486 | request.setValue("Bearer \(clinfo!.session_id)", forHTTPHeaderField:"Authorization") 487 | request.setValue("parsec/150-93b Windows/11 libmatoya/4.0", forHTTPHeaderField: "User-Agent") 488 | 489 | let task = URLSession.shared.dataTask(with:request) 490 | { (data, response, error) in 491 | if let data = data 492 | { 493 | let statusCode:Int = (response as! HTTPURLResponse).statusCode 494 | let decoder = JSONDecoder() 495 | 496 | if statusCode == 200 // 200 OK 497 | { 498 | let data:SelfInfoData = try! decoder.decode(SelfInfo.self, from:data).data 499 | userInfo = IdentifiableUserInfo(id:data.id, username:data.name) 500 | } 501 | else 502 | { 503 | let info:ErrorInfo = try! decoder.decode(ErrorInfo.self, from:data) 504 | 505 | baseAlertText = "Error gathering user info: \(info.error)" 506 | showBaseAlert = true 507 | } 508 | } 509 | } 510 | task.resume() 511 | } 512 | } 513 | 514 | func refreshFriends() 515 | { 516 | withAnimation 517 | { 518 | let clinfo = NetworkHandler.clinfo 519 | if clinfo == nil 520 | { 521 | return 522 | } 523 | 524 | let apiURL = URL(string:"https://kessel-api.parsec.app/friendships")! 525 | 526 | var request = URLRequest(url:apiURL) 527 | request.httpMethod = "GET" 528 | request.setValue("application/json", forHTTPHeaderField:"Content-Type") 529 | request.setValue("Bearer \(clinfo!.session_id)", forHTTPHeaderField:"Authorization") 530 | request.setValue("parsec/150-93b Windows/11 libmatoya/4.0", forHTTPHeaderField: "User-Agent") 531 | 532 | let task = URLSession.shared.dataTask(with:request) 533 | { (data, response, error) in 534 | if let data = data 535 | { 536 | let statusCode:Int = (response as! HTTPURLResponse).statusCode 537 | let decoder = JSONDecoder() 538 | 539 | print("/friendships: \(statusCode)") 540 | print(String(data:data, encoding:.utf8)!) 541 | 542 | if statusCode == 200 // 200 OK 543 | { 544 | let info:FriendInfoList = try! decoder.decode(FriendInfoList.self, from:data) 545 | friends.removeAll() 546 | if let datas = info.data 547 | { 548 | datas.forEach 549 | { f in 550 | friends.append(IdentifiableUserInfo(id:f.user_id, username:f.user_name)) 551 | } 552 | } 553 | 554 | var grammar:String = "friends" 555 | if friends.count == 1 556 | { 557 | grammar = "friend" 558 | } 559 | 560 | friendCountStr = "\(friends.count) \(grammar)" 561 | } 562 | else 563 | { 564 | let info:ErrorInfo = try! decoder.decode(ErrorInfo.self, from:data) 565 | 566 | baseAlertText = "Error gathering friends: \(info.error)" 567 | showBaseAlert = true 568 | } 569 | } 570 | 571 | isRefreshing = false 572 | } 573 | task.resume() 574 | } 575 | } 576 | 577 | func connectTo(_ who:IdentifiableHostInfo) 578 | { 579 | CParsec.initialize() 580 | connectingToName = who.hostname 581 | withAnimation { isConnecting = true } 582 | 583 | var status = CParsec.connect(who.id) 584 | 585 | // Polling status 586 | pollTimer = Timer.scheduledTimer(withTimeInterval:1, repeats:true) 587 | { timer in 588 | status = CParsec.getStatus() 589 | 590 | if status == PARSEC_CONNECTING { return } // wait 591 | 592 | withAnimation { isConnecting = false } 593 | 594 | if status == PARSEC_OK 595 | { 596 | if let c = controller 597 | { 598 | c.setView(.parsec) 599 | } 600 | } 601 | else 602 | { 603 | baseAlertText = "Error connecting to host (code \(status.rawValue))" 604 | showBaseAlert = true 605 | } 606 | 607 | timer.invalidate() 608 | } 609 | } 610 | 611 | func cancelConnection() 612 | { 613 | withAnimation { isConnecting = false } 614 | 615 | CParsec.disconnect() 616 | 617 | pollTimer!.invalidate() 618 | } 619 | 620 | func logout() 621 | { 622 | removeFromKeychain(key:GLBDataModel.shared.SessionKeyChainKey) 623 | NetworkHandler.clinfo = nil 624 | if let c = controller 625 | { 626 | c.setView(.login) 627 | } 628 | } 629 | 630 | func removeFromKeychain(key:String) 631 | { 632 | let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword, kSecAttrAccount as String: key] 633 | let status = SecItemDelete(query as CFDictionary) 634 | if status == errSecSuccess 635 | { 636 | print("Successfully removed data from keychain.") 637 | } 638 | } 639 | } 640 | 641 | struct MainView_Previews:PreviewProvider 642 | { 643 | static var previews:some View 644 | { 645 | MainView(nil) 646 | } 647 | } 648 | 649 | struct IdentifiableHostInfo:Identifiable 650 | { 651 | var id:String // Peer ID 652 | var hostname:String // Computer's Display Name 653 | var user:UserInfo // User Data 654 | var connections:Int // User's Connected To This Host 655 | } 656 | 657 | struct IdentifiableUserInfo:Identifiable 658 | { 659 | var id:Int // User ID 660 | var username:String // User Display Name 661 | } 662 | 663 | private enum Page 664 | { 665 | case hosts 666 | case friends 667 | } 668 | -------------------------------------------------------------------------------- /OpenParsec/NetworkHandler.swift: -------------------------------------------------------------------------------- 1 | class NetworkHandler 2 | { 3 | public static var clinfo:ClientInfo? = nil 4 | } 5 | 6 | struct ErrorInfo:Decodable 7 | { 8 | var error:String 9 | // var codes:Array 10 | } 11 | 12 | struct ClientInfo:Decodable 13 | { 14 | var instance_id:String 15 | var user_id:Int 16 | var session_id:String 17 | var host_peer_id:String 18 | } 19 | 20 | struct UserInfo:Decodable 21 | { 22 | var id:Int 23 | var name:String 24 | var warp:Bool 25 | // var external_id:String 26 | // var external_provider:String 27 | var team_id:String 28 | } 29 | 30 | struct HostInfo:Decodable 31 | { 32 | var user:UserInfo 33 | var peer_id:String 34 | var game_id:String 35 | var description:String 36 | var max_players:Int 37 | var mode:String 38 | var name:String 39 | var event_name:String 40 | var players:Int 41 | // var public:Bool 42 | var guest_access:Bool 43 | var online:Bool 44 | // var self:Bool 45 | var build:String 46 | } 47 | 48 | struct HostInfoList:Decodable 49 | { 50 | var data:Array? 51 | var has_more:Bool 52 | } 53 | 54 | struct SelfInfoData:Decodable 55 | { 56 | var id:Int 57 | var name:String 58 | var email:String 59 | var warp:Bool 60 | var staff:Bool 61 | var team_id:String 62 | var is_confirmed:Bool 63 | var team_is_active:Bool 64 | var is_saml:Bool 65 | var is_gateway_enabled:Bool 66 | var is_relay_enabled:Bool 67 | var has_tfa:Bool 68 | // var app_config:Any 69 | var cohort_channel:String 70 | } 71 | 72 | struct SelfInfo:Decodable 73 | { 74 | var data:SelfInfoData 75 | } 76 | 77 | struct FriendInfo:Decodable 78 | { 79 | var user_id:Int 80 | var user_name:String 81 | } 82 | 83 | struct FriendInfoList:Decodable 84 | { 85 | var data:Array? 86 | var has_more:Bool 87 | } 88 | -------------------------------------------------------------------------------- /OpenParsec/OpenParsec-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | 5 | #import "audio.h" 6 | -------------------------------------------------------------------------------- /OpenParsec/ParsecGLKRenderer.swift: -------------------------------------------------------------------------------- 1 | import GLKit 2 | import ParsecSDK 3 | 4 | class ParsecGLKRenderer:NSObject, GLKViewDelegate, GLKViewControllerDelegate 5 | { 6 | var glkView:GLKView 7 | var glkViewController:GLKViewController 8 | 9 | var lastWidth:CGFloat = 1.0 10 | 11 | var lastImg: CGImage? 12 | let updateImage: () -> Void 13 | 14 | init(_ view:GLKView, _ viewController:GLKViewController,_ updateImage: @escaping () -> Void) 15 | { 16 | self.updateImage = updateImage 17 | glkView = view 18 | glkViewController = viewController 19 | 20 | super.init() 21 | 22 | glkView.delegate = self 23 | glkViewController.delegate = self 24 | 25 | } 26 | 27 | deinit 28 | { 29 | glkView.delegate = nil 30 | glkViewController.delegate = nil 31 | } 32 | 33 | func glkView(_ view:GLKView, drawIn rect:CGRect) 34 | { 35 | let deltaWidth: CGFloat = view.frame.size.width - lastWidth 36 | if deltaWidth > 0.1 || deltaWidth < -0.1 37 | { 38 | CParsec.setFrame(view.frame.size.width, view.frame.size.height, view.contentScaleFactor) 39 | lastWidth = view.frame.size.width 40 | } 41 | CParsec.renderGLFrame(timeout:16) 42 | 43 | updateImage() 44 | 45 | 46 | // glFinish() 47 | //glFlush() 48 | } 49 | 50 | func glkViewControllerUpdate(_ controller:GLKViewController) { } 51 | } 52 | -------------------------------------------------------------------------------- /OpenParsec/ParsecGLKViewController.swift: -------------------------------------------------------------------------------- 1 | //import SwiftUI 2 | //import GLKit 3 | // 4 | //struct ParsecGLKViewController:UIViewControllerRepresentable 5 | //{ 6 | // let glkView = GLKView() 7 | // let glkViewController = GLKViewController() 8 | // let onBeforeRender:() -> Void 9 | // 10 | // func makeCoordinator() -> ParsecGLKRenderer 11 | // { 12 | // ParsecGLKRenderer(glkView, glkViewController, onBeforeRender) 13 | // } 14 | // 15 | // func makeUIViewController(context:UIViewControllerRepresentableContext) -> GLKViewController 16 | // { 17 | // glkView.context = EAGLContext(api:.openGLES3)! 18 | // glkViewController.view = glkView 19 | // glkViewController.preferredFramesPerSecond = 60 20 | // return glkViewController 21 | // } 22 | // 23 | // func updateUIViewController(_ uiViewController:GLKViewController, context:UIViewControllerRepresentableContext) { } 24 | //} 25 | 26 | import UIKit 27 | import GLKit 28 | 29 | class ParsecGLKViewController : ParsecPlayground { 30 | 31 | var glkView: GLKView! 32 | let glkViewController = GLKViewController() 33 | var glkRenderer: ParsecGLKRenderer! 34 | let updateImage:() -> Void 35 | 36 | let viewController: UIViewController 37 | 38 | required init(viewController: UIViewController, updateImage: @escaping () -> Void) { 39 | self.viewController = viewController 40 | self.updateImage = updateImage 41 | } 42 | 43 | public func viewDidLoad() { 44 | glkView = GLKView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)) 45 | glkRenderer = ParsecGLKRenderer(glkView, glkViewController, updateImage) 46 | self.viewController.view.addSubview(glkView) 47 | setupGLKViewController() 48 | 49 | 50 | } 51 | 52 | private func setupGLKViewController() { 53 | glkView.context = EAGLContext(api: .openGLES3)! 54 | glkViewController.view = glkView 55 | glkViewController.preferredFramesPerSecond = 60 56 | self.viewController.addChild(glkViewController) 57 | self.viewController.view.addSubview(glkViewController.view) 58 | self.glkViewController.didMove(toParent: self.viewController) 59 | } 60 | 61 | func cleanUp() { 62 | 63 | } 64 | 65 | func updateSize(width: CGFloat, height: CGFloat) { 66 | glkView.frame.size.width = width 67 | glkView.frame.size.height = height 68 | } 69 | 70 | 71 | } 72 | -------------------------------------------------------------------------------- /OpenParsec/ParsecMetalRenderer.swift: -------------------------------------------------------------------------------- 1 | /*import MetalKit 2 | import ParsecSDK 3 | 4 | class ParsecMetalRenderer:NSObject, MTKViewDelegate 5 | { 6 | var parent:ParsecMetalViewController 7 | var onBeforeRender:() -> Void 8 | var metalDevice:MTLDevice! 9 | var metalCommandQueue:MTLCommandQueue! 10 | var metalTexture:MTLTexture! 11 | var metalTexturePtr:UnsafeMutableRawPointer? 12 | 13 | var lastWidth:CGFloat = 1.0 14 | 15 | init(_ parent:ParsecMetalViewController, _ beforeRender:@escaping () -> Void) 16 | { 17 | self.parent = parent; 18 | onBeforeRender = beforeRender 19 | if let metalDevice = MTLCreateSystemDefaultDevice() 20 | { 21 | self.metalDevice = metalDevice 22 | } 23 | self.metalCommandQueue = metalDevice.makeCommandQueue() 24 | metalTexture = metalDevice.makeTexture(descriptor:MTLTextureDescriptor()) 25 | metalTexturePtr = createTextureRef(&metalTexture) 26 | 27 | super.init() 28 | } 29 | 30 | func mtkView(_ view:MTKView, drawableSizeWillChange size:CGSize) { } 31 | 32 | func draw(in view:MTKView) 33 | { 34 | onBeforeRender() 35 | let deltaWidth: CGFloat = view.frame.size.width - lastWidth 36 | if deltaWidth > 0.1 || deltaWidth < -0.1 37 | { 38 | CParsec.setFrame(view.frame.size.width, view.frame.size.height, view.contentScaleFactor) 39 | lastWidth = view.frame.size.width 40 | } 41 | CParsec.renderMetalFrame(&metalCommandQueue, &metalTexturePtr) 42 | } 43 | }*/ 44 | -------------------------------------------------------------------------------- /OpenParsec/ParsecMetalViewController.swift: -------------------------------------------------------------------------------- 1 | /*import SwiftUI 2 | import MetalKit 3 | 4 | struct ParsecMetalViewController:UIViewRepresentable 5 | { 6 | let onBeforeRender:() -> Void 7 | 8 | func makeCoordinator() -> ParsecMetalRenderer 9 | { 10 | ParsecMetalRenderer(self, onBeforeRender) 11 | } 12 | 13 | func makeUIView(context:UIViewRepresentableContext) -> MTKView 14 | { 15 | let metalView = MTKView() 16 | metalView.delegate = context.coordinator 17 | metalView.preferredFramesPerSecond = 60 18 | metalView.enableSetNeedsDisplay = true 19 | 20 | if let metalDevice = MTLCreateSystemDefaultDevice() 21 | { 22 | metalView.device = metalDevice 23 | } 24 | 25 | metalView.framebufferOnly = false 26 | metalView.drawableSize = metalView.frame.size 27 | return metalView 28 | } 29 | 30 | func updateUIView(_ uiView:MTKView, context:UIViewRepresentableContext) { } 31 | }*/ 32 | -------------------------------------------------------------------------------- /OpenParsec/ParsecSDKBridge.swift: -------------------------------------------------------------------------------- 1 | import ParsecSDK 2 | import MetalKit 3 | import UIKit 4 | 5 | enum RendererType:Int 6 | { 7 | case opengl 8 | case metal 9 | } 10 | 11 | enum DecoderPref:Int 12 | { 13 | case h264 14 | case h265 15 | } 16 | 17 | enum CursorMode:Int 18 | { 19 | case touchpad 20 | case direct 21 | } 22 | 23 | enum RightClickPosition:Int 24 | { 25 | case firstFinger 26 | case middle 27 | case secondFinger 28 | } 29 | 30 | struct KeyBoardKeyEvent { 31 | var input: UIKey? 32 | var isPressBegin: Bool 33 | } 34 | 35 | class ParsecSDKBridge: ParsecService 36 | { 37 | var hostWidth: Float = 1920 38 | 39 | var hostHeight: Float = 1080 40 | 41 | 42 | static let PARSEC_VER:UInt32 = UInt32((PARSEC_VER_MAJOR << 16) | PARSEC_VER_MINOR) 43 | 44 | private var _parsec:OpaquePointer! 45 | private var _audio:OpaquePointer! 46 | private let _audioPtr:UnsafeRawPointer 47 | 48 | private var isVirtualShiftOn = false 49 | 50 | public var clientWidth:Float = 1920 51 | public var clientHeight:Float = 1080 52 | 53 | public var netProtocol:Int32 = 1 54 | public var mediaContainer:Int32 = 0 55 | public var pngCursor:Bool = false 56 | var backgroundTaskRunning = true 57 | var didSetResolution = false 58 | 59 | public var mouseInfo = MouseInfo() 60 | 61 | init() { 62 | print("Parsec SDK Version: " + String(ParsecSDKBridge.PARSEC_VER)) 63 | 64 | ParsecSetLogCallback( 65 | { (level, msg, opaque) in 66 | print("[\(level == LOG_DEBUG ? "D" : "I")] \(String(cString:msg!))") 67 | }, nil) 68 | 69 | audio_init(&_audio) 70 | 71 | ParsecInit(ParsecSDKBridge.PARSEC_VER, nil, nil, &_parsec) 72 | 73 | 74 | self._audioPtr = UnsafeRawPointer(_audio) 75 | 76 | } 77 | 78 | deinit 79 | { 80 | 81 | ParsecDestroy(_parsec) 82 | audio_destroy(&_audio) 83 | } 84 | 85 | func connect(_ peerID:String) -> ParsecStatus 86 | { 87 | var parsecClientCfg = ParsecClientConfig() 88 | parsecClientCfg.video.0.decoderIndex = 1 89 | parsecClientCfg.video.0.resolutionX = 0 90 | parsecClientCfg.video.0.resolutionY = 0 91 | parsecClientCfg.video.0.decoderCompatibility = false 92 | parsecClientCfg.video.0.decoderH265 = true 93 | 94 | parsecClientCfg.video.1.decoderIndex = 1 95 | parsecClientCfg.video.1.resolutionX = 0 96 | parsecClientCfg.video.1.resolutionY = 0 97 | parsecClientCfg.video.1.decoderCompatibility = false 98 | parsecClientCfg.video.1.decoderH265 = true 99 | 100 | parsecClientCfg.mediaContainer = 0 101 | parsecClientCfg.protocol = 1 102 | //parsecClientCfg.secret = "" 103 | parsecClientCfg.pngCursor = false 104 | 105 | self.startBackgroundTask() 106 | 107 | return ParsecClientConnect(_parsec, &parsecClientCfg, NetworkHandler.clinfo?.session_id, peerID) 108 | } 109 | 110 | func disconnect() 111 | { 112 | audio_clear(&_audio) 113 | ParsecClientDisconnect(_parsec) 114 | backgroundTaskRunning = false 115 | } 116 | 117 | func getStatus() -> ParsecStatus 118 | { 119 | return ParsecClientGetStatus(_parsec, nil) 120 | } 121 | 122 | func getStatusEx(_ pcs:inout ParsecClientStatus) -> ParsecStatus 123 | { 124 | self.hostHeight = Float(pcs.decoder.0.height) 125 | self.hostWidth = Float(pcs.decoder.0.width) 126 | return ParsecClientGetStatus(_parsec, &pcs) 127 | 128 | } 129 | 130 | func setFrame(_ width:CGFloat, _ height:CGFloat, _ scale:CGFloat) 131 | { 132 | ParsecClientSetDimensions(_parsec, UInt8(DEFAULT_STREAM), UInt32(width), UInt32(height), Float(scale)) 133 | 134 | clientWidth = Float(width) 135 | clientHeight = Float(height) 136 | mouseInfo.mouseX = Int32(width / 2) 137 | mouseInfo.mouseY = Int32(height / 2) 138 | } 139 | 140 | func renderGLFrame(timeout:UInt32 = 16) // timeout in ms, 16 == 60 FPS, 8 == 120 FPS, etc. 141 | { 142 | ParsecClientGLRenderFrame(_parsec, UInt8(DEFAULT_STREAM), nil, nil, timeout) 143 | } 144 | 145 | /*static func renderMetalFrame(_ queue:inout MTLCommandQueue, _ texturePtr:UnsafeMutablePointer, timeout:UInt32 = 16) // timeout in ms, 16 == 60 FPS, 8 == 120 FPS, etc. 146 | { 147 | ParsecClientMetalRenderFrame(_parsec, UInt8(DEFAULT_STREAM), &queue, texturePtr, nil, nil, timeout) 148 | }*/ 149 | 150 | func pollAudio(timeout:UInt32 = 16) // timeout in ms, 16 == 60 FPS, 8 == 120 FPS, etc. 151 | { 152 | ParsecClientPollAudio(_parsec, audio_cb, timeout, _audioPtr) 153 | } 154 | 155 | var getFirstCursor = false 156 | var mousePositionRelative = false 157 | 158 | func pollEvent(timeout:UInt32 = 16) // timeout in ms, 16 == 60 FPS, 8 == 120 FPS, etc. 159 | { 160 | var e: ParsecClientEvent! 161 | var _event = ParsecClientEvent() 162 | var pollSuccess = false; 163 | withUnsafeMutablePointer(to: &_event, {(_eventPtr) in 164 | pollSuccess = ParsecClientPollEvents(_parsec, timeout, _eventPtr) 165 | e = _eventPtr.pointee 166 | }) 167 | if !pollSuccess { 168 | return 169 | } 170 | if e.type == CLIENT_EVENT_CURSOR { 171 | handleCursorEvent(event: e.cursor) 172 | } else if e.type == CLIENT_EVENT_USER_DATA { 173 | handleUserDataEvent(event: e.userData) 174 | } 175 | } 176 | 177 | func handleUserDataEvent(event: ParsecClientUserDataEvent) { 178 | let pointer = ParsecGetBuffer(_parsec, event.key) 179 | switch event.id { 180 | case 11: 181 | do { 182 | let decoder = JSONDecoder() 183 | let config = try decoder.decode(ParsecUserDataVideoConfig.self, from: Data(bytesNoCopy: pointer!, count: strlen(pointer!), deallocator: .none)) 184 | let videoConfig = config.video[0] 185 | DataManager.model.resolutionX = videoConfig.resolutionX 186 | DataManager.model.resolutionY = videoConfig.resolutionY 187 | DataManager.model.bitrate = videoConfig.encoderMaxBitrate 188 | DataManager.model.constantFps = videoConfig.fullFPS 189 | if !didSetResolution { 190 | didSetResolution = true 191 | DispatchQueue.main.async { 192 | DataManager.model.resolutionX = SettingsHandler.resolution.width 193 | DataManager.model.resolutionY = SettingsHandler.resolution.height 194 | self.updateHostVideoConfig() 195 | } 196 | } 197 | 198 | } catch { 199 | print("error while parsing user data: \(error.localizedDescription)") 200 | } 201 | case 12: 202 | do { 203 | let decoder = JSONDecoder() 204 | let config = try decoder.decode(Array.self, from: Data(bytesNoCopy: pointer!, count: strlen(pointer!), deallocator: .none)) 205 | DataManager.model.displayConfigs = config 206 | } catch { 207 | print("error while parsing user data: \(error.localizedDescription)") 208 | } 209 | default: 210 | break 211 | } 212 | ParsecFree(pointer) 213 | } 214 | 215 | func handleCursorEvent(event: ParsecClientCursorEvent) { 216 | let prevHidden = mouseInfo.cursorHidden 217 | mouseInfo.cursorHidden = event.cursor.hidden 218 | mouseInfo.mousePositionRelative = event.cursor.relative 219 | 220 | if event.cursor.imageUpdate || !getFirstCursor{ 221 | getFirstCursor = true 222 | let imgKey = event.key 223 | let pointer = ParsecGetBuffer(_parsec, imgKey) 224 | if pointer == nil{ 225 | return 226 | } 227 | let size = event.cursor.size 228 | let width = event.cursor.width 229 | let height = event.cursor.height 230 | mouseInfo.cursorWidth = Int(width) 231 | mouseInfo.cursorHeight = Int(height) 232 | // 之前隐藏现在不隐藏了就更新 233 | if prevHidden && !event.cursor.hidden { 234 | mouseInfo.mouseX = Int32(event.cursor.positionX) 235 | mouseInfo.mouseY = Int32(event.cursor.positionY) 236 | } 237 | 238 | mouseInfo.cursorHotX = Int(event.cursor.hotX) 239 | mouseInfo.cursorHotY = Int(event.cursor.hotY) 240 | 241 | let elmentLength: Int = 4 242 | let render: CGColorRenderingIntent = CGColorRenderingIntent.defaultIntent 243 | let rgbColorSpace = CGColorSpaceCreateDeviceRGB() 244 | let bitmapInfo: CGBitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.last.rawValue) 245 | let providerRef: CGDataProvider? = CGDataProvider(data: NSData(bytes: pointer, length: Int(size))) 246 | let cgimage: CGImage? = CGImage(width: Int(width), height: Int(height), bitsPerComponent: 8, bitsPerPixel: 32, bytesPerRow: Int(width) * elmentLength, space: rgbColorSpace, bitmapInfo: bitmapInfo, provider: providerRef!, decode: nil, shouldInterpolate: true, intent: render) 247 | if cgimage != nil { 248 | mouseInfo.cursorImg = cgimage 249 | } 250 | ParsecFree(pointer) 251 | } 252 | } 253 | 254 | func setMuted(_ muted:Bool) 255 | { 256 | audio_mute(muted, _audioPtr) 257 | } 258 | 259 | func applyConfig() 260 | { 261 | var parsecClientCfg = ParsecClientConfig() 262 | 263 | parsecClientCfg.video.0.decoderIndex = 1 264 | parsecClientCfg.video.0.resolutionX = 0 265 | parsecClientCfg.video.0.resolutionY = 0 266 | parsecClientCfg.video.0.decoderCompatibility = false 267 | parsecClientCfg.video.0.decoderH265 = SettingsHandler.decoder == .h265 268 | 269 | parsecClientCfg.video.1.decoderIndex = 1 270 | parsecClientCfg.video.1.resolutionX = 0 271 | parsecClientCfg.video.1.resolutionY = 0 272 | parsecClientCfg.video.1.decoderCompatibility = false 273 | parsecClientCfg.video.1.decoderH265 = SettingsHandler.decoder == .h265 274 | 275 | parsecClientCfg.mediaContainer = mediaContainer 276 | parsecClientCfg.protocol = netProtocol 277 | //parsecClientCfg.secret = "" 278 | parsecClientCfg.pngCursor = pngCursor 279 | 280 | ParsecClientSetConfig(_parsec, &parsecClientCfg); 281 | } 282 | 283 | func sendMouseMessage(_ button:ParsecMouseButton, _ x:Int32, _ y:Int32, _ pressed:Bool) 284 | { 285 | // Send the mouse position 286 | sendMousePosition(x, y) 287 | 288 | // Send the mouse button state 289 | var buttonMessage = ParsecMessage() 290 | buttonMessage.type = MESSAGE_MOUSE_BUTTON 291 | buttonMessage.mouseButton.button = button 292 | buttonMessage.mouseButton.pressed = pressed 293 | ParsecClientSendMessage(_parsec, &buttonMessage) 294 | } 295 | 296 | func sendMouseClickMessage(_ button:ParsecMouseButton, _ pressed:Bool) { 297 | var buttonMessage = ParsecMessage() 298 | buttonMessage.type = MESSAGE_MOUSE_BUTTON 299 | buttonMessage.mouseButton.button = button 300 | buttonMessage.mouseButton.pressed = pressed 301 | ParsecClientSendMessage(_parsec, &buttonMessage) 302 | } 303 | 304 | func sendMouseDelta(_ dx: Int32, _ dy: Int32) { 305 | if mouseInfo.mousePositionRelative { 306 | sendMouseRelativeMove(dx, dy) 307 | } else { 308 | sendMousePosition(mouseInfo.mouseX + dx, mouseInfo.mouseY + dy) 309 | } 310 | 311 | } 312 | static func clamp(_ value: T, minValue: T, maxValue: T) -> T where T : Comparable { 313 | return min(max(value, minValue), maxValue) 314 | } 315 | 316 | func sendMousePosition(_ x:Int32, _ y:Int32) 317 | { 318 | mouseInfo.mouseX = ParsecSDKBridge.clamp(x, minValue: 0, maxValue: Int32(self.clientWidth)) 319 | mouseInfo.mouseY = ParsecSDKBridge.clamp(y, minValue: 0, maxValue: Int32(self.clientHeight)) 320 | var motionMessage = ParsecMessage() 321 | motionMessage.type = MESSAGE_MOUSE_MOTION 322 | motionMessage.mouseMotion.x = x 323 | motionMessage.mouseMotion.y = y 324 | ParsecClientSendMessage(_parsec, &motionMessage) 325 | } 326 | 327 | func sendMouseRelativeMove(_ dx:Int32, _ dy:Int32) 328 | { 329 | var motionMessage = ParsecMessage() 330 | motionMessage.type = MESSAGE_MOUSE_MOTION 331 | motionMessage.mouseMotion.x = dx 332 | motionMessage.mouseMotion.y = dy 333 | motionMessage.mouseMotion.relative = true 334 | ParsecClientSendMessage(_parsec, &motionMessage) 335 | } 336 | 337 | func getKeyCodeByText(text: String) -> (ParsecKeycode?, Bool) { 338 | var keyCode : ParsecKeycode? 339 | var useShift = false 340 | if text.count == 1 { 341 | let char = Character(text) 342 | if char.isLetter || char.isNumber { 343 | keyCode = KeyCodeTranslators.parsecKeyCodeTranslator(text.uppercased()) 344 | if char.isUppercase { 345 | useShift = true 346 | } 347 | } else if char.isNewline { 348 | keyCode = ParsecKeycode(40) 349 | } else if char.isWhitespace{ 350 | keyCode = ParsecKeycode(44) 351 | } else { 352 | let (keycodeRaw, keyMod) = KeyCodeTranslators.getParsecKeycode(for: text) 353 | if keycodeRaw != -1 { 354 | keyCode = ParsecKeycode(UInt32(keycodeRaw)) 355 | if keyMod { 356 | useShift = true 357 | } 358 | } 359 | } 360 | } else { 361 | keyCode = KeyCodeTranslators.parsecKeyCodeTranslator(text) 362 | } 363 | 364 | return (keyCode, useShift) 365 | } 366 | 367 | func sendVirtualKeyboardInput(text: String) { 368 | let (keyCode, useShift) = getKeyCodeByText(text: text) 369 | 370 | guard let keyCode else { 371 | return 372 | } 373 | var keyboardMessagePress = ParsecMessage() 374 | keyboardMessagePress.type = MESSAGE_KEYBOARD 375 | keyboardMessagePress.keyboard.pressed = true 376 | if !isVirtualShiftOn && useShift { 377 | keyboardMessagePress.keyboard.code = ParsecKeycode(rawValue: 225) 378 | ParsecClientSendMessage(_parsec, &keyboardMessagePress) 379 | } 380 | keyboardMessagePress.keyboard.code = keyCode 381 | ParsecClientSendMessage(_parsec, &keyboardMessagePress) 382 | keyboardMessagePress.keyboard.pressed = false 383 | if !isVirtualShiftOn && useShift { 384 | keyboardMessagePress.keyboard.code = ParsecKeycode(rawValue: 225) 385 | ParsecClientSendMessage(_parsec, &keyboardMessagePress) 386 | keyboardMessagePress.keyboard.code = keyCode 387 | } 388 | ParsecClientSendMessage(_parsec, &keyboardMessagePress) 389 | } 390 | 391 | func sendVirtualKeyboardInput(text: String, isOn: Bool) { 392 | let (keyCode, _) = getKeyCodeByText(text: text) 393 | 394 | guard let keyCode else { 395 | return 396 | } 397 | 398 | if keyCode.rawValue == 225 { 399 | isVirtualShiftOn = isOn 400 | } 401 | 402 | var keyboardMessagePress = ParsecMessage() 403 | keyboardMessagePress.type = MESSAGE_KEYBOARD 404 | keyboardMessagePress.keyboard.pressed = isOn 405 | keyboardMessagePress.keyboard.code = keyCode 406 | ParsecClientSendMessage(_parsec, &keyboardMessagePress) 407 | 408 | } 409 | 410 | func sendKeyboardMessage(event:KeyBoardKeyEvent) 411 | { 412 | if event.input == nil { 413 | return 414 | } 415 | 416 | var keyboardMessagePress = ParsecMessage() 417 | keyboardMessagePress.type = MESSAGE_KEYBOARD 418 | keyboardMessagePress.keyboard.code = ParsecKeycode(UInt32(KeyCodeTranslators.uiKeyCodeToInt(key: event.input?.keyCode ?? UIKeyboardHIDUsage.keyboardErrorUndefined))) 419 | keyboardMessagePress.keyboard.pressed = event.isPressBegin 420 | ParsecClientSendMessage(_parsec, &keyboardMessagePress) 421 | } 422 | 423 | func sendGameControllerButtonMessage(controllerId:UInt32, _ button:ParsecGamepadButton, pressed:Bool) 424 | { 425 | var pmsg = ParsecMessage() 426 | pmsg.type = MESSAGE_GAMEPAD_BUTTON 427 | pmsg.gamepadButton.id = controllerId 428 | pmsg.gamepadButton.button = button 429 | pmsg.gamepadButton.pressed = pressed 430 | ParsecClientSendMessage(_parsec, &pmsg) 431 | } 432 | 433 | /*static func sendGameControllerTriggerButtonMessage(controllerId:UInt32, _ button:ParsecGamepadAxis, pressed:Bool) 434 | { 435 | var pmsg = ParsecMessage() 436 | pmsg.type = MESSAGE_GAMEPAD_AXIS 437 | pmsg.gamepadAxis.id = controllerId 438 | pmsg.gamepadAxis.button = button 439 | pmsg.gamepadAxis.pressed = pressed 440 | ParsecClientSendMessage(_parsec, &pmsg) 441 | }*/ 442 | 443 | func sendGameControllerAxisMessage(controllerId:UInt32, _ button:ParsecGamepadAxis, _ value: Int16) 444 | { 445 | var pmsg = ParsecMessage() 446 | pmsg.type = MESSAGE_GAMEPAD_AXIS 447 | pmsg.gamepadAxis.id = controllerId 448 | pmsg.gamepadAxis.axis = button 449 | pmsg.gamepadAxis.value = value 450 | ParsecClientSendMessage(_parsec, &pmsg) 451 | } 452 | 453 | func sendGameControllerUnplugMessage(controllerId:UInt32) 454 | { 455 | var pmsg = ParsecMessage() 456 | pmsg.type = MESSAGE_GAMEPAD_UNPLUG; 457 | pmsg.gamepadUnplug.id = controllerId; 458 | ParsecClientSendMessage(_parsec, &pmsg) 459 | } 460 | 461 | func sendWheelMsg(x: Int32, y: Int32) { 462 | var pmsg = ParsecMessage() 463 | pmsg.type = MESSAGE_MOUSE_WHEEL; 464 | pmsg.mouseWheel.x = x 465 | pmsg.mouseWheel.y = y 466 | ParsecClientSendMessage(_parsec, &pmsg) 467 | } 468 | 469 | func startBackgroundTask(){ 470 | 471 | 472 | let item1 = DispatchWorkItem { 473 | while self.backgroundTaskRunning { 474 | self.pollAudio() 475 | } 476 | 477 | } 478 | 479 | let item2 = DispatchWorkItem { 480 | while self.backgroundTaskRunning { 481 | self.pollEvent() 482 | 483 | 484 | } 485 | 486 | } 487 | let mainQueue = DispatchQueue.global() 488 | mainQueue.async(execute: item1) 489 | mainQueue.async(execute: item2) 490 | } 491 | 492 | func sendUserData(type: ParsecUserDataType, message: Data) { 493 | message.withUnsafeBytes { ptr in 494 | let ptr2 = ptr.baseAddress?.assumingMemoryBound(to: CChar.self) 495 | ParsecClientSendUserData(_parsec, type.rawValue, ptr2) 496 | } 497 | } 498 | 499 | func updateHostVideoConfig() { 500 | var videoConfig = ParsecUserDataVideoConfig() 501 | videoConfig.video[0].resolutionX = DataManager.model.resolutionX 502 | videoConfig.video[0].resolutionY = DataManager.model.resolutionY 503 | videoConfig.video[0].encoderMaxBitrate = DataManager.model.bitrate 504 | videoConfig.video[0].fullFPS = DataManager.model.constantFps 505 | videoConfig.video[0].output = DataManager.model.output 506 | let encoder = JSONEncoder() 507 | let data = try! encoder.encode(videoConfig) 508 | CParsec.sendUserData(type: .setVideoConfig, message: data) 509 | } 510 | } 511 | -------------------------------------------------------------------------------- /OpenParsec/ParsecUserData.swift: -------------------------------------------------------------------------------- 1 | 2 | struct ParsecUserDataVideo : Codable { 3 | var encoderFPS : Int = 0 4 | var resolutionX : Int = 0 5 | var resolutionY : Int = 0 6 | var fullFPS : Bool = false 7 | var hostOS = 0 8 | var output = "none" 9 | var encoderMaxBitrate : Int = 50 10 | } 11 | 12 | struct ParsecUserDataVideoConfig : Codable { 13 | var virtualMicrophone : Int = 0 14 | var virtualTablet : Int = 0 15 | var video : [ParsecUserDataVideo] = [ 16 | ParsecUserDataVideo(), 17 | ParsecUserDataVideo(), 18 | ParsecUserDataVideo() 19 | ] 20 | } 21 | 22 | struct ParsecDisplayConfig : Codable, Hashable { 23 | var name : String = "" 24 | var adapterName : String = "" 25 | var id : String = "" 26 | } 27 | 28 | enum ParsecUserDataType : UInt32 { 29 | case getVideoConfig = 9 30 | case getAdapterInfo = 10 31 | case setVideoConfig = 11 32 | } 33 | -------------------------------------------------------------------------------- /OpenParsec/ParsecView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import ParsecSDK 3 | import Foundation 4 | 5 | struct ParsecStatusBar : View { 6 | @Binding var showMenu : Bool 7 | @State var metricInfo:String = "Loading..." 8 | @Binding var showDCAlert:Bool 9 | @Binding var DCAlertText:String 10 | let timer = Timer.publish(every: 0.2, on: .main, in: .common).autoconnect() 11 | 12 | var body: some View { 13 | // Overlay elements 14 | if showMenu 15 | { 16 | VStack() 17 | { 18 | Text(metricInfo) 19 | .frame(minWidth:200, maxWidth:.infinity, maxHeight:20) 20 | .multilineTextAlignment(.leading) 21 | .font(.system(size: 10)) 22 | .lineSpacing(20) 23 | .lineLimit(nil) 24 | } 25 | .background(Rectangle().fill(Color("BackgroundPrompt").opacity(0.75))) 26 | .foregroundColor(Color("Foreground")) 27 | .frame(maxHeight: .infinity, alignment: .top) 28 | .zIndex(1) 29 | .edgesIgnoringSafeArea(.all) 30 | 31 | } 32 | EmptyView() 33 | .onReceive(timer) { p in 34 | poll() 35 | } 36 | } 37 | 38 | func poll() 39 | { 40 | if showDCAlert 41 | { 42 | return // no need to poll if we aren't connected anymore 43 | } 44 | 45 | var pcs = ParsecClientStatus() 46 | let status = CParsec.getStatusEx(&pcs) 47 | 48 | if status != PARSEC_OK 49 | { 50 | DCAlertText = "Disconnected (code \(status.rawValue))" 51 | showDCAlert = true 52 | return 53 | } 54 | 55 | // FIXME: This may cause memory leak? 56 | 57 | if showMenu 58 | { 59 | let str = String.fromBuffer(&pcs.decoder.0.name.0, length:16) 60 | metricInfo = "Decode \(String(format:"%.2f", pcs.`self`.metrics.0.decodeLatency))ms Encode \(String(format:"%.2f", pcs.`self`.metrics.0.encodeLatency))ms Network \(String(format:"%.2f", pcs.`self`.metrics.0.networkLatency))ms Bitrate \(String(format:"%.2f", pcs.`self`.metrics.0.bitrate))Mbps \(pcs.decoder.0.h265 ? "H265" : "H264") \(pcs.decoder.0.width)x\(pcs.decoder.0.height) \(pcs.decoder.0.color444 ? "4:4:4" : "4:2:0") \(str)" 61 | } 62 | } 63 | } 64 | 65 | struct ParsecView:View 66 | { 67 | var controller:ContentView? 68 | 69 | @State var showDCAlert:Bool = false 70 | @State var DCAlertText:String = "Disconnected (reason unknown)" 71 | @State var metricInfo:String = "Loading..." 72 | 73 | @State var hideOverlay:Bool = false 74 | @State var showMenu:Bool = false 75 | 76 | @State var muted:Bool = false 77 | @State var preferH265:Bool = true 78 | @State var constantFps = false 79 | 80 | @State var resolutions : [ParsecResolution] 81 | @State var bitrates : [Int] 82 | 83 | var parsecViewController : ParsecViewController! 84 | 85 | 86 | //@State var showDisplays:Bool = false 87 | 88 | init(_ controller:ContentView?) 89 | { 90 | self.controller = controller 91 | parsecViewController = ParsecViewController() 92 | _resolutions = State(initialValue: ParsecResolution.resolutions) 93 | _bitrates = State(initialValue: ParsecResolution.bitrates) 94 | } 95 | 96 | var body:some View 97 | { 98 | ZStack() 99 | { 100 | 101 | UIViewControllerWrapper(self.parsecViewController) 102 | .zIndex(1) 103 | .prefersPersistentSystemOverlaysHidden() 104 | 105 | ParsecStatusBar(showMenu: $showMenu, showDCAlert: $showDCAlert, DCAlertText: $DCAlertText) 106 | 107 | VStack() 108 | { 109 | if !hideOverlay 110 | { 111 | HStack() 112 | { 113 | Button(action:{ 114 | if showMenu { 115 | showMenu = false 116 | } else { 117 | showMenu = true 118 | getHostUserData() 119 | } 120 | }) 121 | { 122 | Image("IconTransparent") 123 | .resizable() 124 | .aspectRatio(contentMode: .fit) 125 | .frame(width:48, height:48) 126 | .background(Rectangle().fill(Color("BackgroundPrompt").opacity(showMenu ? 0.75 : 1))) 127 | .cornerRadius(8) 128 | .opacity(showMenu ? 1 : 0.25) 129 | } 130 | .padding() 131 | .edgesIgnoringSafeArea(.all) 132 | Spacer() 133 | } 134 | } 135 | if showMenu 136 | { 137 | HStack() 138 | { 139 | VStack(spacing:3) 140 | { 141 | Button(action:disableOverlay) 142 | { 143 | Text("Hide Overlay") 144 | .padding(8) 145 | .frame(maxWidth:.infinity) 146 | .multilineTextAlignment(.center) 147 | } 148 | Button(action:toggleMute) 149 | { 150 | Text("Sound: \(muted ? "OFF" : "ON")") 151 | .padding(8) 152 | .frame(maxWidth:.infinity) 153 | .multilineTextAlignment(.center) 154 | } 155 | Menu() { 156 | ForEach(resolutions, id: \.self) { resolution in 157 | Button(resolution.desc) { 158 | changeResolution(res: resolution) 159 | } 160 | } 161 | } label: { 162 | Text("Resolution") 163 | .padding(8) 164 | .frame(maxWidth:.infinity) 165 | .multilineTextAlignment(.center) 166 | } 167 | Menu() { 168 | ForEach(bitrates, id: \.self) { bitrate in 169 | Button("\(bitrate) Mbps") { 170 | changeBitRate(bitrate: bitrate) 171 | } 172 | } 173 | } label: { 174 | Text("Bitrate") 175 | .padding(8) 176 | .frame(maxWidth:.infinity) 177 | .multilineTextAlignment(.center) 178 | } 179 | if (DataManager.model.displayConfigs.count > 1) { 180 | Menu() { 181 | Button("Auto") { 182 | changeDisplay(displayId: "none") 183 | } 184 | ForEach(DataManager.model.displayConfigs, id: \.self) { config in 185 | Button("\(config.name) \(config.adapterName)") { 186 | changeDisplay(displayId: config.id) 187 | } 188 | } 189 | } label: { 190 | Text("Switch Display") 191 | .padding(8) 192 | .frame(maxWidth:.infinity) 193 | .multilineTextAlignment(.center) 194 | } 195 | } 196 | 197 | Button(action:toggleConstantFps) 198 | { 199 | Text("Constant FPS: \(constantFps ? "ON" : "OFF")") 200 | .padding(8) 201 | .frame(maxWidth:.infinity) 202 | .multilineTextAlignment(.center) 203 | } 204 | Rectangle() 205 | .fill(Color("Foreground")) 206 | .opacity(0.25) 207 | .frame(height:1) 208 | Button(action:disconnect) 209 | { 210 | Text("Disconnect") 211 | .foregroundColor(.red) 212 | .padding(8) 213 | .frame(maxWidth:.infinity) 214 | .multilineTextAlignment(.center) 215 | } 216 | } 217 | .background(Rectangle().fill(Color("BackgroundPrompt").opacity(0.75))) 218 | .foregroundColor(Color("Foreground")) 219 | .frame(maxWidth:175) 220 | .cornerRadius(8) 221 | .padding(.horizontal) 222 | //.edgesIgnoringSafeArea(.all) 223 | Spacer() 224 | } 225 | } 226 | Spacer() 227 | } 228 | .zIndex(2) 229 | } 230 | .statusBarHidden(SettingsHandler.hideStatusBar) 231 | .alert(isPresented:$showDCAlert) 232 | { 233 | Alert(title:Text(DCAlertText), dismissButton:.default(Text("Close"), action:disconnect)) 234 | } 235 | .onAppear(perform:post) 236 | .edgesIgnoringSafeArea(.all) 237 | 238 | } 239 | 240 | func post() 241 | { 242 | CParsec.applyConfig() 243 | CParsec.setMuted(muted) 244 | 245 | // set client resolution 246 | let screenSize: CGSize = self.parsecViewController.view.frame.size 247 | let scaleFactor = UIScreen.main.nativeScale 248 | ParsecResolution.resolutions[1].width = Int(screenSize.width * scaleFactor) 249 | ParsecResolution.resolutions[1].height = Int(screenSize.height * scaleFactor) 250 | 251 | getHostUserData() 252 | 253 | hideOverlay = SettingsHandler.noOverlay 254 | } 255 | 256 | 257 | func disableOverlay() 258 | { 259 | hideOverlay = true 260 | showMenu = false 261 | } 262 | 263 | func toggleMute() 264 | { 265 | muted.toggle() 266 | CParsec.setMuted(muted) 267 | } 268 | 269 | /*func genDisplaySheet() -> ActionSheet 270 | { 271 | let len:Int = 16 272 | var outputs = [ParsecOutput?](repeating:nil, count:len) 273 | ParsecGetOutputs(&outputs, UInt32(len)) 274 | print("Listing \(outputs.count) displays") 275 | 276 | func getDeviceName(_ output:ParsecOutput) -> String 277 | { 278 | return withUnsafePointer(to:output.device) 279 | { 280 | $0.withMemoryRebound(to:UInt8.self, capacity:MemoryLayout.size(ofValue:$0)) 281 | { 282 | String(cString:$0) 283 | } 284 | } 285 | } 286 | 287 | let buttons = outputs.enumerated().map 288 | { i, output in 289 | Alert.Button.default(Text("\(i) - \(getDeviceName(output))"), action:{print("Selected device \(i)")}) 290 | } 291 | return ActionSheet(title:Text("Select a Display:"), buttons:buttons + [Alert.Button.cancel()]) 292 | }*/ 293 | 294 | func disconnect() 295 | { 296 | CParsec.disconnect() 297 | self.parsecViewController.glkView.cleanUp() 298 | 299 | if let c = controller 300 | { 301 | c.setView(.main) 302 | } 303 | } 304 | 305 | func changeResolution(res: ParsecResolution) { 306 | DataManager.model.resolutionX = res.width 307 | DataManager.model.resolutionY = res.height 308 | CParsec.updateHostVideoConfig() 309 | } 310 | 311 | func changeBitRate(bitrate: Int) { 312 | DataManager.model.bitrate = bitrate 313 | CParsec.updateHostVideoConfig() 314 | } 315 | 316 | func toggleConstantFps() { 317 | DataManager.model.constantFps.toggle() 318 | constantFps = DataManager.model.constantFps 319 | CParsec.updateHostVideoConfig() 320 | } 321 | 322 | func changeDisplay(displayId: String) { 323 | DataManager.model.output = displayId 324 | CParsec.updateHostVideoConfig() 325 | } 326 | 327 | func getHostUserData() { 328 | let data = "".data(using: .utf8)! 329 | CParsec.sendUserData(type: .getVideoConfig, message: data) 330 | CParsec.sendUserData(type: .getAdapterInfo, message: data) 331 | } 332 | 333 | } 334 | 335 | // from https://github.com/utmapp/UTM/blob/117e3a962f2f46f7d847632d65fa7a85a2bb0cfa/Platform/iOS/VMWindowView.swift#L314 336 | private extension View { 337 | func prefersPersistentSystemOverlaysHidden() -> some View { 338 | if #available(iOS 16, *) { 339 | return self.persistentSystemOverlays(.hidden) 340 | } else { 341 | return self 342 | } 343 | } 344 | } 345 | -------------------------------------------------------------------------------- /OpenParsec/ParsecViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PoinerRegion.swift 3 | // OpenParsec 4 | // 5 | // Created by s s on 2024/5/11. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | import ParsecSDK 11 | 12 | 13 | protocol ParsecPlayground { 14 | init(viewController: UIViewController, updateImage: @escaping () -> Void) 15 | func viewDidLoad() 16 | func cleanUp() 17 | func updateSize(width: CGFloat, height: CGFloat) 18 | } 19 | 20 | 21 | class ParsecViewController :UIViewController { 22 | var glkView: ParsecPlayground! 23 | var gamePadController: GamepadController! 24 | var touchController: TouchController! 25 | var u:UIImageView? 26 | var lastImg: CGImage? 27 | 28 | var lastLongPressPoint : CGPoint = CGPoint() 29 | 30 | var keyboardAccessoriesView : UIView? 31 | var keyboardHeight : CGFloat = 0.0 32 | 33 | override var prefersPointerLocked: Bool { 34 | return true 35 | } 36 | 37 | override var prefersHomeIndicatorAutoHidden : Bool { 38 | return true 39 | } 40 | 41 | init() { 42 | super.init(nibName: nil, bundle: nil) 43 | 44 | self.glkView = ParsecGLKViewController(viewController: self, updateImage: updateImage) 45 | 46 | self.gamePadController = GamepadController(viewController: self) 47 | self.touchController = TouchController(viewController: self) 48 | } 49 | 50 | required init?(coder: NSCoder) { 51 | fatalError("init(coder:) has not been implemented") 52 | } 53 | 54 | func updateImage() { 55 | if CParsec.mouseInfo.cursorImg != nil && !CParsec.mouseInfo.cursorHidden { 56 | if lastImg != CParsec.mouseInfo.cursorImg{ 57 | u!.image = UIImage(cgImage: CParsec.mouseInfo.cursorImg!) 58 | lastImg = CParsec.mouseInfo.cursorImg! 59 | } 60 | 61 | u?.frame = CGRect(x: Int(CParsec.mouseInfo.mouseX) - Int(Float(CParsec.mouseInfo.cursorHotX) * SettingsHandler.cursorScale), 62 | y: Int(CParsec.mouseInfo.mouseY) - Int(Float(CParsec.mouseInfo.cursorHotY) * SettingsHandler.cursorScale), 63 | width: Int(Float(CParsec.mouseInfo.cursorWidth) * SettingsHandler.cursorScale), 64 | height: Int(Float(CParsec.mouseInfo.cursorHeight) * SettingsHandler.cursorScale)) 65 | 66 | } else { 67 | u?.image = nil 68 | } 69 | } 70 | 71 | override func viewDidLoad() { 72 | glkView.viewDidLoad() 73 | touchController.viewDidLoad() 74 | gamePadController.viewDidLoad() 75 | 76 | u = UIImageView(frame: CGRect(x: 0,y: 0,width: 100, height: 100)) 77 | view.addSubview(u!) 78 | 79 | becomeFirstResponder() 80 | setNeedsUpdateOfPrefersPointerLocked() 81 | 82 | let pointerInteraction = UIPointerInteraction(delegate: self) 83 | view.addInteraction(pointerInteraction) 84 | 85 | view.isMultipleTouchEnabled = true 86 | view.isUserInteractionEnabled = true 87 | 88 | let panGestureRecognizer = UIPanGestureRecognizer(target:self, action:#selector(self.handlePanGesture(_:))) 89 | panGestureRecognizer.delegate = self 90 | view.addGestureRecognizer(panGestureRecognizer) 91 | 92 | 93 | 94 | // Add tap gesture recognizer for single-finger touch 95 | let singleFingerTapGestureRecognizer = UITapGestureRecognizer(target:self, action:#selector(handleSingleFingerTap(_:))) 96 | singleFingerTapGestureRecognizer.numberOfTouchesRequired = 1 97 | singleFingerTapGestureRecognizer.allowedTouchTypes = [0, 2] 98 | view.addGestureRecognizer(singleFingerTapGestureRecognizer) 99 | 100 | // Add tap gesture recognizer for two-finger touch 101 | let twoFingerTapGestureRecognizer = UITapGestureRecognizer(target:self, action:#selector(handleTwoFingerTap(_:))) 102 | twoFingerTapGestureRecognizer.numberOfTouchesRequired = 2 103 | twoFingerTapGestureRecognizer.allowedTouchTypes = [0] 104 | view.addGestureRecognizer(twoFingerTapGestureRecognizer) 105 | // view.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height) 106 | // view.backgroundColor = UIColor(red: 0x66, green: 0xcc, blue: 0xff, alpha: 1.0) 107 | 108 | let threeFingerTapGestureRecognizer = UITapGestureRecognizer(target:self, action:#selector(handleThreeFinderTap(_:))) 109 | threeFingerTapGestureRecognizer.numberOfTouchesRequired = 3 110 | threeFingerTapGestureRecognizer.allowedTouchTypes = [0] 111 | view.addGestureRecognizer(threeFingerTapGestureRecognizer) 112 | 113 | let longPressGestureRecognizer = UILongPressGestureRecognizer(target:self, action:#selector(handleLongPress(_:))) 114 | longPressGestureRecognizer.numberOfTouchesRequired = 1 115 | longPressGestureRecognizer.allowedTouchTypes = [0, 2] 116 | view.addGestureRecognizer(longPressGestureRecognizer) 117 | 118 | NotificationCenter.default.addObserver( 119 | self, 120 | selector: #selector(keyboardWillShow), 121 | name: UIResponder.keyboardWillShowNotification, 122 | object: nil 123 | ) 124 | 125 | NotificationCenter.default.addObserver( 126 | self, 127 | selector: #selector(keyboardWillHide), 128 | name: UIResponder.keyboardWillHideNotification, 129 | object: nil 130 | ) 131 | 132 | } 133 | 134 | override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { 135 | super.viewWillTransition(to: size, with: coordinator) 136 | 137 | let h = size.height 138 | let w = size.width 139 | 140 | self.glkView.updateSize(width: w, height: h) 141 | CParsec.setFrame(w, h, UIScreen.main.scale) 142 | } 143 | 144 | override func viewDidAppear(_ animated: Bool) { 145 | super.viewDidAppear(animated) 146 | if let parent = parent { 147 | parent.setChildForHomeIndicatorAutoHidden(self) 148 | parent.setChildViewControllerForPointerLock(self) 149 | } 150 | } 151 | 152 | override func viewWillDisappear(_ animated: Bool) { 153 | super.viewWillDisappear(animated) 154 | if let parent = parent { 155 | parent.setChildForHomeIndicatorAutoHidden(nil) 156 | parent.setChildViewControllerForPointerLock(nil) 157 | } 158 | NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil) 159 | NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil) 160 | } 161 | 162 | 163 | override func pressesBegan(_ presses: Set, with event: UIPressesEvent?) { 164 | 165 | for press in presses { 166 | CParsec.sendKeyboardMessage(event:KeyBoardKeyEvent(input: press.key, isPressBegin: true) ) 167 | } 168 | 169 | } 170 | 171 | override func pressesEnded (_ presses: Set, with event: UIPressesEvent?) { 172 | 173 | for press in presses { 174 | CParsec.sendKeyboardMessage(event:KeyBoardKeyEvent(input: press.key, isPressBegin: false) ) 175 | } 176 | 177 | } 178 | 179 | @objc func keyboardWillShow(_ notification: Notification) { 180 | if let keyboardFrame: NSValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue { 181 | let keyboardRectangle = keyboardFrame.cgRectValue 182 | keyboardHeight = keyboardRectangle.height - 50 // minus handle button height 183 | } 184 | } 185 | 186 | @objc func keyboardWillHide(_ notification: Notification) { 187 | view.frame.origin.y = 0 188 | } 189 | 190 | } 191 | 192 | extension ParsecViewController : UIGestureRecognizerDelegate { 193 | 194 | @objc func handlePanGesture(_ gestureRecognizer:UIPanGestureRecognizer) 195 | { 196 | // print("number = \(gestureRecognizer.numberOfTouches) status = \(gestureRecognizer.state.rawValue)") 197 | if gestureRecognizer.numberOfTouches == 2 { 198 | let velocity = gestureRecognizer.velocity(in: gestureRecognizer.view) 199 | 200 | if abs(velocity.y) > 2 { 201 | // Run your function when the user uses two fingers and swipes upwards 202 | CParsec.sendWheelMsg(x: 0, y: Int32(Float(velocity.y) / 20 * SettingsHandler.mouseSensitivity)) 203 | return 204 | } 205 | if SettingsHandler.cursorMode == .direct { 206 | let location = gestureRecognizer.location(in:gestureRecognizer.view) 207 | touchController.onTouch(typeOfTap: 1, location: location, state: gestureRecognizer.state) 208 | } 209 | 210 | } else if gestureRecognizer.numberOfTouches == 1 { 211 | 212 | if SettingsHandler.cursorMode == .direct { 213 | let position = gestureRecognizer.location(in: gestureRecognizer.view) 214 | CParsec.sendMousePosition(Int32(position.x), Int32(position.y)) 215 | } else { 216 | let delta = gestureRecognizer.velocity(in: gestureRecognizer.view) 217 | CParsec.sendMouseDelta(Int32(Float(delta.x) / 60 * SettingsHandler.mouseSensitivity), Int32(Float(delta.y) / 60 * SettingsHandler.mouseSensitivity)) 218 | } 219 | 220 | 221 | if gestureRecognizer.state == .began && SettingsHandler.cursorMode == .direct { 222 | let button = ParsecMouseButton.init(rawValue: 1) 223 | CParsec.sendMouseClickMessage(button, true) 224 | } 225 | 226 | } else if gestureRecognizer.numberOfTouches == 0 { 227 | if (gestureRecognizer.state == .ended || gestureRecognizer.state == .cancelled) && SettingsHandler.cursorMode == .direct { 228 | let button = ParsecMouseButton.init(rawValue: 1) 229 | CParsec.sendMouseClickMessage(button, false) 230 | } 231 | } 232 | 233 | 234 | } 235 | 236 | @objc func handleSingleFingerTap(_ gestureRecognizer:UITapGestureRecognizer) 237 | { 238 | let location = gestureRecognizer.location(in:gestureRecognizer.view) 239 | touchController.onTap(typeOfTap: 1, location: location) 240 | 241 | } 242 | 243 | @objc func handleTwoFingerTap(_ gestureRecognizer:UITapGestureRecognizer) 244 | { 245 | let location : CGPoint; 246 | switch SettingsHandler.rightClickPosition { 247 | case .firstFinger: 248 | location = gestureRecognizer.location(ofTouch: 0, in: gestureRecognizer.view) 249 | break; 250 | case .secondFinger: 251 | location = gestureRecognizer.location(ofTouch: 1, in: gestureRecognizer.view) 252 | break 253 | default: 254 | location = gestureRecognizer.location(in: gestureRecognizer.view) 255 | } 256 | 257 | touchController.onTap(typeOfTap: 3, location: location) 258 | } 259 | 260 | @objc func handleThreeFinderTap(_ gestureRecognizer:UITapGestureRecognizer) { 261 | showKeyboard() 262 | } 263 | 264 | @objc func handleLongPress(_ gestureRecognizer:UIGestureRecognizer) { 265 | if SettingsHandler.cursorMode != .touchpad { 266 | return 267 | } 268 | let button = ParsecMouseButton.init(rawValue: 1) 269 | 270 | if gestureRecognizer.state == .began{ 271 | CParsec.sendMouseClickMessage(button, true) 272 | lastLongPressPoint = gestureRecognizer.location(in: gestureRecognizer.view) 273 | } else if gestureRecognizer.state == .ended { 274 | CParsec.sendMouseClickMessage(button, false) 275 | } else if gestureRecognizer.state == .changed { 276 | let newLocation = gestureRecognizer.location(in: gestureRecognizer.view) 277 | CParsec.sendMouseDelta( 278 | Int32(Float(newLocation.x - lastLongPressPoint.x) * SettingsHandler.mouseSensitivity), 279 | Int32(Float(newLocation.y - lastLongPressPoint.y) * SettingsHandler.mouseSensitivity) 280 | ) 281 | lastLongPressPoint = newLocation 282 | } 283 | } 284 | 285 | } 286 | 287 | extension ParsecViewController : UIPointerInteractionDelegate { 288 | func pointerInteraction(_ interaction: UIPointerInteraction, styleFor region: UIPointerRegion) -> UIPointerStyle? { 289 | return UIPointerStyle.hidden() 290 | } 291 | 292 | 293 | func pointerInteraction(_ inter: UIPointerInteraction, regionFor request: UIPointerRegionRequest, defaultRegion: UIPointerRegion) -> UIPointerRegion? { 294 | let loc = request.location 295 | if let iv = view!.hitTest(loc, with: nil) { 296 | let rect = view!.convert(iv.bounds, from: iv) 297 | let region = UIPointerRegion(rect: rect, identifier: iv.tag) 298 | return region 299 | } 300 | return nil 301 | } 302 | 303 | } 304 | 305 | class KeyBoardButton : UIButton { 306 | let keyText : String 307 | let isToggleable : Bool 308 | var isOn = false 309 | 310 | required init(keyText: String, isToggleable: Bool) { 311 | self.keyText = keyText 312 | self.isToggleable = isToggleable 313 | super.init(frame: .zero) 314 | addTarget(self, action: #selector(handleTouchDown), for: .touchDown) 315 | addTarget(self, action: #selector(handleTouchUp), for: [.touchUpInside, .touchDragExit, .touchCancel]) 316 | 317 | } 318 | 319 | required init?(coder: NSCoder) { 320 | fatalError("init(coder:) has not been implemented") 321 | } 322 | 323 | // Add a press-down animation for feedback 324 | @objc private func handleTouchDown() { 325 | self.alpha = 0.5 326 | } 327 | 328 | // Restore to normal state when touch ends 329 | @objc private func handleTouchUp() { 330 | UIView.animate(withDuration: 0.2) { 331 | self.alpha = 1.0 332 | } 333 | } 334 | } 335 | 336 | // MARK: - Virtual Keyboard 337 | extension ParsecViewController : UIKeyInput, UITextInputTraits { 338 | var hasText: Bool { 339 | return true 340 | } 341 | 342 | var keyboardType: UIKeyboardType { 343 | get { 344 | return .asciiCapable 345 | } 346 | set { 347 | 348 | } 349 | } 350 | 351 | override var canBecomeFirstResponder: Bool { 352 | return true 353 | } 354 | 355 | func insertText(_ text: String) { 356 | CParsec.sendVirtualKeyboardInput(text: text) 357 | } 358 | 359 | func deleteBackward() { 360 | CParsec.sendVirtualKeyboardInput(text: "BACKSPACE") 361 | } 362 | 363 | // copied from moonlight https://github.com/moonlight-stream/moonlight-ios/blob/022352c1667788d8626b659d984a290aa5c25e17/Limelight/Input/StreamView.m#L393 364 | override var inputAccessoryView: UIView? { 365 | 366 | if let keyboardAccessoriesView { 367 | return keyboardAccessoriesView 368 | } 369 | let containerView = UIStackView(frame: CGRect(x: 0, y: 0, width: CGFloat.infinity, height: 94)) 370 | containerView.translatesAutoresizingMaskIntoConstraints = false 371 | 372 | let customToolbarView = UIToolbar(frame: CGRect(x: 0, y: 50, width: self.view.bounds.size.width, height: 44)) 373 | customToolbarView.translatesAutoresizingMaskIntoConstraints = false 374 | 375 | let scrollView = UIScrollView() 376 | scrollView.showsHorizontalScrollIndicator = false 377 | scrollView.translatesAutoresizingMaskIntoConstraints = false 378 | 379 | let buttonStackView = UIStackView() 380 | buttonStackView.axis = .horizontal 381 | buttonStackView.distribution = .equalSpacing 382 | buttonStackView.alignment = .center 383 | buttonStackView.spacing = 8 384 | buttonStackView.translatesAutoresizingMaskIntoConstraints = false 385 | 386 | let windowsBarButton = createKeyboardButton(displayText: "⌘", keyText: "LGUI", isToggleable: true) 387 | let tabBarButton = createKeyboardButton(displayText: "⇥", keyText: "TAB", isToggleable: false) 388 | let shiftBarButton = createKeyboardButton(displayText: "⇧", keyText: "SHIFT", isToggleable: true) 389 | let escapeBarButton = createKeyboardButton(displayText: "⎋", keyText: "UIKeyInputEscape", isToggleable: false) 390 | let controlBarButton = createKeyboardButton(displayText: "⌃", keyText: "CONTROL", isToggleable: true) 391 | let altBarButton = createKeyboardButton(displayText: "⌥", keyText: "LALT", isToggleable: true) 392 | let deleteBarButton = createKeyboardButton(displayText: "Del", keyText: "DELETE", isToggleable: false) 393 | let f1Button = createKeyboardButton(displayText: "F1", keyText: "F1", isToggleable: false) 394 | let f2Button = createKeyboardButton(displayText: "F2", keyText: "F2", isToggleable: false) 395 | let f3Button = createKeyboardButton(displayText: "F3", keyText: "F3", isToggleable: false) 396 | let f4Button = createKeyboardButton(displayText: "F4", keyText: "F4", isToggleable: false) 397 | let f5Button = createKeyboardButton(displayText: "F5", keyText: "F5", isToggleable: false) 398 | let f6Button = createKeyboardButton(displayText: "F6", keyText: "F6", isToggleable: false) 399 | let f7Button = createKeyboardButton(displayText: "F7", keyText: "F7", isToggleable: false) 400 | let f8Button = createKeyboardButton(displayText: "F8", keyText: "F8", isToggleable: false) 401 | let f9Button = createKeyboardButton(displayText: "F9", keyText: "F9", isToggleable: false) 402 | let f10Button = createKeyboardButton(displayText: "F10", keyText: "F10", isToggleable: false) 403 | let f11Button = createKeyboardButton(displayText: "F11", keyText: "F11", isToggleable: false) 404 | let f12Button = createKeyboardButton(displayText: "F12", keyText: "F12", isToggleable: false) 405 | let upButton = createKeyboardButton(displayText: "↑", keyText: "UP", isToggleable: false) 406 | let downButton = createKeyboardButton(displayText: "↓", keyText: "DOWN", isToggleable: false) 407 | let leftButton = createKeyboardButton(displayText: "←", keyText: "LEFT", isToggleable: false) 408 | let rightButton = createKeyboardButton(displayText: "→", keyText: "RIGHT", isToggleable: false) 409 | 410 | 411 | let buttons = [windowsBarButton, escapeBarButton, tabBarButton, shiftBarButton, controlBarButton, altBarButton, deleteBarButton, 412 | f1Button, f2Button, f3Button, f4Button, f5Button, f6Button, f7Button, f8Button, f9Button, f10Button, f11Button, f12Button, 413 | upButton, downButton, leftButton, rightButton 414 | ] 415 | 416 | for button in buttons { 417 | buttonStackView.addArrangedSubview(button) 418 | } 419 | 420 | scrollView.addSubview(buttonStackView) 421 | 422 | 423 | 424 | let scrollViewContainer = UIView() 425 | scrollViewContainer.translatesAutoresizingMaskIntoConstraints = false 426 | scrollViewContainer.addSubview(scrollView) 427 | 428 | 429 | NSLayoutConstraint.activate([ 430 | scrollView.leadingAnchor.constraint(equalTo: scrollViewContainer.leadingAnchor), 431 | scrollView.trailingAnchor.constraint(equalTo: scrollViewContainer.trailingAnchor), 432 | scrollView.topAnchor.constraint(equalTo: scrollViewContainer.topAnchor), 433 | scrollView.bottomAnchor.constraint(equalTo: scrollViewContainer.bottomAnchor) 434 | ]) 435 | 436 | NSLayoutConstraint.activate([ 437 | scrollViewContainer.heightAnchor.constraint(equalToConstant: 44), 438 | scrollViewContainer.widthAnchor.constraint(greaterThanOrEqualToConstant: 100) 439 | ]) 440 | 441 | // 10. Set constraints for the stack view inside the scroll view 442 | NSLayoutConstraint.activate([ 443 | buttonStackView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), 444 | buttonStackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), 445 | buttonStackView.topAnchor.constraint(equalTo: scrollView.topAnchor), 446 | buttonStackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), 447 | buttonStackView.heightAnchor.constraint(equalTo: scrollView.heightAnchor) 448 | ]) 449 | 450 | 451 | let container2 = UIStackView() 452 | container2.axis = .horizontal 453 | container2.distribution = .fill 454 | container2.alignment = .center 455 | container2.addArrangedSubview(scrollViewContainer) 456 | 457 | let doneButton2 = UIButton() 458 | doneButton2.setTitle("Done", for: .normal) 459 | doneButton2.addTarget(self, action: #selector(doneTapped), for: .touchUpInside) 460 | if #available(iOS 15.0, *) { 461 | doneButton2.setTitleColor(.tintColor, for: .normal) 462 | } 463 | container2.addArrangedSubview(doneButton2) 464 | 465 | let scrollViewBarButton = UIBarButtonItem(customView: container2) 466 | 467 | customToolbarView.setItems([scrollViewBarButton], animated: false) 468 | 469 | 470 | // Create a draggable handle button 471 | let handleButton = UIButton(type: .system) 472 | handleButton.setTitle("↑↓", for: .normal) 473 | handleButton.backgroundColor = UIColor.systemGray.withAlphaComponent(0.5) 474 | handleButton.translatesAutoresizingMaskIntoConstraints = false 475 | 476 | let panGestureRecognizer = UIPanGestureRecognizer(target:self, action:#selector(self.handleDragGesture(_:))) 477 | panGestureRecognizer.maximumNumberOfTouches = 1 478 | handleButton.addGestureRecognizer(panGestureRecognizer) 479 | 480 | handleButton.layer.cornerRadius = 20 481 | containerView.addSubview(handleButton) 482 | 483 | containerView.addSubview(customToolbarView) 484 | 485 | NSLayoutConstraint.activate([ 486 | customToolbarView.widthAnchor.constraint(equalTo: containerView.widthAnchor), 487 | customToolbarView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor), 488 | customToolbarView.heightAnchor.constraint(equalToConstant: 44) 489 | ]) 490 | 491 | NSLayoutConstraint.activate([ 492 | handleButton.centerXAnchor.constraint(equalTo: containerView.centerXAnchor), 493 | handleButton.topAnchor.constraint(equalTo: containerView.topAnchor), 494 | handleButton.widthAnchor.constraint(equalToConstant: 40), 495 | handleButton.heightAnchor.constraint(equalToConstant: 40) 496 | ]) 497 | 498 | NSLayoutConstraint.activate([ 499 | containerView.heightAnchor.constraint(equalToConstant: 94), 500 | containerView.widthAnchor.constraint(greaterThanOrEqualToConstant: 200) 501 | ]) 502 | 503 | keyboardAccessoriesView = containerView 504 | return containerView 505 | } 506 | 507 | func createKeyboardButton(displayText: String, keyText: String, isToggleable: Bool) -> UIButton { 508 | let button = KeyBoardButton(keyText: keyText, isToggleable: isToggleable) 509 | 510 | // Set the image and button properties 511 | button.setTitle(displayText, for: .normal) 512 | button.titleLabel?.font = UIFont(name: "System", size: 10.0) 513 | button.frame = CGRect(x: 0, y: 0, width: 36, height: 36) 514 | button.titleLabel?.frame = CGRect(x: 0, y: 0, width: 30, height: 30) 515 | if let label = button.titleLabel { 516 | label.textAlignment = .center 517 | } 518 | button.backgroundColor = .black 519 | button.layer.cornerRadius = 3.0 520 | 521 | button.titleLabel?.contentMode = .scaleAspectFit 522 | 523 | // Set target and action for button 524 | button.addTarget(target, action: #selector(toolbarButtonClicked(_:)), for: .touchUpInside) 525 | 526 | return button 527 | } 528 | 529 | @objc func toolbarButtonClicked(_ sender: KeyBoardButton) { 530 | let isToggleable = sender.isToggleable 531 | var isOn = sender.isOn 532 | 533 | if isToggleable { 534 | isOn.toggle() 535 | if isOn { 536 | sender.backgroundColor = .lightGray 537 | } else { 538 | sender.backgroundColor = .black 539 | } 540 | } 541 | 542 | sender.isOn = isOn 543 | let keyText = sender.keyText 544 | 545 | 546 | if isToggleable { 547 | if isOn { 548 | CParsec.sendVirtualKeyboardInput(text: keyText, isOn: true) 549 | } else { 550 | CParsec.sendVirtualKeyboardInput(text: keyText, isOn: false) 551 | } 552 | } else { 553 | CParsec.sendVirtualKeyboardInput(text: keyText) 554 | } 555 | 556 | } 557 | 558 | @objc func handleDragGesture(_ gestureRecognizer:UIPanGestureRecognizer) { 559 | let v = view.frame.origin.y + gestureRecognizer.velocity(in: nil).y / 50.0 560 | let newY = ParsecSDKBridge.clamp(v, minValue: -keyboardHeight, maxValue: 0) 561 | view.frame.origin.y = newY 562 | } 563 | 564 | @objc func doneTapped() { 565 | // Resign first responder to dismiss the keyboard 566 | resignFirstResponder() 567 | } 568 | 569 | @objc func showKeyboard() { 570 | becomeFirstResponder() 571 | } 572 | 573 | } 574 | -------------------------------------------------------------------------------- /OpenParsec/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /OpenParsec/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import SwiftUI 3 | 4 | class SceneDelegate:UIResponder, UIWindowSceneDelegate 5 | { 6 | var window:UIWindow? 7 | 8 | func scene(_ scene:UIScene, willConnectTo session:UISceneSession, options connectionOptions:UIScene.ConnectionOptions) 9 | { 10 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 11 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 12 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 13 | 14 | // Create the SwiftUI view that provides the window contents. 15 | let contentView = ContentView() 16 | 17 | // Use a UIHostingController as window root view controller. 18 | if let windowScene = scene as? UIWindowScene 19 | { 20 | let window = UIWindow(windowScene: windowScene) 21 | window.rootViewController = UIHostingController(rootView: contentView) 22 | self.window = window 23 | window.makeKeyAndVisible() 24 | } 25 | } 26 | 27 | func sceneDidDisconnect(_ scene:UIScene) 28 | { 29 | // Called as the scene is being released by the system. 30 | // This occurs shortly after the scene enters the background, or when its session is discarded. 31 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 32 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). 33 | } 34 | 35 | func sceneDidBecomeActive(_ scene:UIScene) 36 | { 37 | // Called when the scene has moved from an inactive state to an active state. 38 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 39 | } 40 | 41 | func sceneWillResignActive(_ scene:UIScene) 42 | { 43 | // Called when the scene will move from an active state to an inactive state. 44 | // This may occur due to temporary interruptions (ex. an incoming phone call). 45 | } 46 | 47 | func sceneWillEnterForeground(_ scene:UIScene) 48 | { 49 | // Called as the scene transitions from the background to the foreground. 50 | // Use this method to undo the changes made on entering the background. 51 | } 52 | 53 | func sceneDidEnterBackground(_ scene:UIScene) 54 | { 55 | // Called as the scene transitions from the foreground to the background. 56 | // Use this method to save data, release shared resources, and store enough scene-specific state information 57 | // to restore the scene back to its current state. 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /OpenParsec/SettingsHandler.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct SettingsHandler 4 | { 5 | //public static var renderer:RendererType = .opengl 6 | public static var resolution : ParsecResolution = ParsecResolution.resolutions[1] 7 | public static var decoder:DecoderPref = .h264 8 | public static var cursorMode:CursorMode = .touchpad 9 | public static var cursorScale:Float = 0.5 10 | public static var mouseSensitivity : Float = 1.0 11 | public static var noOverlay:Bool = false 12 | public static var hideStatusBar:Bool = true 13 | public static var rightClickPosition:RightClickPosition = .firstFinger 14 | 15 | public static func load() 16 | { 17 | //if UserDefaults.standard.exists(forKey:"renderer") 18 | // { renderer = RendererType(rawValue:UserDefaults.standard.integer(forKey:"renderer"))! } 19 | if UserDefaults.standard.exists(forKey:"decoder") 20 | { decoder = DecoderPref(rawValue:UserDefaults.standard.integer(forKey:"decoder"))! } 21 | if UserDefaults.standard.exists(forKey:"cursorMode") 22 | { cursorMode = CursorMode(rawValue:UserDefaults.standard.integer(forKey:"cursorMode"))! } 23 | if UserDefaults.standard.exists(forKey:"rightClickPosition") 24 | { rightClickPosition = RightClickPosition(rawValue:UserDefaults.standard.integer(forKey:"rightClickPosition"))! } 25 | if UserDefaults.standard.exists(forKey:"cursorScale") 26 | { cursorScale = UserDefaults.standard.float(forKey:"cursorScale") } 27 | if UserDefaults.standard.exists(forKey:"mouseSensitivity") 28 | { mouseSensitivity = UserDefaults.standard.float(forKey:"mouseSensitivity") } 29 | if UserDefaults.standard.exists(forKey:"noOverlay") 30 | { noOverlay = UserDefaults.standard.bool(forKey:"noOverlay") } 31 | if UserDefaults.standard.exists(forKey:"hideStatusBar") 32 | { hideStatusBar = UserDefaults.standard.bool(forKey:"hideStatusBar") } 33 | 34 | if UserDefaults.standard.exists(forKey:"resolution") { 35 | for res in ParsecResolution.resolutions { 36 | if res.desc == UserDefaults.standard.string(forKey: "resolution") { 37 | resolution = res 38 | break 39 | } 40 | } 41 | } 42 | } 43 | 44 | public static func save() 45 | { 46 | //UserDefaults.standard.set(renderer.rawValue, forKey:"renderer") 47 | UserDefaults.standard.set(decoder.rawValue, forKey:"decoder") 48 | UserDefaults.standard.set(cursorMode.rawValue, forKey:"cursorMode") 49 | UserDefaults.standard.set(rightClickPosition.rawValue, forKey:"rightClickPosition") 50 | UserDefaults.standard.set(cursorScale, forKey:"cursorScale") 51 | UserDefaults.standard.set(mouseSensitivity, forKey: "mouseSensitivity") 52 | UserDefaults.standard.set(noOverlay, forKey:"noOverlay") 53 | UserDefaults.standard.set(resolution.desc, forKey:"resolution") 54 | UserDefaults.standard.set(hideStatusBar, forKey: "hideStatusBar") 55 | } 56 | } 57 | 58 | extension UserDefaults 59 | { 60 | /** 61 | * Checks if a specified key exists within this UserDefaults. 62 | */ 63 | func exists(forKey:String) -> Bool 64 | { 65 | return object(forKey:forKey) != nil 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /OpenParsec/SettingsView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct SettingsView:View 4 | { 5 | @Binding var visible:Bool 6 | 7 | //@State var renderer:RendererType = SettingsHandler.renderer 8 | @State var decoder:DecoderPref = SettingsHandler.decoder 9 | @State var cursorMode:CursorMode = SettingsHandler.cursorMode 10 | @State var rightClickPosition:RightClickPosition = SettingsHandler.rightClickPosition 11 | @State var resolution : ParsecResolution = SettingsHandler.resolution 12 | @State var cursorScale:Float = SettingsHandler.cursorScale 13 | @State var mouseSensitivity:Float = SettingsHandler.mouseSensitivity 14 | @State var noOverlay:Bool = SettingsHandler.noOverlay 15 | @State var hideStatusBar:Bool = SettingsHandler.hideStatusBar 16 | 17 | let resolutionChoices : [Choice] 18 | 19 | init(visible: Binding) { 20 | _visible = visible 21 | var tmp : [Choice] = [] 22 | for res in ParsecResolution.resolutions { 23 | tmp.append(Choice(res.desc, res)) 24 | } 25 | resolutionChoices = tmp 26 | } 27 | 28 | var body:some View 29 | { 30 | ZStack() 31 | { 32 | if (visible) 33 | { 34 | // Background 35 | Rectangle() 36 | .fill(Color.init(red:0, green:0, blue:0, opacity:0.67)) 37 | .edgesIgnoringSafeArea(.all) 38 | } 39 | } 40 | .animation(.linear(duration:0.24)) 41 | 42 | ZStack() 43 | { 44 | if (visible) 45 | { 46 | // Main controls 47 | VStack() 48 | { 49 | // Navigation controls 50 | ZStack() 51 | { 52 | Rectangle() 53 | .fill(Color("BackgroundTab")) 54 | .frame(height:52) 55 | .shadow(color:Color("Shading"), radius:4, y:6) 56 | ZStack() 57 | { 58 | HStack() 59 | { 60 | Button(action:saveAndExit, label:{ Image(systemName:"xmark").scaleEffect(x:-1) }) 61 | .padding() 62 | Spacer() 63 | } 64 | Text("Settings") 65 | .multilineTextAlignment(.center) 66 | .foregroundColor(Color("Foreground")) 67 | .font(.system(size:20, weight:.medium)) 68 | Spacer() 69 | } 70 | .foregroundColor(Color("AccentColor")) 71 | } 72 | .zIndex(1) 73 | 74 | ScrollView() 75 | { 76 | CatTitle("Interactivity") 77 | CatList() 78 | { 79 | CatItem("Mouse Movement") 80 | { 81 | MultiPicker(selection:$cursorMode, options: 82 | [ 83 | Choice("Touchpad", CursorMode.touchpad), 84 | Choice("Direct", CursorMode.direct) 85 | ]) 86 | } 87 | CatItem("Right Click Position") 88 | { 89 | MultiPicker(selection:$rightClickPosition, options: 90 | [ 91 | Choice("First Finger", RightClickPosition.firstFinger), 92 | Choice("Middle", RightClickPosition.middle), 93 | Choice("Second Finger", RightClickPosition.secondFinger) 94 | ]) 95 | } 96 | CatItem("Cursor Scale") 97 | { 98 | Slider(value: $cursorScale, in:0.1...4, step:0.1) 99 | .frame(width: 200) 100 | Text(String(format: "%.1f", cursorScale)) 101 | } 102 | CatItem("Mouse Sensitivity") 103 | { 104 | Slider(value: $mouseSensitivity, in:0.1...4, step:0.1) 105 | .frame(width: 200) 106 | Text(String(format: "%.1f", mouseSensitivity)) 107 | } 108 | } 109 | CatTitle("Graphics") 110 | CatList() 111 | { 112 | /*CatItem("Renderer") 113 | { 114 | SegmentPicker(selection:$renderer, options: 115 | [ 116 | Choice("OpenGL", RendererType.opengl), 117 | Choice("Metal", RendererType.metal) 118 | ]) 119 | .frame(width:165) 120 | }*/ 121 | CatItem("Default Resolution") 122 | { 123 | MultiPicker(selection:$resolution, options:resolutionChoices) 124 | } 125 | CatItem("Decoder") 126 | { 127 | MultiPicker(selection:$decoder, options: 128 | [ 129 | Choice("H.264", DecoderPref.h264), 130 | Choice("Prefer H.265", DecoderPref.h265) 131 | ]) 132 | } 133 | } 134 | CatTitle("Misc") 135 | CatList() 136 | { 137 | CatItem("Never Show Overlay") 138 | { 139 | Toggle("", isOn:$noOverlay) 140 | .frame(width:80) 141 | } 142 | CatItem("Hide Status Bar") 143 | { 144 | Toggle("", isOn:$hideStatusBar) 145 | .frame(width:80) 146 | } 147 | } 148 | Text("More options coming soon.") 149 | .multilineTextAlignment(.center) 150 | .opacity(0.5) 151 | .padding() 152 | } 153 | .foregroundColor(Color("Foreground")) 154 | } 155 | .background(Rectangle().fill(Color("BackgroundGray"))) 156 | .cornerRadius(8) 157 | .padding() 158 | .animation(.none) 159 | } 160 | } 161 | .preferredColorScheme(appScheme) 162 | .scaleEffect(visible ? 1 : 0, anchor:.zero) 163 | .animation(.easeInOut(duration:0.24)) 164 | } 165 | 166 | func saveAndExit() 167 | { 168 | //SettingsHandler.renderer = renderer 169 | SettingsHandler.decoder = decoder 170 | SettingsHandler.resolution = resolution 171 | SettingsHandler.cursorMode = cursorMode 172 | SettingsHandler.cursorScale = cursorScale 173 | SettingsHandler.rightClickPosition = rightClickPosition 174 | SettingsHandler.noOverlay = noOverlay 175 | SettingsHandler.hideStatusBar = hideStatusBar 176 | SettingsHandler.mouseSensitivity = mouseSensitivity 177 | SettingsHandler.save() 178 | 179 | visible = false 180 | } 181 | } 182 | 183 | struct SettingsView_Previews:PreviewProvider 184 | { 185 | @State static var value:Bool = true 186 | 187 | static var previews:some View 188 | { 189 | SettingsView(visible:$value) 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /OpenParsec/Shared.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftUI 3 | 4 | var appScheme:ColorScheme = .dark 5 | 6 | struct GLBData 7 | { 8 | let SessionKeyChainKey = "OPStoredAuthData" 9 | } 10 | 11 | class GLBDataModel 12 | { 13 | static let shared = GLBData() 14 | } 15 | 16 | extension String 17 | { 18 | static func fromBuffer(_ ptr:UnsafeMutablePointer, length len:Int) -> String 19 | { 20 | // convert C char bytes using the UTF8 encoding 21 | let nsstr = NSString(bytes:ptr, length:len, encoding:NSUTF8StringEncoding) 22 | return nsstr! as String 23 | } 24 | } 25 | 26 | class CursorPositionHelper { 27 | static func toHost(_ xp: Int, _ yp : Int) -> (Int, Int) { 28 | let xh = CParsec.hostWidth 29 | let yh = CParsec.hostHeight 30 | let xc = CParsec.clientWidth 31 | let yc = CParsec.clientHeight 32 | 33 | let tc = yc / xc 34 | let th = yh / xh 35 | 36 | var xa: Float 37 | var ya: Float 38 | if th < tc { 39 | xa = Float(xp) * xh / xc 40 | ya = (Float(yp) - 0.5 * (yc - xc*th)) * xh / xc 41 | } else { 42 | ya = Float(yp) * yh / yc 43 | xa = (Float(xp) - 0.5 * (xc - yc/th)) * yh / yc 44 | } 45 | 46 | return (Int(ParsecSDKBridge.clamp(xa, minValue: 0, maxValue: CParsec.hostWidth)), Int(ParsecSDKBridge.clamp(ya,minValue: 0,maxValue: CParsec.hostHeight))) 47 | } 48 | 49 | static func toClient(_ xa: Int, _ ya : Int) -> (Int, Int) { 50 | let xh = CParsec.hostWidth 51 | let yh = CParsec.hostHeight 52 | let xc = CParsec.clientWidth 53 | let yc = CParsec.clientHeight 54 | 55 | let tc = yc / xc 56 | let th = yh / xh 57 | 58 | var xp: Float 59 | var yp: Float 60 | if th < tc { 61 | xp = Float(xa) * xc / xh 62 | yp = Float(ya) * xc / xh + 0.5 * (yc - xc*th) 63 | } else { 64 | yp = Float(ya) * yc / yh 65 | xp = Float(xa) * yc / yh + 0.5 * (xc - yc/th) 66 | } 67 | 68 | return (Int(ParsecSDKBridge.clamp(xp,minValue: 0, maxValue: CParsec.clientWidth)), Int(ParsecSDKBridge.clamp(yp, minValue: 0, maxValue: CParsec.clientHeight))) 69 | } 70 | } 71 | 72 | class SharedModel: ObservableObject { 73 | @Published var resolutionX = 0 74 | @Published var resolutionY = 0 75 | @Published var bitrate = 0 76 | @Published var constantFps = false 77 | @Published var output = "none" 78 | @Published var displayConfigs : [ParsecDisplayConfig] = [] 79 | 80 | } 81 | 82 | class DataManager { 83 | static let model = SharedModel() 84 | } 85 | -------------------------------------------------------------------------------- /OpenParsec/TouchHandlingView.swift: -------------------------------------------------------------------------------- 1 | import ParsecSDK 2 | import UIKit 3 | 4 | 5 | class TouchController 6 | { 7 | let viewController: UIViewController 8 | init(viewController: UIViewController) { 9 | self.viewController = viewController 10 | } 11 | 12 | func onTouch(typeOfTap:Int, location:CGPoint, state:UIGestureRecognizer.State) 13 | { 14 | let x = Int32(location.x) 15 | let y = Int32(location.y) 16 | 17 | // Send the mouse input to the host 18 | let parsecTap = ParsecMouseButton(rawValue:UInt32(typeOfTap)) 19 | switch state 20 | { 21 | case .began: 22 | CParsec.sendMouseMessage(parsecTap, x, y, true) 23 | case .changed: 24 | CParsec.sendMousePosition(x, y) 25 | case .ended, .cancelled: 26 | CParsec.sendMouseMessage(parsecTap, x, y, false) 27 | default: 28 | break 29 | } 30 | } 31 | 32 | func onTap(typeOfTap:Int, location:CGPoint) 33 | { 34 | let parsecTap = ParsecMouseButton(rawValue:UInt32(typeOfTap)) 35 | if SettingsHandler.cursorMode == .direct { 36 | let x = Int32(location.x) 37 | let y = Int32(location.y) 38 | 39 | // Send the mouse input to the host 40 | 41 | CParsec.sendMouseMessage(parsecTap, x, y, true) 42 | CParsec.sendMouseMessage(parsecTap, x, y, false) 43 | } else { 44 | CParsec.sendMouseClickMessage(parsecTap, true) 45 | CParsec.sendMouseClickMessage(parsecTap, false) 46 | } 47 | 48 | } 49 | 50 | public func viewDidLoad() 51 | { 52 | 53 | 54 | 55 | } 56 | 57 | 58 | 59 | 60 | } 61 | -------------------------------------------------------------------------------- /OpenParsec/UIViewControllerWrapper.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import UIKit 3 | 4 | struct UIViewControllerWrapper:UIViewControllerRepresentable 5 | { 6 | typealias UIViewControllerType = Wrapped 7 | 8 | let wrappedController:Wrapped 9 | 10 | init(_ wrappedController:Wrapped) 11 | { 12 | self.wrappedController = wrappedController 13 | } 14 | 15 | func makeUIViewController(context:Context) -> Wrapped 16 | { 17 | return wrappedController 18 | } 19 | 20 | func updateUIViewController(_ uiViewController:Wrapped, context:Context) 21 | { 22 | // No-op 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /OpenParsec/URLImage.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct URLImage:View 4 | { 5 | let url:URL? 6 | let output:(Image) -> RemoteImage 7 | let placeholder:() -> Placeholder 8 | 9 | @State private var _remoteData:UIImage? = nil 10 | 11 | var body:some View 12 | { 13 | if let img = _remoteData 14 | { 15 | output(Image(uiImage:img)) 16 | } 17 | else 18 | { 19 | placeholder() 20 | .onAppear 21 | { 22 | var request = URLRequest(url:url!) 23 | request.httpMethod = "GET" 24 | request.setValue("image/jpeg", forHTTPHeaderField:"Content-Type") 25 | 26 | let task = URLSession.shared.dataTask(with:request) 27 | { (data, response, error) in 28 | DispatchQueue.main.async 29 | { 30 | if let data = data, let uiImage = UIImage(data:data) 31 | { 32 | _remoteData = uiImage 33 | } 34 | } 35 | } 36 | task.resume() 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /OpenParsec/ViewContainerPatch.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewContainerPatch.swift 3 | // OpenParsec 4 | // 5 | // Created by s s on 2024/5/12. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | 12 | // from UTM's https://github.com/utmapp/UTM/blob/b03486b8825d5a0e8b9f93162a49a4c98ebab6a1/Platform/iOS/UTMPatches.swift#L33 13 | final class UTMViewControllerPatches { 14 | static private var isPatched: Bool = false 15 | 16 | /// Installs the patches 17 | /// TODO: Some thread safety/race issues etc 18 | static func patchAll() { 19 | UIViewController.patchViewController() 20 | } 21 | } 22 | 23 | fileprivate extension NSObject { 24 | static func patch(_ original: Selector, with swizzle: Selector, class cls: AnyClass?) { 25 | let originalMethod = class_getInstanceMethod(cls, original)! 26 | let swizzleMethod = class_getInstanceMethod(cls, swizzle)! 27 | method_exchangeImplementations(originalMethod, swizzleMethod) 28 | } 29 | } 30 | 31 | /// We need to set these when the VM starts running since there is no way to do it from SwiftUI right now 32 | extension UIViewController { 33 | private static var _childForHomeIndicatorAutoHiddenStorage: [UIViewController: UIViewController] = [:] 34 | 35 | @objc private dynamic var _childForHomeIndicatorAutoHidden: UIViewController? { 36 | Self._childForHomeIndicatorAutoHiddenStorage[self] 37 | } 38 | 39 | @objc dynamic func setChildForHomeIndicatorAutoHidden(_ value: UIViewController?) { 40 | if let value = value { 41 | Self._childForHomeIndicatorAutoHiddenStorage[self] = value 42 | } else { 43 | Self._childForHomeIndicatorAutoHiddenStorage.removeValue(forKey: self) 44 | } 45 | setNeedsUpdateOfHomeIndicatorAutoHidden() 46 | } 47 | 48 | private static var _childViewControllerForPointerLockStorage: [UIViewController: UIViewController] = [:] 49 | 50 | @objc private dynamic var _childViewControllerForPointerLock: UIViewController? { 51 | Self._childViewControllerForPointerLockStorage[self] 52 | } 53 | 54 | @objc dynamic func setChildViewControllerForPointerLock(_ value: UIViewController?) { 55 | if let value = value { 56 | Self._childViewControllerForPointerLockStorage[self] = value 57 | } else { 58 | Self._childViewControllerForPointerLockStorage.removeValue(forKey: self) 59 | } 60 | setNeedsUpdateOfPrefersPointerLocked() 61 | } 62 | 63 | /// SwiftUI currently does not provide a way to set the View Conrtoller's home indicator or pointer lock 64 | fileprivate static func patchViewController() { 65 | patch(#selector(getter: Self.childForHomeIndicatorAutoHidden), 66 | with: #selector(getter: Self._childForHomeIndicatorAutoHidden), 67 | class: Self.self) 68 | patch(#selector(getter: Self.childViewControllerForPointerLock), 69 | with: #selector(getter: Self._childViewControllerForPointerLock), 70 | class: Self.self) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /OpenParsec/audio.c: -------------------------------------------------------------------------------- 1 | #include "audio.h" 2 | 3 | #include 4 | 5 | #include 6 | 7 | #define NUM_AUDIO_BUF 16 8 | #define BUFFER_SIZE 4096 9 | #define SILENT_SIZE 4096 10 | #define FAKE_SIZE 0 11 | #define ALLOW_DELAY 8 12 | #define LOWEST_NUM_BUFFER 3 13 | bool isMuted = false; 14 | bool isStart = false; 15 | int lastbuf = 0; 16 | 17 | unsigned int silence_inqueue = 0; 18 | unsigned int silence_outqueue = 0; 19 | 20 | AudioQueueBufferRef silence_buf; 21 | typedef struct RecycleChain { 22 | AudioQueueBufferRef *curt; 23 | struct RecycleChain *next; 24 | }RecycleChain; 25 | 26 | typedef struct RecycleChainMgr { 27 | RecycleChain *rc; 28 | RecycleChain *first; 29 | RecycleChain *last_to_queue; 30 | //AudioQueueBufferRef *last_use; 31 | }RecycleChainMgr; 32 | 33 | struct audio { 34 | AudioQueueRef q; 35 | AudioQueueBufferRef audio_buf[NUM_AUDIO_BUF]; 36 | char *mem[NUM_AUDIO_BUF * 2]; 37 | int loc[NUM_AUDIO_BUF]; 38 | RecycleChainMgr rcm; 39 | int32_t fail_num; 40 | int32_t in_use; 41 | }; 42 | 43 | static void audio_queue_callback(void *opaque, AudioQueueRef queue, AudioQueueBufferRef buffer) 44 | { 45 | struct audio *ctx = (struct audio *) opaque; 46 | int deltaBuf = 0; 47 | //int silence_use_count = (int)(silence_buf->mUserData); 48 | 49 | if (ctx == NULL) 50 | return; 51 | 52 | if (ctx->in_use > 0) 53 | { 54 | ctx->in_use -= buffer->mAudioDataByteSize; 55 | } 56 | 57 | if(buffer != silence_buf) 58 | { 59 | buffer->mAudioDataByteSize = FAKE_SIZE; 60 | lastbuf = *((int *)(buffer->mUserData)); 61 | } 62 | else 63 | { 64 | //silence_use_count = 0; 65 | //silence_buf->mUserData = (void *)(0); 66 | ++silence_outqueue; 67 | } 68 | 69 | if (isMuted) return; 70 | 71 | deltaBuf = *((int *)((*ctx->rcm.first->curt)->mUserData)); 72 | deltaBuf = deltaBuf - lastbuf - 1; 73 | if (deltaBuf < 0) deltaBuf += NUM_AUDIO_BUF; 74 | 75 | while(ctx->rcm.last_to_queue->next != ctx->rcm.first) 76 | { 77 | AudioQueueEnqueueBuffer(ctx->q, (*(ctx->rcm.last_to_queue->next->curt)), 0, NULL); 78 | ctx->rcm.last_to_queue = ctx->rcm.last_to_queue->next; 79 | } 80 | 81 | if (deltaBuf + silence_inqueue < LOWEST_NUM_BUFFER + silence_outqueue) 82 | { 83 | int numAddBuffer = ((silence_inqueue >= silence_outqueue) ? (LOWEST_NUM_BUFFER - deltaBuf - (int)(silence_inqueue-silence_outqueue)) : (LOWEST_NUM_BUFFER - deltaBuf - (int)((unsigned int)(0xFFFFFFFF)-silence_outqueue + silence_inqueue + 1))); 84 | if (numAddBuffer > LOWEST_NUM_BUFFER) 85 | { 86 | numAddBuffer = LOWEST_NUM_BUFFER - deltaBuf; 87 | } 88 | else 89 | { 90 | silence_inqueue = silence_outqueue = 0; 91 | } 92 | for (int i=0; iq, silence_buf, 0, NULL); 95 | } 96 | if (numAddBuffer > 0) silence_inqueue += numAddBuffer; 97 | } 98 | 99 | //RecycleChain *tmp = ctx->rcm.last_to_queue->next; 100 | //if ( /*(*tmp->curt)->mAudioDataByteSize != FAKE_SIZE &&*/ tmp != ctx->rcm.first) 101 | //if (deltaBuf > ALLOW_DELAY) 102 | //{ 103 | // //while(ctx->rcm.last_to_queue->next != ctx->rcm.first) 104 | // for (int i = 0; i < deltaBuf - ALLOW_DELAY + 1; ++i) 105 | // { 106 | // AudioQueueEnqueueBuffer(ctx->q, (*(ctx->rcm.last_to_queue->next->curt)), 0, NULL); 107 | // ctx->rcm.last_to_queue = ctx->rcm.last_to_queue->next; 108 | // } 109 | //} 110 | //else 111 | //{ 112 | // int silence_use_count = (int)(silence_buf->mUserData); 113 | // if ( deltaBuf > 0 ) 114 | // { 115 | // AudioQueueEnqueueBuffer(ctx->q, (*(ctx->rcm.last_to_queue->next->curt)), 0, NULL); 116 | // ctx->rcm.last_to_queue = ctx->rcm.last_to_queue->next; 117 | // } 118 | // else if (silence_use_count == 0) 119 | // { 120 | // AudioQueueEnqueueBuffer(ctx->q, silence_buf, 0, NULL); 121 | // //int tmp = (int)(silence_buf->mUserData); 122 | // //++tmp; 123 | // silence_buf->mUserData = (void *)(1); 124 | // } 125 | //} 126 | //else //if ((*ctx->rcm.last_use)->mAudioDataByteSize == FAKE_SIZE) 127 | //{ 128 | // AudioQueueEnqueueBuffer(ctx->q, silence_buf, 0, NULL); 129 | // AudioQueueEnqueueBuffer(ctx->q, silence_buf, 0, NULL); 130 | // AudioQueueEnqueueBuffer(ctx->q, silence_buf, 0, NULL); 131 | // /*AudioQueueStop(ctx->q, true); 132 | // isStart = false; 133 | // ctx->in_use = 0;*/ 134 | //} 135 | 136 | /*if(ctx->rcm.last_to_queue->curt == &buffer && ctx->rcm.last_to_queue->next == ctx->rcm.first) // && (*ctx->rcm.first->curt)->mAudioDataByteSize == FAKE_SIZE) 137 | { 138 | AudioQueueStop(ctx->q, true); 139 | isStart = false; 140 | ctx->in_use = 0; 141 | }*/ 142 | 143 | 144 | //ctx->rcm.last->curt = &buffer; 145 | 146 | 147 | 148 | /*if (ctx->in_use == 0) 149 | AudioQueueStop(ctx->q, true);*/ 150 | } 151 | 152 | void audio_init(struct audio **ctx_out) 153 | { 154 | struct audio *ctx = *ctx_out = calloc(1, sizeof(struct audio)); 155 | RecycleChain *rcTraverse = NULL; 156 | AudioStreamBasicDescription format; 157 | format.mSampleRate = 48000; 158 | format.mFormatID = kAudioFormatLinearPCM; 159 | format.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked; 160 | format.mFramesPerPacket = 1; 161 | format.mChannelsPerFrame = 2; 162 | format.mBitsPerChannel = 16; 163 | format.mBytesPerPacket = 4; 164 | format.mBytesPerFrame = 4; 165 | 166 | // Create and audio playback queue 167 | AudioQueueNewOutput(&format, audio_queue_callback, (void *) ctx, nil, nil, 0, &ctx->q); 168 | 169 | //ctx->rcm.first = ctx->audio_buf[0]; 170 | //ctx->rcm.first = ctx->audio_buf[NUM_AUDIO_BUF-1]; 171 | ctx->rcm.rc = (RecycleChain *)(&ctx->mem[0]); 172 | ctx->rcm.first = ctx->rcm.rc; 173 | rcTraverse = ctx->rcm.rc; 174 | // Create buffers for the queue 175 | for (int32_t x = 0; x < NUM_AUDIO_BUF; x++) { 176 | AudioQueueAllocateBuffer(ctx->q, BUFFER_SIZE, &ctx->audio_buf[x]); 177 | ctx->audio_buf[x]->mAudioDataByteSize = FAKE_SIZE; 178 | ctx->loc[x] = x; 179 | ctx->audio_buf[x]->mUserData = (void *)(&ctx->loc[x]); 180 | rcTraverse->curt = &ctx->audio_buf[x]; 181 | if( x != NUM_AUDIO_BUF - 1) 182 | { 183 | rcTraverse->next = (RecycleChain *)(&ctx->mem[2*(x+1)]); 184 | rcTraverse = rcTraverse->next; 185 | } 186 | else 187 | { 188 | //ctx->rcm.first = rcTraverse; 189 | rcTraverse->next = ctx->rcm.rc; 190 | } 191 | } 192 | isStart = false; 193 | ctx->fail_num = 0; 194 | ctx->in_use = 0; 195 | 196 | silence_inqueue = silence_outqueue = 0; 197 | char silence[SILENT_SIZE] = {0}; 198 | AudioQueueAllocateBuffer(ctx->q, SILENT_SIZE, &silence_buf); 199 | memcpy(silence_buf->mAudioData, &silence[0], SILENT_SIZE); 200 | silence_buf->mAudioDataByteSize = SILENT_SIZE; 201 | silence_buf->mUserData = NULL; 202 | } 203 | 204 | void audio_destroy(struct audio **ctx_out) 205 | { 206 | if (!ctx_out || !*ctx_out) 207 | return; 208 | 209 | struct audio *ctx = *ctx_out; 210 | //AudioQueueStop(ctx->q, true); 211 | 212 | for (int32_t x = 0; x < NUM_AUDIO_BUF; x++) { 213 | if (ctx->audio_buf[x]) 214 | AudioQueueFreeBuffer(ctx->q, ctx->audio_buf[x]); 215 | } 216 | 217 | if (ctx->q) 218 | AudioQueueDispose(ctx->q, true); 219 | 220 | free(ctx); 221 | *ctx_out = NULL; 222 | isStart = false; 223 | AudioQueueFreeBuffer(ctx->q, silence_buf); 224 | silence_inqueue = silence_outqueue = 0; 225 | } 226 | 227 | void audio_clear(struct audio **ctx_out) 228 | { 229 | if (!ctx_out || !*ctx_out) 230 | return; 231 | 232 | //RecycleChain *rcTraverse = NULL; 233 | struct audio *ctx = *ctx_out; 234 | if (ctx->q) 235 | AudioQueueStop(ctx->q, true); 236 | 237 | //rcTraverse = ctx->rcm.rc; 238 | for (int32_t x = 0; x < NUM_AUDIO_BUF; x++) { 239 | ctx->audio_buf[x]->mAudioDataByteSize = FAKE_SIZE; 240 | /*rcTraverse->curt = &ctx->audio_buf[x]; 241 | if( x != NUM_AUDIO_BUF - 1) 242 | { 243 | rcTraverse->next = (RecycleChain *)(&ctx->mem[2*(x+1)]); 244 | rcTraverse = rcTraverse->next; 245 | } 246 | else 247 | { 248 | ctx->rcm.first = rcTraverse; 249 | rcTraverse->next = NULL; 250 | }*/ 251 | } 252 | isStart = false; 253 | ctx->in_use = 0; 254 | ctx->fail_num = 0; 255 | silence_inqueue = silence_outqueue = 0; 256 | } 257 | 258 | void audio_cb(const int16_t *pcm, uint32_t frames, void *opaque) 259 | { 260 | if ( frames == 0 || opaque == NULL || isMuted ) 261 | return; 262 | 263 | struct audio *ctx = (struct audio *) opaque; 264 | AudioQueueBufferRef *find_idle = NULL; 265 | 266 | find_idle = ctx->rcm.first->curt; 267 | if ((*find_idle)->mAudioDataByteSize != FAKE_SIZE) 268 | { 269 | ++ctx->fail_num; 270 | if(ctx->fail_num > 10) audio_clear(&ctx); 271 | return; 272 | } 273 | 274 | memcpy((*find_idle)->mAudioData, pcm, frames * 4); 275 | (*find_idle)->mAudioDataByteSize = frames * 4; 276 | 277 | if(!isStart) 278 | { 279 | ctx->rcm.last_to_queue = ctx->rcm.first; 280 | AudioQueueEnqueueBuffer(ctx->q, (*find_idle), 0, NULL); 281 | } 282 | 283 | ctx->fail_num = 0; 284 | ctx->rcm.first = ctx->rcm.first->next; 285 | //ctx->rcm.last_use = find_idle; 286 | 287 | ctx->in_use += frames; 288 | //if (!isStart && (ctx->in_use > 1600)) 289 | if (ctx->in_use > 1000) 290 | { 291 | AudioQueueStart(ctx->q, NULL); 292 | isStart = true; 293 | } 294 | } 295 | 296 | void audio_mute(bool muted, const void *opaque) 297 | { 298 | if (isMuted == muted) return; 299 | isMuted = muted; 300 | isStart = false; 301 | if (opaque == NULL) return; 302 | 303 | struct audio *ctx = (struct audio *) opaque; 304 | if(ctx->q == NULL) return; 305 | if(isMuted) 306 | { 307 | AudioQueuePause(ctx->q); 308 | audio_clear(&ctx); 309 | } 310 | } 311 | -------------------------------------------------------------------------------- /OpenParsec/audio.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | struct audio; 7 | 8 | void audio_init(struct audio **ctx_out); 9 | void audio_destroy(struct audio **ctx_out); 10 | void audio_clear(struct audio **ctx_out); 11 | void audio_cb(const int16_t *pcm, uint32_t frames, void *opaque); 12 | void audio_mute(bool muted, const void *opaque); 13 | 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #

![icon_transparent.png](OpenParsec/Assets.xcassets/IconTransparent.imageset/icon_transparent.png) ![OpenParsec](OpenParsec/Assets.xcassets/LogoShadow.imageset/logo_shadow.png)

2 | 3 | OpenParsec is a simple, open-source Parsec client for iOS/iPadOS written in Swift using the SwiftUI framework and the Parsec SDK. 4 | 5 | This project is still a major WIP, so apologies for the currently lackluster documentation. I'm also very new to both Swift and SwiftUI so I'm sure there are many places for improvement. 6 | 7 | Before building, make sure you have the Parsec SDK framework symlinked or copied to the `Frameworks` folder. Builds were tested on Xcode Version 12.5. 8 | 9 | ## Touch Control 10 | You can set the touch mode you want to use in settings. Touchpad mode and direct touch mode are supported. 11 | 12 | When streaming, you can tap with 3 fingers to bring up the on-screen keyboard. 13 | 14 | ## Mouse & keyboard 15 | USB mouse & keyboard are supported. 16 | 17 | ## Game Controllers 18 | When streaming, press any trigger button in your controller and parsec will recognize it. Make sure to configure the host properly (install virtual USB driver etc.) before using game controllers. 19 | 20 | ## Lag / Low Bitrate Issue 21 | If you encounter lags from nowhere or your bitrate hardly goes over 10 Mbps, download Steam Link and do a network test. If you see constant lag spike in the graph, then it's a problem with Apple and there's little we can do to solve this problem. See [here](https://github.com/moonlight-stream/moonlight-ios/issues/627) for more disscussion. 22 | 23 | If you can't change your wireless router's channel to 149 like me, my personal experience is that you can try to power off the device you are using to stream as well as any nearby Apple devices, especially Mac, then only power on the device you are using to stream and do the aforementioned network test again. You can turn on other devices if the lag spike is gone and it may sustain for couple hours or days. -------------------------------------------------------------------------------- /altstore.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "OpenParsec", 3 | "subtitle": "Open Parsec client for iOS/iPadOS", 4 | "description": "An open-source Parsec client for iOS/iPadOS. This repo implements mouse & keyboard & touch screen support.", 5 | "iconURL": "https://raw.githubusercontent.com/hugeBlack/OpenParsec/main/OpenParsec/Assets.xcassets/IconTransparent.imageset/icon_transparent.png", 6 | "headerURL": "https://raw.githubusercontent.com/hugeBlack/OpenParsec/main/OpenParsec/Assets.xcassets/LogoShadow.imageset/logo_shadow.png", 7 | "website": "https://github.com/hugeBlack/OpenParsec", 8 | "apps": [ 9 | { 10 | "beta": true, 11 | "name": "OpenParsec", 12 | "bundleIdentifier": "com.aigch.OpenParsec1", 13 | "developerName": "hugeBlack", 14 | "subtitle": "Open Parsec client for iOS/iPadOS", 15 | "localizedDescription": "An open-source Parsec client for iOS/iPadOS. This repo implements mouse & keyboard & touch screen support.", 16 | "iconURL": "https://raw.githubusercontent.com/hugeBlack/OpenParsec/main/OpenParsec/Assets.xcassets/IconTransparent.imageset/icon_transparent.png", 17 | "tintColor": "#BB01F0", 18 | "screenshotURLs": [], 19 | "versions": [ 20 | { 21 | "version": "nightly", 22 | "date": "2024-11-24", 23 | "downloadURL": "https://github.com/hugeBlack/OpenParsec/releases/download/nightly/OpenParsec.ipa", 24 | "size": 1800157 25 | } 26 | ], 27 | "appPermissions": {} 28 | } 29 | ], 30 | "news": [] 31 | } 32 | --------------------------------------------------------------------------------