Merge branch 'main' into releases-and-new-betas
16
.github/workflows/appcast.yml
vendored
|
|
@ -14,16 +14,16 @@ jobs:
|
|||
# If you're using actions/checkout@v4 you must set persist-credentials to false in most cases for the deployment to work correctly.
|
||||
persist-credentials: false
|
||||
|
||||
- name: Cache 📦
|
||||
uses: actions/cache@v4.0.0
|
||||
with:
|
||||
path: AppCast/vendor/bundle
|
||||
key: ${{ runner.os }}-gems-v1.0-${{ hashFiles('AppCast/Gemfile') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-gems-
|
||||
# - name: Cache 📦
|
||||
# uses: actions/cache@v4.1.1
|
||||
# with:
|
||||
# path: AppCast/vendor/bundle
|
||||
# key: ${{ runner.os }}-gems-v1.0-${{ hashFiles('AppCast/Gemfile') }}
|
||||
# restore-keys: |
|
||||
# ${{ runner.os }}-gems-
|
||||
|
||||
- name: Setup Ruby, JRuby and TruffleRuby
|
||||
uses: ruby/setup-ruby@v1.169.0
|
||||
uses: ruby/setup-ruby@v1.197.0
|
||||
with:
|
||||
ruby-version: '3.0'
|
||||
|
||||
|
|
|
|||
4
.github/workflows/ci.yml
vendored
|
|
@ -8,10 +8,10 @@ on:
|
|||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: macos-13
|
||||
runs-on: macos-15
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Run tests
|
||||
env:
|
||||
DEVELOPER_DIR: /Applications/Xcode_15.0.1.app
|
||||
DEVELOPER_DIR: /Applications/Xcode_16.4.app
|
||||
run: xcodebuild test -scheme Xcodes
|
||||
|
|
|
|||
2
.github/workflows/release-drafter.yml
vendored
|
|
@ -11,6 +11,6 @@ jobs:
|
|||
update_release_draft:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: release-drafter/release-drafter@v5
|
||||
- uses: release-drafter/release-drafter@v6
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
|
|
|||
22
.github/workflows/xcstrings.yml
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
name: XCStrings Validation
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: macos-13
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Clone SwiftPolyglot
|
||||
run: git clone --branch 0.3.1 -- https://github.com/appdecostudio/SwiftPolyglot.git ../SwiftPolyglot
|
||||
|
||||
- name: validate translations
|
||||
run: |
|
||||
swift build --package-path ../SwiftPolyglot --configuration release
|
||||
swift run --package-path ../SwiftPolyglot swiftpolyglot "ca,de,el,es,fi,fr,hi,it,ja,ko,nl,pl,pt-BR,ru,tr,uk,zh-Hans,zh-Hant" --errorOnMissing
|
||||
|
|
@ -8,7 +8,7 @@ source "https://rubygems.org"
|
|||
#
|
||||
# This will help ensure the proper Jekyll version is running.
|
||||
# Happy Jekylling!
|
||||
gem "jekyll", "~> 3.9.0"
|
||||
gem "jekyll", "~> 4.4.1"
|
||||
|
||||
gem "jekyll-github-metadata", group: :jekyll_plugins
|
||||
|
||||
|
|
|
|||
|
|
@ -1,88 +1,110 @@
|
|||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
addressable (2.8.0)
|
||||
public_suffix (>= 2.0.2, < 5.0)
|
||||
addressable (2.8.7)
|
||||
public_suffix (>= 2.0.2, < 7.0)
|
||||
base64 (0.3.0)
|
||||
bigdecimal (3.2.2)
|
||||
colorator (1.1.0)
|
||||
concurrent-ruby (1.1.7)
|
||||
em-websocket (0.5.2)
|
||||
concurrent-ruby (1.3.5)
|
||||
csv (3.3.5)
|
||||
em-websocket (0.5.3)
|
||||
eventmachine (>= 0.12.9)
|
||||
http_parser.rb (~> 0.6.0)
|
||||
http_parser.rb (~> 0)
|
||||
eventmachine (1.2.7)
|
||||
faraday (1.3.0)
|
||||
faraday-net_http (~> 1.0)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
ruby2_keywords
|
||||
faraday-net_http (1.0.1)
|
||||
ffi (1.14.2)
|
||||
ffi (1.17.2)
|
||||
ffi (1.17.2-x86_64-darwin)
|
||||
forwardable-extended (2.6.0)
|
||||
http_parser.rb (0.6.0)
|
||||
i18n (0.9.5)
|
||||
google-protobuf (4.31.1)
|
||||
bigdecimal
|
||||
rake (>= 13)
|
||||
google-protobuf (4.31.1-x86_64-darwin)
|
||||
bigdecimal
|
||||
rake (>= 13)
|
||||
http_parser.rb (0.8.0)
|
||||
i18n (1.14.7)
|
||||
concurrent-ruby (~> 1.0)
|
||||
jekyll (3.9.0)
|
||||
jekyll (4.4.1)
|
||||
addressable (~> 2.4)
|
||||
base64 (~> 0.2)
|
||||
colorator (~> 1.0)
|
||||
csv (~> 3.0)
|
||||
em-websocket (~> 0.5)
|
||||
i18n (~> 0.7)
|
||||
jekyll-sass-converter (~> 1.0)
|
||||
i18n (~> 1.0)
|
||||
jekyll-sass-converter (>= 2.0, < 4.0)
|
||||
jekyll-watch (~> 2.0)
|
||||
kramdown (>= 1.17, < 3)
|
||||
json (~> 2.6)
|
||||
kramdown (~> 2.3, >= 2.3.1)
|
||||
kramdown-parser-gfm (~> 1.0)
|
||||
liquid (~> 4.0)
|
||||
mercenary (~> 0.3.3)
|
||||
mercenary (~> 0.3, >= 0.3.6)
|
||||
pathutil (~> 0.9)
|
||||
rouge (>= 1.7, < 4)
|
||||
rouge (>= 3.0, < 5.0)
|
||||
safe_yaml (~> 1.0)
|
||||
terminal-table (>= 1.8, < 4.0)
|
||||
webrick (~> 1.7)
|
||||
jekyll-github-metadata (2.13.0)
|
||||
jekyll (>= 3.4, < 5.0)
|
||||
octokit (~> 4.0, != 4.4.0)
|
||||
jekyll-sass-converter (1.5.2)
|
||||
sass (~> 3.4)
|
||||
jekyll-sass-converter (3.1.0)
|
||||
sass-embedded (~> 1.75)
|
||||
jekyll-watch (2.2.1)
|
||||
listen (~> 3.0)
|
||||
kramdown (2.3.1)
|
||||
rexml
|
||||
json (2.12.2)
|
||||
kramdown (2.5.1)
|
||||
rexml (>= 3.3.9)
|
||||
kramdown-parser-gfm (1.1.0)
|
||||
kramdown (~> 2.0)
|
||||
liquid (4.0.3)
|
||||
listen (3.4.1)
|
||||
liquid (4.0.4)
|
||||
listen (3.9.0)
|
||||
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||
rb-inotify (~> 0.9, >= 0.9.10)
|
||||
mercenary (0.3.6)
|
||||
mercenary (0.4.0)
|
||||
multipart-post (2.1.1)
|
||||
octokit (4.20.0)
|
||||
faraday (>= 0.9)
|
||||
sawyer (~> 0.8.0, >= 0.5.3)
|
||||
pathutil (0.16.2)
|
||||
forwardable-extended (~> 2.6)
|
||||
public_suffix (4.0.6)
|
||||
rb-fsevent (0.10.4)
|
||||
rb-inotify (0.10.1)
|
||||
public_suffix (6.0.2)
|
||||
rake (13.3.0)
|
||||
rb-fsevent (0.11.2)
|
||||
rb-inotify (0.11.1)
|
||||
ffi (~> 1.0)
|
||||
rexml (3.2.5)
|
||||
rouge (3.26.0)
|
||||
rexml (3.4.1)
|
||||
rouge (4.5.2)
|
||||
ruby2_keywords (0.0.2)
|
||||
safe_yaml (1.0.5)
|
||||
sass (3.7.4)
|
||||
sass-listen (~> 4.0.0)
|
||||
sass-listen (4.0.0)
|
||||
rb-fsevent (~> 0.9, >= 0.9.4)
|
||||
rb-inotify (~> 0.9, >= 0.9.7)
|
||||
sass-embedded (1.89.2)
|
||||
google-protobuf (~> 4.31)
|
||||
rake (>= 13)
|
||||
sass-embedded (1.89.2-x86_64-darwin)
|
||||
google-protobuf (~> 4.31)
|
||||
sawyer (0.8.2)
|
||||
addressable (>= 2.3.5)
|
||||
faraday (> 0.8, < 2.0)
|
||||
terminal-table (3.0.2)
|
||||
unicode-display_width (>= 1.1.1, < 3)
|
||||
thread_safe (0.3.6)
|
||||
tzinfo (1.2.10)
|
||||
thread_safe (~> 0.1)
|
||||
tzinfo-data (1.2020.6)
|
||||
tzinfo (>= 1.0.0)
|
||||
unicode-display_width (2.6.0)
|
||||
wdm (0.1.1)
|
||||
webrick (1.9.1)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
x86_64-darwin-20
|
||||
|
||||
DEPENDENCIES
|
||||
jekyll (~> 3.9.0)
|
||||
jekyll (~> 4.4.1)
|
||||
jekyll-github-metadata
|
||||
kramdown-parser-gfm
|
||||
tzinfo (~> 1.2)
|
||||
|
|
@ -90,4 +112,4 @@ DEPENDENCIES
|
|||
wdm (~> 0.1.0)
|
||||
|
||||
BUNDLED WITH
|
||||
2.2.5
|
||||
2.6.9
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ We love your input! We want to make contributing to this project as easy and tra
|
|||
- Proposing new features
|
||||
- Becoming a maintainer
|
||||
|
||||
## We Develop with Github
|
||||
We use github to host code, to track issues and feature requests, as well as accept pull requests.
|
||||
## We Develop with GitHub
|
||||
We use GitHub to host code, to track issues and feature requests, as well as accept pull requests.
|
||||
|
||||
## All Code Changes Happen Through Pull Requests
|
||||
Pull requests are the best way to propose changes to the codebase We actively welcome your pull requests:
|
||||
|
|
@ -23,7 +23,7 @@ Pull requests are the best way to propose changes to the codebase We actively w
|
|||
## Any contributions you make will be under the MIT Software License
|
||||
In short, when you submit code changes, your submissions are understood to be under the same [MIT License](http://choosealicense.com/licenses/mit/) that covers the project. Feel free to contact the maintainers if that's a concern.
|
||||
|
||||
## Report bugs using Github's [issues](https://github.com/robotsandpencils/xcodesapp/issues)
|
||||
## Report bugs using GitHub [issues](https://github.com/robotsandpencils/xcodesapp/issues)
|
||||
We use GitHub issues to track public bugs. Report a bug by [opening a new issue](); it's that easy!
|
||||
|
||||
## Write bug reports with detail, background, and sample code
|
||||
|
|
|
|||
12
README.md
|
|
@ -22,11 +22,14 @@ XcodesApp is now part of the `XcodesOrg` - [read more here](nextstep.md)
|
|||
- Just click a button to make a version active with `xcode-select`.
|
||||
- View release notes, OS compatibility, included SDKs and compilers from [Xcode Releases](https://xcodereleases.com).
|
||||
- Dark/Light Mode supported
|
||||
- Security Key Authentication supported
|
||||
|
||||
## Platforms/Runtimes
|
||||
|
||||
- Xcodes supports downloading the Apple runtimes via the app. Simply click on the Platform, and Xcodes will install automatically for you.
|
||||
|
||||
**Note: iOS 18+, tvOS 18+, watchOS 11+, visionOS 2+ requires that Xcode 16.1 Beta 3+ be installed and active.**
|
||||
|
||||
## Experiments
|
||||
|
||||
- Thanks to the wonderful work of [https://github.com/saagarjha/unxip](https://github.com/saagarjha/unxip), turn on the experiment to increase your unxipping time by up to 70%! More can be found on his repo, but bugs, high memory may occur if used.
|
||||
|
|
@ -50,13 +53,13 @@ The following languages are supported because of the following community users!
|
|||
|Ukranian 🇺🇦 |[@gelosi](https://github.com/gelosi)|Japanese 🇯🇵|[@tatsuz0u](https://github.com/tatsuz0u)|
|
||||
|German 🇩🇪|[@drct](https://github.com/drct)|Dutch 🇳🇱|[@jfversluis](https://github/com/jfversluis)|
|
||||
|Brazilian Portuguese 🇧🇷|[@brunomunizaf](https://github.com/brunomunizaf)|Polish 🇵🇱|[@jakex7](https://github.com/jakex7)|
|
||||
|Catalan|[@ferranabello](https://github.com/ferranabello)|
|
||||
|Catalan|[@ferranabello](https://github.com/ferranabello)|Greek 🇬🇷|[@alladinian](https://github.com/alladinian)
|
||||
|
||||
Want to add more languages? Simply create a PR with the updated strings file.
|
||||
|
||||
## Installation
|
||||
v1.X - requires MacOS 11 or newer
|
||||
v2.X - requires MacOS 13
|
||||
v1.X - requires macOS 11 or newer
|
||||
v2.X - requires macOS 13
|
||||
|
||||
### Install with Homebrew
|
||||
|
||||
|
|
@ -160,7 +163,8 @@ popd
|
|||
# Attach the zip that was created in the Product directory to the release
|
||||
# Publish the release
|
||||
|
||||
# Update the [Homebrew Cask](https://github.com/RobotsAndPencils/homebrew-cask/blob/master/Casks/xcodes.rb).
|
||||
shasum -a 256 xcodes.zip
|
||||
# Update the [Homebrew Cask](https://github.com/XcodesOrg/homebrew-cask/blob/master/Casks/x/xcodes.rb).
|
||||
```
|
||||
</details>
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,10 @@
|
|||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
15F5B8902CCF09B900705E2F /* CryptoKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 15F5B88F2CCF09B900705E2F /* CryptoKit.framework */; };
|
||||
33027E342CA8C18800CB387C /* LibFido2Swift in Frameworks */ = {isa = PBXBuildFile; productRef = 334A932B2CA885A400A5E079 /* LibFido2Swift */; };
|
||||
3328073F2CA5E2C80036F691 /* SignInSecurityKeyPinView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3328073E2CA5E2C80036F691 /* SignInSecurityKeyPinView.swift */; };
|
||||
332807412CA5EA820036F691 /* SignInSecurityKeyTouchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 332807402CA5EA820036F691 /* SignInSecurityKeyTouchView.swift */; };
|
||||
36741BFD291E4FDB00A85AAE /* DownloadPreferencePane.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36741BFC291E4FDB00A85AAE /* DownloadPreferencePane.swift */; };
|
||||
36741BFF291E50F500A85AAE /* FileError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36741BFE291E50F500A85AAE /* FileError.swift */; };
|
||||
536CFDD2263C94DE00026CE0 /* SignedInView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536CFDD1263C94DE00026CE0 /* SignedInView.swift */; };
|
||||
|
|
@ -25,6 +29,7 @@
|
|||
B0C6AD042AD6E65700E64698 /* ReleaseDateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0C6AD032AD6E65700E64698 /* ReleaseDateView.swift */; };
|
||||
B0C6AD0B2AD9178E00E64698 /* IdenticalBuildView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0C6AD0A2AD9178E00E64698 /* IdenticalBuildView.swift */; };
|
||||
B0C6AD0D2AD91D7900E64698 /* IconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0C6AD0C2AD91D7900E64698 /* IconView.swift */; };
|
||||
BDBAB7452B9FF55800694B0B /* TrailingIconLabelStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDBAB7442B9FF55800694B0B /* TrailingIconLabelStyle.swift */; };
|
||||
CA11E7BA2598476C00D2EE1C /* XcodeCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA11E7B92598476C00D2EE1C /* XcodeCommands.swift */; };
|
||||
CA2518EC25A7FF2B00F08414 /* AppStateUpdateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA2518EB25A7FF2B00F08414 /* AppStateUpdateTests.swift */; };
|
||||
CA25192A25A9644800F08414 /* XcodeInstallState.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA25192925A9644800F08414 /* XcodeInstallState.swift */; };
|
||||
|
|
@ -110,17 +115,19 @@
|
|||
CAFE4AB425B7D3AF0064FE51 /* AdvancedPreferencePane.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFE4AB325B7D3AF0064FE51 /* AdvancedPreferencePane.swift */; };
|
||||
CAFE4ABC25B7D54B0064FE51 /* UpdatesPreferencePane.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFE4ABB25B7D54B0064FE51 /* UpdatesPreferencePane.swift */; };
|
||||
CAFFFED8259CDA5000903F81 /* XcodeListViewRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFFFED7259CDA5000903F81 /* XcodeListViewRow.swift */; };
|
||||
D93F95C12E0C8C1A00238FB5 /* TagView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93F95C02E0C8C1A00238FB5 /* TagView.swift */; };
|
||||
E689540325BE8C64000EBCEA /* DockProgress in Frameworks */ = {isa = PBXBuildFile; productRef = E689540225BE8C64000EBCEA /* DockProgress */; };
|
||||
E81D7EA02805250100A205FC /* Collection+.swift in Sources */ = {isa = PBXBuildFile; fileRef = E81D7E9F2805250100A205FC /* Collection+.swift */; };
|
||||
E832EAF82B0FBCF4001B570D /* RuntimeInstallationStepDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E832EAF72B0FBCF4001B570D /* RuntimeInstallationStepDetailView.swift */; };
|
||||
E83FDC442CBB649100679C6B /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = E83FDC432CBB649100679C6B /* Sparkle */; };
|
||||
E84B7D0D2B296A8900DBDA2B /* NavigationSplitViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = E84B7D0C2B296A8900DBDA2B /* NavigationSplitViewWrapper.swift */; };
|
||||
E84E4F522B323A5F003F3959 /* CornerRadiusModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E84E4F512B323A5F003F3959 /* CornerRadiusModifier.swift */; };
|
||||
E84E4F542B333864003F3959 /* PlatformsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E84E4F532B333864003F3959 /* PlatformsListView.swift */; };
|
||||
E84E4F572B335094003F3959 /* OrderedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = E84E4F562B335094003F3959 /* OrderedCollections */; };
|
||||
E862D43B2CC8B26F00BAA376 /* SRP in Frameworks */ = {isa = PBXBuildFile; productRef = E862D43A2CC8B26F00BAA376 /* SRP */; };
|
||||
E86671272B309D2F0048559A /* PlatformsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E86671262B309D2F0048559A /* PlatformsView.swift */; };
|
||||
E87AB3C52939B65E00D72F43 /* Hardware.swift in Sources */ = {isa = PBXBuildFile; fileRef = E87AB3C42939B65E00D72F43 /* Hardware.swift */; };
|
||||
E87DD6EB25D053FA00D86808 /* Progress+.swift in Sources */ = {isa = PBXBuildFile; fileRef = E87DD6EA25D053FA00D86808 /* Progress+.swift */; };
|
||||
E891A1C42B43ACF900A1B9D1 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = E891A1C32B43ACF900A1B9D1 /* Sparkle */; };
|
||||
E89342FA25EDCC17007CF557 /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E89342F925EDCC17007CF557 /* NotificationManager.swift */; };
|
||||
E8977EA325C11E1500835F80 /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8977EA225C11E1500835F80 /* PreferencesView.swift */; };
|
||||
E8B20CBF2A2EDEC20057D816 /* SDKs+Xcode.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8B20CBE2A2EDEC20057D816 /* SDKs+Xcode.swift */; };
|
||||
|
|
@ -133,6 +140,7 @@
|
|||
E8DA461125FAF7FB002E85EF /* NotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8DA461025FAF7FB002E85EF /* NotificationsView.swift */; };
|
||||
E8E98A9025D8631800EC89A0 /* InstallationStepRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFBC3FF259AC17F00E2A3D8 /* InstallationStepRowView.swift */; };
|
||||
E8E98A9625D863D700EC89A0 /* InstallationStepDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8E98A9525D863D700EC89A0 /* InstallationStepDetailView.swift */; };
|
||||
E8EE58C02E1CC2A50003FA9F /* RuntimeArchitecture.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8EE58BF2E1CC2A50003FA9F /* RuntimeArchitecture.swift */; };
|
||||
E8F44A1E296B4CD7002D6592 /* Path in Frameworks */ = {isa = PBXBuildFile; productRef = E8F44A1D296B4CD7002D6592 /* Path */; };
|
||||
E8FA00542B5B109800769CE0 /* com.xcodesorg.xcodesapp.Helper in Copy Helper */ = {isa = PBXBuildFile; fileRef = CA9FF8AE2595967A00E47BAF /* com.xcodesorg.xcodesapp.Helper */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
|
||||
E8FD5727291EE4AC001E004C /* AsyncNetworkService in Frameworks */ = {isa = PBXBuildFile; productRef = E8FD5726291EE4AC001E004C /* AsyncNetworkService */; };
|
||||
|
|
@ -191,6 +199,9 @@
|
|||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
15F5B88F2CCF09B900705E2F /* CryptoKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CryptoKit.framework; path = System/Library/Frameworks/CryptoKit.framework; sourceTree = SDKROOT; };
|
||||
3328073E2CA5E2C80036F691 /* SignInSecurityKeyPinView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInSecurityKeyPinView.swift; sourceTree = "<group>"; };
|
||||
332807402CA5EA820036F691 /* SignInSecurityKeyTouchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInSecurityKeyTouchView.swift; sourceTree = "<group>"; };
|
||||
36741BFC291E4FDB00A85AAE /* DownloadPreferencePane.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DownloadPreferencePane.swift; sourceTree = "<group>"; };
|
||||
36741BFE291E50F500A85AAE /* FileError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileError.swift; sourceTree = "<group>"; };
|
||||
536CFDD1263C94DE00026CE0 /* SignedInView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignedInView.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -209,6 +220,7 @@
|
|||
B0C6AD032AD6E65700E64698 /* ReleaseDateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReleaseDateView.swift; sourceTree = "<group>"; };
|
||||
B0C6AD0A2AD9178E00E64698 /* IdenticalBuildView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdenticalBuildView.swift; sourceTree = "<group>"; };
|
||||
B0C6AD0C2AD91D7900E64698 /* IconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconView.swift; sourceTree = "<group>"; };
|
||||
BDBAB7442B9FF55800694B0B /* TrailingIconLabelStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrailingIconLabelStyle.swift; sourceTree = "<group>"; };
|
||||
CA11E7B92598476C00D2EE1C /* XcodeCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcodeCommands.swift; sourceTree = "<group>"; };
|
||||
CA2518EB25A7FF2B00F08414 /* AppStateUpdateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStateUpdateTests.swift; sourceTree = "<group>"; };
|
||||
CA25192925A9644800F08414 /* XcodeInstallState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcodeInstallState.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -311,6 +323,7 @@
|
|||
CAFE4ABB25B7D54B0064FE51 /* UpdatesPreferencePane.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdatesPreferencePane.swift; sourceTree = "<group>"; };
|
||||
CAFFFED7259CDA5000903F81 /* XcodeListViewRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcodeListViewRow.swift; sourceTree = "<group>"; };
|
||||
CAFFFEEE259CEAC400903F81 /* RingProgressViewStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RingProgressViewStyle.swift; sourceTree = "<group>"; };
|
||||
D93F95C02E0C8C1A00238FB5 /* TagView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagView.swift; sourceTree = "<group>"; };
|
||||
E81D7E9F2805250100A205FC /* Collection+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+.swift"; sourceTree = "<group>"; };
|
||||
E832EAF72B0FBCF4001B570D /* RuntimeInstallationStepDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuntimeInstallationStepDetailView.swift; sourceTree = "<group>"; };
|
||||
E84B7D0C2B296A8900DBDA2B /* NavigationSplitViewWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationSplitViewWrapper.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -330,6 +343,7 @@
|
|||
E8D655BF288DD04700A139C2 /* SelectedActionType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectedActionType.swift; sourceTree = "<group>"; };
|
||||
E8DA461025FAF7FB002E85EF /* NotificationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsView.swift; sourceTree = "<group>"; };
|
||||
E8E98A9525D863D700EC89A0 /* InstallationStepDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallationStepDetailView.swift; sourceTree = "<group>"; };
|
||||
E8EE58BF2E1CC2A50003FA9F /* RuntimeArchitecture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuntimeArchitecture.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
|
|
@ -344,12 +358,15 @@
|
|||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
15F5B8902CCF09B900705E2F /* CryptoKit.framework in Frameworks */,
|
||||
33027E342CA8C18800CB387C /* LibFido2Swift in Frameworks */,
|
||||
CABFA9E42592F08E00380FEE /* Version in Frameworks */,
|
||||
CABFA9FD2592F13300380FEE /* LegibleError in Frameworks */,
|
||||
E689540325BE8C64000EBCEA /* DockProgress in Frameworks */,
|
||||
CA9FF86D25951C6E00E47BAF /* XCModel in Frameworks */,
|
||||
CABFA9F82592F0F900380FEE /* KeychainAccess in Frameworks */,
|
||||
E891A1C42B43ACF900A1B9D1 /* Sparkle in Frameworks */,
|
||||
E83FDC442CBB649100679C6B /* Sparkle in Frameworks */,
|
||||
E862D43B2CC8B26F00BAA376 /* SRP in Frameworks */,
|
||||
CAA858CD25A3D8BC00ACF8C0 /* ErrorHandling in Frameworks */,
|
||||
E8C0EB1A291EF43E0081528A /* XcodesKit in Frameworks */,
|
||||
E8FD5727291EE4AC001E004C /* AsyncNetworkService in Frameworks */,
|
||||
|
|
@ -374,12 +391,14 @@
|
|||
63EAA4E9259944340046AB8F /* Common */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D93F95C02E0C8C1A00238FB5 /* TagView.swift */,
|
||||
CAC281CC259F97FA00B8AB0B /* ObservingProgressIndicator.swift */,
|
||||
63EAA4EA259944450046AB8F /* ProgressButton.swift */,
|
||||
CA452BAF259FD9770072DFA4 /* ProgressIndicator.swift */,
|
||||
536CFDD3263C9A8000026CE0 /* XcodesSheet.swift */,
|
||||
53CBAB2B263DCC9100410495 /* XcodesAlert.swift */,
|
||||
E84B7D0C2B296A8900DBDA2B /* NavigationSplitViewWrapper.swift */,
|
||||
BDBAB7442B9FF55800694B0B /* TrailingIconLabelStyle.swift */,
|
||||
);
|
||||
path = Common;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -405,6 +424,7 @@
|
|||
CA538A12255A4F7C00E64DD7 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
15F5B88F2CCF09B900705E2F /* CryptoKit.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -451,6 +471,8 @@
|
|||
CA735108257BF96D00EA9CF8 /* AttributedText.swift */,
|
||||
CA73510C257BFCEF00EA9CF8 /* NSAttributedString+.swift */,
|
||||
CA5D781D257365D6008EDE9D /* PinCodeTextView.swift */,
|
||||
3328073E2CA5E2C80036F691 /* SignInSecurityKeyPinView.swift */,
|
||||
332807402CA5EA820036F691 /* SignInSecurityKeyTouchView.swift */,
|
||||
CAA1CB34255A5AD5003FD669 /* SignInCredentialsView.swift */,
|
||||
CAA1CB44255A5B60003FD669 /* SignIn2FAView.swift */,
|
||||
CAA1CB48255A5C97003FD669 /* SignInSMSView.swift */,
|
||||
|
|
@ -640,6 +662,7 @@
|
|||
E8E98A9425D863B100EC89A0 /* InfoPane */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E8EE58BF2E1CC2A50003FA9F /* RuntimeArchitecture.swift */,
|
||||
B0403CEF2AD92D7B00137C09 /* ReleaseNotesView.swift */,
|
||||
B0403CF32AD9381D00137C09 /* SDKsView.swift */,
|
||||
B0403CF52AD9849E00137C09 /* CompilersView.swift */,
|
||||
|
|
@ -710,7 +733,9 @@
|
|||
E8C0EB19291EF43E0081528A /* XcodesKit */,
|
||||
E8F44A1D296B4CD7002D6592 /* Path */,
|
||||
E84E4F562B335094003F3959 /* OrderedCollections */,
|
||||
E891A1C32B43ACF900A1B9D1 /* Sparkle */,
|
||||
E83FDC432CBB649100679C6B /* Sparkle */,
|
||||
334A932B2CA885A400A5E079 /* LibFido2Swift */,
|
||||
E862D43A2CC8B26F00BAA376 /* SRP */,
|
||||
);
|
||||
productName = XcodesMac;
|
||||
productReference = CAD2E79E2449574E00113D76 /* Xcodes.app */;
|
||||
|
|
@ -783,6 +808,7 @@
|
|||
"pt-BR",
|
||||
nl,
|
||||
pl,
|
||||
ar,
|
||||
);
|
||||
mainGroup = CAD2E7952449574E00113D76;
|
||||
packageReferences = (
|
||||
|
|
@ -797,7 +823,8 @@
|
|||
E8FD5725291EE4AC001E004C /* XCRemoteSwiftPackageReference "AsyncHTTPNetworkService" */,
|
||||
E8F44A1C296B4CD7002D6592 /* XCRemoteSwiftPackageReference "Path" */,
|
||||
E84E4F552B335094003F3959 /* XCRemoteSwiftPackageReference "swift-collections" */,
|
||||
E891A1C22B43ACA400A1B9D1 /* XCRemoteSwiftPackageReference "Sparkle" */,
|
||||
E83FDC422CBB649100679C6B /* XCRemoteSwiftPackageReference "Sparkle" */,
|
||||
33027E282CA8BB5800CB387C /* XCRemoteSwiftPackageReference "LibFido2Swift" */,
|
||||
);
|
||||
productRefGroup = CAD2E79F2449574E00113D76 /* Products */;
|
||||
projectDirPath = "";
|
||||
|
|
@ -885,6 +912,7 @@
|
|||
CABFAA492593162500380FEE /* Bundle+InfoPlistValues.swift in Sources */,
|
||||
CA9FF8662595130600E47BAF /* View+IsHidden.swift in Sources */,
|
||||
CAE4248C259A68B800B8B246 /* Optional+IsNotNil.swift in Sources */,
|
||||
3328073F2CA5E2C80036F691 /* SignInSecurityKeyPinView.swift in Sources */,
|
||||
B0C6AD0D2AD91D7900E64698 /* IconView.swift in Sources */,
|
||||
CA9FF9362595B44700E47BAF /* HelperClient.swift in Sources */,
|
||||
B0C6AD042AD6E65700E64698 /* ReleaseDateView.swift in Sources */,
|
||||
|
|
@ -910,8 +938,11 @@
|
|||
E84E4F522B323A5F003F3959 /* CornerRadiusModifier.swift in Sources */,
|
||||
B0403CF22AD934B600137C09 /* CompatibilityView.swift in Sources */,
|
||||
B0403CFE2ADA712C00137C09 /* InfoPaneControls.swift in Sources */,
|
||||
D93F95C12E0C8C1A00238FB5 /* TagView.swift in Sources */,
|
||||
53CBAB2C263DCC9100410495 /* XcodesAlert.swift in Sources */,
|
||||
332807412CA5EA820036F691 /* SignInSecurityKeyTouchView.swift in Sources */,
|
||||
CA61A6E0259835580008926E /* Xcode.swift in Sources */,
|
||||
E8EE58C02E1CC2A50003FA9F /* RuntimeArchitecture.swift in Sources */,
|
||||
CAE4247F259A666100B8B246 /* MainWindow.swift in Sources */,
|
||||
CA452BB0259FD9770072DFA4 /* ProgressIndicator.swift in Sources */,
|
||||
B0403CF02AD92D7B00137C09 /* ReleaseNotesView.swift in Sources */,
|
||||
|
|
@ -919,6 +950,7 @@
|
|||
CA9FF84E2595079F00E47BAF /* ScrollingTextView.swift in Sources */,
|
||||
E832EAF82B0FBCF4001B570D /* RuntimeInstallationStepDetailView.swift in Sources */,
|
||||
CABFA9C12592EEEA00380FEE /* Version+.swift in Sources */,
|
||||
BDBAB7452B9FF55800694B0B /* TrailingIconLabelStyle.swift in Sources */,
|
||||
E8D655C0288DD04700A139C2 /* SelectedActionType.swift in Sources */,
|
||||
36741BFD291E4FDB00A85AAE /* DownloadPreferencePane.swift in Sources */,
|
||||
E84E4F542B333864003F3959 /* PlatformsListView.swift in Sources */,
|
||||
|
|
@ -1054,7 +1086,7 @@
|
|||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 12.0;
|
||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
|
|
@ -1074,7 +1106,7 @@
|
|||
CODE_SIGN_IDENTITY = "-";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 23;
|
||||
CURRENT_PROJECT_VERSION = 31;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"Xcodes/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_HARDENED_RUNTIME = NO;
|
||||
|
|
@ -1086,7 +1118,7 @@
|
|||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||
MARKETING_VERSION = 2.0.2;
|
||||
MARKETING_VERSION = 2.4.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.xcodesorg.xcodesapp;
|
||||
PRODUCT_NAME = Xcodes;
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
|
@ -1249,7 +1281,7 @@
|
|||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 12.0;
|
||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
|
|
@ -1307,7 +1339,7 @@
|
|||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 12.0;
|
||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
SDKROOT = macosx;
|
||||
|
|
@ -1326,7 +1358,7 @@
|
|||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 23;
|
||||
CURRENT_PROJECT_VERSION = 31;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"Xcodes/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = ZU6GR6B2FY;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
|
|
@ -1338,7 +1370,7 @@
|
|||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||
MARKETING_VERSION = 2.0.2;
|
||||
MARKETING_VERSION = 2.4.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.xcodesorg.xcodesapp;
|
||||
PRODUCT_NAME = Xcodes;
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
|
@ -1354,7 +1386,7 @@
|
|||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 23;
|
||||
CURRENT_PROJECT_VERSION = 31;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"Xcodes/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = ZU6GR6B2FY;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
|
|
@ -1366,7 +1398,7 @@
|
|||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||
MARKETING_VERSION = 2.0.2;
|
||||
MARKETING_VERSION = 2.4.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.xcodesorg.xcodesapp;
|
||||
PRODUCT_NAME = Xcodes;
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
|
@ -1464,6 +1496,14 @@
|
|||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCRemoteSwiftPackageReference section */
|
||||
33027E282CA8BB5800CB387C /* XCRemoteSwiftPackageReference "LibFido2Swift" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/kinoroy/LibFido2Swift.git";
|
||||
requirement = {
|
||||
kind = upToNextMinorVersion;
|
||||
minimumVersion = 0.1.4;
|
||||
};
|
||||
};
|
||||
CA9FF86B25951C6E00E47BAF /* XCRemoteSwiftPackageReference "data" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/xcodereleases/data";
|
||||
|
|
@ -1525,7 +1565,15 @@
|
|||
repositoryURL = "https://github.com/sindresorhus/DockProgress";
|
||||
requirement = {
|
||||
kind = upToNextMinorVersion;
|
||||
minimumVersion = 3.2.0;
|
||||
minimumVersion = 4.3.1;
|
||||
};
|
||||
};
|
||||
E83FDC422CBB649100679C6B /* XCRemoteSwiftPackageReference "Sparkle" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/sparkle-project/Sparkle";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 2.6.4;
|
||||
};
|
||||
};
|
||||
E84E4F552B335094003F3959 /* XCRemoteSwiftPackageReference "swift-collections" */ = {
|
||||
|
|
@ -1536,14 +1584,6 @@
|
|||
minimumVersion = 1.0.5;
|
||||
};
|
||||
};
|
||||
E891A1C22B43ACA400A1B9D1 /* XCRemoteSwiftPackageReference "Sparkle" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/sparkle-project/Sparkle";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 2.5.2;
|
||||
};
|
||||
};
|
||||
E8F44A1C296B4CD7002D6592 /* XCRemoteSwiftPackageReference "Path" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/mxcl/Path.swift";
|
||||
|
|
@ -1563,6 +1603,10 @@
|
|||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
334A932B2CA885A400A5E079 /* LibFido2Swift */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = LibFido2Swift;
|
||||
};
|
||||
CA9FF86C25951C6E00E47BAF /* XCModel */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = CA9FF86B25951C6E00E47BAF /* XCRemoteSwiftPackageReference "data" */;
|
||||
|
|
@ -1607,15 +1651,19 @@
|
|||
package = E689540125BE8C64000EBCEA /* XCRemoteSwiftPackageReference "DockProgress" */;
|
||||
productName = DockProgress;
|
||||
};
|
||||
E83FDC432CBB649100679C6B /* Sparkle */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = E83FDC422CBB649100679C6B /* XCRemoteSwiftPackageReference "Sparkle" */;
|
||||
productName = Sparkle;
|
||||
};
|
||||
E84E4F562B335094003F3959 /* OrderedCollections */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = E84E4F552B335094003F3959 /* XCRemoteSwiftPackageReference "swift-collections" */;
|
||||
productName = OrderedCollections;
|
||||
};
|
||||
E891A1C32B43ACF900A1B9D1 /* Sparkle */ = {
|
||||
E862D43A2CC8B26F00BAA376 /* SRP */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = E891A1C22B43ACA400A1B9D1 /* XCRemoteSwiftPackageReference "Sparkle" */;
|
||||
productName = Sparkle;
|
||||
productName = SRP;
|
||||
};
|
||||
E8C0EB19291EF43E0081528A /* XcodesKit */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
|
|
|
|||
|
|
@ -10,6 +10,15 @@
|
|||
"version": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "big-num",
|
||||
"repositoryURL": "https://github.com/adam-fowler/big-num",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "5c5511ad06aeb2b97d0868f7394e14a624bfb1c7",
|
||||
"version": "2.0.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "CombineExpectations",
|
||||
"repositoryURL": "https://github.com/groue/CombineExpectations",
|
||||
|
|
@ -33,8 +42,8 @@
|
|||
"repositoryURL": "https://github.com/sindresorhus/DockProgress",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "7100b68571e2dafe3a06ad5905b80fc3b0107b4b",
|
||||
"version": "3.2.0"
|
||||
"revision": "d4f23b5a8f5ca0fac393eb7ba78c2fe3e32e52da",
|
||||
"version": "4.3.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -64,6 +73,15 @@
|
|||
"version": "1.0.4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "LibFido2Swift",
|
||||
"repositoryURL": "https://github.com/kinoroy/LibFido2Swift.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "94d496d6f850dcbb3e8c4a27cd7eeabfad9f14e3",
|
||||
"version": "0.1.4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "Path.swift",
|
||||
"repositoryURL": "https://github.com/mxcl/Path.swift",
|
||||
|
|
@ -78,8 +96,8 @@
|
|||
"repositoryURL": "https://github.com/sparkle-project/Sparkle/",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "47d3d90aee3c52b6f61d04ceae426e607df62347",
|
||||
"version": "2.5.2"
|
||||
"revision": "0ef1ee0220239b3776f433314515fd849025673f",
|
||||
"version": "2.6.4"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -91,6 +109,24 @@
|
|||
"version": "1.0.5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "swift-crypto",
|
||||
"repositoryURL": "https://github.com/apple/swift-crypto",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "ddb07e896a2a8af79512543b1c7eb9797f8898a5",
|
||||
"version": "1.1.7"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "swift-srp",
|
||||
"repositoryURL": "https://github.com/xcodesOrg/swift-srp",
|
||||
"state": {
|
||||
"branch": "main",
|
||||
"revision": "543aa0122a0257b992f6c7d62d18a26e3dffb8fe",
|
||||
"version": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "SwiftSoup",
|
||||
"repositoryURL": "https://github.com/scinfu/SwiftSoup",
|
||||
|
|
|
|||
|
|
@ -1,24 +1,26 @@
|
|||
// swift-tools-version:5.3
|
||||
// swift-tools-version:5.7
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "AppleAPI",
|
||||
platforms: [.macOS(.v10_15)],
|
||||
platforms: [.macOS(.v11)],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||
.library(
|
||||
name: "AppleAPI",
|
||||
targets: ["AppleAPI"]),
|
||||
],
|
||||
dependencies: [],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/xcodesOrg/swift-srp", branch: "main")
|
||||
],
|
||||
targets: [
|
||||
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
||||
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
||||
.target(
|
||||
name: "AppleAPI",
|
||||
dependencies: []),
|
||||
dependencies: [.product(name: "SRP", package: "swift-srp")]),
|
||||
.testTarget(
|
||||
name: "AppleAPITests",
|
||||
dependencies: ["AppleAPI"]),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
import Foundation
|
||||
import Combine
|
||||
import SRP
|
||||
import Crypto
|
||||
import CommonCrypto
|
||||
|
||||
|
||||
public class Client {
|
||||
private static let authTypes = ["sa", "hsa", "non-sa", "hsa2"]
|
||||
|
|
@ -8,8 +12,12 @@ public class Client {
|
|||
|
||||
// MARK: - Login
|
||||
|
||||
public func login(accountName: String, password: String) -> AnyPublisher<AuthenticationState, Swift.Error> {
|
||||
public func srpLogin(accountName: String, password: String) -> AnyPublisher<AuthenticationState, Swift.Error> {
|
||||
var serviceKey: String!
|
||||
|
||||
let client = SRPClient(configuration: SRPConfiguration<SHA256>(.N2048))
|
||||
let clientKeys = client.generateKeys()
|
||||
let a = clientKeys.public
|
||||
|
||||
return Current.network.dataTask(with: URLRequest.itcServiceKey)
|
||||
.map(\.data)
|
||||
|
|
@ -24,11 +32,45 @@ public class Client {
|
|||
.map { return (serviceKey, $0)}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
.flatMap { (serviceKey, hashcash) -> AnyPublisher<URLSession.DataTaskPublisher.Output, Swift.Error> in
|
||||
.flatMap { (serviceKey, hashcash) -> AnyPublisher<(String, String, ServerSRPInitResponse), Swift.Error> in
|
||||
|
||||
return Current.network.dataTask(with: URLRequest.SRPInit(serviceKey: serviceKey, a: Data(a.bytes).base64EncodedString(), accountName: accountName))
|
||||
.map(\.data)
|
||||
.decode(type: ServerSRPInitResponse.self, decoder: JSONDecoder())
|
||||
.map { return (serviceKey, hashcash, $0) }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
.flatMap { (serviceKey, hashcash, srpInit) -> AnyPublisher<URLSession.DataTaskPublisher.Output, Swift.Error> in
|
||||
guard let decodedB = Data(base64Encoded: srpInit.b) else {
|
||||
return Fail(error: AuthenticationError.srpInvalidPublicKey)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
guard let decodedSalt = Data(base64Encoded: srpInit.salt) else {
|
||||
return Fail(error: AuthenticationError.srpInvalidPublicKey)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
let iterations = srpInit.iteration
|
||||
|
||||
do {
|
||||
guard let encryptedPassword = self.pbkdf2(password: password, saltData: decodedSalt, keyByteCount: 32, prf: CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA256), rounds: iterations, protocol: srpInit.protocol) else {
|
||||
return Fail(error: AuthenticationError.srpInvalidPublicKey)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
return Current.network.dataTask(with: URLRequest.signIn(serviceKey: serviceKey, accountName: accountName, password: password, hashcash: hashcash))
|
||||
let sharedSecret = try client.calculateSharedSecret(password: encryptedPassword, salt: [UInt8](decodedSalt), clientKeys: clientKeys, serverPublicKey: .init([UInt8](decodedB)))
|
||||
|
||||
let m1 = client.calculateClientProof(username: accountName, salt: [UInt8](decodedSalt), clientPublicKey: a, serverPublicKey: .init([UInt8](decodedB)), sharedSecret: .init(sharedSecret.bytes))
|
||||
let m2 = client.calculateServerProof(clientPublicKey: a, clientProof: m1, sharedSecret: .init([UInt8](sharedSecret.bytes)))
|
||||
|
||||
return Current.network.dataTask(with: URLRequest.SRPComplete(serviceKey: serviceKey, hashcash: hashcash, accountName: accountName, c: srpInit.c, m1: Data(m1).base64EncodedString(), m2: Data(m2).base64EncodedString()))
|
||||
.mapError { $0 as Swift.Error }
|
||||
.eraseToAnyPublisher()
|
||||
} catch {
|
||||
return Fail(error: AuthenticationError.srpInvalidPublicKey)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
.flatMap { result -> AnyPublisher<AuthenticationState, Swift.Error> in
|
||||
let (data, response) = result
|
||||
|
|
@ -118,7 +160,7 @@ public class Client {
|
|||
case .twoStep:
|
||||
return Fail(error: AuthenticationError.accountUsesTwoStepAuthentication)
|
||||
.eraseToAnyPublisher()
|
||||
case .twoFactor:
|
||||
case .twoFactor, .securityKey:
|
||||
return self.handleTwoFactor(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt, authOptions: authOptions)
|
||||
.eraseToAnyPublisher()
|
||||
case .unknown:
|
||||
|
|
@ -139,7 +181,10 @@ public class Client {
|
|||
// SMS wasn't sent automatically because user needs to choose a phone to send to
|
||||
} else if authOptions.canFallBackToSMS {
|
||||
option = .smsPendingChoice
|
||||
// Code is shown on trusted devices
|
||||
// Code is shown on trusted devices
|
||||
} else if authOptions.fsaChallenge != nil {
|
||||
option = .securityKey
|
||||
// User needs to use a physical security key to respond to the challenge
|
||||
} else {
|
||||
option = .codeSent
|
||||
}
|
||||
|
|
@ -193,6 +238,33 @@ public class Client {
|
|||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
public func submitChallenge(response: Data, sessionData: AppleSessionData) -> AnyPublisher<AuthenticationState, Error> {
|
||||
Result {
|
||||
URLRequest.respondToChallenge(serviceKey: sessionData.serviceKey, sessionID: sessionData.sessionID, scnt: sessionData.scnt, response: response)
|
||||
}
|
||||
.publisher
|
||||
.flatMap { request in
|
||||
Current.network.dataTask(with: request)
|
||||
.mapError { $0 as Error }
|
||||
.tryMap { (data, response) throws -> (Data, URLResponse) in
|
||||
guard let urlResponse = response as? HTTPURLResponse else { return (data, response) }
|
||||
switch urlResponse.statusCode {
|
||||
case 200..<300:
|
||||
return (data, urlResponse)
|
||||
case 400, 401:
|
||||
throw AuthenticationError.incorrectSecurityCode
|
||||
case 412:
|
||||
throw AuthenticationError.appleIDAndPrivacyAcknowledgementRequired
|
||||
case let code:
|
||||
throw AuthenticationError.badStatusCode(statusCode: code, data: data, response: urlResponse)
|
||||
}
|
||||
}
|
||||
.flatMap { (data, response) -> AnyPublisher<AuthenticationState, Error> in
|
||||
self.updateSession(serviceKey: sessionData.serviceKey, sessionID: sessionData.sessionID, scnt: sessionData.scnt)
|
||||
}
|
||||
}.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
// MARK: - Session
|
||||
|
||||
/// Use the olympus session endpoint to see if the existing session is still valid
|
||||
|
|
@ -227,6 +299,49 @@ public class Client {
|
|||
.mapError { $0 as Error }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func sha256(data : Data) -> Data {
|
||||
var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
|
||||
data.withUnsafeBytes {
|
||||
_ = CC_SHA256($0.baseAddress, CC_LONG(data.count), &hash)
|
||||
}
|
||||
return Data(hash)
|
||||
}
|
||||
|
||||
private func pbkdf2(password: String, saltData: Data, keyByteCount: Int, prf: CCPseudoRandomAlgorithm, rounds: Int, protocol srpProtocol: SRPProtocol) -> Data? {
|
||||
guard let passwordData = password.data(using: .utf8) else { return nil }
|
||||
let hashedPasswordDataRaw = sha256(data: passwordData)
|
||||
let hashedPasswordData = switch srpProtocol {
|
||||
case .s2k: hashedPasswordDataRaw
|
||||
// the legacy s2k_fo protocol requires hex-encoding the digest before performing PBKDF2.
|
||||
case .s2k_fo: Data(hashedPasswordDataRaw.hexEncodedString().lowercased().utf8)
|
||||
}
|
||||
|
||||
var derivedKeyData = Data(repeating: 0, count: keyByteCount)
|
||||
let derivedCount = derivedKeyData.count
|
||||
let derivationStatus: Int32 = derivedKeyData.withUnsafeMutableBytes { derivedKeyBytes in
|
||||
let keyBuffer: UnsafeMutablePointer<UInt8> =
|
||||
derivedKeyBytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
|
||||
return saltData.withUnsafeBytes { saltBytes -> Int32 in
|
||||
let saltBuffer: UnsafePointer<UInt8> = saltBytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
|
||||
return hashedPasswordData.withUnsafeBytes { hashedPasswordBytes -> Int32 in
|
||||
let passwordBuffer: UnsafePointer<UInt8> = hashedPasswordBytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
|
||||
return CCKeyDerivationPBKDF(
|
||||
CCPBKDFAlgorithm(kCCPBKDF2),
|
||||
passwordBuffer,
|
||||
hashedPasswordData.count,
|
||||
saltBuffer,
|
||||
saltData.count,
|
||||
prf,
|
||||
UInt32(rounds),
|
||||
keyBuffer,
|
||||
derivedCount)
|
||||
}
|
||||
}
|
||||
}
|
||||
return derivationStatus == kCCSuccess ? derivedKeyData : nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Types
|
||||
|
|
@ -252,6 +367,7 @@ public enum AuthenticationError: Swift.Error, LocalizedError, Equatable {
|
|||
case notDeveloperAppleId
|
||||
case notAuthorized
|
||||
case invalidResult(resultString: String?)
|
||||
case srpInvalidPublicKey
|
||||
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
|
|
@ -286,6 +402,8 @@ public enum AuthenticationError: Swift.Error, LocalizedError, Equatable {
|
|||
return "You are not authorized. Please Sign in with your Apple ID first."
|
||||
case let .invalidResult(resultString):
|
||||
return resultString ?? "If you continue to have problems, please submit a bug report in the Help menu."
|
||||
case .srpInvalidPublicKey:
|
||||
return "Invalid Key"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -326,27 +444,37 @@ public enum TwoFactorOption: Equatable {
|
|||
case smsSent(AuthOptionsResponse.TrustedPhoneNumber)
|
||||
case codeSent
|
||||
case smsPendingChoice
|
||||
case securityKey
|
||||
}
|
||||
|
||||
public struct FSAChallenge: Equatable, Decodable {
|
||||
public let challenge: String
|
||||
public let keyHandles: [String]
|
||||
public let allowedCredentials: String
|
||||
}
|
||||
|
||||
public struct AuthOptionsResponse: Equatable, Decodable {
|
||||
public let trustedPhoneNumbers: [TrustedPhoneNumber]?
|
||||
public let trustedDevices: [TrustedDevice]?
|
||||
public let securityCode: SecurityCodeInfo
|
||||
public let securityCode: SecurityCodeInfo?
|
||||
public let noTrustedDevices: Bool?
|
||||
public let serviceErrors: [ServiceError]?
|
||||
public let fsaChallenge: FSAChallenge?
|
||||
|
||||
public init(
|
||||
trustedPhoneNumbers: [AuthOptionsResponse.TrustedPhoneNumber]?,
|
||||
trustedDevices: [AuthOptionsResponse.TrustedDevice]?,
|
||||
securityCode: AuthOptionsResponse.SecurityCodeInfo,
|
||||
noTrustedDevices: Bool? = nil,
|
||||
serviceErrors: [ServiceError]? = nil
|
||||
serviceErrors: [ServiceError]? = nil,
|
||||
fsaChallenge: FSAChallenge? = nil
|
||||
) {
|
||||
self.trustedPhoneNumbers = trustedPhoneNumbers
|
||||
self.trustedDevices = trustedDevices
|
||||
self.securityCode = securityCode
|
||||
self.noTrustedDevices = noTrustedDevices
|
||||
self.serviceErrors = serviceErrors
|
||||
self.fsaChallenge = fsaChallenge
|
||||
}
|
||||
|
||||
public var kind: Kind {
|
||||
|
|
@ -354,6 +482,8 @@ public struct AuthOptionsResponse: Equatable, Decodable {
|
|||
return .twoStep
|
||||
} else if trustedPhoneNumbers != nil {
|
||||
return .twoFactor
|
||||
} else if fsaChallenge != nil {
|
||||
return .securityKey
|
||||
} else {
|
||||
return .unknown
|
||||
}
|
||||
|
|
@ -416,7 +546,7 @@ public struct AuthOptionsResponse: Equatable, Decodable {
|
|||
}
|
||||
|
||||
public enum Kind: Equatable {
|
||||
case twoStep, twoFactor, unknown
|
||||
case twoStep, twoFactor, securityKey, unknown
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -453,3 +583,24 @@ public struct AppleProvider: Decodable, Equatable {
|
|||
public struct AppleUser: Decodable, Equatable {
|
||||
public let fullName: String
|
||||
}
|
||||
|
||||
public struct ServerSRPInitResponse: Decodable {
|
||||
let iteration: Int
|
||||
let salt: String
|
||||
let b: String
|
||||
let c: String
|
||||
let `protocol`: SRPProtocol
|
||||
}
|
||||
|
||||
|
||||
|
||||
extension String {
|
||||
func base64ToU8Array() -> Data {
|
||||
return Data(base64Encoded: self) ?? Data()
|
||||
}
|
||||
}
|
||||
extension Data {
|
||||
func hexEncodedString() -> String {
|
||||
return map { String(format: "%02hhx", $0) }.joined()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,11 @@ public extension URL {
|
|||
static let trust = URL(string: "https://idmsa.apple.com/appleauth/auth/2sv/trust")!
|
||||
static let federate = URL(string: "https://idmsa.apple.com/appleauth/auth/federate")!
|
||||
static let olympusSession = URL(string: "https://appstoreconnect.apple.com/olympus/v1/session")!
|
||||
static let keyAuth = URL(string: "https://idmsa.apple.com/appleauth/auth/verify/security/key")!
|
||||
|
||||
static let srpInit = URL(string: "https://idmsa.apple.com/appleauth/auth/signin/init")!
|
||||
static let srpComplete = URL(string: "https://idmsa.apple.com/appleauth/auth/signin/complete?isRememberMeEnabled=false")!
|
||||
|
||||
}
|
||||
|
||||
public extension URLRequest {
|
||||
|
|
@ -105,6 +110,19 @@ public extension URLRequest {
|
|||
}
|
||||
return request
|
||||
}
|
||||
|
||||
static func respondToChallenge(serviceKey: String, sessionID: String, scnt: String, response: Data) -> URLRequest {
|
||||
var request = URLRequest(url: .keyAuth)
|
||||
request.allHTTPHeaderFields = request.allHTTPHeaderFields ?? [:]
|
||||
request.allHTTPHeaderFields?["X-Apple-ID-Session-Id"] = sessionID
|
||||
request.allHTTPHeaderFields?["X-Apple-Widget-Key"] = serviceKey
|
||||
request.allHTTPHeaderFields?["scnt"] = scnt
|
||||
request.allHTTPHeaderFields?["Accept"] = "application/json"
|
||||
request.allHTTPHeaderFields?["Content-Type"] = "application/json"
|
||||
request.httpMethod = "POST"
|
||||
request.httpBody = response
|
||||
return request
|
||||
}
|
||||
|
||||
static func trust(serviceKey: String, sessionID: String, scnt: String) -> URLRequest {
|
||||
var request = URLRequest(url: .trust)
|
||||
|
|
@ -136,4 +154,51 @@ public extension URLRequest {
|
|||
|
||||
return request
|
||||
}
|
||||
|
||||
static func SRPInit(serviceKey: String, a: String, accountName: String) -> URLRequest {
|
||||
struct ServerSRPInitRequest: Encodable {
|
||||
public let a: String
|
||||
public let accountName: String
|
||||
public let protocols: [SRPProtocol]
|
||||
}
|
||||
|
||||
var request = URLRequest(url: .srpInit)
|
||||
request.httpMethod = "POST"
|
||||
request.allHTTPHeaderFields = request.allHTTPHeaderFields ?? [:]
|
||||
request.allHTTPHeaderFields?["Accept"] = "application/json"
|
||||
request.allHTTPHeaderFields?["Content-Type"] = "application/json"
|
||||
request.allHTTPHeaderFields?["X-Requested-With"] = "XMLHttpRequest"
|
||||
request.allHTTPHeaderFields?["X-Apple-Widget-Key"] = serviceKey
|
||||
|
||||
request.httpBody = try? JSONEncoder().encode(ServerSRPInitRequest(a: a, accountName: accountName, protocols: [.s2k, .s2k_fo]))
|
||||
return request
|
||||
}
|
||||
|
||||
static func SRPComplete(serviceKey: String, hashcash: String, accountName: String, c: String, m1: String, m2: String) -> URLRequest {
|
||||
struct ServerSRPCompleteRequest: Encodable {
|
||||
let accountName: String
|
||||
let c: String
|
||||
let m1: String
|
||||
let m2: String
|
||||
let rememberMe: Bool
|
||||
}
|
||||
|
||||
var request = URLRequest(url: .srpComplete)
|
||||
request.httpMethod = "POST"
|
||||
request.allHTTPHeaderFields = request.allHTTPHeaderFields ?? [:]
|
||||
request.allHTTPHeaderFields?["Accept"] = "application/json"
|
||||
request.allHTTPHeaderFields?["Content-Type"] = "application/json"
|
||||
request.allHTTPHeaderFields?["X-Requested-With"] = "XMLHttpRequest"
|
||||
request.allHTTPHeaderFields?["X-Apple-Widget-Key"] = serviceKey
|
||||
request.allHTTPHeaderFields?["X-Apple-HC"] = hashcash
|
||||
|
||||
request.httpBody = try? JSONEncoder().encode(ServerSRPCompleteRequest(accountName: accountName, c: c, m1: m1, m2: m2, rememberMe: false))
|
||||
return request
|
||||
}
|
||||
}
|
||||
|
||||
public enum SRPProtocol: String, Codable {
|
||||
case s2k, s2k_fo
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ extension AppState {
|
|||
|
||||
// check to see if we should auto install for the user
|
||||
public func autoInstallIfNeeded() {
|
||||
guard let storageValue = UserDefaults.standard.object(forKey: "autoInstallation") as? Int, let autoInstallType = AutoInstallationType(rawValue: storageValue) else { return }
|
||||
|
||||
guard let storageValue = Current.defaults.get(forKey: "autoInstallation") as? Int, let autoInstallType = AutoInstallationType(rawValue: storageValue) else { return }
|
||||
|
||||
if autoInstallType == .none { return }
|
||||
|
||||
// get newest xcode version
|
||||
|
|
@ -227,6 +227,7 @@ extension AppState {
|
|||
self.error = error
|
||||
self.presentedAlert = .generic(title: localizeString("Alert.InstallArchive.Error.Title"), message: error.legibleLocalizedDescription)
|
||||
}
|
||||
resetDockProgressTracking()
|
||||
})
|
||||
.catch { _ in
|
||||
Just(installedXcode)
|
||||
|
|
@ -473,19 +474,24 @@ extension AppState {
|
|||
// MARK: - Dock Progress Tracking
|
||||
|
||||
private func setupDockProgress() {
|
||||
DockProgress.progressInstance = nil
|
||||
DockProgress.style = .bar
|
||||
Task { @MainActor in
|
||||
DockProgress.progressInstance = nil
|
||||
DockProgress.style = .bar
|
||||
|
||||
let progress = Progress(totalUnitCount: AppState.totalProgressUnits)
|
||||
progress.kind = .file
|
||||
progress.fileOperationKind = .downloading
|
||||
overallProgress = progress
|
||||
|
||||
DockProgress.progressInstance = overallProgress
|
||||
}
|
||||
|
||||
let progress = Progress(totalUnitCount: AppState.totalProgressUnits)
|
||||
progress.kind = .file
|
||||
progress.fileOperationKind = .downloading
|
||||
overallProgress = progress
|
||||
|
||||
DockProgress.progressInstance = overallProgress
|
||||
}
|
||||
|
||||
func resetDockProgressTracking() {
|
||||
DockProgress.progress = 1 // Only way to completely remove overlay with DockProgress is setting progress to complete
|
||||
Task { @MainActor in
|
||||
DockProgress.progress = 1 // Only way to completely remove overlay with DockProgress is setting progress to complete
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import OSLog
|
|||
import Combine
|
||||
import Path
|
||||
import AppleAPI
|
||||
import Version
|
||||
|
||||
extension AppState {
|
||||
func updateDownloadableRuntimes() {
|
||||
|
|
@ -15,10 +16,10 @@ extension AppState {
|
|||
var updatedRuntime = runtime
|
||||
|
||||
// This loops through and matches up the simulatorVersion to the mappings
|
||||
let simulatorBuildUpdate = downloadableRuntimes.sdkToSimulatorMappings.first { SDKToSimulatorMapping in
|
||||
let simulatorBuildUpdate = downloadableRuntimes.sdkToSimulatorMappings.filter { SDKToSimulatorMapping in
|
||||
SDKToSimulatorMapping.simulatorBuildUpdate == runtime.simulatorVersion.buildUpdate
|
||||
}
|
||||
updatedRuntime.sdkBuildUpdate = simulatorBuildUpdate?.sdkBuildUpdate
|
||||
updatedRuntime.sdkBuildUpdate = simulatorBuildUpdate.map { $0.sdkBuildUpdate }
|
||||
return updatedRuntime
|
||||
}
|
||||
|
||||
|
|
@ -48,6 +49,73 @@ extension AppState {
|
|||
}
|
||||
|
||||
func downloadRuntime(runtime: DownloadableRuntime) {
|
||||
guard let selectedXcode = self.allXcodes.first(where: { $0.selected }) else {
|
||||
Logger.appState.error("No selected Xcode")
|
||||
DispatchQueue.main.async {
|
||||
self.presentedAlert = .generic(title: localizeString("Alert.Install.Error.Title"), message: "No selected Xcode. Please make an Xcode active")
|
||||
}
|
||||
return
|
||||
}
|
||||
// new runtimes
|
||||
if runtime.contentType == .cryptexDiskImage {
|
||||
// only selected xcodes > 16.1 beta 3 can download runtimes via a xcodebuild -downloadPlatform version
|
||||
// only Runtimes coming from cryptexDiskImage can be downloaded via xcodebuild
|
||||
if selectedXcode.version > Version(major: 16, minor: 0, patch: 0) {
|
||||
downloadRuntimeViaXcodeBuild(runtime: runtime)
|
||||
} else {
|
||||
// not supported
|
||||
Logger.appState.error("Trying to download a runtime we can't download")
|
||||
DispatchQueue.main.async {
|
||||
self.presentedAlert = .generic(title: localizeString("Alert.Install.Error.Title"), message: localizeString("Alert.Install.Error.Need.Xcode16.1"))
|
||||
}
|
||||
return
|
||||
}
|
||||
} else {
|
||||
downloadRuntimeObseleteWay(runtime: runtime)
|
||||
}
|
||||
}
|
||||
|
||||
func downloadRuntimeViaXcodeBuild(runtime: DownloadableRuntime) {
|
||||
|
||||
let downloadRuntimeTask = Current.shell.downloadRuntime(runtime.platform.shortName, runtime.simulatorVersion.buildUpdate)
|
||||
runtimePublishers[runtime.identifier] = Task { [weak self] in
|
||||
guard let self = self else { return }
|
||||
do {
|
||||
for try await progress in downloadRuntimeTask {
|
||||
if progress.isIndeterminate {
|
||||
DispatchQueue.main.async {
|
||||
self.setInstallationStep(of: runtime, to: .installing, postNotification: false)
|
||||
}
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
self.setInstallationStep(of: runtime, to: .downloading(progress: progress), postNotification: false)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Logger.appState.debug("Done downloading runtime - \(runtime.name)")
|
||||
|
||||
DispatchQueue.main.async {
|
||||
guard let index = self.downloadableRuntimes.firstIndex(where: { $0.identifier == runtime.identifier }) else { return }
|
||||
self.downloadableRuntimes[index].installState = .installed
|
||||
self.update()
|
||||
}
|
||||
|
||||
} catch {
|
||||
Logger.appState.error("Error downloading runtime: \(error.localizedDescription)")
|
||||
DispatchQueue.main.async {
|
||||
self.error = error
|
||||
if let error = error as? String {
|
||||
self.presentedAlert = .generic(title: localizeString("Alert.Install.Error.Title"), message: error)
|
||||
} else {
|
||||
self.presentedAlert = .generic(title: localizeString("Alert.Install.Error.Title"), message: error.legibleLocalizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func downloadRuntimeObseleteWay(runtime: DownloadableRuntime) {
|
||||
runtimePublishers[runtime.identifier] = Task {
|
||||
do {
|
||||
let downloadedURL = try await downloadRunTimeFull(runtime: runtime)
|
||||
|
|
@ -57,6 +125,9 @@ extension AppState {
|
|||
self.setInstallationStep(of: runtime, to: .installing)
|
||||
}
|
||||
switch runtime.contentType {
|
||||
case .cryptexDiskImage:
|
||||
// not supported yet (do we need to for old packages?)
|
||||
throw "Installing via cryptexDiskImage not support - please install manually from \(downloadedURL.description)"
|
||||
case .package:
|
||||
// not supported yet (do we need to for old packages?)
|
||||
throw "Installing via package not support - please install manually from \(downloadedURL.description)"
|
||||
|
|
@ -80,19 +151,31 @@ extension AppState {
|
|||
Logger.appState.error("Error downloading runtime: \(error.localizedDescription)")
|
||||
DispatchQueue.main.async {
|
||||
self.error = error
|
||||
self.presentedAlert = .generic(title: localizeString("Alert.Install.Error.Title"), message: error.legibleLocalizedDescription)
|
||||
if let error = error as? String {
|
||||
self.presentedAlert = .generic(title: localizeString("Alert.Install.Error.Title"), message: error)
|
||||
} else {
|
||||
self.presentedAlert = .generic(title: localizeString("Alert.Install.Error.Title"), message: error.legibleLocalizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func downloadRunTimeFull(runtime: DownloadableRuntime) async throws -> URL {
|
||||
guard let source = runtime.source else {
|
||||
throw "Invalid runtime source"
|
||||
}
|
||||
|
||||
guard let downloadPath = runtime.downloadPath else {
|
||||
throw "Invalid runtime downloadPath"
|
||||
}
|
||||
|
||||
// sets a proper cookie for runtimes
|
||||
try await validateADCSession(path: runtime.downloadPath)
|
||||
try await validateADCSession(path: downloadPath)
|
||||
|
||||
let downloader = Downloader(rawValue: UserDefaults.standard.string(forKey: "downloader") ?? "aria2") ?? .aria2
|
||||
let downloader = Downloader(rawValue: Current.defaults.string(forKey: "downloader") ?? "aria2") ?? .aria2
|
||||
|
||||
let url = URL(string: runtime.source)!
|
||||
let url = URL(string: source)!
|
||||
let expectedRuntimePath = Path.xcodesApplicationSupport/"\(url.lastPathComponent)"
|
||||
// aria2 downloads directly to the destination (instead of into /tmp first) so we need to make sure that the download isn't incomplete
|
||||
let aria2DownloadMetadataPath = expectedRuntimePath.parent/(expectedRuntimePath.basename() + ".aria2")
|
||||
|
|
@ -123,9 +206,15 @@ extension AppState {
|
|||
}
|
||||
|
||||
public func downloadRuntimeWithAria2(_ runtime: DownloadableRuntime, to destination: Path, aria2Path: Path) -> AsyncThrowingStream<Progress, Error> {
|
||||
let cookies = AppleAPI.Current.network.session.configuration.httpCookieStorage?.cookies(for: runtime.url) ?? []
|
||||
guard let url = runtime.url else {
|
||||
return AsyncThrowingStream<Progress, Error> { continuation in
|
||||
continuation.finish(throwing: "Invalid or non existant runtime url")
|
||||
}
|
||||
}
|
||||
|
||||
let cookies = AppleAPI.Current.network.session.configuration.httpCookieStorage?.cookies(for: url) ?? []
|
||||
|
||||
return Current.shell.downloadWithAria2Async(aria2Path, runtime.url, destination, cookies)
|
||||
return Current.shell.downloadWithAria2Async(aria2Path, url, destination, cookies)
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -140,7 +229,10 @@ extension AppState {
|
|||
runtimePublishers[runtime.identifier] = nil
|
||||
|
||||
// If the download is cancelled by the user, clean up the download files that aria2 creates.
|
||||
let url = URL(string: runtime.source)!
|
||||
guard let source = runtime.source else {
|
||||
return
|
||||
}
|
||||
let url = URL(string: source)!
|
||||
let expectedRuntimePath = Path.xcodesApplicationSupport/"\(url.lastPathComponent)"
|
||||
let aria2DownloadMetadataPath = expectedRuntimePath.parent/(expectedRuntimePath.basename() + ".aria2")
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,26 @@ import Version
|
|||
import os.log
|
||||
import DockProgress
|
||||
import XcodesKit
|
||||
import LibFido2Swift
|
||||
|
||||
enum PreferenceKey: String {
|
||||
case installPath
|
||||
case localPath
|
||||
case unxipExperiment
|
||||
case createSymLinkOnSelect
|
||||
case onSelectActionType
|
||||
case showOpenInRosettaOption
|
||||
case autoInstallation
|
||||
case SUEnableAutomaticChecks
|
||||
case includePrereleaseVersions
|
||||
case downloader
|
||||
case dataSource
|
||||
case xcodeListCategory
|
||||
case allowedMajorVersions
|
||||
case hideSupportXcodes
|
||||
|
||||
func isManaged() -> Bool { UserDefaults.standard.objectIsForced(forKey: self.rawValue) }
|
||||
}
|
||||
|
||||
class AppState: ObservableObject {
|
||||
private let client = AppleAPI.Client()
|
||||
|
|
@ -66,18 +86,24 @@ class AppState: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
var disableLocalPathChange: Bool { PreferenceKey.localPath.isManaged() }
|
||||
|
||||
@Published var installPath = "" {
|
||||
didSet {
|
||||
Current.defaults.set(installPath, forKey: "installPath")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var disableInstallPathChange: Bool { PreferenceKey.installPath.isManaged() }
|
||||
|
||||
@Published var unxipExperiment = false {
|
||||
didSet {
|
||||
Current.defaults.set(unxipExperiment, forKey: "unxipExperiment")
|
||||
}
|
||||
}
|
||||
|
||||
var disableUnxipExperiment: Bool { PreferenceKey.unxipExperiment.isManaged() }
|
||||
|
||||
@Published var createSymLinkOnSelect = false {
|
||||
didSet {
|
||||
Current.defaults.set(createSymLinkOnSelect, forKey: "createSymLinkOnSelect")
|
||||
|
|
@ -85,7 +111,7 @@ class AppState: ObservableObject {
|
|||
}
|
||||
|
||||
var createSymLinkOnSelectDisabled: Bool {
|
||||
return onSelectActionType == .rename
|
||||
return onSelectActionType == .rename || PreferenceKey.createSymLinkOnSelect.isManaged()
|
||||
}
|
||||
|
||||
@Published var onSelectActionType = SelectedActionType.none {
|
||||
|
|
@ -98,12 +124,20 @@ class AppState: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
var onSelectActionTypeDisabled: Bool { PreferenceKey.onSelectActionType.isManaged() }
|
||||
|
||||
@Published var showOpenInRosettaOption = false {
|
||||
didSet {
|
||||
Current.defaults.set(showOpenInRosettaOption, forKey: "showOpenInRosettaOption")
|
||||
}
|
||||
}
|
||||
|
||||
@Published var terminateAfterLastWindowClosed = false {
|
||||
didSet {
|
||||
Current.defaults.set(terminateAfterLastWindowClosed, forKey: "terminateAfterLastWindowClosed")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Runtimes
|
||||
|
||||
@Published var downloadableRuntimes: [DownloadableRuntime] = []
|
||||
|
|
@ -173,13 +207,14 @@ class AppState: ObservableObject {
|
|||
onSelectActionType = SelectedActionType(rawValue: Current.defaults.string(forKey: "onSelectActionType") ?? "none") ?? .none
|
||||
installPath = Current.defaults.string(forKey: "installPath") ?? Path.defaultInstallDirectory.string
|
||||
showOpenInRosettaOption = Current.defaults.bool(forKey: "showOpenInRosettaOption") ?? false
|
||||
terminateAfterLastWindowClosed = Current.defaults.bool(forKey: "terminateAfterLastWindowClosed") ?? false
|
||||
}
|
||||
|
||||
// MARK: Timer
|
||||
/// Runs a timer every 6 hours when app is open to check if it needs to auto install any xcodes
|
||||
func setupAutoInstallTimer() {
|
||||
guard let storageValue = UserDefaults.standard.object(forKey: "autoInstallation") as? Int, let autoInstallType = AutoInstallationType(rawValue: storageValue) else { return }
|
||||
|
||||
guard let storageValue = Current.defaults.get(forKey: "autoInstallation") as? Int, let autoInstallType = AutoInstallationType(rawValue: storageValue) else { return }
|
||||
|
||||
if autoInstallType == .none { return }
|
||||
|
||||
autoInstallTimer = Timer.scheduledTimer(withTimeInterval: 60*60*6, repeats: true) { [weak self] _ in
|
||||
|
|
@ -242,7 +277,7 @@ class AppState: ObservableObject {
|
|||
|
||||
func signIn(username: String, password: String) {
|
||||
authError = nil
|
||||
signIn(username: username, password: password)
|
||||
signIn(username: username.lowercased(), password: password)
|
||||
.sink(
|
||||
receiveCompletion: { _ in },
|
||||
receiveValue: { _ in }
|
||||
|
|
@ -255,7 +290,7 @@ class AppState: ObservableObject {
|
|||
Current.defaults.set(username, forKey: "username")
|
||||
|
||||
isProcessingAuthRequest = true
|
||||
return client.login(accountName: username, password: password)
|
||||
return client.srpLogin(accountName: username, password: password)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.handleEvents(
|
||||
receiveOutput: { authenticationState in
|
||||
|
|
@ -270,11 +305,17 @@ class AppState: ObservableObject {
|
|||
}
|
||||
|
||||
func handleTwoFactorOption(_ option: TwoFactorOption, authOptions: AuthOptionsResponse, serviceKey: String, sessionID: String, scnt: String) {
|
||||
self.presentedSheet = .twoFactor(.init(
|
||||
option: option,
|
||||
authOptions: authOptions,
|
||||
sessionData: AppleSessionData(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt)
|
||||
))
|
||||
let sessionData = AppleSessionData(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt)
|
||||
|
||||
if option == .securityKey, fido2DeviceIsPresent() && !fido2DeviceNeedsPin() {
|
||||
createAndSubmitSecurityKeyAssertationWithPinCode(nil, sessionData: sessionData, authOptions: authOptions)
|
||||
} else {
|
||||
self.presentedSheet = .twoFactor(.init(
|
||||
option: option,
|
||||
authOptions: authOptions,
|
||||
sessionData: sessionData
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
func requestSMS(to trustedPhoneNumber: AuthOptionsResponse.TrustedPhoneNumber, authOptions: AuthOptionsResponse, sessionData: AppleSessionData) {
|
||||
|
|
@ -320,6 +361,83 @@ class AppState: ObservableObject {
|
|||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
private lazy var fido2 = FIDO2()
|
||||
|
||||
func createAndSubmitSecurityKeyAssertationWithPinCode(_ pinCode: String?, sessionData: AppleSessionData, authOptions: AuthOptionsResponse) {
|
||||
self.presentedSheet = .securityKeyTouchToConfirm
|
||||
|
||||
guard let fsaChallenge = authOptions.fsaChallenge else {
|
||||
// This shouldn't happen
|
||||
// we shouldn't have called this method without setting the fsaChallenge
|
||||
// so this is an assertionFailure
|
||||
assertionFailure()
|
||||
self.authError = "Something went wrong. Please file a bug report"
|
||||
return
|
||||
}
|
||||
|
||||
// The challenge is encoded in Base64URL encoding
|
||||
let challengeUrl = fsaChallenge.challenge
|
||||
let challenge = FIDO2.base64urlToBase64(base64url: challengeUrl)
|
||||
let origin = "https://idmsa.apple.com"
|
||||
let rpId = "apple.com"
|
||||
// Allowed creds is sent as a comma separated string
|
||||
let validCreds = fsaChallenge.allowedCredentials.split(separator: ",").map(String.init)
|
||||
|
||||
Task {
|
||||
do {
|
||||
let response = try fido2.respondToChallenge(args: ChallengeArgs(rpId: rpId, validCredentials: validCreds, devPin: pinCode, challenge: challenge, origin: origin))
|
||||
|
||||
Task { @MainActor in
|
||||
self.isProcessingAuthRequest = true
|
||||
}
|
||||
|
||||
let respData = try JSONEncoder().encode(response)
|
||||
client.submitChallenge(response: respData, sessionData: AppleSessionData(serviceKey: sessionData.serviceKey, sessionID: sessionData.sessionID, scnt: sessionData.scnt))
|
||||
.receive(on: DispatchQueue.main)
|
||||
.handleEvents(
|
||||
receiveOutput: { authenticationState in
|
||||
self.authenticationState = authenticationState
|
||||
},
|
||||
receiveCompletion: { completion in
|
||||
self.handleAuthenticationFlowCompletion(completion)
|
||||
self.isProcessingAuthRequest = false
|
||||
}
|
||||
).sink(
|
||||
receiveCompletion: { _ in },
|
||||
receiveValue: { _ in }
|
||||
).store(in: &cancellables)
|
||||
} catch FIDO2Error.canceledByUser {
|
||||
// User cancelled the auth flow
|
||||
// we don't have to show an error
|
||||
// because the sheet will already be dismissed
|
||||
} catch {
|
||||
Task { @MainActor in
|
||||
authError = error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fido2DeviceIsPresent() -> Bool {
|
||||
fido2.hasDeviceAttached()
|
||||
}
|
||||
|
||||
func fido2DeviceNeedsPin() -> Bool {
|
||||
do {
|
||||
return try fido2.deviceHasPin()
|
||||
} catch {
|
||||
Task { @MainActor in
|
||||
authError = error
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func cancelSecurityKeyAssertationRequest() {
|
||||
self.fido2.cancel()
|
||||
}
|
||||
|
||||
private func handleAuthenticationFlowCompletion(_ completion: Subscribers.Completion<Error>) {
|
||||
switch completion {
|
||||
case let .failure(error):
|
||||
|
|
@ -408,7 +526,7 @@ class AppState: ObservableObject {
|
|||
func checkMinVersionAndInstall(id: Xcode.ID) {
|
||||
guard let availableXcode = availableXcodes.first(where: { $0.version == id }) else { return }
|
||||
|
||||
// Check to see if users MacOS is supported
|
||||
// Check to see if users macOS is supported
|
||||
if let requiredMacOSVersion = availableXcode.requiredMacOSVersion {
|
||||
if hasMinSupportedOS(requiredMacOSVersion: requiredMacOSVersion) {
|
||||
// prompt
|
||||
|
|
@ -436,6 +554,11 @@ class AppState: ObservableObject {
|
|||
guard let availableXcode = availableXcodes.first(where: { $0.version == id }) else { return }
|
||||
|
||||
installationPublishers[id] = signInIfNeeded()
|
||||
.handleEvents(
|
||||
receiveSubscription: { [unowned self] _ in
|
||||
self.setInstallationStep(of: availableXcode.version, to: .authenticating)
|
||||
}
|
||||
)
|
||||
.flatMap { [unowned self] in
|
||||
// signInIfNeeded might finish before the user actually authenticates if UI is involved.
|
||||
// This publisher will wait for the @Published authentication state to change to authenticated or unauthenticated before finishing,
|
||||
|
|
@ -479,7 +602,7 @@ class AppState: ObservableObject {
|
|||
.mapError { $0 as Error }
|
||||
}
|
||||
.flatMap { [unowned self] in
|
||||
self.install(.version(availableXcode), downloader: Downloader(rawValue: UserDefaults.standard.string(forKey: "downloader") ?? "aria2") ?? .aria2)
|
||||
self.install(.version(availableXcode), downloader: Downloader(rawValue: Current.defaults.string(forKey: "downloader") ?? "aria2") ?? .aria2)
|
||||
}
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink(
|
||||
|
|
@ -505,7 +628,7 @@ class AppState: ObservableObject {
|
|||
func installWithoutLogin(id: Xcode.ID) {
|
||||
guard let availableXcode = availableXcodes.first(where: { $0.version == id }) else { return }
|
||||
|
||||
installationPublishers[id] = self.install(.version(availableXcode), downloader: Downloader(rawValue: UserDefaults.standard.string(forKey: "downloader") ?? "aria2") ?? .aria2)
|
||||
installationPublishers[id] = self.install(.version(availableXcode), downloader: Downloader(rawValue: Current.defaults.string(forKey: "downloader") ?? "aria2") ?? .aria2)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink(
|
||||
receiveCompletion: { [unowned self] completion in
|
||||
|
|
|
|||
|
|
@ -14,4 +14,6 @@ public enum DataSource: String, CaseIterable, Identifiable, CustomStringConverti
|
|||
case .xcodeReleases: return "Xcode Releases"
|
||||
}
|
||||
}
|
||||
|
||||
var isManaged: Bool { PreferenceKey.dataSource.isManaged() }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,4 +13,6 @@ public enum Downloader: String, CaseIterable, Identifiable, CustomStringConverti
|
|||
case .aria2: return "aria2"
|
||||
}
|
||||
}
|
||||
|
||||
var isManaged: Bool { PreferenceKey.downloader.isManaged() }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -116,7 +116,8 @@ public struct Shell {
|
|||
return AsyncThrowingStream<Progress, Error> { continuation in
|
||||
|
||||
Task {
|
||||
var progress = Progress()
|
||||
// Assume progress will not have data races, so we manually opt-out isolation checks.
|
||||
nonisolated(unsafe) var progress = Progress()
|
||||
progress.kind = .file
|
||||
progress.fileOperationKind = .downloading
|
||||
|
||||
|
|
@ -195,6 +196,77 @@ public struct Shell {
|
|||
return Process.run(unxipPath.url, workingDirectory: url.deletingLastPathComponent(), ["\(url.path)"])
|
||||
}
|
||||
|
||||
public var downloadRuntime: (String, String) -> AsyncThrowingStream<Progress, Error> = { platform, version in
|
||||
return AsyncThrowingStream<Progress, Error> { continuation in
|
||||
Task {
|
||||
// Assume progress will not have data races, so we manually opt-out isolation checks.
|
||||
nonisolated(unsafe) var progress = Progress()
|
||||
progress.kind = .file
|
||||
progress.fileOperationKind = .downloading
|
||||
|
||||
let process = Process()
|
||||
let xcodeBuildPath = Path.root.usr.bin.join("xcodebuild").url
|
||||
|
||||
process.executableURL = xcodeBuildPath
|
||||
process.arguments = [
|
||||
"-downloadPlatform",
|
||||
"\(platform)",
|
||||
"-buildVersion",
|
||||
"\(version)"
|
||||
]
|
||||
|
||||
let stdOutPipe = Pipe()
|
||||
process.standardOutput = stdOutPipe
|
||||
let stdErrPipe = Pipe()
|
||||
process.standardError = stdErrPipe
|
||||
|
||||
let observer = NotificationCenter.default.addObserver(
|
||||
forName: .NSFileHandleDataAvailable,
|
||||
object: nil,
|
||||
queue: OperationQueue.main
|
||||
) { note in
|
||||
guard
|
||||
// This should always be the case for Notification.Name.NSFileHandleDataAvailable
|
||||
let handle = note.object as? FileHandle,
|
||||
handle === stdOutPipe.fileHandleForReading || handle === stdErrPipe.fileHandleForReading
|
||||
else { return }
|
||||
|
||||
defer { handle.waitForDataInBackgroundAndNotify() }
|
||||
|
||||
let string = String(decoding: handle.availableData, as: UTF8.self)
|
||||
|
||||
// TODO: fix warning. ObservingProgressView is currently tied to an updating progress
|
||||
progress.updateFromXcodebuild(text: string)
|
||||
|
||||
continuation.yield(progress)
|
||||
}
|
||||
|
||||
stdOutPipe.fileHandleForReading.waitForDataInBackgroundAndNotify()
|
||||
stdErrPipe.fileHandleForReading.waitForDataInBackgroundAndNotify()
|
||||
|
||||
continuation.onTermination = { @Sendable _ in
|
||||
process.terminate()
|
||||
NotificationCenter.default.removeObserver(observer, name: .NSFileHandleDataAvailable, object: nil)
|
||||
}
|
||||
|
||||
do {
|
||||
try process.run()
|
||||
} catch {
|
||||
continuation.finish(throwing: error)
|
||||
}
|
||||
|
||||
process.waitUntilExit()
|
||||
|
||||
NotificationCenter.default.removeObserver(observer, name: .NSFileHandleDataAvailable, object: nil)
|
||||
|
||||
guard process.terminationReason == .exit, process.terminationStatus == 0 else {
|
||||
continuation.finish(throwing: ProcessExecutionError(process: process, standardOutput: "", standardError: ""))
|
||||
return
|
||||
}
|
||||
continuation.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct Files {
|
||||
|
|
|
|||
|
|
@ -70,5 +70,38 @@ extension Progress {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
func updateFromXcodebuild(text: String) {
|
||||
self.totalUnitCount = 100
|
||||
self.completedUnitCount = 0
|
||||
self.localizedAdditionalDescription = "" // to not show the addtional
|
||||
|
||||
do {
|
||||
|
||||
let downloadPattern = #"(\d+\.\d+)% \(([\d.]+ (?:MB|GB)) of ([\d.]+ GB)\)"#
|
||||
let downloadRegex = try NSRegularExpression(pattern: downloadPattern)
|
||||
|
||||
// Search for matches in the text
|
||||
if let match = downloadRegex.firstMatch(in: text, range: NSRange(text.startIndex..., in: text)) {
|
||||
// Extract the percentage - simpler then trying to extract size MB/GB and convert to bytes.
|
||||
if let percentRange = Range(match.range(at: 1), in: text), let percentDouble = Double(text[percentRange]) {
|
||||
let percent = Int64(percentDouble.rounded())
|
||||
self.completedUnitCount = percent
|
||||
}
|
||||
}
|
||||
|
||||
// "Downloading tvOS 18.1 Simulator (22J5567a): Installing..." or
|
||||
// "Downloading tvOS 18.1 Simulator (22J5567a): Installing (registering download)..."
|
||||
if text.range(of: "Installing") != nil {
|
||||
// sets the progress to indeterminite to show animating progress
|
||||
self.totalUnitCount = 0
|
||||
self.completedUnitCount = 0
|
||||
}
|
||||
|
||||
} catch {
|
||||
Logger.appState.error("Invalid regular expression")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,12 +35,11 @@ struct XcodeCommands: Commands {
|
|||
|
||||
struct InstallButton: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
@State private var isLoading = false
|
||||
|
||||
let xcode: Xcode?
|
||||
|
||||
var body: some View {
|
||||
ProgressButton(isInProgress: isLoading) {
|
||||
Button {
|
||||
install()
|
||||
} label: {
|
||||
Text("Install")
|
||||
|
|
@ -49,7 +48,6 @@ struct InstallButton: View {
|
|||
}
|
||||
|
||||
private func install() {
|
||||
isLoading = true
|
||||
guard let xcode = xcode else { return }
|
||||
appState.checkMinVersionAndInstall(id: xcode.id)
|
||||
}
|
||||
|
|
@ -61,7 +59,7 @@ struct CancelInstallButton: View {
|
|||
|
||||
var body: some View {
|
||||
Button(action: cancelInstall) {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
Label("Cancel", systemImage: "xmark")
|
||||
}
|
||||
.help(localizeString("StopInstallation"))
|
||||
.buttonStyle(.plain)
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ struct AcknowledgmentsView: View {
|
|||
)!
|
||||
.addingAttribute(.foregroundColor, value: NSColor.labelColor)
|
||||
)
|
||||
.frame(minWidth: 500, minHeight: 500)
|
||||
.frame(minWidth: 600, minHeight: 500)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,27 +20,16 @@ struct NavigationSplitViewWrapper<Sidebar, Detail>: View where Sidebar: View, De
|
|||
}
|
||||
|
||||
var body: some View {
|
||||
if #available(iOS 16, macOS 13, tvOS 16, watchOS 9, visionOS 1, *) {
|
||||
// Use the latest API available
|
||||
NavigationSplitView {
|
||||
|
||||
if #available(macOS 14, *) {
|
||||
sidebar
|
||||
.toolbar(removing: .sidebarToggle)
|
||||
} else {
|
||||
sidebar
|
||||
}
|
||||
} detail: {
|
||||
detail
|
||||
}
|
||||
} else {
|
||||
// Alternative code for earlier versions of OS.
|
||||
NavigationView {
|
||||
// The first column is the sidebar.
|
||||
NavigationSplitView {
|
||||
if #available(macOS 14, *) {
|
||||
sidebar
|
||||
.navigationSplitViewColumnWidth(min: 290, ideal: 290)
|
||||
} else {
|
||||
sidebar
|
||||
detail
|
||||
}
|
||||
.navigationViewStyle(.columns)
|
||||
} detail: {
|
||||
detail
|
||||
}
|
||||
.navigationSplitViewStyle(.balanced)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ public struct ObservingProgressIndicator: View {
|
|||
self.progress = progress
|
||||
cancellable = progress.publisher(for: \.fractionCompleted)
|
||||
.combineLatest(progress.publisher(for: \.localizedAdditionalDescription))
|
||||
.combineLatest(progress.publisher(for: \.isIndeterminate))
|
||||
.throttle(for: 1.0, scheduler: DispatchQueue.main, latest: true)
|
||||
.sink { [weak self] _ in self?.objectWillChange.send() }
|
||||
}
|
||||
|
|
@ -82,6 +83,18 @@ struct ObservingProgressBar_Previews: PreviewProvider {
|
|||
style: .bar,
|
||||
showsAdditionalDescription: true
|
||||
)
|
||||
|
||||
ObservingProgressIndicator(
|
||||
configure(Progress()) {
|
||||
$0.kind = .file
|
||||
$0.fileOperationKind = .downloading
|
||||
$0.totalUnitCount = 0
|
||||
$0.completedUnitCount = 0
|
||||
},
|
||||
controlSize: .regular,
|
||||
style: .bar,
|
||||
showsAdditionalDescription: true
|
||||
)
|
||||
}
|
||||
.previewLayout(.sizeThatFits)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,10 @@ struct ProgressIndicator: NSViewRepresentable {
|
|||
nsView.doubleValue = doubleValue
|
||||
nsView.controlSize = controlSize
|
||||
nsView.isIndeterminate = isIndeterminate
|
||||
nsView.usesThreadedAnimation = true
|
||||
|
||||
nsView.style = style
|
||||
nsView.startAnimation(nil)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
24
Xcodes/Frontend/Common/TagView.swift
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
//
|
||||
// TagView.swift
|
||||
// Xcodes
|
||||
//
|
||||
// Created by Matt Kiazyk on 2025-06-25.//
|
||||
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct TagView: View {
|
||||
let text: String
|
||||
|
||||
var body: some View {
|
||||
Text(text)
|
||||
.font(.system(size: 10))
|
||||
.foregroundColor(.primary)
|
||||
.padding(.horizontal, 5)
|
||||
.padding(.vertical, 2)
|
||||
.background(
|
||||
Capsule()
|
||||
.fill(.quaternary)
|
||||
)
|
||||
}
|
||||
}
|
||||
22
Xcodes/Frontend/Common/TrailingIconLabelStyle.swift
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
//
|
||||
// TrailingIconLabelStyle.swift
|
||||
// Xcodes
|
||||
//
|
||||
// Created by Daniel Chick on 3/11/24.
|
||||
// Copyright © 2024 Robots and Pencils. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct TrailingIconLabelStyle: LabelStyle {
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
HStack {
|
||||
configuration.title
|
||||
configuration.icon
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension LabelStyle where Self == TrailingIconLabelStyle {
|
||||
static var trailingIcon: Self { Self() }
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import AppleAPI
|
|||
enum XcodesSheet: Identifiable {
|
||||
case signIn
|
||||
case twoFactor(SecondFactorData)
|
||||
case securityKeyTouchToConfirm
|
||||
|
||||
var id: Int { Kind(self).hashValue }
|
||||
|
||||
|
|
@ -16,12 +17,13 @@ enum XcodesSheet: Identifiable {
|
|||
|
||||
extension XcodesSheet {
|
||||
private enum Kind: Hashable {
|
||||
case signIn, twoFactor(TwoFactorOption)
|
||||
case signIn, twoFactor(TwoFactorOption), securityKeyTouchToConfirm
|
||||
|
||||
enum TwoFactorOption {
|
||||
case smsSent
|
||||
case codeSent
|
||||
case smsPendingChoice
|
||||
case securityKeyPin
|
||||
}
|
||||
|
||||
init(_ sheet: XcodesSheet) {
|
||||
|
|
@ -32,7 +34,9 @@ extension XcodesSheet {
|
|||
case .smsSent: self = .twoFactor(.smsSent)
|
||||
case .smsPendingChoice: self = .twoFactor(.smsPendingChoice)
|
||||
case .codeSent: self = .twoFactor(.codeSent)
|
||||
case .securityKey: self = .twoFactor(.securityKeyPin)
|
||||
}
|
||||
case .securityKeyTouchToConfirm: self = .securityKeyTouchToConfirm
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,9 @@ struct CompilersView: View {
|
|||
if let compilers = compilers {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Compilers").font(.headline)
|
||||
Text(Self.content(from: compilers)).font(.subheadline)
|
||||
Text(Self.content(from: compilers))
|
||||
.font(.subheadline)
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
} else {
|
||||
EmptyView()
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ extension View {
|
|||
struct Previews_CornerRadius_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
HStack {
|
||||
Text("XCODES RULES!")
|
||||
Text(verbatim: "XCODES RULES!")
|
||||
}.xcodesBackground()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,17 +6,18 @@
|
|||
// Copyright © 2023 Robots and Pencils. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Path
|
||||
import SwiftUI
|
||||
import Version
|
||||
|
||||
struct IconView: View {
|
||||
let installState: XcodeInstallState
|
||||
let xcode: Xcode
|
||||
|
||||
var body: some View {
|
||||
if case let .installed(path) = installState {
|
||||
if case let .installed(path) = xcode.installState {
|
||||
Image(nsImage: NSWorkspace.shared.icon(forFile: path.string))
|
||||
} else {
|
||||
Image(systemName: "app.fill")
|
||||
Image(xcode.version.isPrerelease ? "xcode-beta" : "xcode")
|
||||
.resizable()
|
||||
.frame(width: 32, height: 32)
|
||||
.foregroundColor(.secondary)
|
||||
|
|
@ -25,13 +26,19 @@ struct IconView: View {
|
|||
}
|
||||
|
||||
#Preview("Installed") {
|
||||
IconView(installState: XcodeInstallState.installed(Path("/Applications/Xcode.app")!))
|
||||
.frame(width: 300, height: 100)
|
||||
.padding()
|
||||
IconView(xcode: Xcode(version: Version("12.3.0")!, installState: .installed(Path("/Applications/Xcode-12.3.0.app")!), selected: true, icon: nil))
|
||||
.frame(width: 300, height: 100)
|
||||
.padding()
|
||||
}
|
||||
|
||||
#Preview("Installed") {
|
||||
IconView(xcode: Xcode(version: Version("12.3.0")!, installState: .notInstalled, selected: true, icon: nil))
|
||||
.frame(width: 300, height: 100)
|
||||
.padding()
|
||||
}
|
||||
|
||||
#Preview("Not Installed") {
|
||||
IconView(installState: XcodeInstallState.notInstalled)
|
||||
.frame(width: 300, height: 100)
|
||||
.padding()
|
||||
IconView(xcode: Xcode(version: Version("12.0.0-1234A")!, installState: .notInstalled, selected: false, icon: nil))
|
||||
.frame(width: 300, height: 100)
|
||||
.padding()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ struct IdenticalBuildsView: View {
|
|||
.font(.headline)
|
||||
|
||||
ForEach(builds, id: \.description) { version in
|
||||
Text("• \(version.appleDescription)")
|
||||
Text(verbatim: "• \(version.appleDescription)")
|
||||
.font(.subheadline)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,30 +9,35 @@ import struct XCModel.SDKs
|
|||
struct InfoPane: View {
|
||||
let xcode: Xcode
|
||||
var body: some View {
|
||||
if #available(macOS 14.0, *) {
|
||||
mainContent
|
||||
.contentMargins(10, for: .scrollContent)
|
||||
} else {
|
||||
mainContent
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
private var mainContent: some View {
|
||||
ScrollView(.vertical) {
|
||||
HStack(alignment: .top) {
|
||||
VStack {
|
||||
VStack(spacing: 5) {
|
||||
HStack {
|
||||
IconView(installState: xcode.installState)
|
||||
IconView(xcode: xcode)
|
||||
|
||||
Text(verbatim: "Xcode \(xcode.description) \(xcode.version.buildMetadataIdentifiersDisplay)")
|
||||
.font(.title)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
InfoPaneControls(xcode: xcode)
|
||||
}
|
||||
.xcodesBackground()
|
||||
|
||||
|
||||
VStack {
|
||||
Text("Platforms")
|
||||
.font(.title3)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
PlatformsView(xcode: xcode)
|
||||
}
|
||||
.xcodesBackground()
|
||||
|
||||
PlatformsView(xcode: xcode)
|
||||
}
|
||||
.frame(minWidth: 380)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
ReleaseDateView(date: xcode.releaseDate, url: xcode.releaseNotesURL)
|
||||
|
|
@ -64,15 +69,16 @@ struct InfoPane: View {
|
|||
#Preview(XcodePreviewName.allCases[2].rawValue) { makePreviewContent(for: 2) }
|
||||
#Preview(XcodePreviewName.allCases[3].rawValue) { makePreviewContent(for: 3) }
|
||||
#Preview(XcodePreviewName.allCases[4].rawValue) { makePreviewContent(for: 4) }
|
||||
#Preview(XcodePreviewName.allCases[5].rawValue) { makePreviewContent(for: 5) }
|
||||
|
||||
private func makePreviewContent(for index: Int) -> some View {
|
||||
let name = XcodePreviewName.allCases[index]
|
||||
let name = XcodePreviewName.allCases[index]
|
||||
return InfoPane(xcode: xcodeDict[name]!)
|
||||
.environmentObject(configure(AppState()) {
|
||||
$0.allXcodes = [xcodeDict[name]!]
|
||||
$0.allXcodes = [xcodeDict[name]!]
|
||||
})
|
||||
.frame(width: 300, height: 400)
|
||||
.padding()
|
||||
.frame(width: 600, height: 400)
|
||||
.padding()
|
||||
}
|
||||
|
||||
enum XcodePreviewName: String, CaseIterable, Identifiable {
|
||||
|
|
@ -81,7 +87,8 @@ enum XcodePreviewName: String, CaseIterable, Identifiable {
|
|||
case Populated_Uninstalled
|
||||
case Basic_Installed
|
||||
case Basic_Installing
|
||||
|
||||
case Basic_Unarchiving
|
||||
|
||||
var id: XcodePreviewName { self }
|
||||
}
|
||||
|
||||
|
|
@ -141,17 +148,25 @@ var xcodeDict: [XcodePreviewName: Xcode] = [
|
|||
sdks: nil,
|
||||
compilers: nil
|
||||
),
|
||||
.Basic_Unarchiving: .init(
|
||||
version: _versionWithMeta,
|
||||
installState: .installing(.unarchiving),
|
||||
selected: false,
|
||||
icon: nil,
|
||||
sdks: nil,
|
||||
compilers: nil
|
||||
),
|
||||
]
|
||||
|
||||
var downloadableRuntimes: [DownloadableRuntime] = {
|
||||
var runtimes = try! JSONDecoder().decode([DownloadableRuntime].self, from: Current.files.contents(atPath: Path.runtimeCacheFile.string)!)
|
||||
// set iOS to installed
|
||||
let iOSIndex = runtimes.firstIndex { $0.sdkBuildUpdate == "19E239" }!
|
||||
let iOSIndex = 0//runtimes.firstIndex { $0.sdkBuildUpdate.contains == "19E239" }!
|
||||
var iOSRuntime = runtimes[iOSIndex]
|
||||
iOSRuntime.installState = .installed
|
||||
runtimes[iOSIndex] = iOSRuntime
|
||||
|
||||
let watchOSIndex = runtimes.firstIndex { $0.sdkBuildUpdate == "20R362" }!
|
||||
let watchOSIndex = 0//runtimes.firstIndex { $0.sdkBuildUpdate.first == "20R362" }!
|
||||
var runtime = runtimes[watchOSIndex]
|
||||
runtime.installState = .installing(
|
||||
RuntimeInstallationStep.downloading(
|
||||
|
|
@ -163,7 +178,7 @@ var downloadableRuntimes: [DownloadableRuntime] = {
|
|||
$0.completedUnitCount = 848_444_920
|
||||
$0.throughput = 9_211_681
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
runtimes[watchOSIndex] = runtime
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ struct InfoPaneControls: View {
|
|||
case .installing(let installationStep):
|
||||
HStack(alignment: .top) {
|
||||
InstallationStepDetailView(installationStep: installationStep)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
CancelInstallButton(xcode: xcode)
|
||||
}
|
||||
case .installed(_):
|
||||
|
|
@ -39,6 +40,7 @@ struct InfoPaneControls: View {
|
|||
#Preview(XcodePreviewName.allCases[2].rawValue) { makePreviewContent(for: 2) }
|
||||
#Preview(XcodePreviewName.allCases[3].rawValue) { makePreviewContent(for: 3) }
|
||||
#Preview(XcodePreviewName.allCases[4].rawValue) { makePreviewContent(for: 4) }
|
||||
#Preview(XcodePreviewName.allCases[5].rawValue) { makePreviewContent(for: 5) }
|
||||
|
||||
private func makePreviewContent(for index: Int) -> some View {
|
||||
let name = XcodePreviewName.allCases[index]
|
||||
|
|
@ -47,6 +49,6 @@ private func makePreviewContent(for index: Int) -> some View {
|
|||
.environmentObject(configure(AppState()) {
|
||||
$0.allXcodes = [xcodeDict[name]!]
|
||||
})
|
||||
.frame(width: 300)
|
||||
.frame(width: 500)
|
||||
.padding()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ struct InstallationStepDetailView: View {
|
|||
showsAdditionalDescription: true
|
||||
)
|
||||
|
||||
case .unarchiving, .moving, .trashingArchive, .checkingSecurity, .finishing:
|
||||
case .authenticating, .unarchiving, .moving, .trashingArchive, .checkingSecurity, .finishing:
|
||||
ProgressView()
|
||||
.scaleEffect(0.5)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ import XcodesKit
|
|||
|
||||
struct PlatformsView: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
|
||||
@AppStorage("selectedRuntimeArchitecture") private var selectedRuntimeArchitecture: RuntimeArchitecture = .arm64
|
||||
|
||||
let xcode: Xcode
|
||||
|
||||
var body: some View {
|
||||
|
|
@ -19,17 +20,50 @@ struct PlatformsView: View {
|
|||
let builds = xcode.sdks?.allBuilds()
|
||||
let runtimes = builds?.flatMap { sdkBuild in
|
||||
appState.downloadableRuntimes.filter {
|
||||
$0.sdkBuildUpdate == sdkBuild
|
||||
$0.sdkBuildUpdate?.contains(sdkBuild) ?? false &&
|
||||
($0.architectures?.isEmpty ?? true ||
|
||||
$0.architectures?.contains(selectedRuntimeArchitecture.rawValue) ?? false)
|
||||
}
|
||||
}
|
||||
|
||||
ForEach(runtimes ?? [], id: \.simulatorVersion.buildUpdate) { runtime in
|
||||
runtimeView(runtime: runtime)
|
||||
.frame(minWidth: 200)
|
||||
.padding()
|
||||
.background(.quinary)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 5, style: .continuous))
|
||||
|
||||
let architectures = Set((runtimes ?? []).flatMap { $0.architectures ?? [] })
|
||||
|
||||
VStack {
|
||||
HStack {
|
||||
Text("Platforms")
|
||||
.font(.title3)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
if !architectures.isEmpty {
|
||||
Spacer()
|
||||
Button {
|
||||
switch selectedRuntimeArchitecture {
|
||||
case .arm64: selectedRuntimeArchitecture = .x86_64
|
||||
case .x86_64: selectedRuntimeArchitecture = .arm64
|
||||
}
|
||||
} label: {
|
||||
switch selectedRuntimeArchitecture {
|
||||
case .arm64:
|
||||
Label(selectedRuntimeArchitecture.displayValue, systemImage: "m4.button.horizontal")
|
||||
.labelStyle(.trailingIcon)
|
||||
case .x86_64:
|
||||
Label(selectedRuntimeArchitecture.displayValue, systemImage: "cpu.fill")
|
||||
.labelStyle(.trailingIcon)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ForEach(runtimes ?? [], id: \.identifier) { runtime in
|
||||
runtimeView(runtime: runtime)
|
||||
.frame(minWidth: 200)
|
||||
.padding()
|
||||
.background(.quinary)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 5, style: .continuous))
|
||||
}
|
||||
}
|
||||
.xcodesBackground()
|
||||
|
||||
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
|
|
@ -39,34 +73,37 @@ struct PlatformsView: View {
|
|||
runtime.icon()
|
||||
Text("\(runtime.visibleIdentifier)")
|
||||
.font(.headline)
|
||||
ForEach(runtime.architectures ?? [], id: \.self) { architecture in
|
||||
TagView(text: architecture)
|
||||
}
|
||||
pathIfAvailable(xcode: xcode, runtime: runtime)
|
||||
|
||||
if runtime.installState == .notInstalled {
|
||||
// TODO: Update the downloadableRuntimes with the appropriate installState so we don't have to check path awkwardly
|
||||
if appState.runtimeInstallPath(xcode: xcode, runtime: runtime) != nil {
|
||||
EmptyView()
|
||||
} else {
|
||||
HStack {
|
||||
Spacer()
|
||||
DownloadRuntimeButton(runtime: runtime)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
Text(runtime.downloadFileSizeString)
|
||||
.font(.subheadline)
|
||||
.frame(width: 70, alignment: .trailing)
|
||||
}
|
||||
switch runtime.installState {
|
||||
case .installed:
|
||||
EmptyView()
|
||||
case .notInstalled:
|
||||
// TODO: Update the downloadableRuntimes with the appropriate installState so we don't have to check path awkwardly
|
||||
if let path = appState.runtimeInstallPath(xcode: xcode, runtime: runtime) {
|
||||
EmptyView()
|
||||
} else {
|
||||
HStack {
|
||||
Spacer()
|
||||
DownloadRuntimeButton(runtime: runtime)
|
||||
}
|
||||
}
|
||||
|
||||
case .installing(let installationStep):
|
||||
HStack(alignment: .top, spacing: 5){
|
||||
RuntimeInstallationStepDetailView(installationStep: installationStep)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
Spacer()
|
||||
CancelRuntimeInstallButton(runtime: runtime)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if case let .installing(installationStep) = runtime.installState {
|
||||
HStack(alignment: .top, spacing: 5){
|
||||
RuntimeInstallationStepDetailView(installationStep: installationStep)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
Spacer()
|
||||
CancelRuntimeInstallButton(runtime: runtime)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
17
Xcodes/Frontend/InfoPane/RuntimeArchitecture.swift
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
//
|
||||
// RuntimeArchitecture.swift
|
||||
// Xcodes
|
||||
//
|
||||
// Created by Matt Kiazyk on 2025-07-07.
|
||||
//
|
||||
|
||||
enum RuntimeArchitecture: String, CaseIterable, Identifiable {
|
||||
case arm64
|
||||
case x86_64
|
||||
|
||||
var id: Self { self }
|
||||
|
||||
var displayValue: String {
|
||||
return rawValue
|
||||
}
|
||||
}
|
||||
|
|
@ -26,8 +26,12 @@ struct RuntimeInstallationStepDetailView: View {
|
|||
)
|
||||
|
||||
case .installing, .trashingArchive:
|
||||
ProgressView()
|
||||
.scaleEffect(0.5)
|
||||
ObservingProgressIndicator(
|
||||
Progress(),
|
||||
controlSize: .regular,
|
||||
style: .bar,
|
||||
showsAdditionalDescription: false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,9 @@ struct SDKsView: View {
|
|||
} else {
|
||||
VStack(alignment: .leading) {
|
||||
Text("SDKs").font(.headline)
|
||||
Text(content).font(.subheadline)
|
||||
Text(content)
|
||||
.font(.subheadline)
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,11 +16,10 @@ struct MainWindow: View {
|
|||
@AppStorage("isShowingInfoPane") private var isShowingInfoPane = false
|
||||
@AppStorage("xcodeListCategory") private var category: XcodeListCategory = .all
|
||||
@AppStorage("isInstalledOnly") private var isInstalledOnly = false
|
||||
|
||||
|
||||
var body: some View {
|
||||
NavigationSplitViewWrapper {
|
||||
XcodeListView(selectedXcodeID: $selectedXcodeID, searchText: searchText, category: category, isInstalledOnly: isInstalledOnly)
|
||||
.frame(minWidth: 250)
|
||||
.layoutPriority(1)
|
||||
.alert(item: $appState.xcodeBeingConfirmedForUninstallation) { xcode in
|
||||
Alert(title: Text(String(format: localizeString("Alert.Uninstall.Title"), xcode.description)),
|
||||
|
|
@ -42,7 +41,6 @@ struct MainWindow: View {
|
|||
UnselectedView()
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.toolbar {
|
||||
ToolbarItemGroup {
|
||||
Button(action: { appState.presentedSheet = .signIn }, label: {
|
||||
|
|
@ -56,11 +54,7 @@ struct MainWindow: View {
|
|||
.help("PreferencesDescription")
|
||||
} else {
|
||||
Button(action: {
|
||||
if #available(macOS 13, *) {
|
||||
NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil)
|
||||
} else {
|
||||
NSApp.sendAction(Selector(("showPreferencesWindow:")), to: nil, from: nil)
|
||||
}
|
||||
NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil)
|
||||
}, label: {
|
||||
Label("Preferences", systemImage: "gearshape")
|
||||
})
|
||||
|
|
@ -82,6 +76,9 @@ struct MainWindow: View {
|
|||
case .twoFactor(let secondFactorData):
|
||||
secondFactorView(secondFactorData)
|
||||
.environmentObject(appState)
|
||||
case .securityKeyTouchToConfirm:
|
||||
SignInSecurityKeyTouchView(isPresented: $appState.presentedSheet.isNotNil)
|
||||
.environmentObject(appState)
|
||||
}
|
||||
}
|
||||
.alert(item: $appState.presentedAlert, content: { presentedAlert in
|
||||
|
|
@ -113,6 +110,8 @@ struct MainWindow: View {
|
|||
SignInSMSView(isPresented: $appState.presentedSheet.isNotNil, trustedPhoneNumber: trustedPhoneNumber, authOptions: secondFactorData.authOptions, sessionData: secondFactorData.sessionData)
|
||||
case .smsPendingChoice:
|
||||
SignInPhoneListView(isPresented: $appState.presentedSheet.isNotNil, authOptions: secondFactorData.authOptions, sessionData: secondFactorData.sessionData)
|
||||
case .securityKey:
|
||||
SignInSecurityKeyPinView(isPresented: $appState.presentedSheet.isNotNil, authOptions: secondFactorData.authOptions, sessionData: secondFactorData.sessionData)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -36,8 +36,10 @@ struct AdvancedPreferencePane: View {
|
|||
self.appState.installPath = path.string
|
||||
}
|
||||
}
|
||||
.disabled(appState.disableInstallPathChange)
|
||||
Text("InstallPathDescription")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
}
|
||||
|
|
@ -71,8 +73,10 @@ struct AdvancedPreferencePane: View {
|
|||
self.appState.localPath = path.string
|
||||
}
|
||||
}
|
||||
.disabled(appState.disableLocalPathChange)
|
||||
Text("LocalCachePathDescription")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
}
|
||||
|
|
@ -80,18 +84,22 @@ struct AdvancedPreferencePane: View {
|
|||
|
||||
GroupBox(label: Text("Active/Select")) {
|
||||
VStack(alignment: .leading) {
|
||||
Picker("OnSelect", selection: $appState.onSelectActionType) {
|
||||
Picker(selection: $appState.onSelectActionType) {
|
||||
|
||||
Text(SelectedActionType.none.description)
|
||||
.tag(SelectedActionType.none)
|
||||
Text(SelectedActionType.rename.description)
|
||||
.tag(SelectedActionType.rename)
|
||||
} label: {
|
||||
Text(verbatim: "OnSelect")
|
||||
}
|
||||
.labelsHidden()
|
||||
.pickerStyle(.inline)
|
||||
|
||||
.disabled(appState.onSelectActionTypeDisabled)
|
||||
|
||||
Text(appState.onSelectActionType.detailedDescription)
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
Spacer()
|
||||
.frame(height: 20)
|
||||
|
|
@ -100,6 +108,7 @@ struct AdvancedPreferencePane: View {
|
|||
.disabled(appState.createSymLinkOnSelectDisabled)
|
||||
Text("AutomaticallyCreateSymbolicLinkDescription")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
|
@ -112,6 +121,7 @@ struct AdvancedPreferencePane: View {
|
|||
.disabled(appState.createSymLinkOnSelectDisabled)
|
||||
Text("ShowOpenInRosettaDescription")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
.groupBoxStyle(PreferencesGroupBoxStyle())
|
||||
|
|
@ -126,16 +136,18 @@ struct AdvancedPreferencePane: View {
|
|||
case .installed:
|
||||
Text("HelperInstalled")
|
||||
case .notInstalled:
|
||||
HStack {
|
||||
Text("HelperNotInstalled")
|
||||
VStack(alignment: .leading) {
|
||||
Button("InstallHelper") {
|
||||
appState.installHelperIfNecessary()
|
||||
}
|
||||
Text("HelperNotInstalled")
|
||||
.font(.footnote)
|
||||
}
|
||||
}
|
||||
|
||||
Text("PrivilegedHelperDescription")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
Spacer()
|
||||
|
|
@ -151,9 +163,9 @@ struct AdvancedPreferencePane_Previews: PreviewProvider {
|
|||
Group {
|
||||
AdvancedPreferencePane()
|
||||
.environmentObject(AppState())
|
||||
.frame(maxWidth: 500)
|
||||
.frame(maxWidth: 600)
|
||||
}
|
||||
.frame(width: 500, height: 700, alignment: .center)
|
||||
.frame(width: 600, height: 700, alignment: .center)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -161,11 +173,8 @@ struct AdvancedPreferencePane_Previews: PreviewProvider {
|
|||
struct PreferencesGroupBoxStyle: GroupBoxStyle {
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
HStack(alignment: .top, spacing: 20) {
|
||||
HStack {
|
||||
Spacer()
|
||||
configuration.label
|
||||
}
|
||||
.frame(width: 120)
|
||||
configuration.label
|
||||
.frame(width: 180, alignment: .trailing)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
configuration.content
|
||||
|
|
|
|||
|
|
@ -18,13 +18,17 @@ struct DownloadPreferencePane: View {
|
|||
}
|
||||
}
|
||||
.labelsHidden()
|
||||
|
||||
AttributedText(dataSourceFootnote)
|
||||
.fixedSize()
|
||||
|
||||
Text("DataSourceDescription")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
|
||||
}
|
||||
.groupBoxStyle(PreferencesGroupBoxStyle())
|
||||
|
||||
.disabled(dataSource.isManaged)
|
||||
|
||||
GroupBox(label: Text("Downloader")) {
|
||||
VStack(alignment: .leading) {
|
||||
Picker("Downloader", selection: $downloader) {
|
||||
|
|
@ -34,49 +38,27 @@ struct DownloadPreferencePane: View {
|
|||
}
|
||||
}
|
||||
.labelsHidden()
|
||||
|
||||
AttributedText(downloaderFootnote)
|
||||
.fixedSize()
|
||||
|
||||
Text("DownloaderDescription")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
|
||||
}
|
||||
.groupBoxStyle(PreferencesGroupBoxStyle())
|
||||
|
||||
.disabled(downloader.isManaged)
|
||||
}
|
||||
}
|
||||
|
||||
private var dataSourceFootnote: NSAttributedString {
|
||||
let string = localizeString("DataSourceDescription")
|
||||
let attributedString = NSMutableAttributedString(
|
||||
string: string,
|
||||
attributes: [
|
||||
.font: NSFont.preferredFont(forTextStyle: .footnote, options: [:]),
|
||||
.foregroundColor: NSColor.labelColor
|
||||
]
|
||||
)
|
||||
attributedString.addAttribute(.link, value: URL(string: "https://xcodereleases.com")!, range: NSRange(string.range(of: "Xcode Releases")!, in: string))
|
||||
return attributedString
|
||||
}
|
||||
|
||||
private var downloaderFootnote: NSAttributedString {
|
||||
let string = localizeString("DownloaderDescription")
|
||||
let attributedString = NSMutableAttributedString(
|
||||
string: string,
|
||||
attributes: [
|
||||
.font: NSFont.preferredFont(forTextStyle: .footnote, options: [:]),
|
||||
.foregroundColor: NSColor.labelColor
|
||||
]
|
||||
)
|
||||
attributedString.addAttribute(.link, value: URL(string: "https://github.com/aria2/aria2")!, range: NSRange(string.range(of: "aria2")!, in: string))
|
||||
return attributedString
|
||||
}
|
||||
}
|
||||
|
||||
struct DownloadPreferencePane_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
GeneralPreferencePane()
|
||||
DownloadPreferencePane()
|
||||
.environmentObject(AppState())
|
||||
.frame(maxWidth: 500)
|
||||
.frame(maxWidth: 600)
|
||||
.frame(minHeight: 300)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import AppleAPI
|
||||
import SwiftUI
|
||||
import Path
|
||||
import SwiftUI
|
||||
|
||||
struct ExperimentsPreferencePane: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
|
|
@ -13,29 +13,17 @@ struct ExperimentsPreferencePane: View {
|
|||
"UseUnxipExperiment",
|
||||
isOn: $appState.unxipExperiment
|
||||
)
|
||||
AttributedText(unxipFootnote)
|
||||
.disabled(appState.disableUnxipExperiment)
|
||||
Text("FasterUnxipDescription")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
.groupBoxStyle(PreferencesGroupBoxStyle())
|
||||
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
|
||||
private var unxipFootnote: NSAttributedString {
|
||||
let string = localizeString("FasterUnxipDescription")
|
||||
let attributedString = NSMutableAttributedString(
|
||||
string: string,
|
||||
attributes: [
|
||||
.font: NSFont.preferredFont(forTextStyle: .footnote, options: [:]),
|
||||
.foregroundColor: NSColor.labelColor
|
||||
]
|
||||
)
|
||||
attributedString.addAttribute(.link, value: URL(string: "https://twitter.com/_saagarjha")!, range: NSRange(string.range(of: "@_saagarjha")!, in: string))
|
||||
attributedString.addAttribute(.link, value: URL(string: "https://github.com/saagarjha/unxip")!, range: NSRange(string.range(of: "https://github.com/saagarjha/unxip")!, in: string))
|
||||
return attributedString
|
||||
}
|
||||
}
|
||||
|
||||
struct ExperimentsPreferencePane_Previews: PreviewProvider {
|
||||
|
|
@ -43,7 +31,7 @@ struct ExperimentsPreferencePane_Previews: PreviewProvider {
|
|||
Group {
|
||||
ExperimentsPreferencePane()
|
||||
.environmentObject(AppState())
|
||||
.frame(maxWidth: 500)
|
||||
.frame(maxWidth: 600)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,12 @@ struct GeneralPreferencePane: View {
|
|||
NotificationsView().environmentObject(appState)
|
||||
}
|
||||
.groupBoxStyle(PreferencesGroupBoxStyle())
|
||||
Divider()
|
||||
|
||||
GroupBox(label: Text("Misc")) {
|
||||
Toggle("TerminateAfterLastWindowClosed", isOn: $appState.terminateAfterLastWindowClosed)
|
||||
}
|
||||
.groupBoxStyle(PreferencesGroupBoxStyle())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -29,7 +35,7 @@ struct GeneralPreferencePane_Previews: PreviewProvider {
|
|||
Group {
|
||||
GeneralPreferencePane()
|
||||
.environmentObject(AppState())
|
||||
.frame(maxWidth: 500)
|
||||
.frame(maxWidth: 600)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,6 @@ struct PreferencesView: View {
|
|||
.tag(Tabs.experiment)
|
||||
}
|
||||
.padding(20)
|
||||
.frame(width: 500)
|
||||
.frame(width: 600)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,11 +15,13 @@ struct UpdatesPreferencePane: View {
|
|||
"AutomaticInstallNewVersion",
|
||||
isOn: $autoInstallationType.isAutoInstalling
|
||||
)
|
||||
|
||||
.disabled(updater.disableAutoInstallNewVersions)
|
||||
|
||||
Toggle(
|
||||
"IncludePreRelease",
|
||||
isOn: $autoInstallationType.isAutoInstallingBeta
|
||||
)
|
||||
.disabled(updater.disableIncludePrereleaseVersions)
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
|
|
@ -34,18 +36,23 @@ struct UpdatesPreferencePane: View {
|
|||
isOn: $updater.automaticallyChecksForUpdates
|
||||
)
|
||||
.fixedSize(horizontal: true, vertical: false)
|
||||
|
||||
.disabled(updater.disableAutoUpdateXcodesApp)
|
||||
|
||||
Toggle(
|
||||
"IncludePreRelease",
|
||||
isOn: $updater.includePrereleaseVersions
|
||||
)
|
||||
|
||||
.disabled(updater.disableAutoUpdateXcodesAppPrereleaseVersions)
|
||||
|
||||
Button("CheckNow") {
|
||||
updater.checkForUpdates()
|
||||
}
|
||||
|
||||
.padding(.top)
|
||||
.disabled(updater.disableAutoUpdateXcodesApp)
|
||||
|
||||
Text(String(format: localizeString("LastChecked"), lastUpdatedString))
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
|
|
@ -81,12 +88,18 @@ class ObservableUpdater: ObservableObject {
|
|||
private var lastUpdateCheckDateObservation: NSKeyValueObservation?
|
||||
@Published var includePrereleaseVersions = false {
|
||||
didSet {
|
||||
UserDefaults.standard.setValue(includePrereleaseVersions, forKey: "includePrereleaseVersions")
|
||||
|
||||
Current.defaults.set(includePrereleaseVersions, forKey: "includePrereleaseVersions")
|
||||
|
||||
updaterDelegate.includePrereleaseVersions = includePrereleaseVersions
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var disableAutoInstallNewVersions: Bool { PreferenceKey.autoInstallation.isManaged() }
|
||||
var disableIncludePrereleaseVersions: Bool { PreferenceKey.autoInstallation.isManaged() }
|
||||
|
||||
var disableAutoUpdateXcodesApp: Bool { PreferenceKey.SUEnableAutomaticChecks.isManaged() }
|
||||
var disableAutoUpdateXcodesAppPrereleaseVersions: Bool { PreferenceKey.includePrereleaseVersions.isManaged() }
|
||||
|
||||
init() {
|
||||
updater = SPUStandardUpdaterController(startingUpdater: true, updaterDelegate: updaterDelegate, userDriverDelegate: nil).updater
|
||||
|
||||
|
|
@ -109,7 +122,7 @@ class ObservableUpdater: ObservableObject {
|
|||
self.lastUpdateCheckDate = updater.lastUpdateCheckDate
|
||||
}
|
||||
)
|
||||
includePrereleaseVersions = UserDefaults.standard.bool(forKey: "includePrereleaseVersions")
|
||||
includePrereleaseVersions = Current.defaults.bool(forKey: "includePrereleaseVersions") ?? false
|
||||
}
|
||||
|
||||
func checkForUpdates() {
|
||||
|
|
@ -140,7 +153,9 @@ struct UpdatesPreferencePane_Previews: PreviewProvider {
|
|||
Group {
|
||||
UpdatesPreferencePane()
|
||||
.environmentObject(AppState())
|
||||
.frame(maxWidth: 500)
|
||||
.environmentObject(ObservableUpdater())
|
||||
.frame(maxWidth: 600)
|
||||
.frame(minHeight: 300)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,12 +10,12 @@ struct SignIn2FAView: View {
|
|||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Text(String(format: localizeString("DigitCodeDescription"), authOptions.securityCode.length))
|
||||
Text(String(format: localizeString("DigitCodeDescription"), authOptions.securityCode!.length))
|
||||
.fixedSize(horizontal: true, vertical: false)
|
||||
|
||||
HStack {
|
||||
Spacer()
|
||||
PinCodeTextField(code: $code, numberOfDigits: authOptions.securityCode.length) {
|
||||
PinCodeTextField(code: $code, numberOfDigits: authOptions.securityCode!.length) {
|
||||
appState.submitSecurityCode(.device(code: $0), sessionData: sessionData)
|
||||
}
|
||||
Spacer()
|
||||
|
|
@ -32,7 +32,7 @@ struct SignIn2FAView: View {
|
|||
Text("Continue")
|
||||
}
|
||||
.keyboardShortcut(.defaultAction)
|
||||
.disabled(code.count != authOptions.securityCode.length)
|
||||
.disabled(code.count != authOptions.securityCode!.length)
|
||||
}
|
||||
.frame(height: 25)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,14 @@
|
|||
import SwiftUI
|
||||
|
||||
struct SignInCredentialsView: View {
|
||||
private enum FocusedField {
|
||||
case username, password
|
||||
}
|
||||
|
||||
@EnvironmentObject var appState: AppState
|
||||
@State private var username: String = ""
|
||||
@State private var password: String = ""
|
||||
@FocusState private var focusedField: FocusedField?
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
|
|
@ -13,12 +18,16 @@ struct SignInCredentialsView: View {
|
|||
HStack {
|
||||
Text("AppleID")
|
||||
.frame(minWidth: 100, alignment: .trailing)
|
||||
TextField("example@icloud.com", text: $username)
|
||||
TextField(text: $username) {
|
||||
Text(verbatim: "example@icloud.com")
|
||||
}
|
||||
.focused($focusedField, equals: .username)
|
||||
}
|
||||
HStack {
|
||||
Text("Password")
|
||||
.frame(minWidth: 100, alignment: .trailing)
|
||||
SecureField("Required", text: $password)
|
||||
.focused($focusedField, equals: .password)
|
||||
}
|
||||
if appState.authError != nil {
|
||||
HStack {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import SwiftUI
|
||||
import AppleAPI
|
||||
import SwiftUI
|
||||
|
||||
struct SignInPhoneListView: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
|
|
@ -7,12 +7,12 @@ struct SignInPhoneListView: View {
|
|||
@State private var selectedPhoneNumberID: AuthOptionsResponse.TrustedPhoneNumber.ID?
|
||||
let authOptions: AuthOptionsResponse
|
||||
let sessionData: AppleSessionData
|
||||
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
if let phoneNumbers = authOptions.trustedPhoneNumbers, !phoneNumbers.isEmpty {
|
||||
Text(String(format: localizeString("SelectTrustedPhone"), authOptions.securityCode.length))
|
||||
|
||||
Text(String(format: localizeString("SelectTrustedPhone"), authOptions.securityCode!.length))
|
||||
|
||||
List(phoneNumbers, selection: $selectedPhoneNumberID) {
|
||||
Text($0.numberWithDialCode)
|
||||
}
|
||||
|
|
@ -22,19 +22,18 @@ struct SignInPhoneListView: View {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
AttributedText(
|
||||
NSAttributedString(string: localizeString("NoTrustedPhones"))
|
||||
.convertingURLsToLinkAttributes()
|
||||
)
|
||||
Text("NoTrustedPhones")
|
||||
.font(.callout)
|
||||
Spacer()
|
||||
}
|
||||
|
||||
|
||||
HStack {
|
||||
Button("Cancel", action: { isPresented = false })
|
||||
.keyboardShortcut(.cancelAction)
|
||||
Spacer()
|
||||
ProgressButton(isInProgress: appState.isProcessingAuthRequest,
|
||||
action: { appState.requestSMS(to: authOptions.trustedPhoneNumbers!.first { $0.id == selectedPhoneNumberID }!, authOptions: authOptions, sessionData: sessionData) }) {
|
||||
action: { appState.requestSMS(to: authOptions.trustedPhoneNumbers!.first { $0.id == selectedPhoneNumberID }!, authOptions: authOptions, sessionData: sessionData) })
|
||||
{
|
||||
Text("Continue")
|
||||
}
|
||||
.keyboardShortcut(.defaultAction)
|
||||
|
|
@ -54,9 +53,10 @@ struct SignInPhoneListView_Previews: PreviewProvider {
|
|||
SignInPhoneListView(
|
||||
isPresented: .constant(true),
|
||||
authOptions: AuthOptionsResponse(
|
||||
trustedPhoneNumbers: [.init(id: 0, numberWithDialCode: "(•••) •••-••90")],
|
||||
trustedPhoneNumbers: [.init(id: 0, numberWithDialCode: "(•••) •••-••90")],
|
||||
trustedDevices: nil,
|
||||
securityCode: .init(length: 6)),
|
||||
securityCode: .init(length: 6)
|
||||
),
|
||||
sessionData: AppleSessionData(serviceKey: "", sessionID: "", scnt: "")
|
||||
)
|
||||
.environmentObject(AppState())
|
||||
|
|
@ -64,9 +64,10 @@ struct SignInPhoneListView_Previews: PreviewProvider {
|
|||
SignInPhoneListView(
|
||||
isPresented: .constant(true),
|
||||
authOptions: AuthOptionsResponse(
|
||||
trustedPhoneNumbers: [],
|
||||
trustedPhoneNumbers: [],
|
||||
trustedDevices: nil,
|
||||
securityCode: .init(length: 6)),
|
||||
securityCode: .init(length: 6)
|
||||
),
|
||||
sessionData: AppleSessionData(serviceKey: "", sessionID: "", scnt: "")
|
||||
)
|
||||
.environmentObject(AppState())
|
||||
|
|
|
|||
|
|
@ -11,11 +11,11 @@ struct SignInSMSView: View {
|
|||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Text(String(format: localizeString("EnterDigitCodeDescription"), authOptions.securityCode.length, trustedPhoneNumber.numberWithDialCode))
|
||||
Text(String(format: localizeString("EnterDigitCodeDescription"), authOptions.securityCode!.length, trustedPhoneNumber.numberWithDialCode))
|
||||
|
||||
HStack {
|
||||
Spacer()
|
||||
PinCodeTextField(code: $code, numberOfDigits: authOptions.securityCode.length) {
|
||||
PinCodeTextField(code: $code, numberOfDigits: authOptions.securityCode!.length) {
|
||||
appState.submitSecurityCode(.sms(code: $0, phoneNumberId: trustedPhoneNumber.id), sessionData: sessionData)
|
||||
}
|
||||
Spacer()
|
||||
|
|
@ -31,7 +31,7 @@ struct SignInSMSView: View {
|
|||
Text("Continue")
|
||||
}
|
||||
.keyboardShortcut(.defaultAction)
|
||||
.disabled(code.count != authOptions.securityCode.length)
|
||||
.disabled(code.count != authOptions.securityCode!.length)
|
||||
}
|
||||
.frame(height: 25)
|
||||
}
|
||||
|
|
|
|||
70
Xcodes/Frontend/SignIn/SignInSecurityKeyPinView.swift
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
//
|
||||
// SignInSecurityKeyPin.swift
|
||||
// Xcodes
|
||||
//
|
||||
// Created by Kino on 2024-09-26.
|
||||
// Copyright © 2024 Robots and Pencils. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import AppleAPI
|
||||
|
||||
struct SignInSecurityKeyPinView: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
@Binding var isPresented: Bool
|
||||
@State private var pin: String = ""
|
||||
let authOptions: AuthOptionsResponse
|
||||
let sessionData: AppleSessionData
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Text(localizeString("SecurityKeyPinDescription"))
|
||||
.fixedSize(horizontal: true, vertical: false)
|
||||
|
||||
HStack {
|
||||
Spacer()
|
||||
SecureField("PIN", text: $pin)
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
|
||||
HStack {
|
||||
Button("Cancel", action: { isPresented = false })
|
||||
.keyboardShortcut(.cancelAction)
|
||||
Spacer()
|
||||
|
||||
Button("PIN not set", action: submitWithoutPinCode)
|
||||
|
||||
ProgressButton(isInProgress: appState.isProcessingAuthRequest,
|
||||
action: submitPinCode) {
|
||||
Text("Continue")
|
||||
}
|
||||
.keyboardShortcut(.defaultAction)
|
||||
// FIDO2 device pin codes must be at least 4 code points
|
||||
// https://docs.yubico.com/yesdk/users-manual/application-fido2/fido2-pin.html
|
||||
.disabled(pin.count < 4)
|
||||
}
|
||||
.frame(height: 25)
|
||||
}
|
||||
.padding()
|
||||
.emittingError($appState.authError, recoveryHandler: { _ in })
|
||||
}
|
||||
|
||||
func submitPinCode() {
|
||||
appState.createAndSubmitSecurityKeyAssertationWithPinCode(pin, sessionData: sessionData, authOptions: authOptions)
|
||||
}
|
||||
|
||||
func submitWithoutPinCode() {
|
||||
appState.createAndSubmitSecurityKeyAssertationWithPinCode(nil, sessionData: sessionData, authOptions: authOptions)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
SignInSecurityKeyPinView(isPresented: .constant(true),
|
||||
authOptions: AuthOptionsResponse(
|
||||
trustedPhoneNumbers: nil,
|
||||
trustedDevices: nil,
|
||||
securityCode: .init(length: 6)
|
||||
), sessionData: AppleSessionData(serviceKey: "", sessionID: "", scnt: ""))
|
||||
.environmentObject(AppState())
|
||||
}
|
||||
54
Xcodes/Frontend/SignIn/SignInSecurityKeyTouchView.swift
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
//
|
||||
// SignInSecurityKeyPin.swift
|
||||
// Xcodes
|
||||
//
|
||||
// Created by Kino on 2024-09-26.
|
||||
// Copyright © 2024 Robots and Pencils. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import AppleAPI
|
||||
|
||||
struct SignInSecurityKeyTouchView: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
@Binding var isPresented: Bool
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .center) {
|
||||
Image(systemName: "key.radiowaves.forward")
|
||||
.font(.system(size: 32)).bold()
|
||||
.padding(.bottom)
|
||||
HStack {
|
||||
Spacer()
|
||||
Text(localizeString("SecurityKeyTouchDescription"))
|
||||
.fixedSize(horizontal: true, vertical: false)
|
||||
Spacer()
|
||||
}
|
||||
HStack {
|
||||
Button("Cancel", action: self.cancel)
|
||||
.keyboardShortcut(.cancelAction)
|
||||
Spacer()
|
||||
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle())
|
||||
.scaleEffect(x: 0.5, y: 0.5, anchor: .center)
|
||||
.isHidden(!appState.isProcessingAuthRequest)
|
||||
|
||||
.keyboardShortcut(.defaultAction)
|
||||
}
|
||||
.frame(height: 25)
|
||||
}
|
||||
.padding()
|
||||
.emittingError($appState.authError, recoveryHandler: { _ in })
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
appState.cancelSecurityKeyAssertationRequest()
|
||||
isPresented = false
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
SignInSecurityKeyTouchView(isPresented: .constant(true))
|
||||
.environmentObject(AppState())
|
||||
}
|
||||
|
|
@ -14,10 +14,10 @@ extension View {
|
|||
struct View_IsHidden_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
Text("Not Hidden")
|
||||
Text(verbatim: "Not Hidden")
|
||||
.isHidden(false)
|
||||
|
||||
Text("Hidden")
|
||||
Text(verbatim: "Hidden")
|
||||
.isHidden(true)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -97,33 +97,45 @@ struct AppStoreButtonStyle_Previews: PreviewProvider {
|
|||
Group {
|
||||
ForEach([ColorScheme.light, .dark], id: \.self) { colorScheme in
|
||||
Group {
|
||||
Button("OPEN", action: {})
|
||||
Button{ } label: {
|
||||
Text(verbatim: "OPEN")
|
||||
}
|
||||
.buttonStyle(AppStoreButtonStyle(primary: true, highlighted: false))
|
||||
.padding()
|
||||
.background(Color(.textBackgroundColor))
|
||||
.previewDisplayName("Primary")
|
||||
Button("OPEN", action: {})
|
||||
Button{ } label: {
|
||||
Text(verbatim: "OPEN")
|
||||
}
|
||||
.buttonStyle(AppStoreButtonStyle(primary: true, highlighted: true))
|
||||
.padding()
|
||||
.background(Color(.controlAccentColor))
|
||||
.previewDisplayName("Primary, Highlighted")
|
||||
Button("OPEN", action: {})
|
||||
Button{ } label: {
|
||||
Text(verbatim: "OPEN")
|
||||
}
|
||||
.buttonStyle(AppStoreButtonStyle(primary: true, highlighted: false))
|
||||
.padding()
|
||||
.disabled(true)
|
||||
.background(Color(.textBackgroundColor))
|
||||
.previewDisplayName("Primary, Disabled")
|
||||
Button("INSTALL", action: {})
|
||||
Button{ } label: {
|
||||
Text(verbatim: "INSTALL")
|
||||
}
|
||||
.buttonStyle(AppStoreButtonStyle(primary: false, highlighted: false))
|
||||
.padding()
|
||||
.background(Color(.textBackgroundColor))
|
||||
.previewDisplayName("Secondary")
|
||||
Button("INSTALL", action: {})
|
||||
Button{ } label: {
|
||||
Text(verbatim: "INSTALL")
|
||||
}
|
||||
.buttonStyle(AppStoreButtonStyle(primary: false, highlighted: true))
|
||||
.padding()
|
||||
.background(Color(.controlAccentColor))
|
||||
.previewDisplayName("Secondary, Highlighted")
|
||||
Button("INSTALL", action: {})
|
||||
Button{ } label: {
|
||||
Text(verbatim: "INSTALL")
|
||||
}
|
||||
.buttonStyle(AppStoreButtonStyle(primary: false, highlighted: false))
|
||||
.padding()
|
||||
.disabled(true)
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import SwiftUI
|
|||
|
||||
struct BottomStatusModifier: ViewModifier {
|
||||
@EnvironmentObject var appState: AppState
|
||||
@AppStorage(PreferenceKey.hideSupportXcodes.rawValue) var hideSupportXcodes = false
|
||||
|
||||
@SwiftUI.Environment(\.openURL) var openURL: OpenURLAction
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
|
|
@ -20,17 +22,19 @@ struct BottomStatusModifier: ViewModifier {
|
|||
Divider()
|
||||
HStack {
|
||||
Text(appState.bottomStatusBarMessage)
|
||||
.font(.subheadline)
|
||||
.font(.subheadline)
|
||||
Spacer()
|
||||
Button(action: {
|
||||
openURL(URL(string: "https://opencollective.com/xcodesapp")!)
|
||||
}) {
|
||||
HStack {
|
||||
Image(systemName: "heart.circle")
|
||||
Text("Support.Xcodes")
|
||||
if !hideSupportXcodes {
|
||||
Button(action: {
|
||||
openURL(URL(string: "https://opencollective.com/xcodesapp")!)
|
||||
}) {
|
||||
HStack {
|
||||
Image(systemName: "heart.circle")
|
||||
Text("Support.Xcodes")
|
||||
}
|
||||
}
|
||||
}
|
||||
Text(Bundle.main.shortVersion!)
|
||||
Text(verbatim: "\(Bundle.main.shortVersion!) (\(Bundle.main.version!))")
|
||||
.font(.subheadline)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: 30, alignment: .leading)
|
||||
|
|
@ -51,8 +55,34 @@ extension View {
|
|||
|
||||
struct Previews_BottomStatusBar_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
HStack {
|
||||
|
||||
}.bottomStatusBar()
|
||||
Group {
|
||||
HStack {
|
||||
|
||||
}
|
||||
.bottomStatusBar()
|
||||
.environmentObject({ () -> AppState in
|
||||
let a = AppState()
|
||||
return a }()
|
||||
)
|
||||
.defaultAppStorage({ () -> UserDefaults in
|
||||
let d = UserDefaults(suiteName: "hide_support")!
|
||||
d.set(true, forKey: PreferenceKey.hideSupportXcodes.rawValue)
|
||||
return d
|
||||
}())
|
||||
|
||||
HStack {
|
||||
|
||||
}
|
||||
.bottomStatusBar()
|
||||
.environmentObject({ () -> AppState in
|
||||
let a = AppState()
|
||||
return a }()
|
||||
)
|
||||
.defaultAppStorage({ () -> UserDefaults in
|
||||
let d = UserDefaults(suiteName: "show_support")!
|
||||
d.set(false, forKey: PreferenceKey.hideSupportXcodes.rawValue)
|
||||
return d
|
||||
}())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ struct InstallationStepRowView: View {
|
|||
controlSize: .small,
|
||||
style: .spinning
|
||||
)
|
||||
case .unarchiving, .moving, .trashingArchive, .checkingSecurity, .finishing:
|
||||
case .authenticating, .unarchiving, .moving, .trashingArchive, .checkingSecurity, .finishing:
|
||||
ProgressView()
|
||||
.scaleEffect(0.5)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ struct MainToolbarModifier: ViewModifier {
|
|||
@Binding var category: XcodeListCategory
|
||||
@Binding var isInstalledOnly: Bool
|
||||
@Binding var isShowingInfoPane: Bool
|
||||
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.toolbar { toolbar }
|
||||
|
|
@ -13,9 +13,8 @@ struct MainToolbarModifier: ViewModifier {
|
|||
|
||||
private var toolbar: some ToolbarContent {
|
||||
ToolbarItemGroup {
|
||||
|
||||
ProgressButton(
|
||||
isInProgress: appState.isUpdating,
|
||||
isInProgress: appState.isUpdating,
|
||||
action: appState.update
|
||||
) {
|
||||
Label("Refresh", systemImage: "arrow.clockwise")
|
||||
|
|
@ -23,6 +22,7 @@ struct MainToolbarModifier: ViewModifier {
|
|||
.keyboardShortcut(KeyEquivalent("r"))
|
||||
.help("RefreshDescription")
|
||||
Spacer()
|
||||
|
||||
Button(action: {
|
||||
switch category {
|
||||
case .all: category = .release
|
||||
|
|
@ -35,39 +35,22 @@ struct MainToolbarModifier: ViewModifier {
|
|||
case .all:
|
||||
Label("All", systemImage: "line.horizontal.3.decrease.circle")
|
||||
case .release:
|
||||
if #available(macOS 11.3, *) {
|
||||
Label("ReleaseOnly", systemImage: "line.horizontal.3.decrease.circle.fill")
|
||||
.labelStyle(TitleAndIconLabelStyle())
|
||||
.labelStyle(.trailingIcon)
|
||||
.foregroundColor(.accentColor)
|
||||
} else {
|
||||
Label("ReleaseOnly", systemImage: "line.horizontal.3.decrease.circle.fill")
|
||||
.labelStyle(TitleOnlyLabelStyle())
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
case .beta:
|
||||
if #available(macOS 11.3, *) {
|
||||
Label("BetaOnly", systemImage: "line.horizontal.3.decrease.circle.fill")
|
||||
.labelStyle(TitleAndIconLabelStyle())
|
||||
.foregroundColor(.accentColor)
|
||||
} else {
|
||||
Label("BetaOnly", systemImage: "line.horizontal.3.decrease.circle.fill")
|
||||
.labelStyle(TitleOnlyLabelStyle())
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
Label("BetaOnly", systemImage: "line.horizontal.3.decrease.circle.fill")
|
||||
.labelStyle(.trailingIcon)
|
||||
.foregroundColor(.accentColor)
|
||||
case .releasePlusNewBetas:
|
||||
if #available(macOS 11.3, *) {
|
||||
Label("ReleasePlusNewBetas", systemImage: "line.horizontal.3.decrease.circle.fill")
|
||||
.labelStyle(TitleAndIconLabelStyle())
|
||||
.foregroundColor(.accentColor)
|
||||
} else {
|
||||
Label("ReleasePlusNewBetas", systemImage: "line.horizontal.3.decrease.circle.fill")
|
||||
.labelStyle(TitleOnlyLabelStyle())
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
Label("ReleasePlusNewBetas", systemImage: "line.horizontal.3.decrease.circle.fill")
|
||||
.labelStyle(.trailingIcon)
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
}
|
||||
.help("FilterAvailableDescription")
|
||||
|
||||
.disabled(category.isManaged)
|
||||
|
||||
Button(action: {
|
||||
isInstalledOnly.toggle()
|
||||
}) {
|
||||
|
|
@ -76,11 +59,9 @@ struct MainToolbarModifier: ViewModifier {
|
|||
.foregroundColor(.accentColor)
|
||||
} else {
|
||||
Label("Filter", systemImage: "arrow.down.app")
|
||||
|
||||
}
|
||||
}
|
||||
.help("FilterInstalledDescription")
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -91,7 +72,7 @@ extension View {
|
|||
isInstalledOnly: Binding<Bool>,
|
||||
isShowingInfoPane: Binding<Bool>
|
||||
) -> some View {
|
||||
self.modifier(
|
||||
modifier(
|
||||
MainToolbarModifier(
|
||||
category: category,
|
||||
isInstalledOnly: isInstalledOnly,
|
||||
|
|
|
|||
|
|
@ -16,4 +16,6 @@ enum XcodeListCategory: String, CaseIterable, Identifiable, CustomStringConverti
|
|||
case .releasePlusNewBetas: return localizeString("ReleasePlusNewBetas")
|
||||
}
|
||||
}
|
||||
|
||||
var isManaged: Bool { PreferenceKey.xcodeListCategory.isManaged() }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@ struct XcodeListView: View {
|
|||
private let searchText: String
|
||||
private let category: XcodeListCategory
|
||||
private let isInstalledOnly: Bool
|
||||
|
||||
@AppStorage(PreferenceKey.allowedMajorVersions.rawValue) private var allowedMajorVersions = Int.max
|
||||
|
||||
init(selectedXcodeID: Binding<Xcode.ID?>, searchText: String, category: XcodeListCategory, isInstalledOnly: Bool) {
|
||||
self._selectedXcodeID = selectedXcodeID
|
||||
self.searchText = searchText
|
||||
|
|
@ -36,6 +37,22 @@ struct XcodeListView: View {
|
|||
}
|
||||
}
|
||||
|
||||
let latestMajor = xcodes.sorted(\.version)
|
||||
.filter { $0.version.isNotPrerelease }
|
||||
.last?
|
||||
.version
|
||||
.major
|
||||
|
||||
xcodes = xcodes.filter {
|
||||
if $0.installState.notInstalled,
|
||||
let latestMajor = latestMajor,
|
||||
$0.version.major < (latestMajor - min(latestMajor,allowedMajorVersions)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
if !searchText.isEmpty {
|
||||
xcodes = xcodes.filter { $0.description.contains(searchText) }
|
||||
}
|
||||
|
|
@ -54,7 +71,8 @@ struct XcodeListView: View {
|
|||
.listStyle(.sidebar)
|
||||
.safeAreaInset(edge: .bottom, spacing: 0) {
|
||||
PlatformsPocket()
|
||||
.padding()
|
||||
.padding(.horizontal)
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -63,19 +81,21 @@ struct PlatformsPocket: View {
|
|||
@SwiftUI.Environment(\.openWindow) private var openWindow
|
||||
|
||||
var body: some View {
|
||||
Button(action: {
|
||||
openWindow(id: "platforms") }
|
||||
Button(action: {
|
||||
openWindow(id: "platforms")
|
||||
}
|
||||
) {
|
||||
VStack(spacing: 5) {
|
||||
HStack(spacing: 5) {
|
||||
Image(systemName: "square.3.layers.3d")
|
||||
.font(.title)
|
||||
Text("Platforms")
|
||||
.font(.callout)
|
||||
.font(.title3.weight(.medium))
|
||||
Text("PlatformsDescription")
|
||||
Spacer()
|
||||
}
|
||||
.frame(width: 70, height: 70)
|
||||
.background(.quaternary)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 5, style: .continuous))
|
||||
|
||||
.font(.body.weight(.medium))
|
||||
.padding(.horizontal)
|
||||
.padding(.vertical, 12)
|
||||
.background(.quaternary.opacity(0.75))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
|
@ -93,6 +113,9 @@ struct XcodeListView_Previews: PreviewProvider {
|
|||
Xcode(version: Version("12.2.0")!, installState: .notInstalled, selected: false, icon: nil),
|
||||
Xcode(version: Version("12.1.0")!, installState: .installing(.downloading(progress: configure(Progress(totalUnitCount: 100)) { $0.completedUnitCount = 40 })), selected: false, icon: nil),
|
||||
Xcode(version: Version("12.0.0")!, installState: .installed(Path("/Applications/Xcode-12.3.0.app")!), selected: false, icon: nil),
|
||||
Xcode(version: Version("10.1.0")!, installState: .notInstalled, selected: false, icon: nil),
|
||||
Xcode(version: Version("10.0.0")!, installState: .installed(Path("/Applications/Xcode-10.0.0.app")!), selected: false, icon: nil),
|
||||
Xcode(version: Version("9.0.0")!, installState: .notInstalled, selected: false, icon: nil),
|
||||
]
|
||||
return a
|
||||
}())
|
||||
|
|
|
|||
|
|
@ -30,9 +30,6 @@ struct XcodeListViewRow: View {
|
|||
Text(verbatim: path.string)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
} else {
|
||||
Text(verbatim: "")
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -42,6 +39,7 @@ struct XcodeListViewRow: View {
|
|||
.padding(.trailing, 16)
|
||||
installControl(for: xcode)
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
.contextMenu {
|
||||
switch xcode.installState {
|
||||
case .notInstalled:
|
||||
|
|
@ -75,9 +73,10 @@ struct XcodeListViewRow: View {
|
|||
if let icon = xcode.icon {
|
||||
Image(nsImage: icon)
|
||||
} else {
|
||||
Color.clear
|
||||
Image(xcode.version.isPrerelease ? "xcode-beta" : "xcode")
|
||||
.resizable()
|
||||
.frame(width: 32, height: 32)
|
||||
.foregroundColor(.secondary)
|
||||
.opacity(0.2)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
12
Xcodes/Resources/Assets.xcassets/xcode-beta.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Image.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Xcodes/Resources/Assets.xcassets/xcode-beta.imageset/Image.png
vendored
Normal file
|
After Width: | Height: | Size: 82 KiB |
12
Xcodes/Resources/Assets.xcassets/xcode.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "xcode.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Xcodes/Resources/Assets.xcassets/xcode.imageset/xcode.png
vendored
Normal file
|
After Width: | Height: | Size: 23 KiB |
|
|
@ -40,6 +40,6 @@
|
|||
<key>SUFeedURL</key>
|
||||
<string>https://www.xcodes.app/appcast.xml</string>
|
||||
<key>SUPublicEDKey</key>
|
||||
<string>GrqOrFQHxfqoLFCAGc9luvsAWQifHtG9gQ3NVJ583tE=</string>
|
||||
<string>SEcz0vgUSeBTOoAXYe+64zea95G6lIf5NgzFs3InYJQ=</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{\rtf1\ansi\ansicpg1252\cocoartf2759
|
||||
{\rtf1\ansi\ansicpg1252\cocoartf2822
|
||||
\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset0 .SFNS-Regular;}
|
||||
{\colortbl;\red255\green255\blue255;}
|
||||
{\*\expandedcolortbl;;}
|
||||
|
|
@ -58,6 +58,33 @@ SOFTWARE.\
|
|||
\
|
||||
\
|
||||
|
||||
\fs34 LibFido2Swift\
|
||||
\
|
||||
|
||||
\fs26 MIT License\
|
||||
\
|
||||
Copyright (c) 2024 Kino Roy\
|
||||
\
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy\
|
||||
of this software and associated documentation files (the "Software"), to deal\
|
||||
in the Software without restriction, including without limitation the rights\
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\
|
||||
copies of the Software, and to permit persons to whom the Software is\
|
||||
furnished to do so, subject to the following conditions:\
|
||||
\
|
||||
The above copyright notice and this permission notice shall be included in all\
|
||||
copies or substantial portions of the Software.\
|
||||
\
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\
|
||||
SOFTWARE.\
|
||||
\
|
||||
\
|
||||
|
||||
\fs34 ErrorHandling\
|
||||
\
|
||||
|
||||
|
|
@ -86,6 +113,241 @@ SOFTWARE.\
|
|||
\
|
||||
\
|
||||
|
||||
\fs34 big-num\
|
||||
\
|
||||
|
||||
\fs26 MIT License\
|
||||
\
|
||||
Copyright (c) 2019 Adam Fowler\
|
||||
\
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy\
|
||||
of this software and associated documentation files (the "Software"), to deal\
|
||||
in the Software without restriction, including without limitation the rights\
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\
|
||||
copies of the Software, and to permit persons to whom the Software is\
|
||||
furnished to do so, subject to the following conditions:\
|
||||
\
|
||||
The above copyright notice and this permission notice shall be included in all\
|
||||
copies or substantial portions of the Software.\
|
||||
\
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\
|
||||
SOFTWARE.\
|
||||
\
|
||||
\
|
||||
|
||||
\fs34 swift-crypto\
|
||||
\
|
||||
|
||||
\fs26 \
|
||||
Apache License\
|
||||
Version 2.0, January 2004\
|
||||
http://www.apache.org/licenses/\
|
||||
\
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\
|
||||
\
|
||||
1. Definitions.\
|
||||
\
|
||||
"License" shall mean the terms and conditions for use, reproduction,\
|
||||
and distribution as defined by Sections 1 through 9 of this document.\
|
||||
\
|
||||
"Licensor" shall mean the copyright owner or entity authorized by\
|
||||
the copyright owner that is granting the License.\
|
||||
\
|
||||
"Legal Entity" shall mean the union of the acting entity and all\
|
||||
other entities that control, are controlled by, or are under common\
|
||||
control with that entity. For the purposes of this definition,\
|
||||
"control" means (i) the power, direct or indirect, to cause the\
|
||||
direction or management of such entity, whether by contract or\
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the\
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.\
|
||||
\
|
||||
"You" (or "Your") shall mean an individual or Legal Entity\
|
||||
exercising permissions granted by this License.\
|
||||
\
|
||||
"Source" form shall mean the preferred form for making modifications,\
|
||||
including but not limited to software source code, documentation\
|
||||
source, and configuration files.\
|
||||
\
|
||||
"Object" form shall mean any form resulting from mechanical\
|
||||
transformation or translation of a Source form, including but\
|
||||
not limited to compiled object code, generated documentation,\
|
||||
and conversions to other media types.\
|
||||
\
|
||||
"Work" shall mean the work of authorship, whether in Source or\
|
||||
Object form, made available under the License, as indicated by a\
|
||||
copyright notice that is included in or attached to the work\
|
||||
(an example is provided in the Appendix below).\
|
||||
\
|
||||
"Derivative Works" shall mean any work, whether in Source or Object\
|
||||
form, that is based on (or derived from) the Work and for which the\
|
||||
editorial revisions, annotations, elaborations, or other modifications\
|
||||
represent, as a whole, an original work of authorship. For the purposes\
|
||||
of this License, Derivative Works shall not include works that remain\
|
||||
separable from, or merely link (or bind by name) to the interfaces of,\
|
||||
the Work and Derivative Works thereof.\
|
||||
\
|
||||
"Contribution" shall mean any work of authorship, including\
|
||||
the original version of the Work and any modifications or additions\
|
||||
to that Work or Derivative Works thereof, that is intentionally\
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner\
|
||||
or by an individual or Legal Entity authorized to submit on behalf of\
|
||||
the copyright owner. For the purposes of this definition, "submitted"\
|
||||
means any form of electronic, verbal, or written communication sent\
|
||||
to the Licensor or its representatives, including but not limited to\
|
||||
communication on electronic mailing lists, source code control systems,\
|
||||
and issue tracking systems that are managed by, or on behalf of, the\
|
||||
Licensor for the purpose of discussing and improving the Work, but\
|
||||
excluding communication that is conspicuously marked or otherwise\
|
||||
designated in writing by the copyright owner as "Not a Contribution."\
|
||||
\
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity\
|
||||
on behalf of whom a Contribution has been received by Licensor and\
|
||||
subsequently incorporated within the Work.\
|
||||
\
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of\
|
||||
this License, each Contributor hereby grants to You a perpetual,\
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable\
|
||||
copyright license to reproduce, prepare Derivative Works of,\
|
||||
publicly display, publicly perform, sublicense, and distribute the\
|
||||
Work and such Derivative Works in Source or Object form.\
|
||||
\
|
||||
3. Grant of Patent License. Subject to the terms and conditions of\
|
||||
this License, each Contributor hereby grants to You a perpetual,\
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable\
|
||||
(except as stated in this section) patent license to make, have made,\
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,\
|
||||
where such license applies only to those patent claims licensable\
|
||||
by such Contributor that are necessarily infringed by their\
|
||||
Contribution(s) alone or by combination of their Contribution(s)\
|
||||
with the Work to which such Contribution(s) was submitted. If You\
|
||||
institute patent litigation against any entity (including a\
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work\
|
||||
or a Contribution incorporated within the Work constitutes direct\
|
||||
or contributory patent infringement, then any patent licenses\
|
||||
granted to You under this License for that Work shall terminate\
|
||||
as of the date such litigation is filed.\
|
||||
\
|
||||
4. Redistribution. You may reproduce and distribute copies of the\
|
||||
Work or Derivative Works thereof in any medium, with or without\
|
||||
modifications, and in Source or Object form, provided that You\
|
||||
meet the following conditions:\
|
||||
\
|
||||
(a) You must give any other recipients of the Work or\
|
||||
Derivative Works a copy of this License; and\
|
||||
\
|
||||
(b) You must cause any modified files to carry prominent notices\
|
||||
stating that You changed the files; and\
|
||||
\
|
||||
(c) You must retain, in the Source form of any Derivative Works\
|
||||
that You distribute, all copyright, patent, trademark, and\
|
||||
attribution notices from the Source form of the Work,\
|
||||
excluding those notices that do not pertain to any part of\
|
||||
the Derivative Works; and\
|
||||
\
|
||||
(d) If the Work includes a "NOTICE" text file as part of its\
|
||||
distribution, then any Derivative Works that You distribute must\
|
||||
include a readable copy of the attribution notices contained\
|
||||
within such NOTICE file, excluding those notices that do not\
|
||||
pertain to any part of the Derivative Works, in at least one\
|
||||
of the following places: within a NOTICE text file distributed\
|
||||
as part of the Derivative Works; within the Source form or\
|
||||
documentation, if provided along with the Derivative Works; or,\
|
||||
within a display generated by the Derivative Works, if and\
|
||||
wherever such third-party notices normally appear. The contents\
|
||||
of the NOTICE file are for informational purposes only and\
|
||||
do not modify the License. You may add Your own attribution\
|
||||
notices within Derivative Works that You distribute, alongside\
|
||||
or as an addendum to the NOTICE text from the Work, provided\
|
||||
that such additional attribution notices cannot be construed\
|
||||
as modifying the License.\
|
||||
\
|
||||
You may add Your own copyright statement to Your modifications and\
|
||||
may provide additional or different license terms and conditions\
|
||||
for use, reproduction, or distribution of Your modifications, or\
|
||||
for any such Derivative Works as a whole, provided Your use,\
|
||||
reproduction, and distribution of the Work otherwise complies with\
|
||||
the conditions stated in this License.\
|
||||
\
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,\
|
||||
any Contribution intentionally submitted for inclusion in the Work\
|
||||
by You to the Licensor shall be under the terms and conditions of\
|
||||
this License, without any additional terms or conditions.\
|
||||
Notwithstanding the above, nothing herein shall supersede or modify\
|
||||
the terms of any separate license agreement you may have executed\
|
||||
with Licensor regarding such Contributions.\
|
||||
\
|
||||
6. Trademarks. This License does not grant permission to use the trade\
|
||||
names, trademarks, service marks, or product names of the Licensor,\
|
||||
except as required for reasonable and customary use in describing the\
|
||||
origin of the Work and reproducing the content of the NOTICE file.\
|
||||
\
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or\
|
||||
agreed to in writing, Licensor provides the Work (and each\
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,\
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\
|
||||
implied, including, without limitation, any warranties or conditions\
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the\
|
||||
appropriateness of using or redistributing the Work and assume any\
|
||||
risks associated with Your exercise of permissions under this License.\
|
||||
\
|
||||
8. Limitation of Liability. In no event and under no legal theory,\
|
||||
whether in tort (including negligence), contract, or otherwise,\
|
||||
unless required by applicable law (such as deliberate and grossly\
|
||||
negligent acts) or agreed to in writing, shall any Contributor be\
|
||||
liable to You for damages, including any direct, indirect, special,\
|
||||
incidental, or consequential damages of any character arising as a\
|
||||
result of this License or out of the use or inability to use the\
|
||||
Work (including but not limited to damages for loss of goodwill,\
|
||||
work stoppage, computer failure or malfunction, or any and all\
|
||||
other commercial damages or losses), even if such Contributor\
|
||||
has been advised of the possibility of such damages.\
|
||||
\
|
||||
9. Accepting Warranty or Additional Liability. While redistributing\
|
||||
the Work or Derivative Works thereof, You may choose to offer,\
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,\
|
||||
or other liability obligations and/or rights consistent with this\
|
||||
License. However, in accepting such obligations, You may act only\
|
||||
on Your own behalf and on Your sole responsibility, not on behalf\
|
||||
of any other Contributor, and only if You agree to indemnify,\
|
||||
defend, and hold each Contributor harmless for any liability\
|
||||
incurred by, or claims asserted against, such Contributor by reason\
|
||||
of your accepting any such warranty or additional liability.\
|
||||
\
|
||||
END OF TERMS AND CONDITIONS\
|
||||
\
|
||||
APPENDIX: How to apply the Apache License to your work.\
|
||||
\
|
||||
To apply the Apache License to your work, attach the following\
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"\
|
||||
replaced with your own identifying information. (Don't include\
|
||||
the brackets!) The text should be enclosed in the appropriate\
|
||||
comment syntax for the file format. We also recommend that a\
|
||||
file or class name and description of purpose be included on the\
|
||||
same "printed page" as the copyright notice for easier\
|
||||
identification within third-party archives.\
|
||||
\
|
||||
Copyright [yyyy] [name of copyright owner]\
|
||||
\
|
||||
Licensed under the Apache License, Version 2.0 (the "License");\
|
||||
you may not use this file except in compliance with the License.\
|
||||
You may obtain a copy of the License at\
|
||||
\
|
||||
http://www.apache.org/licenses/LICENSE-2.0\
|
||||
\
|
||||
Unless required by applicable law or agreed to in writing, software\
|
||||
distributed under the License is distributed on an "AS IS" BASIS,\
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\
|
||||
See the License for the specific language governing permissions and\
|
||||
limitations under the License.\
|
||||
\
|
||||
\
|
||||
|
||||
\fs34 Path.swift\
|
||||
\
|
||||
|
||||
|
|
@ -552,12 +814,219 @@ For more information, please refer to <<http://unlicense.org/>>\
|
|||
\
|
||||
\
|
||||
|
||||
\fs34 swift-srp\
|
||||
\
|
||||
|
||||
\fs26 Apache License\
|
||||
Version 2.0, January 2004\
|
||||
http://www.apache.org/licenses/\
|
||||
\
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\
|
||||
\
|
||||
1. Definitions.\
|
||||
\
|
||||
"License" shall mean the terms and conditions for use, reproduction,\
|
||||
and distribution as defined by Sections 1 through 9 of this document.\
|
||||
\
|
||||
"Licensor" shall mean the copyright owner or entity authorized by\
|
||||
the copyright owner that is granting the License.\
|
||||
\
|
||||
"Legal Entity" shall mean the union of the acting entity and all\
|
||||
other entities that control, are controlled by, or are under common\
|
||||
control with that entity. For the purposes of this definition,\
|
||||
"control" means (i) the power, direct or indirect, to cause the\
|
||||
direction or management of such entity, whether by contract or\
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the\
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.\
|
||||
\
|
||||
"You" (or "Your") shall mean an individual or Legal Entity\
|
||||
exercising permissions granted by this License.\
|
||||
\
|
||||
"Source" form shall mean the preferred form for making modifications,\
|
||||
including but not limited to software source code, documentation\
|
||||
source, and configuration files.\
|
||||
\
|
||||
"Object" form shall mean any form resulting from mechanical\
|
||||
transformation or translation of a Source form, including but\
|
||||
not limited to compiled object code, generated documentation,\
|
||||
and conversions to other media types.\
|
||||
\
|
||||
"Work" shall mean the work of authorship, whether in Source or\
|
||||
Object form, made available under the License, as indicated by a\
|
||||
copyright notice that is included in or attached to the work\
|
||||
(an example is provided in the Appendix below).\
|
||||
\
|
||||
"Derivative Works" shall mean any work, whether in Source or Object\
|
||||
form, that is based on (or derived from) the Work and for which the\
|
||||
editorial revisions, annotations, elaborations, or other modifications\
|
||||
represent, as a whole, an original work of authorship. For the purposes\
|
||||
of this License, Derivative Works shall not include works that remain\
|
||||
separable from, or merely link (or bind by name) to the interfaces of,\
|
||||
the Work and Derivative Works thereof.\
|
||||
\
|
||||
"Contribution" shall mean any work of authorship, including\
|
||||
the original version of the Work and any modifications or additions\
|
||||
to that Work or Derivative Works thereof, that is intentionally\
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner\
|
||||
or by an individual or Legal Entity authorized to submit on behalf of\
|
||||
the copyright owner. For the purposes of this definition, "submitted"\
|
||||
means any form of electronic, verbal, or written communication sent\
|
||||
to the Licensor or its representatives, including but not limited to\
|
||||
communication on electronic mailing lists, source code control systems,\
|
||||
and issue tracking systems that are managed by, or on behalf of, the\
|
||||
Licensor for the purpose of discussing and improving the Work, but\
|
||||
excluding communication that is conspicuously marked or otherwise\
|
||||
designated in writing by the copyright owner as "Not a Contribution."\
|
||||
\
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity\
|
||||
on behalf of whom a Contribution has been received by Licensor and\
|
||||
subsequently incorporated within the Work.\
|
||||
\
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of\
|
||||
this License, each Contributor hereby grants to You a perpetual,\
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable\
|
||||
copyright license to reproduce, prepare Derivative Works of,\
|
||||
publicly display, publicly perform, sublicense, and distribute the\
|
||||
Work and such Derivative Works in Source or Object form.\
|
||||
\
|
||||
3. Grant of Patent License. Subject to the terms and conditions of\
|
||||
this License, each Contributor hereby grants to You a perpetual,\
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable\
|
||||
(except as stated in this section) patent license to make, have made,\
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,\
|
||||
where such license applies only to those patent claims licensable\
|
||||
by such Contributor that are necessarily infringed by their\
|
||||
Contribution(s) alone or by combination of their Contribution(s)\
|
||||
with the Work to which such Contribution(s) was submitted. If You\
|
||||
institute patent litigation against any entity (including a\
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work\
|
||||
or a Contribution incorporated within the Work constitutes direct\
|
||||
or contributory patent infringement, then any patent licenses\
|
||||
granted to You under this License for that Work shall terminate\
|
||||
as of the date such litigation is filed.\
|
||||
\
|
||||
4. Redistribution. You may reproduce and distribute copies of the\
|
||||
Work or Derivative Works thereof in any medium, with or without\
|
||||
modifications, and in Source or Object form, provided that You\
|
||||
meet the following conditions:\
|
||||
\
|
||||
(a) You must give any other recipients of the Work or\
|
||||
Derivative Works a copy of this License; and\
|
||||
\
|
||||
(b) You must cause any modified files to carry prominent notices\
|
||||
stating that You changed the files; and\
|
||||
\
|
||||
(c) You must retain, in the Source form of any Derivative Works\
|
||||
that You distribute, all copyright, patent, trademark, and\
|
||||
attribution notices from the Source form of the Work,\
|
||||
excluding those notices that do not pertain to any part of\
|
||||
the Derivative Works; and\
|
||||
\
|
||||
(d) If the Work includes a "NOTICE" text file as part of its\
|
||||
distribution, then any Derivative Works that You distribute must\
|
||||
include a readable copy of the attribution notices contained\
|
||||
within such NOTICE file, excluding those notices that do not\
|
||||
pertain to any part of the Derivative Works, in at least one\
|
||||
of the following places: within a NOTICE text file distributed\
|
||||
as part of the Derivative Works; within the Source form or\
|
||||
documentation, if provided along with the Derivative Works; or,\
|
||||
within a display generated by the Derivative Works, if and\
|
||||
wherever such third-party notices normally appear. The contents\
|
||||
of the NOTICE file are for informational purposes only and\
|
||||
do not modify the License. You may add Your own attribution\
|
||||
notices within Derivative Works that You distribute, alongside\
|
||||
or as an addendum to the NOTICE text from the Work, provided\
|
||||
that such additional attribution notices cannot be construed\
|
||||
as modifying the License.\
|
||||
\
|
||||
You may add Your own copyright statement to Your modifications and\
|
||||
may provide additional or different license terms and conditions\
|
||||
for use, reproduction, or distribution of Your modifications, or\
|
||||
for any such Derivative Works as a whole, provided Your use,\
|
||||
reproduction, and distribution of the Work otherwise complies with\
|
||||
the conditions stated in this License.\
|
||||
\
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,\
|
||||
any Contribution intentionally submitted for inclusion in the Work\
|
||||
by You to the Licensor shall be under the terms and conditions of\
|
||||
this License, without any additional terms or conditions.\
|
||||
Notwithstanding the above, nothing herein shall supersede or modify\
|
||||
the terms of any separate license agreement you may have executed\
|
||||
with Licensor regarding such Contributions.\
|
||||
\
|
||||
6. Trademarks. This License does not grant permission to use the trade\
|
||||
names, trademarks, service marks, or product names of the Licensor,\
|
||||
except as required for reasonable and customary use in describing the\
|
||||
origin of the Work and reproducing the content of the NOTICE file.\
|
||||
\
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or\
|
||||
agreed to in writing, Licensor provides the Work (and each\
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,\
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\
|
||||
implied, including, without limitation, any warranties or conditions\
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the\
|
||||
appropriateness of using or redistributing the Work and assume any\
|
||||
risks associated with Your exercise of permissions under this License.\
|
||||
\
|
||||
8. Limitation of Liability. In no event and under no legal theory,\
|
||||
whether in tort (including negligence), contract, or otherwise,\
|
||||
unless required by applicable law (such as deliberate and grossly\
|
||||
negligent acts) or agreed to in writing, shall any Contributor be\
|
||||
liable to You for damages, including any direct, indirect, special,\
|
||||
incidental, or consequential damages of any character arising as a\
|
||||
result of this License or out of the use or inability to use the\
|
||||
Work (including but not limited to damages for loss of goodwill,\
|
||||
work stoppage, computer failure or malfunction, or any and all\
|
||||
other commercial damages or losses), even if such Contributor\
|
||||
has been advised of the possibility of such damages.\
|
||||
\
|
||||
9. Accepting Warranty or Additional Liability. While redistributing\
|
||||
the Work or Derivative Works thereof, You may choose to offer,\
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,\
|
||||
or other liability obligations and/or rights consistent with this\
|
||||
License. However, in accepting such obligations, You may act only\
|
||||
on Your own behalf and on Your sole responsibility, not on behalf\
|
||||
of any other Contributor, and only if You agree to indemnify,\
|
||||
defend, and hold each Contributor harmless for any liability\
|
||||
incurred by, or claims asserted against, such Contributor by reason\
|
||||
of your accepting any such warranty or additional liability.\
|
||||
\
|
||||
END OF TERMS AND CONDITIONS\
|
||||
\
|
||||
APPENDIX: How to apply the Apache License to your work.\
|
||||
\
|
||||
To apply the Apache License to your work, attach the following\
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"\
|
||||
replaced with your own identifying information. (Don't include\
|
||||
the brackets!) The text should be enclosed in the appropriate\
|
||||
comment syntax for the file format. We also recommend that a\
|
||||
file or class name and description of purpose be included on the\
|
||||
same "printed page" as the copyright notice for easier\
|
||||
identification within third-party archives.\
|
||||
\
|
||||
Copyright [yyyy] [name of copyright owner]\
|
||||
\
|
||||
Licensed under the Apache License, Version 2.0 (the "License");\
|
||||
you may not use this file except in compliance with the License.\
|
||||
You may obtain a copy of the License at\
|
||||
\
|
||||
http://www.apache.org/licenses/LICENSE-2.0\
|
||||
\
|
||||
Unless required by applicable law or agreed to in writing, software\
|
||||
distributed under the License is distributed on an "AS IS" BASIS,\
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\
|
||||
See the License for the specific language governing permissions and\
|
||||
limitations under the License.\
|
||||
\
|
||||
\
|
||||
|
||||
\fs34 DockProgress\
|
||||
\
|
||||
|
||||
\fs26 MIT License\
|
||||
\
|
||||
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com)\
|
||||
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)\
|
||||
\
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\
|
||||
\
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ struct XcodesApp: App {
|
|||
@StateObject private var updater = ObservableUpdater()
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup("Xcodes") {
|
||||
Window("Xcodes", id: "main") {
|
||||
MainWindow()
|
||||
.environmentObject(appState)
|
||||
.environmentObject(updater)
|
||||
|
|
@ -51,19 +51,19 @@ struct XcodesApp: App {
|
|||
|
||||
CommandGroup(replacing: CommandGroupPlacement.help) {
|
||||
Button("Menu.GitHubRepo") {
|
||||
let xcodesRepoURL = URL(string: "https://github.com/RobotsAndPencils/XcodesApp/")!
|
||||
let xcodesRepoURL = URL(string: "https://github.com/XcodesOrg/XcodesApp/")!
|
||||
openURL(xcodesRepoURL)
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
Button("Menu.ReportABug") {
|
||||
let bugReportURL = URL(string: "https://github.com/RobotsAndPencils/XcodesApp/issues/new?assignees=&labels=bug&template=bug_report.md&title=")!
|
||||
let bugReportURL = URL(string: "https://github.com/XcodesOrg/XcodesApp/issues/new?assignees=&labels=bug&template=bug_report.md&title=")!
|
||||
openURL(bugReportURL)
|
||||
}
|
||||
|
||||
Button("Menu.RequestNewFeature") {
|
||||
let featureRequestURL = URL(string: "https://github.com/RobotsAndPencils/XcodesApp/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=")!
|
||||
let featureRequestURL = URL(string: "https://github.com/XcodesOrg/XcodesApp/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=")!
|
||||
openURL(featureRequestURL)
|
||||
}
|
||||
}
|
||||
|
|
@ -166,12 +166,12 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||
}
|
||||
|
||||
func applicationDidFinishLaunching(_: Notification) {}
|
||||
|
||||
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||
return Current.defaults.bool(forKey: "terminateAfterLastWindowClosed") ?? false
|
||||
}
|
||||
}
|
||||
|
||||
func localizeString(_ key: String, comment: String = "") -> String {
|
||||
if #available(macOS 12, *) {
|
||||
return String(localized: String.LocalizationValue(key))
|
||||
} else {
|
||||
return NSLocalizedString(key, comment: comment)
|
||||
}
|
||||
return String(localized: String.LocalizationValue(key))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ public struct DownloadableRuntimesResponse: Codable {
|
|||
public struct DownloadableRuntime: Codable, Identifiable, Hashable {
|
||||
public let category: Category
|
||||
public let simulatorVersion: SimulatorVersion
|
||||
public let source: String
|
||||
public let source: String?
|
||||
public let architectures: [String]?
|
||||
public let dictionaryVersion: Int
|
||||
public let contentType: ContentType
|
||||
public let platform: Platform
|
||||
|
|
@ -21,16 +22,19 @@ public struct DownloadableRuntime: Codable, Identifiable, Hashable {
|
|||
public let hostRequirements: HostRequirements?
|
||||
public let name: String
|
||||
public let authentication: Authentication?
|
||||
public var url: URL {
|
||||
return URL(string: source)!
|
||||
public var url: URL? {
|
||||
if let source {
|
||||
return URL(string: source)!
|
||||
}
|
||||
return nil
|
||||
}
|
||||
public var downloadPath: String {
|
||||
url.path
|
||||
public var downloadPath: String? {
|
||||
url?.path
|
||||
}
|
||||
|
||||
// dynamically updated - not decoded
|
||||
public var installState: RuntimeInstallState = .notInstalled
|
||||
public var sdkBuildUpdate: String?
|
||||
public var sdkBuildUpdate: [String]?
|
||||
|
||||
enum CodingKeys: CodingKey {
|
||||
case category
|
||||
|
|
@ -46,10 +50,11 @@ public struct DownloadableRuntime: Codable, Identifiable, Hashable {
|
|||
case name
|
||||
case authentication
|
||||
case sdkBuildUpdate
|
||||
case architectures
|
||||
}
|
||||
|
||||
var betaNumber: Int? {
|
||||
enum Regex { static let shared = try! NSRegularExpression(pattern: "b[0-9]+$") }
|
||||
enum Regex { static let shared = try! NSRegularExpression(pattern: "b[0-9]+") }
|
||||
guard var foundString = Regex.shared.firstString(in: identifier) else { return nil }
|
||||
foundString.removeFirst()
|
||||
return Int(foundString)!
|
||||
|
|
@ -91,6 +96,7 @@ public struct SDKToSimulatorMapping: Codable {
|
|||
public let sdkBuildUpdate: String
|
||||
public let simulatorBuildUpdate: String
|
||||
public let sdkIdentifier: String
|
||||
public let downloadableIdentifiers: [String]?
|
||||
}
|
||||
|
||||
extension DownloadableRuntime {
|
||||
|
|
@ -117,6 +123,7 @@ extension DownloadableRuntime {
|
|||
public enum ContentType: String, Codable {
|
||||
case diskImage = "diskImage"
|
||||
case package = "package"
|
||||
case cryptexDiskImage = "cryptexDiskImage"
|
||||
}
|
||||
|
||||
public enum Platform: String, Codable {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import Foundation
|
|||
|
||||
// A numbered step
|
||||
public enum XcodeInstallationStep: Equatable, CustomStringConvertible {
|
||||
case authenticating
|
||||
case downloading(progress: Progress)
|
||||
case unarchiving
|
||||
case moving(destination: String)
|
||||
|
|
@ -22,6 +23,8 @@ public enum XcodeInstallationStep: Equatable, CustomStringConvertible {
|
|||
|
||||
public var message: String {
|
||||
switch self {
|
||||
case .authenticating:
|
||||
return localizeString("Authenticating")
|
||||
case .downloading:
|
||||
return localizeString("Downloading")
|
||||
case .unarchiving:
|
||||
|
|
@ -39,16 +42,17 @@ public enum XcodeInstallationStep: Equatable, CustomStringConvertible {
|
|||
|
||||
public var stepNumber: Int {
|
||||
switch self {
|
||||
case .downloading: return 1
|
||||
case .unarchiving: return 2
|
||||
case .moving: return 3
|
||||
case .trashingArchive: return 4
|
||||
case .checkingSecurity: return 5
|
||||
case .finishing: return 6
|
||||
case .authenticating: return 1
|
||||
case .downloading: return 2
|
||||
case .unarchiving: return 3
|
||||
case .moving: return 4
|
||||
case .trashingArchive: return 5
|
||||
case .checkingSecurity: return 6
|
||||
case .finishing: return 7
|
||||
}
|
||||
}
|
||||
|
||||
public var stepCount: Int { 6 }
|
||||
public var stepCount: Int { 7 }
|
||||
}
|
||||
|
||||
func localizeString(_ key: String, comment: String = "") -> String {
|
||||
|
|
|
|||
|
|
@ -22,9 +22,14 @@ public struct RuntimeService {
|
|||
|
||||
// Apple gives a plist for download
|
||||
let (data, _) = try await networkService.requestData(urlRequest, validators: [])
|
||||
let decodedResponse = try PropertyListDecoder().decode(DownloadableRuntimesResponse.self, from: data)
|
||||
|
||||
return decodedResponse
|
||||
do {
|
||||
let decodedResponse = try PropertyListDecoder().decode(DownloadableRuntimesResponse.self, from: data)
|
||||
return decodedResponse
|
||||
} catch {
|
||||
print("error: \(error)")
|
||||
throw error
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public func installedRuntimes() async throws -> [InstalledRuntime] {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
import Path
|
||||
import CryptoKit
|
||||
import Version
|
||||
@testable import Xcodes
|
||||
import XCTest
|
||||
import CommonCrypto
|
||||
import BigNum
|
||||
import SRP
|
||||
|
||||
class AppStateUpdateTests: XCTestCase {
|
||||
var subject: AppState!
|
||||
|
|
@ -258,4 +262,81 @@ class AppStateUpdateTests: XCTestCase {
|
|||
XCTAssertEqual(subject.allXcodes.map(\.version), [Version("12.4.0")!, Version("12.3.0-RC")!])
|
||||
XCTAssertEqual(subject.allXcodes.map(\.identicalBuilds), [[], []])
|
||||
}
|
||||
|
||||
func sha256(data : Data) -> Data {
|
||||
var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
|
||||
data.withUnsafeBytes {
|
||||
_ = CC_SHA256($0.baseAddress, CC_LONG(data.count), &hash)
|
||||
}
|
||||
return Data(hash)
|
||||
}
|
||||
|
||||
func testSIRP() throws {
|
||||
/*
|
||||
Obtaind by running fastlane spaceauth --verbose -u anand.appleseed@example.com with some custom logging
|
||||
|
||||
Starting SIRP Apple ID login
|
||||
a: {"a":"VLEKLa+n2cyeYNWbECm45CuS4kCdCxodlTDGlW1FKaUyOrv/RbtN2sM0pVE12zI7k3VkocPC3rN5DZBIkahR6I8JHj/J97dtTvzsR+aNRWTYCT2HGP1PBI0QArp3eitAbFqTWI4+Zw+oOnV8+AYdH/wjbq7gOK4C4dvIHE+FzRwIlmguPb5qu2r47R9W3y1msVdoUGlFBOMOMb7Gyq7F0MaEIFH63lNzGomwq74mfss/cFqurd6fxU+Y7tdVTPZw1GWyBEPiXWpk8sNm2zE+S6zWo5tOsICprU75IC9galh1igfzN7VNe0SUFLNFTbFK+Bb1SFAOrAbBZOmyOG5uSQ==","accountName":"anand.appleseed@example.com","protocols":["s2k","s2k_fo"]}
|
||||
Received SIRP signin init response: {"iteration"=>20309, "salt"=>"fIjNflgqSJXACWwwvhDU+w==", "protocol"=>"s2k", "b"=>"PMbU75wwG6rDTySXn2ASWyfQuPoW5ham15SzIscpInwOPE2uk7sePsW4ra0dHcLDUMFQn/LgBggIKOo7YZ9hf1VReiAzXwSKSHdJHjHUURTC2eNpANGUPO1qzuXYgc/MP3MR+GipKHsz+KTLT+8wLjNaiCIHsL/7evJBMw9QqiwhyXlAIm5mGZfhdTVbGpLz2/QzrFmI6pUTrHpio6m1Q74DH3FBxxIeiIcuEdGdeVt9iUweowBRyf2woasTvSV1fbMQbl+lsWPwzt/a73+J30eOGFdSubqSVYh2pV0OLqRz7zPzJars12teCWUV+0WUIaxb14Mp7tlmqcTPuqZe9w==", "c"=>"d-533-eccbc4e9-9564-11ef-84a6-018111c8cc60:PRN"}
|
||||
encrypted_password: 40532b4de9353fc537dc62ee84eacebd7ecb5ec26efca98bd01b0380e302100f 32
|
||||
bb: 7672345903537871991962715758896796468138571329014139041563495295907370682045347022183702983061785424983278857706335295545994877883818377653653442007828499221881058994644619578239367613808278802931379172730746665773282250642455690715139985911303055104847971308813151718669484181874342088801251592138154023949370621963319928723678385968989085032385411532317797659749008135679504901238396934480214258070495365760319076978872181485178648397361564241555189629889320567561713407566532187413091018319494367244540399242523126294027225387432028960726767445027313453858210115810946641002311734776929442587065438110307439763191
|
||||
x: 726436461883978586175291668001486484510457416477927591386889224605776454162
|
||||
u: 49415306980415573732801389514223606278850554555635359953422678270536095422201
|
||||
private 23161374166158551996079451276150657702422963034121842124445818241826569345033578345120284496449280736328513302994568402583647660370960353252836732307301957364261384324957527103960720408713825427474127658415917826326829664923997096839970397226662116904369925262192683131695683487505523842260218490007066160096482662715246662018133837725691586205535995663334471723776536640973591229093933458552240634178864509015968350855952558520147559154646484379002445961375384929682566525908284011230815908584648931495968206840416022306138033496705677078512266958633477047047323620540878121579549170392075029336954975132431417099801
|
||||
S: 4f75b6ea99c2d7d121357cce80c75c8e1bf74a65e8f66f75f8f66a481301afb8bebf0e54a3fac4f8bfdd60c77d6e670c87968b341f62175e25eb1d4f496e4e6596e1a387f2840688a35002419b70115b7902a46544cc7b31eb4c909c0acaeb752835d1562a687c431421510ebc7535c007a2bd12a4f7696c8c96a75a491b1eb9189ade2bef23dd5b0bb962b4f03e7fba7f6ba6fe67ba34cc18647daf3e474876f85dac5a15eb51c99d1ed78783579ffd6c8b6911f72564d87dc8f76519c8fc1535b83743ed5f7d6b8461d7154ce2a874cbb45bf63018352b9b997fbafbd6b15eac2a544a801c0152470796f3b69a84a4a653e5186b30efeeb148ff3c32ab8e08
|
||||
K: c5207f707186a52f1adee41bf0a7bc41e51e6dffc25cdaeca8acb7de2710b20a
|
||||
hN: 65908899099613711898698321155848703477601840791750658211391687862255842366922
|
||||
hG: 23094592799618609623465742609366149076596436609130823198107630312273622653270
|
||||
hxor 73599884097654065452785151481733181870375477364472235597514429707329935690908
|
||||
response: {"accountName":"anand.appleseed@example.com","c":"d-533-eccbc4e9-9564-11ef-84a6-018111c8cc60:PRN","m1":"f/Bkq8gBTYxl7SyiRd4SXTyE/jM/g6E0mVyZIQDIsPg=","m2":"R2rgqC9cMAtWiXUImOrvs4oF+ccibf8KaFsZQ22WokM=","rememberMe":false}
|
||||
*/
|
||||
|
||||
let publicKey = Data(base64Encoded: "VLEKLa+n2cyeYNWbECm45CuS4kCdCxodlTDGlW1FKaUyOrv/RbtN2sM0pVE12zI7k3VkocPC3rN5DZBIkahR6I8JHj/J97dtTvzsR+aNRWTYCT2HGP1PBI0QArp3eitAbFqTWI4+Zw+oOnV8+AYdH/wjbq7gOK4C4dvIHE+FzRwIlmguPb5qu2r47R9W3y1msVdoUGlFBOMOMb7Gyq7F0MaEIFH63lNzGomwq74mfss/cFqurd6fxU+Y7tdVTPZw1GWyBEPiXWpk8sNm2zE+S6zWo5tOsICprU75IC9galh1igfzN7VNe0SUFLNFTbFK+Bb1SFAOrAbBZOmyOG5uSQ==".data(using: .utf8)!)
|
||||
|
||||
let clientKeys = SRPKeyPair(public: .init([UInt8](publicKey!)),
|
||||
private: .init(BigNum("23161374166158551996079451276150657702422963034121842124445818241826569345033578345120284496449280736328513302994568402583647660370960353252836732307301957364261384324957527103960720408713825427474127658415917826326829664923997096839970397226662116904369925262192683131695683487505523842260218490007066160096482662715246662018133837725691586205535995663334471723776536640973591229093933458552240634178864509015968350855952558520147559154646484379002445961375384929682566525908284011230815908584648931495968206840416022306138033496705677078512266958633477047047323620540878121579549170392075029336954975132431417099801")!))
|
||||
|
||||
let password = sha256(data: "example".data(using: .utf8)!)
|
||||
let salt = Data(base64Encoded: "fIjNflgqSJXACWwwvhDU+w==".data(using: .utf8)!)!
|
||||
let iterations: Int = 20309
|
||||
let derivedKeyLength: Int = 32
|
||||
var keyArray = Array<UInt8>(repeating: 0, count: derivedKeyLength)
|
||||
|
||||
let result:Int32 = keyArray.withUnsafeMutableBytes { keyBytes -> Int32 in
|
||||
let keyBuffer = UnsafeMutablePointer<UInt8>(keyBytes.baseAddress!.assumingMemoryBound(to: UInt8.self))
|
||||
return password.withUnsafeBytes { passwordDigestBytes -> Int32 in
|
||||
let passwordBuffer = UnsafePointer<UInt8>(passwordDigestBytes.baseAddress!.assumingMemoryBound(to: UInt8.self))
|
||||
return salt.withUnsafeBytes { saltBytes -> Int32 in
|
||||
let saltBuffer = UnsafePointer<UInt8>(saltBytes.baseAddress!.assumingMemoryBound(to: UInt8.self))
|
||||
return CCKeyDerivationPBKDF(
|
||||
CCPBKDFAlgorithm(kCCPBKDF2),
|
||||
passwordBuffer,
|
||||
password.count,
|
||||
saltBuffer,
|
||||
salt.count,
|
||||
CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA256),
|
||||
UInt32(iterations),
|
||||
keyBuffer,
|
||||
derivedKeyLength)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let expectedKey: [UInt8] = [0x40, 0x53, 0x2b, 0x4d, 0xe9, 0x35, 0x3f, 0xc5, 0x37, 0xdc, 0x62, 0xee, 0x84, 0xea, 0xce, 0xbd, 0x7e, 0xcb, 0x5e, 0xc2, 0x6e, 0xfc, 0xa9, 0x8b, 0xd0, 0x1b, 0x03, 0x80, 0xe3, 0x02, 0x10, 0x0f]
|
||||
|
||||
XCTAssertEqual(expectedKey, keyArray)
|
||||
|
||||
let decodedB = Data(base64Encoded: "PMbU75wwG6rDTySXn2ASWyfQuPoW5ham15SzIscpInwOPE2uk7sePsW4ra0dHcLDUMFQn/LgBggIKOo7YZ9hf1VReiAzXwSKSHdJHjHUURTC2eNpANGUPO1qzuXYgc/MP3MR+GipKHsz+KTLT+8wLjNaiCIHsL/7evJBMw9QqiwhyXlAIm5mGZfhdTVbGpLz2/QzrFmI6pUTrHpio6m1Q74DH3FBxxIeiIcuEdGdeVt9iUweowBRyf2woasTvSV1fbMQbl+lsWPwzt/a73+J30eOGFdSubqSVYh2pV0OLqRz7zPzJars12teCWUV+0WUIaxb14Mp7tlmqcTPuqZe9w==".data(using: .utf8)!)!
|
||||
|
||||
let client = SRPClient(configuration: SRPConfiguration<SHA256>(.N2048))
|
||||
let sharedSecret = try client.calculateSharedSecret(password: Data(keyArray), salt: [UInt8](salt), clientKeys: clientKeys, serverPublicKey: .init([UInt8](decodedB)))
|
||||
|
||||
let accountName = "anand.appleseed@example.com"
|
||||
let m1 = client.calculateClientProof(username: accountName, salt: [UInt8](salt), clientPublicKey: clientKeys.public, serverPublicKey: .init([UInt8](decodedB)), sharedSecret: .init(sharedSecret.bytes))
|
||||
let m2 = client.calculateServerProof(clientPublicKey: clientKeys.public, clientProof: m1, sharedSecret: .init([UInt8](sharedSecret.bytes)))
|
||||
|
||||
XCTAssertEqual(Data(m1).base64EncodedString(), "f/Bkq8gBTYxl7SyiRd4SXTyE/jM/g6E0mVyZIQDIsPg=")
|
||||
XCTAssertEqual(Data(m2).base64EncodedString(), "R2rgqC9cMAtWiXUImOrvs4oF+ccibf8KaFsZQ22WokM=")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 223 KiB After Width: | Height: | Size: 143 KiB |
|
Before Width: | Height: | Size: 211 KiB After Width: | Height: | Size: 137 KiB |
BIN
screenshot.png
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 263 KiB After Width: | Height: | Size: 224 KiB |
|
Before Width: | Height: | Size: 244 KiB After Width: | Height: | Size: 193 KiB |