mirror of
https://github.com/XcodesOrg/XcodesApp.git
synced 2026-03-25 08:55:46 +00:00
Compare commits
409 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a0d3353b9 | ||
|
|
da8d22c6d3 | ||
|
|
4f9b7f8f6a | ||
|
|
f34e291433 | ||
|
|
789eedcdb7 | ||
|
|
f9262d9a05 | ||
|
|
4776af75ee | ||
|
|
cab5533b49 | ||
|
|
fc50db1cee | ||
|
|
576585b7de | ||
|
|
6cf9647840 | ||
|
|
90dccd8180 | ||
|
|
372d5b1f4e | ||
|
|
4063796833 | ||
|
|
be2eea0532 | ||
|
|
a6dec75603 | ||
|
|
9527fb2961 | ||
|
|
084bd8f4a1 | ||
|
|
f93da1c502 | ||
|
|
79ef494504 | ||
|
|
cdf3675a5e | ||
|
|
5974a85800 | ||
|
|
4ccb6e7f06 | ||
|
|
22e4991f9a | ||
|
|
059c51b5dd | ||
|
|
00dcf3816e | ||
|
|
08738d6912 | ||
|
|
472e36ed0f | ||
|
|
bfb8c2cbb8 | ||
|
|
a7b3f78813 | ||
|
|
2e2b16e759 | ||
|
|
0bc8e42a9b | ||
|
|
ceae881d9a | ||
|
|
debc41f688 | ||
|
|
4b9d86b22e | ||
|
|
bbec397613 | ||
|
|
a434d26921 | ||
|
|
78615418b5 | ||
|
|
54cca28dbe | ||
|
|
10ce2b8511 | ||
|
|
14b2b3da10 | ||
|
|
f3f3bf0e4f | ||
|
|
3621601658 | ||
|
|
9630a68224 | ||
|
|
227f42af37 | ||
|
|
8fbf9eef8a | ||
|
|
b302365454 | ||
|
|
a61338804c | ||
|
|
259ad0789a | ||
|
|
cc03660576 | ||
|
|
a43bf63aab | ||
|
|
cfef2879b5 | ||
|
|
36424a78e0 | ||
|
|
3d9cf73fc1 | ||
|
|
2dc1bcdcbb | ||
|
|
afa6dff0b9 | ||
|
|
17f3d365b8 | ||
|
|
7b154501b6 | ||
|
|
8654756d67 | ||
|
|
85c9bdba55 | ||
|
|
0a02b29a4b | ||
|
|
3a8722c07d | ||
|
|
29bf77007a | ||
|
|
a75c54f2f6 | ||
|
|
42c2c6bfc0 | ||
|
|
69e667cf87 | ||
|
|
dc22b913f3 | ||
|
|
9b107ec98c | ||
|
|
2ed84ef792 | ||
|
|
e04ed029de | ||
|
|
e3f996da6e | ||
|
|
aca4e0ac89 | ||
|
|
dd9a348298 | ||
|
|
d6efd8fa74 | ||
|
|
b81eab0cc4 | ||
|
|
dc31fe884c | ||
|
|
283c1a4739 | ||
|
|
cac0ebfab7 | ||
|
|
59331bcb38 | ||
|
|
3d6908f526 | ||
|
|
b91a7e53ad | ||
|
|
f8970f44d5 | ||
|
|
6050c78613 | ||
|
|
cf85e2fc5a | ||
|
|
15630109d5 | ||
|
|
7171b90c88 | ||
|
|
3d5fdd528b | ||
|
|
06558610e9 | ||
|
|
5e5fdf0d3a | ||
|
|
8277554048 | ||
|
|
ba11d4129e | ||
|
|
389d8d2a63 | ||
|
|
f4567bdf1e | ||
|
|
27bbf6f2a3 | ||
|
|
c1670b250a | ||
|
|
8e78c1c8c5 | ||
|
|
c31a1ef6fa | ||
|
|
37a86643b9 | ||
|
|
c245a1e69f | ||
|
|
0f5e42b88e | ||
|
|
dd46112769 | ||
|
|
c3e568e234 | ||
|
|
f269dcbc98 | ||
|
|
604358bf6c | ||
|
|
1a3ca60f46 | ||
|
|
27d5b03321 | ||
|
|
20b9833b67 | ||
|
|
e855a1fb62 | ||
|
|
908e7ba3ea | ||
|
|
7daa3e600e | ||
|
|
a936bf84b2 | ||
|
|
897f449b7c | ||
|
|
5ebe24060f | ||
|
|
13496d8a7d | ||
|
|
4b6616fdea | ||
|
|
74237f8fc1 | ||
|
|
c94c2c1979 | ||
|
|
b36165100b | ||
|
|
8a6f5a7deb | ||
|
|
600d4cbafa | ||
|
|
8811a6d0d8 | ||
|
|
f973e7d744 | ||
|
|
6e64db26fb | ||
|
|
97bf958301 | ||
|
|
dc5f637886 | ||
|
|
a587d53798 | ||
|
|
a433bc4f72 | ||
|
|
66deeb0d57 | ||
|
|
4a4b469e3f | ||
|
|
7628f85ba8 | ||
|
|
f64e97f7a2 | ||
|
|
1152bbfb28 | ||
|
|
33dc6347f2 | ||
|
|
7599178042 | ||
|
|
6fda52ed37 | ||
|
|
8f1f6e9363 | ||
|
|
848ce1d362 | ||
|
|
38ae157be5 | ||
|
|
7f3e8125c0 | ||
|
|
7f14a4b8b5 | ||
|
|
cd80053be6 | ||
|
|
021d0aa745 | ||
|
|
180dd52eee | ||
|
|
348c89871e | ||
|
|
21c26c9793 | ||
|
|
4e177b5fbf | ||
|
|
411f76788c | ||
|
|
88b15b989a | ||
|
|
45626e2789 | ||
|
|
1411bd30b3 | ||
|
|
f3472d8cc6 | ||
|
|
b9d37054ea | ||
|
|
48df320aa8 | ||
|
|
feac373350 | ||
|
|
9e1eff06a3 | ||
|
|
35314ce36f | ||
|
|
9d73cd2091 | ||
|
|
91b24fc162 | ||
|
|
b3fa08b43e | ||
|
|
c417978976 | ||
|
|
aa34f3b10d | ||
|
|
c595eba5df | ||
|
|
61bee6c84f | ||
|
|
0aefecf9bf | ||
|
|
acaafaeef5 | ||
|
|
7bdb227f3f | ||
|
|
7cf2c6f244 | ||
|
|
b17fbf56a8 | ||
|
|
dbf30000ca | ||
|
|
7359ae19ae | ||
|
|
c09b56086b | ||
|
|
b5e50c6eed | ||
|
|
f4c9a6d683 | ||
|
|
9a5e127387 | ||
|
|
238f974ce3 | ||
|
|
b89fc21b8b | ||
|
|
bf0dc9d855 | ||
|
|
74d8f395a5 | ||
|
|
4394c48e11 | ||
|
|
f62c5139f0 | ||
|
|
6cd8584058 | ||
|
|
96e4682c56 | ||
|
|
f58fced154 | ||
|
|
fd6baa81c4 | ||
|
|
9a866acf53 | ||
|
|
6f432eef7f | ||
|
|
5632820780 | ||
|
|
5993385a73 | ||
|
|
238ac71517 | ||
|
|
e9a95a3e63 | ||
|
|
21e2a10eec | ||
|
|
bf4650edcb | ||
|
|
14f00a0e09 | ||
|
|
d7dc67c046 | ||
|
|
8534e2850b | ||
|
|
5054ead2ca | ||
|
|
dc691ad38c | ||
|
|
4ba5d31427 | ||
|
|
791ae598a3 | ||
|
|
950045b427 | ||
|
|
34d7f6834e | ||
|
|
cc9d5e9e16 | ||
|
|
b4e598fc49 | ||
|
|
6b6088d58d | ||
|
|
e5c4fd431a | ||
|
|
ae55e0fb15 | ||
|
|
c7f9e54796 | ||
|
|
e520a96021 | ||
|
|
53ec9156ff | ||
|
|
8d7412fd18 | ||
|
|
5aed44f969 | ||
|
|
91f75843da | ||
|
|
6f0cdc72ec | ||
|
|
49eb2f4cf2 | ||
|
|
8a7597e70b | ||
|
|
144c591a41 | ||
|
|
0e76a18168 | ||
|
|
a078b8f7d0 | ||
|
|
a00dae9b0a | ||
|
|
c4bd1d2789 | ||
|
|
ab1ebe016f | ||
|
|
9cb4f243b0 | ||
|
|
c534d2494d | ||
|
|
19808c58e4 | ||
|
|
b1c986af3e | ||
|
|
2a9227a0d5 | ||
|
|
71ed9a3617 | ||
|
|
da6bfc775c | ||
|
|
66a60fcf19 | ||
|
|
0ad800a5db | ||
|
|
9d7f122d11 | ||
|
|
0252bf1fe0 | ||
|
|
36be5fd743 | ||
|
|
2418d22f71 | ||
|
|
fb7e1f65c1 | ||
|
|
8a0595adc9 | ||
|
|
7686620b1e | ||
|
|
f1604538f3 | ||
|
|
e4d21ac18b | ||
|
|
99c4b49f28 | ||
|
|
9ec90a3ca8 | ||
|
|
159f1caa5c | ||
|
|
0dc13d8eee | ||
|
|
dede435725 | ||
|
|
4acd676f69 | ||
|
|
dada7f2716 | ||
|
|
e1ea64c647 | ||
|
|
1cf4381c3a | ||
|
|
111f08fdcf | ||
|
|
4d95a270ae | ||
|
|
2184c872d3 | ||
|
|
7aaf0563e6 | ||
|
|
efcf6508f5 | ||
|
|
39f39212f6 | ||
|
|
7167c40072 | ||
|
|
79c1f67811 | ||
|
|
805f754bc8 | ||
|
|
48e9b1568e | ||
|
|
b968149235 | ||
|
|
3c5f86023e | ||
|
|
c85b3c2b6c | ||
|
|
36a9bd267b | ||
|
|
cd69ddc40e | ||
|
|
ede9bd1c46 | ||
|
|
7070575a32 | ||
|
|
638543070b | ||
|
|
98b986a30d | ||
|
|
f25fc3923e | ||
|
|
487bd2d1dd | ||
|
|
0884f85359 | ||
|
|
78a62c4eba | ||
|
|
c0cff4822c | ||
|
|
9622b563b3 | ||
|
|
2d3a26374b | ||
|
|
eda3e2680b | ||
|
|
0d631e6628 | ||
|
|
1f032a5719 | ||
|
|
c81a638e35 | ||
|
|
b3e91fad7a | ||
|
|
57bf6d8a80 | ||
|
|
7690c19115 | ||
|
|
ad749c1cb0 | ||
|
|
c5ada02a31 | ||
|
|
c1836a78c4 | ||
|
|
4649ba0c8a | ||
|
|
c153a93b1b | ||
|
|
f470986e2b | ||
|
|
b650261ddd | ||
|
|
ec4dc2b3d0 | ||
|
|
a596e5ff6c | ||
|
|
6ffce23616 | ||
|
|
2fdc06031c | ||
|
|
2f7d45c67a | ||
|
|
a2379583ef | ||
|
|
4e55aac679 | ||
|
|
d406972e81 | ||
|
|
9e787f98d8 | ||
|
|
d172df68e3 | ||
|
|
d08d2d38e4 | ||
|
|
112829b553 | ||
|
|
05f5c2cebf | ||
|
|
cecceea62f | ||
|
|
77ca8a2571 | ||
|
|
f1fea6365c | ||
|
|
03eb166c3d | ||
|
|
e56d83dbe3 | ||
|
|
7079e062d2 | ||
|
|
487cbb0045 | ||
|
|
5b18a90f81 | ||
|
|
b9dc7bf757 | ||
|
|
1bd5017350 | ||
|
|
ab4a3444ed | ||
|
|
8ff65381e6 | ||
|
|
ca363e1398 | ||
|
|
d9917ce2c4 | ||
|
|
44a85d63db | ||
|
|
19f1fd876d | ||
|
|
75ae0ab456 | ||
|
|
2f749deb8d | ||
|
|
6fcb4aa857 | ||
|
|
5edeb60835 | ||
|
|
47710b6bed | ||
|
|
330f590d8e | ||
|
|
b8c6a7cda8 | ||
|
|
cf57fde101 | ||
|
|
74ef10b697 | ||
|
|
5b95ef2af6 | ||
|
|
965b87002f | ||
|
|
5b1cc4a93d | ||
|
|
13715930af | ||
|
|
35fb72db68 | ||
|
|
dc5a8b03b6 | ||
|
|
b6e654c6f2 | ||
|
|
622fac605c | ||
|
|
de35bed9fa | ||
|
|
9dd91a2ce8 | ||
|
|
5977210252 | ||
|
|
8654a2075a | ||
|
|
7325502853 | ||
|
|
fc35950885 | ||
|
|
415d86718c | ||
|
|
581af76948 | ||
|
|
fb42bfc013 | ||
|
|
2de476b101 | ||
|
|
a556d95ee9 | ||
|
|
4f25905f4c | ||
|
|
8169ee0a22 | ||
|
|
927f842c61 | ||
|
|
9cf10125ca | ||
|
|
3353eb1112 | ||
|
|
9dd569c605 | ||
|
|
507fa37bb5 | ||
|
|
907e503f71 | ||
|
|
4ecc245db6 | ||
|
|
84702ad63b | ||
|
|
0607f57077 | ||
|
|
9b6a479798 | ||
|
|
e7f0adf69f | ||
|
|
d8e1069a92 | ||
|
|
60582094af | ||
|
|
cc4165252e | ||
|
|
3529b19295 | ||
|
|
771cb3242c | ||
|
|
7245d54a7c | ||
|
|
2d22210d5b | ||
|
|
3b1254a48e | ||
|
|
95c9776788 | ||
|
|
93f34dd31b | ||
|
|
ce906c5ce9 | ||
|
|
d3e07242b1 | ||
|
|
5423f20c26 | ||
|
|
74a870d4ff | ||
|
|
48b5611e0b | ||
|
|
371c94e3fa | ||
|
|
9d681927d2 | ||
|
|
ba031403d6 | ||
|
|
846fbc0635 | ||
|
|
0bc8edf75e | ||
|
|
b439eb555b | ||
|
|
d955ec8bd2 | ||
|
|
c2c67f1269 | ||
|
|
76bb3fbae0 | ||
|
|
29503ad9cf | ||
|
|
a0efd467c6 | ||
|
|
2f37ae0e41 | ||
|
|
9452db4749 | ||
|
|
b5550d0557 | ||
|
|
7e459f8933 | ||
|
|
7263ff3f67 | ||
|
|
16bd85c918 | ||
|
|
87cdbd7ec0 | ||
|
|
4a48d55ac7 | ||
|
|
f20ba75384 | ||
|
|
c496238fb9 | ||
|
|
7eb5a8f073 | ||
|
|
834dd9807a | ||
|
|
e0be252899 | ||
|
|
5b43677f30 | ||
|
|
816c00460d | ||
|
|
550d917b33 | ||
|
|
45ea417a98 | ||
|
|
b56a73c8fe | ||
|
|
495dfedf46 | ||
|
|
0b6ae5d7ec | ||
|
|
b4a4f8e329 | ||
|
|
cb1120624b | ||
|
|
da93339a3d | ||
|
|
beca9b4187 | ||
|
|
2030ef2d5a |
170 changed files with 31098 additions and 4746 deletions
32
.github/workflows/appcast.yml
vendored
32
.github/workflows/appcast.yml
vendored
|
|
@ -9,23 +9,23 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout 🛎
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
# If you're using actions/checkout@v3 you must set persist-credentials to false in most cases for the deployment to work correctly.
|
||||
# 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@v3.0.11
|
||||
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: Ruby ♦️
|
||||
uses: actions/setup-ruby@v1.1.3
|
||||
- name: Setup Ruby, JRuby and TruffleRuby
|
||||
uses: ruby/setup-ruby@v1.197.0
|
||||
with:
|
||||
ruby-version: '2.7'
|
||||
ruby-version: '3.0'
|
||||
|
||||
- name: Bundler 💎
|
||||
working-directory: AppCast
|
||||
|
|
@ -43,8 +43,8 @@ jobs:
|
|||
run: bundle exec jekyll build
|
||||
|
||||
- name: Publish 🚀
|
||||
uses: JamesIves/github-pages-deploy-action@releases/v3
|
||||
uses: JamesIves/github-pages-deploy-action@releases/v4
|
||||
with:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
BRANCH: gh-pages
|
||||
FOLDER: AppCast/_site
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
branch: gh-pages
|
||||
folder: AppCast/_site
|
||||
|
|
|
|||
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
|
|
@ -8,10 +8,10 @@ on:
|
|||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: macos-12
|
||||
runs-on: macos-15
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Run tests
|
||||
env:
|
||||
DEVELOPER_DIR: /Applications/Xcode_13.2.1.app
|
||||
DEVELOPER_DIR: /Applications/Xcode_16.4.app
|
||||
run: xcodebuild test -scheme Xcodes
|
||||
|
|
|
|||
2
.github/workflows/release-drafter.yml
vendored
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
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
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import Foundation
|
||||
|
||||
let machServiceName = "com.robotsandpencils.XcodesApp.Helper"
|
||||
let clientBundleID = "com.robotsandpencils.XcodesApp"
|
||||
let machServiceName = "com.xcodesorg.xcodesapp.Helper"
|
||||
let clientBundleID = "com.xcodesorg.xcodesapp"
|
||||
let subjectOrganizationalUnit = Bundle.main.infoDictionary!["CODE_SIGNING_SUBJECT_ORGANIZATIONAL_UNIT"] as! String
|
||||
|
||||
@objc(HelperXPCProtocol)
|
||||
|
|
|
|||
BIN
IconDark.png
Normal file
BIN
IconDark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
BIN
IconMono.png
Normal file
BIN
IconMono.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
84
README.md
84
README.md
|
|
@ -1,14 +1,18 @@
|
|||
<h1><img src="icon.png" align="center" width=50 height=50 /> Xcodes.app</h1>
|
||||
<h1><img src="icon.png" align="center" width=50 height=50 /> <img src="IconDark.png" align="center" width=50 height=50 /> <img src="IconMono.png" align="center" width=50 height=50 /> Xcodes.app</h1>
|
||||
|
||||
The easiest way to install and switch between multiple versions of Xcode.
|
||||
|
||||
_If you're looking for a command-line version of Xcodes.app, try [`xcodes`](https://github.com/RobotsAndPencils/xcodes)._
|
||||
_If you're looking for a command-line version of Xcodes.app, try [`xcodes`](https://github.com/XcodesOrg/xcodes)._
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
### :tada: Announcement
|
||||
|
||||
XcodesApp is now part of the `XcodesOrg` - [read more here](nextstep.md)
|
||||
|
||||
## Features
|
||||
|
||||
- List all available Xcode versions from [Xcode Releases'](https://xcodereleases.com) data or the Apple Developer website.
|
||||
|
|
@ -18,17 +22,30 @@ _If you're looking for a command-line version of Xcodes.app, try [`xcodes`](http
|
|||
- 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
|
||||
- Support installing Platforms/Runtimes
|
||||
- Support installing Apple Silicon variants
|
||||
|
||||
## 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.**
|
||||
|
||||
## Apple Silicon Variants
|
||||
|
||||
As of Xcode 26, Apple provides Apple Silicon as well as Universal variants for Xcode versions as well as each runtime. Simply tap on which variant you want installed. To install the Apple Silicon runtime variant Xcode 26 is required to be 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.
|
||||
- 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.
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## Localization
|
||||
|
||||
Xcodes supports localization in several languages.
|
||||
Xcodes supports localization in several languages.
|
||||
|
||||
The following languages are supported because of the following community users!
|
||||
|
||||
|
|
@ -36,36 +53,46 @@ The following languages are supported because of the following community users!
|
|||
|-|-|-|-|
|
||||
|French 🇫🇷 |[@dompepin](https://github.com/dompepin)|Italian 🇮🇹 |[gualtierofrigerio](https://github.com/gualtierofrigerio)|
|
||||
|Spanish 🇪🇸🇲 |[@cesartru88](https://github.com/cesartru88)|Korean 🇰🇷 |[@ryan-son](https://github.com/ryan-son)|
|
||||
|Russian 🇷🇺 |[@alexmazlov](https://github.com/alexmazlov)|Turkish 🇹🇷 |[@egrimo](https://github.com/egrimo)|
|
||||
|Russian 🇷🇺 |[@alexmazlov](https://github.com/alexmazlov)|Turkish 🇹🇷 |[@egesucu](https://github.com/egesucu)|
|
||||
|Hindi 🇮🇳 |[@KGurpreet](https://github.com/KGurpreet)|Chinese-Simplified 🇨🇳|[@megabitsenmzq](https://github.com/megabitsenmzq)|
|
||||
|Finnish 🇫🇮 |[@marcusziade](https://github.com/marcusziade)|Chinese-Traditional 🇹🇼|[@itszero](https://github.com/itszero)|
|
||||
|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)||
|
||||
|Brazilian Portuguese 🇧🇷|[@brunomunizaf](https://github.com/brunomunizaf)|Polish 🇵🇱|[@jakex7](https://github.com/jakex7)|
|
||||
|Catalan|[@ferranabello](https://github.com/ferranabello)|Greek 🇬🇷|[@alladinian](https://github.com/alladinian)
|
||||
|Thai 🇹🇭|[@neetrath](https://github.com/neetrath)|
|
||||
|
||||
Want to add more languages? Simply create a PR with the updated strings file.
|
||||
|
||||
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
|
||||
v3.X - requires macOS 13 - architecture variants and updated icon.
|
||||
|
||||
Xcodes.app runs on macOS Big Sur 11.0 or later.
|
||||
### Install with Homebrew
|
||||
|
||||
### Homebrew Cask
|
||||
Developer ID-signed and notarized release builds are available on Homebrew. These don't require Xcode to already be installed in order to use.
|
||||
|
||||
```sh
|
||||
brew install --cask xcodes
|
||||
|
||||
# These are Developer ID-signed and notarized release builds and don't require Xcode to already be installed in order to use.
|
||||
```
|
||||
|
||||
### Download a release
|
||||
### Manually install
|
||||
|
||||
1. Download the latest version [here](https://github.com/RobotsAndPencils/XcodesApp/releases/latest) using the **Xcodes.zip** asset. These are Developer ID-signed and notarized release builds and don't require Xcode to already be installed in order to use.
|
||||
1. Download the latest version [here](https://github.com/XcodesOrg/XcodesApp/releases/latest) using the **Xcodes.zip** asset. These are Developer ID-signed and notarized release builds and don't require Xcode to already be installed in order to use.
|
||||
2. Move the unzipped `Xcodes.app` to your `/Applications` directory
|
||||
|
||||
## Support
|
||||
|
||||
Xcodes.app and CLI is updated, maintained with contributors like yourself. Even open source libraries and tools come with expenses. If you would like to support Xcodes or donate to the development and maintenance of the tool, it would be greatly appreciated. There is absolutely no obligation!
|
||||
|
||||
<a href="https://opencollective.com/xcodesapp" target="_blank">
|
||||
<img src="https://opencollective.com/xcodesapp/donate/button@2x.png?color=blue" class="buymeacoffee" width=200 />
|
||||
</a>
|
||||
|
||||
## Development
|
||||
|
||||
You'll need macOS 12 Big Sur and Xcode 13 in order to build and run Xcodes.app.
|
||||
|
||||
If you aren't a Robots and Pencils employee you'll need to change the CODE_SIGNING_SUBJECT_ORGANIZATIONAL_UNIT build setting to your Apple Developer team ID in order for code signing validation to succeed between the main app and the privileged helper.
|
||||
You'll need macOS 15.6 Ventura and Xcode 26 in order to build and run Xcodes.app.
|
||||
|
||||
`Unxip` and `aria2` must be compiled as a universal binary
|
||||
```
|
||||
|
|
@ -77,10 +104,9 @@ If you aren't a Robots and Pencils employee you'll need to change the CODE_SIGNI
|
|||
# combine for universal binary
|
||||
lipo -create -output unxip unxip_intel unxip_m1
|
||||
# check it
|
||||
lipo -archs unxip
|
||||
lipo -archs unxip
|
||||
```
|
||||
|
||||
Notable design decisions are recorded in [DECISIONS.md](./DECISIONS.md). The Apple authentication flow is described in [Apple.paw](./Apple.paw), which will allow you to play with the API endpoints that are involved using the [Paw](https://paw.cloud) app.
|
||||
|
||||
[`xcode-install`](https://github.com/xcpretty/xcode-install) and [fastlane/spaceship](https://github.com/fastlane/fastlane/tree/master/spaceship) both deserve credit for figuring out the hard parts of what makes this possible.
|
||||
|
||||
|
|
@ -94,8 +120,8 @@ Follow the steps below to build and release a new version of Xcodes.app. For any
|
|||
# Update the version number in Xcode and commit the change, if necessary
|
||||
|
||||
# Question: Did anything in XPCHelper change?
|
||||
# - com.robotsandpencils.XcodesApp.Helper folder and HelperXPCShared
|
||||
# - if so, bump the version number in com.robotsandpencils.XcodesApp.Helper target.
|
||||
# - com.xcodesorg.xcodesapp.Helper folder and HelperXPCShared
|
||||
# - if so, bump the version number in com.xcodesorg.xcodesapp.Helper target.
|
||||
# Note: you do not have to bump the version number if nothing has changed.
|
||||
# Note2: If you do bump the version, the end user, must re-install the XPCHelper and give permission again.
|
||||
|
||||
|
|
@ -133,7 +159,7 @@ pushd Product
|
|||
../scripts/sign_update Xcodes.zip
|
||||
popd
|
||||
|
||||
# Go to https://github.com/RobotsAndPencils/XcodesApp/releases
|
||||
# Go to https://github.com/XcodesOrg/XcodesApp/releases
|
||||
# If there are uncategorized PRs, add the appropriate label and run the Release Drafter action manually
|
||||
# Edit the latest draft release
|
||||
# Set its tag to the tag you just pushed
|
||||
|
|
@ -144,7 +170,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>
|
||||
|
||||
|
|
@ -152,9 +179,4 @@ popd
|
|||
|
||||
[Matt Kiazyk](https://github.com/mattkiazyk) - [Twitter](https://www.twitter.com/mattkiazyk)
|
||||
|
||||
|
||||
<a href="http://www.robotsandpencils.com"><img src="R&PLogo.png" width="153" height="74" /></a>
|
||||
|
||||
Made with ❤️ by [Robots & Pencils](http://www.robotsandpencils.com)
|
||||
|
||||
[Twitter](https://twitter.com/xcodesApp) | [GitHub](https://github.com/robotsandpencils)
|
||||
[Twitter](https://twitter.com/xcodesApp) | [GitHub](https://github.com/xcodesOrg) | [Mastadon](https://iosdev.space/@XcodesApp) |
|
||||
|
|
|
|||
26
Scripts/fix_libfido2_framework.sh
Executable file
26
Scripts/fix_libfido2_framework.sh
Executable file
|
|
@ -0,0 +1,26 @@
|
|||
#!/bin/sh
|
||||
|
||||
# Fix libfido2.framework structure for macOS validation
|
||||
# If this script is not run, the build will fail because xcodebuild is expecting the library in a specific structure
|
||||
FRAMEWORK_PATH="${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/Contents/Frameworks/libfido2.framework"
|
||||
|
||||
if [ -d "$FRAMEWORK_PATH" ] && [ -f "$FRAMEWORK_PATH/Info.plist" ] && [ ! -d "$FRAMEWORK_PATH/Versions" ]; then
|
||||
echo "Fixing libfido2.framework bundle structure..."
|
||||
|
||||
# Create proper bundle structure
|
||||
mkdir -p "$FRAMEWORK_PATH/Versions/A/Resources"
|
||||
|
||||
# Move files to proper locations
|
||||
mv "$FRAMEWORK_PATH/Info.plist" "$FRAMEWORK_PATH/Versions/A/Resources/"
|
||||
mv "$FRAMEWORK_PATH/libfido2" "$FRAMEWORK_PATH/Versions/A/"
|
||||
if [ -f "$FRAMEWORK_PATH/LICENSE" ]; then
|
||||
mv "$FRAMEWORK_PATH/LICENSE" "$FRAMEWORK_PATH/Versions/A/"
|
||||
fi
|
||||
|
||||
# Create symbolic links
|
||||
ln -sf A "$FRAMEWORK_PATH/Versions/Current"
|
||||
ln -sf Versions/Current/libfido2 "$FRAMEWORK_PATH/libfido2"
|
||||
ln -sf Versions/Current/Resources "$FRAMEWORK_PATH/Resources"
|
||||
|
||||
echo "libfido2.framework structure fixed"
|
||||
fi
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
#!/bin/bash
|
||||
|
||||
PRIVILEGED_HELPER_LABEL=com.robotsandpencils.XcodesApp.Helper
|
||||
PRIVILEGED_HELPER_LABEL=com.xcodesorg.xcodesapp.Helper
|
||||
|
||||
sudo rm /Library/PrivilegedHelperTools/$PRIVILEGED_HELPER_LABEL
|
||||
sudo rm /Library/LaunchDaemons/$PRIVILEGED_HELPER_LABEL.plist
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,6 +1,24 @@
|
|||
{
|
||||
"object": {
|
||||
"pins": [
|
||||
{
|
||||
"package": "AsyncNetworkService",
|
||||
"repositoryURL": "https://github.com/RobotsAndPencils/AsyncHTTPNetworkService",
|
||||
"state": {
|
||||
"branch": "main",
|
||||
"revision": "97770856c4e429f880d4b4dd68cfaf286dc00c30",
|
||||
"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",
|
||||
|
|
@ -11,12 +29,12 @@
|
|||
}
|
||||
},
|
||||
{
|
||||
"package": "XcodeReleases",
|
||||
"repositoryURL": "https://github.com/xcodereleases/data",
|
||||
"package": "DockProgress",
|
||||
"repositoryURL": "https://github.com/sindresorhus/DockProgress",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "b47228c688b608e34b3b84079ab6052a24c7a981",
|
||||
"version": null
|
||||
"revision": "d4f23b5a8f5ca0fac393eb7ba78c2fe3e32e52da",
|
||||
"version": "4.3.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -46,13 +64,22 @@
|
|||
"version": "1.0.4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "LibFido2Swift",
|
||||
"repositoryURL": "https://github.com/kinoroy/LibFido2Swift",
|
||||
"state": {
|
||||
"branch": "main",
|
||||
"revision": "b87a93300c5b35307c9f26ae490963196bd927f1",
|
||||
"version": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "Path.swift",
|
||||
"repositoryURL": "https://github.com/mxcl/Path.swift",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "dac007e907a4f4c565cfdc55a9ce148a761a11d5",
|
||||
"version": "0.16.3"
|
||||
"revision": "8e355c28e9393c42e58b18c54cace2c42c98a616",
|
||||
"version": "1.4.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -60,8 +87,35 @@
|
|||
"repositoryURL": "https://github.com/sparkle-project/Sparkle/",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "286edd1fa22505a9e54d170e9fd07d775ea233f2",
|
||||
"version": "2.1.0"
|
||||
"revision": "0ef1ee0220239b3776f433314515fd849025673f",
|
||||
"version": "2.6.4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "swift-collections",
|
||||
"repositoryURL": "https://github.com/apple/swift-collections.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "a902f1823a7ff3c9ab2fba0f992396b948eda307",
|
||||
"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
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,78 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1500"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "CA9FF8AD2595967A00E47BAF"
|
||||
BuildableName = "com.xcodesorg.xcodesapp.Helper"
|
||||
BlueprintName = "com.xcodesorg.xcodesapp.Helper"
|
||||
ReferencedContainer = "container:Xcodes.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
shouldAutocreateTestPlan = "YES">
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES"
|
||||
viewDebuggingEnabled = "No">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "CA9FF8AD2595967A00E47BAF"
|
||||
BuildableName = "com.xcodesorg.xcodesapp.Helper"
|
||||
BlueprintName = "com.xcodesorg.xcodesapp.Helper"
|
||||
ReferencedContainer = "container:Xcodes.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "CA9FF8AD2595967A00E47BAF"
|
||||
BuildableName = "com.xcodesorg.xcodesapp.Helper"
|
||||
BlueprintName = "com.xcodesorg.xcodesapp.Helper"
|
||||
ReferencedContainer = "container:Xcodes.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
|
|
@ -35,7 +35,7 @@ private extension Xcode {
|
|||
try? task.run()
|
||||
let handle = pipe.fileHandleForReading
|
||||
let data = handle.readDataToEndOfFile()
|
||||
let path = String(data: data, encoding: String.Encoding.utf8)
|
||||
let path = String(data: data, encoding: String.Encoding.utf8)?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return (path?.isEmpty ?? true) ? nil : path
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,18 +12,66 @@ 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)
|
||||
.decode(type: ServiceKeyResponse.self, decoder: JSONDecoder())
|
||||
.flatMap { serviceKeyResponse -> AnyPublisher<URLSession.DataTaskPublisher.Output, Swift.Error> in
|
||||
.flatMap { serviceKeyResponse -> AnyPublisher<(String, String), Swift.Error> in
|
||||
serviceKey = serviceKeyResponse.authServiceKey
|
||||
return Current.network.dataTask(with: URLRequest.signIn(serviceKey: serviceKey, accountName: accountName, password: password))
|
||||
.mapError { $0 as Swift.Error }
|
||||
|
||||
// Fixes issue https://github.com/RobotsAndPencils/XcodesApp/issues/360
|
||||
// On 2023-02-23, Apple added a custom implementation of hashcash to their auth flow
|
||||
// Without this addition, Apple ID's would get set to locked
|
||||
return self.loadHashcash(accountName: accountName, serviceKey: serviceKey)
|
||||
.map { return (serviceKey, $0)}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
.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()
|
||||
}
|
||||
|
||||
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
|
||||
return Just(data)
|
||||
|
|
@ -56,6 +108,44 @@ public class Client {
|
|||
.mapError { $0 as Swift.Error }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func loadHashcash(accountName: String, serviceKey: String) -> AnyPublisher<String, Swift.Error> {
|
||||
|
||||
Result {
|
||||
try URLRequest.federate(account: accountName, serviceKey: serviceKey)
|
||||
}
|
||||
.publisher
|
||||
.flatMap { request in
|
||||
Current.network.dataTask(with: request)
|
||||
.mapError { $0 as Error }
|
||||
.tryMap { (data, response) throws -> (String) in
|
||||
guard let urlResponse = response as? HTTPURLResponse else {
|
||||
throw AuthenticationError.invalidSession
|
||||
}
|
||||
switch urlResponse.statusCode {
|
||||
case 200..<300:
|
||||
|
||||
let httpResponse = response as! HTTPURLResponse
|
||||
guard let bitsString = httpResponse.allHeaderFields["X-Apple-HC-Bits"] as? String, let bits = UInt(bitsString) else {
|
||||
throw AuthenticationError.invalidHashcash
|
||||
}
|
||||
guard let challenge = httpResponse.allHeaderFields["X-Apple-HC-Challenge"] as? String else {
|
||||
throw AuthenticationError.invalidHashcash
|
||||
}
|
||||
guard let hashcash = Hashcash().mint(resource: challenge, bits: bits) else {
|
||||
throw AuthenticationError.invalidHashcash
|
||||
}
|
||||
return (hashcash)
|
||||
case 400, 401:
|
||||
throw AuthenticationError.invalidHashcash
|
||||
case let code:
|
||||
throw AuthenticationError.badStatusCode(statusCode: code, data: data, response: urlResponse)
|
||||
}
|
||||
}
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
|
||||
}
|
||||
|
||||
func handleTwoStepOrFactor(data: Data, response: URLResponse, serviceKey: String) -> AnyPublisher<AuthenticationState, Swift.Error> {
|
||||
let httpResponse = response as! HTTPURLResponse
|
||||
|
|
@ -70,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:
|
||||
|
|
@ -91,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
|
||||
}
|
||||
|
|
@ -132,6 +225,8 @@ public class Client {
|
|||
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)
|
||||
}
|
||||
|
|
@ -143,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
|
||||
|
|
@ -177,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
|
||||
|
|
@ -190,6 +355,7 @@ public enum AuthenticationState: Equatable {
|
|||
|
||||
public enum AuthenticationError: Swift.Error, LocalizedError, Equatable {
|
||||
case invalidSession
|
||||
case invalidHashcash
|
||||
case invalidUsernameOrPassword(username: String)
|
||||
case incorrectSecurityCode
|
||||
case unexpectedSignInResponse(statusCode: Int, message: String?)
|
||||
|
|
@ -201,11 +367,14 @@ public enum AuthenticationError: Swift.Error, LocalizedError, Equatable {
|
|||
case notDeveloperAppleId
|
||||
case notAuthorized
|
||||
case invalidResult(resultString: String?)
|
||||
case srpInvalidPublicKey
|
||||
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
case .invalidSession:
|
||||
return "Your authentication session is invalid. Try signing in again."
|
||||
case .invalidHashcash:
|
||||
return "Could not create a hashcash for the session."
|
||||
case .invalidUsernameOrPassword:
|
||||
return "Invalid username and password combination."
|
||||
case .incorrectSecurityCode:
|
||||
|
|
@ -233,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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -273,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 {
|
||||
|
|
@ -301,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
|
||||
}
|
||||
|
|
@ -363,7 +546,7 @@ public struct AuthOptionsResponse: Equatable, Decodable {
|
|||
}
|
||||
|
||||
public enum Kind: Equatable {
|
||||
case twoStep, twoFactor, unknown
|
||||
case twoStep, twoFactor, securityKey, unknown
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -400,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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
96
Xcodes/AppleAPI/Sources/AppleAPI/Hashcash.swift
Normal file
96
Xcodes/AppleAPI/Sources/AppleAPI/Hashcash.swift
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
//
|
||||
// Hashcash.swift
|
||||
//
|
||||
//
|
||||
// Created by Matt Kiazyk on 2023-02-23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CryptoKit
|
||||
import CommonCrypto
|
||||
|
||||
/*
|
||||
# This App Store Connect hashcash spec was generously donated by...
|
||||
#
|
||||
# __ _
|
||||
# __ _ _ __ _ __ / _|(_) __ _ _ _ _ __ ___ ___
|
||||
# / _` || '_ \ | '_ \ | |_ | | / _` || | | || '__|/ _ \/ __|
|
||||
# | (_| || |_) || |_) || _|| || (_| || |_| || | | __/\__ \
|
||||
# \__,_|| .__/ | .__/ |_| |_| \__, | \__,_||_| \___||___/
|
||||
# |_| |_| |___/
|
||||
#
|
||||
#
|
||||
*/
|
||||
public struct Hashcash {
|
||||
/// A function to returned a minted hash, using a bit and resource string
|
||||
///
|
||||
/**
|
||||
X-APPLE-HC: 1:11:20230223170600:4d74fb15eb23f465f1f6fcbf534e5877::6373
|
||||
^ ^ ^ ^ ^
|
||||
| | | | +-- Counter
|
||||
| | | +-- Resource
|
||||
| | +-- Date YYMMDD[hhmm[ss]]
|
||||
| +-- Bits (number of leading zeros)
|
||||
+-- Version
|
||||
|
||||
We can't use an off-the-shelf Hashcash because Apple's implementation is not quite the same as the spec/convention.
|
||||
1. The spec calls for a nonce called "Rand" to be inserted between the Ext and Counter. They don't do that at all.
|
||||
2. The Counter conventionally encoded as base-64 but Apple just uses the decimal number's string representation.
|
||||
|
||||
Iterate from Counter=0 to Counter=N finding an N that makes the SHA1(X-APPLE-HC) lead with Bits leading zero bits
|
||||
We get the "Resource" from the X-Apple-HC-Challenge header and Bits from X-Apple-HC-Bits
|
||||
*/
|
||||
/// - Parameters:
|
||||
/// - resource: a string to be used for minting
|
||||
/// - bits: grabbed from `X-Apple-HC-Bits` header
|
||||
/// - date: Default uses Date() otherwise used for testing to check.
|
||||
/// - Returns: A String hash to use in `X-Apple-HC` header on /signin
|
||||
public func mint(resource: String,
|
||||
bits: UInt = 10,
|
||||
date: String? = nil) -> String? {
|
||||
|
||||
let ver = "1"
|
||||
|
||||
var ts: String
|
||||
if let date = date {
|
||||
ts = date
|
||||
} else {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "yyMMddHHmmss"
|
||||
ts = formatter.string(from: Date())
|
||||
}
|
||||
|
||||
let challenge = "\(ver):\(bits):\(ts):\(resource):"
|
||||
|
||||
var counter = 0
|
||||
|
||||
while true {
|
||||
guard let digest = ("\(challenge):\(counter)").sha1 else {
|
||||
print("ERROR: Can't generate SHA1 digest")
|
||||
return nil
|
||||
}
|
||||
|
||||
if digest == bits {
|
||||
return "\(challenge):\(counter)"
|
||||
}
|
||||
counter += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension String {
|
||||
var sha1: Int? {
|
||||
|
||||
let data = Data(self.utf8)
|
||||
var digest = [UInt8](repeating: 0, count:Int(CC_SHA1_DIGEST_LENGTH))
|
||||
data.withUnsafeBytes {
|
||||
_ = CC_SHA1($0.baseAddress, CC_LONG(data.count), &digest)
|
||||
}
|
||||
let bigEndianValue = digest.withUnsafeBufferPointer {
|
||||
($0.baseAddress!.withMemoryRebound(to: UInt32.self, capacity: 1) { $0 })
|
||||
}.pointee
|
||||
let value = UInt32(bigEndian: bigEndianValue)
|
||||
return value.leadingZeroBitCount
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -7,7 +7,13 @@ public extension URL {
|
|||
static let requestSecurityCode = URL(string: "https://idmsa.apple.com/appleauth/auth/verify/phone")!
|
||||
static func submitSecurityCode(_ code: SecurityCode) -> URL { URL(string: "https://idmsa.apple.com/appleauth/auth/verify/\(code.urlPathComponent)/securitycode")! }
|
||||
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 {
|
||||
|
|
@ -15,7 +21,7 @@ public extension URLRequest {
|
|||
return URLRequest(url: .itcServiceKey)
|
||||
}
|
||||
|
||||
static func signIn(serviceKey: String, accountName: String, password: String) -> URLRequest {
|
||||
static func signIn(serviceKey: String, accountName: String, password: String, hashcash: String) -> URLRequest {
|
||||
struct Body: Encodable {
|
||||
let accountName: String
|
||||
let password: String
|
||||
|
|
@ -27,6 +33,7 @@ public extension URLRequest {
|
|||
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.allHTTPHeaderFields?["Accept"] = "application/json, text/javascript"
|
||||
request.httpMethod = "POST"
|
||||
request.httpBody = try! JSONEncoder().encode(Body(accountName: accountName, password: password))
|
||||
|
|
@ -103,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)
|
||||
|
|
@ -117,4 +137,68 @@ public extension URLRequest {
|
|||
static var olympusSession: URLRequest {
|
||||
return URLRequest(url: .olympusSession)
|
||||
}
|
||||
|
||||
static func federate(account: String, serviceKey: String) throws -> URLRequest {
|
||||
struct FederateRequest: Encodable {
|
||||
let accountName: String
|
||||
let rememberMe: Bool
|
||||
}
|
||||
var request = URLRequest(url: .signIn)
|
||||
request.allHTTPHeaderFields?["Accept"] = "application/json"
|
||||
request.allHTTPHeaderFields?["Content-Type"] = "application/json"
|
||||
request.httpMethod = "GET"
|
||||
|
||||
// let encoder = JSONEncoder()
|
||||
// encoder.outputFormatting = .withoutEscapingSlashes
|
||||
// request.httpBody = try encoder.encode(FederateRequest(accountName: account, rememberMe: true))
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2,14 +2,26 @@ import XCTest
|
|||
@testable import AppleAPI
|
||||
|
||||
final class AppleAPITests: XCTestCase {
|
||||
func testExample() {
|
||||
// This is an example of a functional test case.
|
||||
// Use XCTAssert and related functions to verify your tests produce the correct
|
||||
// results.
|
||||
XCTAssertEqual(AppleAPI().text, "Hello, World!")
|
||||
|
||||
func testValidHashCashMint() {
|
||||
let bits: UInt = 11
|
||||
let resource = "4d74fb15eb23f465f1f6fcbf534e5877"
|
||||
let testDate = "20230223170600"
|
||||
|
||||
let stamp = Hashcash().mint(resource: resource, bits: bits, date: testDate)
|
||||
XCTAssertEqual(stamp, "1:11:20230223170600:4d74fb15eb23f465f1f6fcbf534e5877::6373")
|
||||
}
|
||||
func testValidHashCashMint2() {
|
||||
let bits: UInt = 10
|
||||
let resource = "bb63edf88d2f9c39f23eb4d6f0281158"
|
||||
let testDate = "20230224001754"
|
||||
|
||||
let stamp = Hashcash().mint(resource: resource, bits: bits, date: testDate)
|
||||
XCTAssertEqual(stamp, "1:10:20230224001754:bb63edf88d2f9c39f23eb4d6f0281158::866")
|
||||
}
|
||||
|
||||
static var allTests = [
|
||||
("testExample", testExample),
|
||||
("testValidHashCashMint", testValidHashCashMint),
|
||||
("testValidHashCashMint2", testValidHashCashMint2),
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,14 +5,16 @@ import AppleAPI
|
|||
import Version
|
||||
import LegibleError
|
||||
import os.log
|
||||
import DockProgress
|
||||
import XcodesKit
|
||||
|
||||
/// Downloads and installs Xcodes
|
||||
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
|
||||
|
|
@ -43,6 +45,8 @@ extension AppState {
|
|||
|
||||
Logger.appState.info("Using \(downloader) downloader")
|
||||
|
||||
setupDockProgress()
|
||||
|
||||
return validateSession()
|
||||
.flatMap { _ in
|
||||
self.getXcodeArchive(installationType, downloader: downloader)
|
||||
|
|
@ -51,6 +55,8 @@ extension AppState {
|
|||
self.installArchivedXcode(xcode, at: url)
|
||||
}
|
||||
.catch { error -> AnyPublisher<InstalledXcode, Swift.Error> in
|
||||
self.resetDockProgressTracking()
|
||||
|
||||
switch error {
|
||||
case InstallationError.damagedXIP(let damagedXIPURL):
|
||||
guard attemptNumber < 1 else { return Fail(error: error).eraseToAnyPublisher() }
|
||||
|
|
@ -99,6 +105,7 @@ extension AppState {
|
|||
self.downloadOrUseExistingArchive(for: availableXcode, downloader: downloader, progressChanged: { [unowned self] progress in
|
||||
DispatchQueue.main.async {
|
||||
self.setInstallationStep(of: availableXcode.version, to: .downloading(progress: progress))
|
||||
self.overallProgress.addChild(progress, withPendingUnitCount: AppState.totalProgressUnits - AppState.unxipProgressWeight)
|
||||
}
|
||||
})
|
||||
.map { return (availableXcode, $0) }
|
||||
|
|
@ -151,6 +158,7 @@ extension AppState {
|
|||
cookies
|
||||
)
|
||||
progressChanged(progress)
|
||||
|
||||
return publisher
|
||||
.map { _ in destination.url }
|
||||
.eraseToAnyPublisher()
|
||||
|
|
@ -165,6 +173,7 @@ extension AppState {
|
|||
to: destination.url,
|
||||
resumingWith: resumeData ?? persistedResumeData)
|
||||
progressChanged(progress)
|
||||
|
||||
return publisher
|
||||
.map { $0.saveLocation }
|
||||
.eraseToAnyPublisher()
|
||||
|
|
@ -176,6 +185,9 @@ extension AppState {
|
|||
}
|
||||
|
||||
public func installArchivedXcode(_ availableXcode: AvailableXcode, at archiveURL: URL) -> AnyPublisher<InstalledXcode, Error> {
|
||||
unxipProgress.completedUnitCount = 0
|
||||
overallProgress.addChild(unxipProgress, withPendingUnitCount: AppState.unxipProgressWeight)
|
||||
|
||||
do {
|
||||
let destinationURL = Path.installDirectory.join("Xcode-\(availableXcode.version.descriptionWithoutBuildMetadata).app").url
|
||||
switch archiveURL.pathExtension {
|
||||
|
|
@ -215,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)
|
||||
|
|
@ -415,6 +428,9 @@ extension AppState {
|
|||
}
|
||||
self.presentedAlert = .privilegedHelper
|
||||
}
|
||||
|
||||
unxipProgress.completedUnitCount = AppState.totalProgressUnits
|
||||
resetDockProgressTracking()
|
||||
|
||||
return helperInstallConsentSubject
|
||||
.flatMap {
|
||||
|
|
@ -455,15 +471,48 @@ extension AppState {
|
|||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
// MARK: - Dock Progress Tracking
|
||||
|
||||
private func setupDockProgress() {
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func resetDockProgressTracking() {
|
||||
Task { @MainActor in
|
||||
DockProgress.progress = 1 // Only way to completely remove overlay with DockProgress is setting progress to complete
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
func setInstallationStep(of version: Version, to step: InstallationStep) {
|
||||
func setInstallationStep(of version: Version, to step: XcodeInstallationStep) {
|
||||
DispatchQueue.main.async {
|
||||
guard let index = self.allXcodes.firstIndex(where: { $0.version.isEquivalent(to: version) }) else { return }
|
||||
self.allXcodes[index].installState = .installing(step)
|
||||
|
||||
let xcode = self.allXcodes[index]
|
||||
Current.notificationManager.scheduleNotification(title: xcode.id.appleDescription, body: step.description, category: .normal)
|
||||
Current.notificationManager.scheduleNotification(title: xcode.version.major.description + "." + xcode.version.appleDescription, body: step.description, category: .normal)
|
||||
}
|
||||
}
|
||||
|
||||
func setInstallationStep(of runtime: DownloadableRuntime, to step: RuntimeInstallationStep, postNotification: Bool = true) {
|
||||
DispatchQueue.main.async {
|
||||
guard let index = self.downloadableRuntimes.firstIndex(where: { $0.identifier == runtime.identifier }) else { return }
|
||||
self.downloadableRuntimes[index].installState = .installing(step)
|
||||
if postNotification {
|
||||
Current.notificationManager.scheduleNotification(title: runtime.name, body: step.description, category: .normal)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
356
Xcodes/Backend/AppState+Runtimes.swift
Normal file
356
Xcodes/Backend/AppState+Runtimes.swift
Normal file
|
|
@ -0,0 +1,356 @@
|
|||
import Foundation
|
||||
import XcodesKit
|
||||
import OSLog
|
||||
import Combine
|
||||
import Path
|
||||
import AppleAPI
|
||||
import Version
|
||||
|
||||
extension AppState {
|
||||
func updateDownloadableRuntimes() {
|
||||
Task {
|
||||
do {
|
||||
|
||||
let downloadableRuntimes = try await self.runtimeService.downloadableRuntimes()
|
||||
let runtimes = downloadableRuntimes.downloadables.map { runtime in
|
||||
var updatedRuntime = runtime
|
||||
|
||||
// This loops through and matches up the simulatorVersion to the mappings
|
||||
let simulatorBuildUpdate = downloadableRuntimes.sdkToSimulatorMappings.filter { SDKToSimulatorMapping in
|
||||
SDKToSimulatorMapping.simulatorBuildUpdate == runtime.simulatorVersion.buildUpdate
|
||||
}
|
||||
updatedRuntime.sdkBuildUpdate = simulatorBuildUpdate.map { $0.sdkBuildUpdate }
|
||||
return updatedRuntime
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.downloadableRuntimes = runtimes
|
||||
}
|
||||
try? cacheDownloadableRuntimes(runtimes)
|
||||
} catch {
|
||||
Logger.appState.error("Error downloading runtimes: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateInstalledRuntimes() {
|
||||
Task {
|
||||
do {
|
||||
Logger.appState.info("Loading Installed runtimes")
|
||||
let runtimes = try await self.runtimeService.localInstalledRuntimes()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.installedRuntimes = runtimes
|
||||
}
|
||||
} catch {
|
||||
Logger.appState.error("Error loading installed runtimes: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
if runtime.architectures?.isAppleSilicon ?? false {
|
||||
// Need Xcode 26 but with some RC/Beta's its simpler to just to greater > 25
|
||||
if selectedXcode.version > Version(major: 25, 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.Xcode26"))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
} else {
|
||||
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, runtime.architectures?.isAppleSilicon ?? false ? Architecture.arm64.rawValue : nil)
|
||||
|
||||
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)
|
||||
if !Task.isCancelled {
|
||||
Logger.appState.debug("Installing runtime: \(runtime.name)")
|
||||
DispatchQueue.main.async {
|
||||
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)"
|
||||
case .diskImage:
|
||||
try await self.installFromImage(dmgURL: downloadedURL)
|
||||
DispatchQueue.main.async {
|
||||
self.setInstallationStep(of: runtime, to: .trashingArchive)
|
||||
}
|
||||
try Current.files.removeItem(at: downloadedURL)
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
guard let index = self.downloadableRuntimes.firstIndex(where: { $0.identifier == runtime.identifier }) else { return }
|
||||
self.downloadableRuntimes[index].installState = .installed
|
||||
}
|
||||
updateInstalledRuntimes()
|
||||
}
|
||||
|
||||
}
|
||||
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 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: downloadPath)
|
||||
|
||||
let downloader = Downloader(rawValue: Current.defaults.string(forKey: "downloader") ?? "aria2") ?? .aria2
|
||||
|
||||
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")
|
||||
var aria2DownloadIsIncomplete = false
|
||||
if case .aria2 = downloader, aria2DownloadMetadataPath.exists {
|
||||
aria2DownloadIsIncomplete = true
|
||||
}
|
||||
if Current.files.fileExistsAtPath(expectedRuntimePath.string), aria2DownloadIsIncomplete == false {
|
||||
Logger.appState.info("Found existing runtime that will be used for installation at \(expectedRuntimePath).")
|
||||
return expectedRuntimePath.url
|
||||
}
|
||||
|
||||
Logger.appState.info("Downloading \(runtime.visibleIdentifier) with \(downloader)")
|
||||
switch downloader {
|
||||
case .aria2:
|
||||
let aria2Path = Path(url: Bundle.main.url(forAuxiliaryExecutable: "aria2c")!)!
|
||||
for try await progress in downloadRuntimeWithAria2(runtime, to: expectedRuntimePath, aria2Path: aria2Path) {
|
||||
DispatchQueue.main.async {
|
||||
self.setInstallationStep(of: runtime, to: .downloading(progress: progress), postNotification: false)
|
||||
}
|
||||
}
|
||||
Logger.appState.debug("Done downloading runtime")
|
||||
|
||||
case .urlSession:
|
||||
throw "Downloading runtimes with URLSession is not supported. Please use aria2"
|
||||
}
|
||||
return expectedRuntimePath.url
|
||||
}
|
||||
|
||||
public func downloadRuntimeWithAria2(_ runtime: DownloadableRuntime, to destination: Path, aria2Path: Path) -> AsyncThrowingStream<Progress, Error> {
|
||||
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, url, destination, cookies)
|
||||
}
|
||||
|
||||
|
||||
public func installFromImage(dmgURL: URL) async throws {
|
||||
try await self.runtimeService.installRuntimeImage(dmgURL: dmgURL)
|
||||
}
|
||||
|
||||
func cancelRuntimeInstall(runtime: DownloadableRuntime) {
|
||||
// Cancel the publisher
|
||||
|
||||
runtimePublishers[runtime.identifier]?.cancel()
|
||||
runtimePublishers[runtime.identifier] = nil
|
||||
|
||||
// If the download is cancelled by the user, clean up the download files that aria2 creates.
|
||||
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")
|
||||
|
||||
try? Current.files.removeItem(at: expectedRuntimePath.url)
|
||||
try? Current.files.removeItem(at: aria2DownloadMetadataPath.url)
|
||||
|
||||
guard let index = self.downloadableRuntimes.firstIndex(where: { $0.identifier == runtime.identifier }) else { return }
|
||||
self.downloadableRuntimes[index].installState = .notInstalled
|
||||
|
||||
updateInstalledRuntimes()
|
||||
}
|
||||
|
||||
func runtimeInstallPath(xcode: Xcode, runtime: DownloadableRuntime) -> Path? {
|
||||
if let coreSimulatorInfo = coreSimulatorInfo(runtime: runtime) {
|
||||
let urlString = coreSimulatorInfo.path["relative"]!
|
||||
// app was not allowed to open up file:// url's so remove
|
||||
let fileRemovedString = urlString.replacingOccurrences(of: "file://", with: "")
|
||||
let url = URL(fileURLWithPath: fileRemovedString)
|
||||
|
||||
return Path(url: url)!
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func coreSimulatorInfo(runtime: DownloadableRuntime) -> CoreSimulatorImage? {
|
||||
return installedRuntimes.filter({
|
||||
$0.runtimeInfo.build == runtime.simulatorVersion.buildUpdate &&
|
||||
((runtime.architectures ?? []).isEmpty ? true :
|
||||
$0.runtimeInfo.supportedArchitectures == runtime.architectures )}).first
|
||||
}
|
||||
|
||||
func deleteRuntime(runtime: DownloadableRuntime) async throws {
|
||||
if let info = coreSimulatorInfo(runtime: runtime) {
|
||||
try await runtimeService.deleteRuntime(identifier: info.uuid)
|
||||
|
||||
// give it some time to actually finish deleting before updating
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
|
||||
self?.updateInstalledRuntimes()
|
||||
}
|
||||
} else {
|
||||
throw "No simulator found with \(runtime.identifier)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension AnyPublisher {
|
||||
func async() async throws -> Output {
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
var cancellable: AnyCancellable?
|
||||
|
||||
cancellable = first()
|
||||
.sink { result in
|
||||
switch result {
|
||||
case .finished:
|
||||
break
|
||||
case let .failure(error):
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
cancellable?.cancel()
|
||||
} receiveValue: { value in
|
||||
continuation.resume(with: .success(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
extension AnyPublisher where Failure: Error {
|
||||
struct Subscriber {
|
||||
fileprivate let send: (Output) -> Void
|
||||
fileprivate let complete: (Subscribers.Completion<Failure>) -> Void
|
||||
|
||||
func send(_ value: Output) { self.send(value) }
|
||||
func send(completion: Subscribers.Completion<Failure>) { self.complete(completion) }
|
||||
}
|
||||
|
||||
init(_ closure: (Subscriber) -> AnyCancellable) {
|
||||
let subject = PassthroughSubject<Output, Failure>()
|
||||
|
||||
let subscriber = Subscriber(
|
||||
send: subject.send,
|
||||
complete: subject.send(completion:)
|
||||
)
|
||||
let cancel = closure(subscriber)
|
||||
|
||||
self = subject
|
||||
.handleEvents(receiveCancel: cancel.cancel)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
|
||||
extension AnyPublisher where Failure == Error {
|
||||
init(taskPriority: TaskPriority? = nil, asyncFunc: @escaping () async throws -> Output) {
|
||||
self.init { subscriber in
|
||||
let task = Task(priority: taskPriority) {
|
||||
do {
|
||||
subscriber.send(try await asyncFunc())
|
||||
subscriber.send(completion: .finished)
|
||||
} catch {
|
||||
subscriber.send(completion: .failure(error))
|
||||
}
|
||||
}
|
||||
return AnyCancellable { task.cancel() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,8 +3,8 @@ import Foundation
|
|||
import Path
|
||||
import Version
|
||||
import SwiftSoup
|
||||
import struct XCModel.Xcode
|
||||
import AppleAPI
|
||||
import XcodesKit
|
||||
|
||||
extension AppState {
|
||||
|
||||
|
|
@ -36,6 +36,8 @@ extension AppState {
|
|||
|
||||
func update() {
|
||||
guard !isUpdating else { return }
|
||||
updateDownloadableRuntimes()
|
||||
updateInstalledRuntimes()
|
||||
updatePublisher = updateSelectedXcodePath()
|
||||
.flatMap { _ in
|
||||
self.updateAvailableXcodes(from: self.dataSource)
|
||||
|
|
@ -125,6 +127,21 @@ extension AppState {
|
|||
withIntermediateDirectories: true)
|
||||
try data.write(to: Path.cacheFile.url)
|
||||
}
|
||||
|
||||
// MARK: Runtime Cache
|
||||
|
||||
func loadCacheDownloadableRuntimes() throws {
|
||||
guard let data = Current.files.contents(atPath: Path.runtimeCacheFile.string) else { return }
|
||||
let runtimes = try JSONDecoder().decode([DownloadableRuntime].self, from: data)
|
||||
self.downloadableRuntimes = runtimes
|
||||
}
|
||||
|
||||
func cacheDownloadableRuntimes(_ runtimes: [DownloadableRuntime]) throws {
|
||||
let data = try JSONEncoder().encode(runtimes)
|
||||
try FileManager.default.createDirectory(at: Path.runtimeCacheFile.url.deletingLastPathComponent(),
|
||||
withIntermediateDirectories: true)
|
||||
try data.write(to: Path.runtimeCacheFile.url)
|
||||
}
|
||||
}
|
||||
|
||||
extension AppState {
|
||||
|
|
@ -193,8 +210,8 @@ extension AppState {
|
|||
private func xcodeReleases() -> AnyPublisher<[AvailableXcode], Error> {
|
||||
Current.network.dataTask(with: URLRequest(url: URL(string: "https://xcodereleases.com/data.json")!))
|
||||
.map(\.data)
|
||||
.decode(type: [XCModel.Xcode].self, decoder: JSONDecoder())
|
||||
.map { xcReleasesXcodes in
|
||||
.decode(type: [XcodeRelease].self, decoder: JSONDecoder())
|
||||
.map { xcReleasesXcodes in
|
||||
let xcodes = xcReleasesXcodes.compactMap { xcReleasesXcode -> AvailableXcode? in
|
||||
guard
|
||||
let downloadURL = xcReleasesXcode.links?.download?.url,
|
||||
|
|
@ -215,7 +232,8 @@ extension AppState {
|
|||
requiredMacOSVersion: xcReleasesXcode.requires,
|
||||
releaseNotesURL: xcReleasesXcode.links?.notes?.url,
|
||||
sdks: xcReleasesXcode.sdks,
|
||||
compilers: xcReleasesXcode.compilers
|
||||
compilers: xcReleasesXcode.compilers,
|
||||
architectures: xcReleasesXcode.architectures
|
||||
)
|
||||
}
|
||||
return xcodes
|
||||
|
|
|
|||
|
|
@ -7,9 +7,33 @@ import KeychainAccess
|
|||
import Path
|
||||
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
|
||||
case xcodeListArchitectures
|
||||
|
||||
func isManaged() -> Bool { UserDefaults.standard.objectIsForced(forKey: self.rawValue) }
|
||||
}
|
||||
|
||||
class AppState: ObservableObject {
|
||||
private let client = AppleAPI.Client()
|
||||
internal let runtimeService = RuntimeService()
|
||||
|
||||
// MARK: - Published Properties
|
||||
|
||||
|
|
@ -45,6 +69,7 @@ class AppState: ObservableObject {
|
|||
@Published var isProcessingAuthRequest = false
|
||||
@Published var xcodeBeingConfirmedForUninstallation: Xcode?
|
||||
@Published var presentedAlert: XcodesAlert?
|
||||
@Published var presentedPreferenceAlert: XcodesPreferencesAlert?
|
||||
@Published var helperInstallState: HelperInstallState = .notInstalled
|
||||
/// Whether the user is being prepared for the helper installation alert with an explanation.
|
||||
/// This closure will be performed after the user chooses whether or not to proceed.
|
||||
|
|
@ -62,18 +87,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")
|
||||
|
|
@ -81,7 +112,7 @@ class AppState: ObservableObject {
|
|||
}
|
||||
|
||||
var createSymLinkOnSelectDisabled: Bool {
|
||||
return onSelectActionType == .rename
|
||||
return onSelectActionType == .rename || PreferenceKey.createSymLinkOnSelect.isManaged()
|
||||
}
|
||||
|
||||
@Published var onSelectActionType = SelectedActionType.none {
|
||||
|
|
@ -93,14 +124,47 @@ 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] = []
|
||||
@Published var installedRuntimes: [CoreSimulatorImage] = []
|
||||
|
||||
// MARK: - Publisher Cancellables
|
||||
|
||||
var cancellables = Set<AnyCancellable>()
|
||||
private var installationPublishers: [Version: AnyCancellable] = [:]
|
||||
private var installationPublishers: [XcodeID: AnyCancellable] = [:]
|
||||
internal var runtimePublishers: [String: Task<(), any Error>] = [:]
|
||||
private var selectPublisher: AnyCancellable?
|
||||
private var uninstallPublisher: AnyCancellable?
|
||||
private var autoInstallTimer: Timer?
|
||||
|
||||
// MARK: - Dock Progress Tracking
|
||||
|
||||
public static let totalProgressUnits = Int64(10)
|
||||
public static let unxipProgressWeight = Int64(1)
|
||||
var overallProgress = Progress()
|
||||
var unxipProgress = {
|
||||
let progress = Progress(totalUnitCount: totalProgressUnits)
|
||||
progress.kind = .file
|
||||
progress.fileOperationKind = .copying
|
||||
return progress
|
||||
}()
|
||||
|
||||
// MARK: -
|
||||
|
||||
var dataSource: DataSource {
|
||||
|
|
@ -131,6 +195,7 @@ class AppState: ObservableObject {
|
|||
init() {
|
||||
guard !isTesting else { return }
|
||||
try? loadCachedAvailableXcodes()
|
||||
try? loadCacheDownloadableRuntimes()
|
||||
checkIfHelperIsInstalled()
|
||||
setupAutoInstallTimer()
|
||||
setupDefaults()
|
||||
|
|
@ -142,13 +207,15 @@ class AppState: ObservableObject {
|
|||
createSymLinkOnSelect = Current.defaults.bool(forKey: "createSymLinkOnSelect") ?? false
|
||||
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
|
||||
|
|
@ -160,11 +227,23 @@ class AppState: ObservableObject {
|
|||
func validateADCSession(path: String) -> AnyPublisher<Void, Error> {
|
||||
return Current.network.dataTask(with: URLRequest.downloadADCAuth(path: path))
|
||||
.receive(on: DispatchQueue.main)
|
||||
.tryMap { _ in
|
||||
.tryMap { result -> Void in
|
||||
let httpResponse = result.response as! HTTPURLResponse
|
||||
if httpResponse.statusCode == 401 {
|
||||
throw AuthenticationError.notAuthorized
|
||||
}
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func validateADCSession(path: String) async throws {
|
||||
let result = try await Current.network.dataTaskAsync(with: URLRequest.downloadADCAuth(path: path))
|
||||
let httpResponse = result.1 as! HTTPURLResponse
|
||||
if httpResponse.statusCode == 401 {
|
||||
throw AuthenticationError.notAuthorized
|
||||
}
|
||||
}
|
||||
|
||||
func validateSession() -> AnyPublisher<Void, Error> {
|
||||
|
||||
return Current.network.validateSession()
|
||||
|
|
@ -199,7 +278,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 }
|
||||
|
|
@ -212,7 +291,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
|
||||
|
|
@ -227,11 +306,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) {
|
||||
|
|
@ -277,6 +362,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):
|
||||
|
|
@ -362,15 +524,12 @@ class AppState: ObservableObject {
|
|||
|
||||
// MARK: - Install
|
||||
|
||||
func checkMinVersionAndInstall(id: Xcode.ID) {
|
||||
guard let availableXcode = availableXcodes.first(where: { $0.version == id }) else { return }
|
||||
func checkMinVersionAndInstall(id: XcodeID) {
|
||||
guard let availableXcode = availableXcodes.first(where: { $0.xcodeID == id }) else { return }
|
||||
|
||||
// Check to see if users MacOS is supported
|
||||
// Check to see if users macOS is supported
|
||||
if let requiredMacOSVersion = availableXcode.requiredMacOSVersion {
|
||||
let split = requiredMacOSVersion.components(separatedBy: ".").compactMap { Int($0) }
|
||||
let xcodeMinimumMacOSVersion = OperatingSystemVersion(majorVersion: split[safe: 0] ?? 0, minorVersion: split[safe: 1] ?? 0, patchVersion: split[safe: 2] ?? 0)
|
||||
|
||||
if !ProcessInfo.processInfo.isOperatingSystemAtLeast(xcodeMinimumMacOSVersion) {
|
||||
if hasMinSupportedOS(requiredMacOSVersion: requiredMacOSVersion) {
|
||||
// prompt
|
||||
self.presentedAlert = .checkMinSupportedVersion(xcode: availableXcode, macOS: ProcessInfo.processInfo.operatingSystemVersion.versionString())
|
||||
return
|
||||
|
|
@ -385,10 +544,22 @@ class AppState: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
func install(id: Xcode.ID) {
|
||||
guard let availableXcode = availableXcodes.first(where: { $0.version == id }) else { return }
|
||||
func hasMinSupportedOS(requiredMacOSVersion: String) -> Bool {
|
||||
let split = requiredMacOSVersion.components(separatedBy: ".").compactMap { Int($0) }
|
||||
let xcodeMinimumMacOSVersion = OperatingSystemVersion(majorVersion: split[safe: 0] ?? 0, minorVersion: split[safe: 1] ?? 0, patchVersion: split[safe: 2] ?? 0)
|
||||
|
||||
return !ProcessInfo.processInfo.isOperatingSystemAtLeast(xcodeMinimumMacOSVersion)
|
||||
}
|
||||
|
||||
func install(id: XcodeID) {
|
||||
guard let availableXcode = availableXcodes.first(where: { $0.xcodeID == 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,
|
||||
|
|
@ -432,7 +603,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(
|
||||
|
|
@ -440,7 +611,11 @@ class AppState: ObservableObject {
|
|||
self.installationPublishers[id] = nil
|
||||
if case let .failure(error) = completion {
|
||||
// Prevent setting the app state error if it is an invalid session, we will present the sign in view instead
|
||||
if error as? AuthenticationError != .invalidSession {
|
||||
if let error = error as? AuthenticationError, case .notAuthorized = error {
|
||||
self.error = error
|
||||
self.presentedAlert = .unauthenticated
|
||||
|
||||
} else if error as? AuthenticationError != .invalidSession {
|
||||
self.error = error
|
||||
self.presentedAlert = .generic(title: localizeString("Alert.Install.Error.Title"), message: error.legibleLocalizedDescription)
|
||||
}
|
||||
|
|
@ -456,9 +631,9 @@ class AppState: ObservableObject {
|
|||
/// Skips using the username/password to log in to Apple, and simply gets a Auth Cookie used in downloading
|
||||
/// As of Nov 2022 this was returning a 403 forbidden
|
||||
func installWithoutLogin(id: Xcode.ID) {
|
||||
guard let availableXcode = availableXcodes.first(where: { $0.version == id }) else { return }
|
||||
guard let availableXcode = availableXcodes.first(where: { $0.xcodeID == 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
|
||||
|
|
@ -479,10 +654,12 @@ class AppState: ObservableObject {
|
|||
}
|
||||
|
||||
func cancelInstall(id: Xcode.ID) {
|
||||
guard let availableXcode = availableXcodes.first(where: { $0.version == id }) else { return }
|
||||
guard let availableXcode = availableXcodes.first(where: { $0.xcodeID == id }) else { return }
|
||||
|
||||
// Cancel the publisher
|
||||
installationPublishers[id] = nil
|
||||
|
||||
resetDockProgressTracking()
|
||||
|
||||
// If the download is cancelled by the user, clean up the download files that aria2 creates.
|
||||
// This isn't done as part of the publisher with handleEvents(receiveCancel:) because it shouldn't happen when e.g. the app quits.
|
||||
|
|
@ -519,10 +696,10 @@ class AppState: ObservableObject {
|
|||
)
|
||||
}
|
||||
|
||||
func reveal(xcode: Xcode) {
|
||||
func reveal(_ path: Path?) {
|
||||
// TODO: show error if not
|
||||
guard let installedXcodePath = xcode.installedPath else { return }
|
||||
NSWorkspace.shared.activateFileViewerSelecting([installedXcodePath.url])
|
||||
guard let path = path else { return }
|
||||
NSWorkspace.shared.activateFileViewerSelecting([path.url])
|
||||
}
|
||||
|
||||
func reveal(path: String) {
|
||||
|
|
@ -585,13 +762,18 @@ class AppState: ObservableObject {
|
|||
)
|
||||
}
|
||||
|
||||
func open(xcode: Xcode) {
|
||||
func open(xcode: Xcode, openInRosetta: Bool? = false) {
|
||||
switch xcode.installState {
|
||||
case let .installed(path):
|
||||
NSWorkspace.shared.openApplication(at: path.url, configuration: .init())
|
||||
default:
|
||||
Logger.appState.error("\(xcode.id) is not installed")
|
||||
return
|
||||
case let .installed(path):
|
||||
let config = NSWorkspace.OpenConfiguration.init()
|
||||
if (openInRosetta ?? false) {
|
||||
config.architecture = CPU_TYPE_X86_64
|
||||
}
|
||||
config.allowsRunningApplicationSubstitution = false
|
||||
NSWorkspace.shared.openApplication(at: path.url, configuration: config)
|
||||
default:
|
||||
Logger.appState.error("\(xcode.id.version) is not installed")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -603,8 +785,8 @@ class AppState: ObservableObject {
|
|||
NSPasteboard.general.setString(installedXcodePath.string, forType: .string)
|
||||
}
|
||||
|
||||
func copyReleaseNote(xcode: Xcode) {
|
||||
guard let url = xcode.releaseNotesURL else { return }
|
||||
func copyReleaseNote(from url: URL?) {
|
||||
guard let url = url else { return }
|
||||
NSPasteboard.general.declareTypes([.URL, .string], owner: nil)
|
||||
NSPasteboard.general.writeObjects([url as NSURL])
|
||||
NSPasteboard.general.setString(url.absoluteString, forType: .string)
|
||||
|
|
@ -686,7 +868,7 @@ class AppState: ObservableObject {
|
|||
// If build metadata matches exactly, replace the available version with the installed version.
|
||||
// This should handle Apple versions from /downloads/more which don't have build metadata identifiers.
|
||||
if let index = adjustedAvailableXcodes.map(\.version).firstIndex(where: { $0.buildMetadataIdentifiers == installedXcode.version.buildMetadataIdentifiers }) {
|
||||
adjustedAvailableXcodes[index].version = installedXcode.version
|
||||
adjustedAvailableXcodes[index].xcodeID = installedXcode.xcodeID
|
||||
}
|
||||
// If an installed version is the same as one that's listed online which doesn't have build metadata, replace it with the installed version
|
||||
// Not all prerelease Apple versions available online include build metadata
|
||||
|
|
@ -694,7 +876,7 @@ class AppState: ObservableObject {
|
|||
availableXcode.version.isEquivalent(to: installedXcode.version) &&
|
||||
availableXcode.version.buildMetadataIdentifiers.isEmpty
|
||||
}) {
|
||||
adjustedAvailableXcodes[index].version = installedXcode.version
|
||||
adjustedAvailableXcodes[index].xcodeID = installedXcode.xcodeID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -711,14 +893,21 @@ class AppState: ObservableObject {
|
|||
// Include this version if there's only one with this build identifier
|
||||
return availableXcodesWithIdenticalBuildIdentifiers.count == 1 ||
|
||||
// Or if there's more than one with this build identifier and this is the release version
|
||||
availableXcodesWithIdenticalBuildIdentifiers.count > 1 && availableXcode.version.prereleaseIdentifiers.isEmpty
|
||||
}
|
||||
|
||||
availableXcodesWithIdenticalBuildIdentifiers.count > 1 && (availableXcode.version.prereleaseIdentifiers.isEmpty || availableXcode.architectures?.count ?? 0 != 0)
|
||||
}
|
||||
.map { availableXcode -> Xcode in
|
||||
let installedXcode = installedXcodes.first(where: { installedXcode in
|
||||
availableXcode.version.isEquivalent(to: installedXcode.version)
|
||||
// if we want to have only specific Xcodes as selected instead of the Architecture Equivalent.
|
||||
// if availableXcode.architectures == nil {
|
||||
// return availableXcode.version.isEquivalent(to: installedXcode.version)
|
||||
// } else {
|
||||
// return availableXcode.xcodeID == installedXcode.xcodeID
|
||||
// }
|
||||
return availableXcode.version.isEquivalent(to: installedXcode.version)
|
||||
})
|
||||
|
||||
let identicalBuilds: [Version]
|
||||
let identicalBuilds: [XcodeID]
|
||||
let prereleaseAvailableXcodesWithIdenticalBuildIdentifiers = availableXcodes
|
||||
.filter {
|
||||
return $0.version.buildMetadataIdentifiers == availableXcode.version.buildMetadataIdentifiers &&
|
||||
|
|
@ -728,13 +917,13 @@ class AppState: ObservableObject {
|
|||
}
|
||||
// If this is the release version, add the identical builds to it
|
||||
if !prereleaseAvailableXcodesWithIdenticalBuildIdentifiers.isEmpty, availableXcode.version.prereleaseIdentifiers.isEmpty {
|
||||
identicalBuilds = [availableXcode.version] + prereleaseAvailableXcodesWithIdenticalBuildIdentifiers.map(\.version)
|
||||
identicalBuilds = [availableXcode.xcodeID] + prereleaseAvailableXcodesWithIdenticalBuildIdentifiers.map(\.xcodeID)
|
||||
} else {
|
||||
identicalBuilds = []
|
||||
}
|
||||
|
||||
// If the existing install state is "installing", keep it
|
||||
let existingXcodeInstallState = allXcodes.first { $0.version == availableXcode.version && $0.installState.installing }?.installState
|
||||
let existingXcodeInstallState = allXcodes.first { $0.id == availableXcode.xcodeID && $0.installState.installing }?.installState
|
||||
// Otherwise, determine it from whether there's an installed Xcode
|
||||
let defaultXcodeInstallState: XcodeInstallState = installedXcode.map { .installed($0.path) } ?? .notInstalled
|
||||
|
||||
|
|
@ -749,7 +938,8 @@ class AppState: ObservableObject {
|
|||
releaseDate: availableXcode.releaseDate,
|
||||
sdks: availableXcode.sdks,
|
||||
compilers: availableXcode.compilers,
|
||||
downloadFileSize: availableXcode.fileSize
|
||||
downloadFileSize: availableXcode.fileSize,
|
||||
architectures: availableXcode.architectures
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -771,6 +961,7 @@ class AppState: ObservableObject {
|
|||
|
||||
self.allXcodes = newAllXcodes.sorted { $0.version > $1.version }
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import Foundation
|
||||
import Version
|
||||
import struct XCModel.SDKs
|
||||
import struct XCModel.Compilers
|
||||
import XcodesKit
|
||||
|
||||
/// A version of Xcode that's available for installation
|
||||
public struct AvailableXcode: Codable {
|
||||
public var version: Version
|
||||
public var version: Version {
|
||||
return xcodeID.version
|
||||
}
|
||||
public let url: URL
|
||||
public let filename: String
|
||||
public let releaseDate: Date?
|
||||
|
|
@ -14,9 +15,11 @@ public struct AvailableXcode: Codable {
|
|||
public let sdks: SDKs?
|
||||
public let compilers: Compilers?
|
||||
public let fileSize: Int64?
|
||||
public let architectures: [Architecture]?
|
||||
public var downloadPath: String {
|
||||
return url.path
|
||||
}
|
||||
public var xcodeID: XcodeID
|
||||
|
||||
public init(
|
||||
version: Version,
|
||||
|
|
@ -27,9 +30,9 @@ public struct AvailableXcode: Codable {
|
|||
releaseNotesURL: URL? = nil,
|
||||
sdks: SDKs? = nil,
|
||||
compilers: Compilers? = nil,
|
||||
fileSize: Int64? = nil
|
||||
fileSize: Int64? = nil,
|
||||
architectures: [Architecture]? = nil
|
||||
) {
|
||||
self.version = version
|
||||
self.url = url
|
||||
self.filename = filename
|
||||
self.releaseDate = releaseDate
|
||||
|
|
@ -38,5 +41,7 @@ public struct AvailableXcode: Codable {
|
|||
self.sdks = sdks
|
||||
self.compilers = compilers
|
||||
self.fileSize = fileSize
|
||||
self.architectures = architectures
|
||||
self.xcodeID = XcodeID(version: version, architectures: architectures)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,4 +14,6 @@ public enum DataSource: String, CaseIterable, Identifiable, CustomStringConverti
|
|||
case .xcodeReleases: return "Xcode Releases"
|
||||
}
|
||||
}
|
||||
|
||||
var isManaged: Bool { PreferenceKey.dataSource.isManaged() }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ extension DateFormatter {
|
|||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "MM/dd/yy HH:mm"
|
||||
formatter.locale = Locale(identifier: "en_US_POSIX")
|
||||
formatter.calendar = .init(identifier: .iso8601)
|
||||
return formatter
|
||||
}()
|
||||
|
||||
|
|
|
|||
|
|
@ -13,4 +13,6 @@ public enum Downloader: String, CaseIterable, Identifiable, CustomStringConverti
|
|||
case .aria2: return "aria2"
|
||||
}
|
||||
}
|
||||
|
||||
var isManaged: Bool { PreferenceKey.downloader.isManaged() }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import Foundation
|
||||
import Path
|
||||
|
||||
extension Entry {
|
||||
static func isAppBundle(kind: Kind, path: Path) -> Bool {
|
||||
kind == .directory &&
|
||||
extension Path {
|
||||
static func isAppBundle(path: Path) -> Bool {
|
||||
path.isDirectory &&
|
||||
path.extension == "app" &&
|
||||
!path.isSymlink
|
||||
}
|
||||
static func infoPlist(kind: Kind, path: Path) -> InfoPlist? {
|
||||
static func infoPlist(path: Path) -> InfoPlist? {
|
||||
let infoPlistPath = path.join("Contents").join("Info.plist")
|
||||
guard
|
||||
let infoPlistData = try? Data(contentsOf: infoPlistPath.url),
|
||||
|
|
@ -18,10 +18,10 @@ extension Entry {
|
|||
}
|
||||
|
||||
var isAppBundle: Bool {
|
||||
Entry.isAppBundle(kind: kind, path: path)
|
||||
Path.isAppBundle(path: self)
|
||||
}
|
||||
|
||||
var infoPlist: InfoPlist? {
|
||||
Entry.infoPlist(kind: kind, path: path)
|
||||
Path.infoPlist(path: self)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import Foundation
|
|||
import Path
|
||||
import AppleAPI
|
||||
import KeychainAccess
|
||||
|
||||
import XcodesKit
|
||||
/**
|
||||
Lightweight dependency injection using global mutable state :P
|
||||
|
||||
|
|
@ -112,11 +112,168 @@ public struct Shell {
|
|||
return (progress, publisher)
|
||||
}
|
||||
|
||||
public var downloadWithAria2Async: (Path, URL, Path, [HTTPCookie]) -> AsyncThrowingStream<Progress, Error> = { aria2Path, url, destination, cookies 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()
|
||||
process.executableURL = aria2Path.url
|
||||
process.arguments = [
|
||||
"--header=Cookie: \(cookies.map { "\($0.name)=\($0.value)" }.joined(separator: "; "))",
|
||||
"--max-connection-per-server=16",
|
||||
"--split=16",
|
||||
"--summary-interval=1",
|
||||
"--stop-with-process=\(ProcessInfo.processInfo.processIdentifier)", // if xcodes quits, stop aria2 process
|
||||
"--dir=\(destination.parent.string)",
|
||||
"--out=\(destination.basename())",
|
||||
"--human-readable=false", // sets the output to use bytes instead of formatting
|
||||
url.absoluteString,
|
||||
]
|
||||
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.updateFromAria2(string: 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 {
|
||||
if let aria2cError = Aria2CError(exitStatus: process.terminationStatus) {
|
||||
continuation.finish(throwing: aria2cError)
|
||||
} else {
|
||||
continuation.finish(throwing: ProcessExecutionError(process: process, standardOutput: "", standardError: ""))
|
||||
}
|
||||
return
|
||||
}
|
||||
continuation.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public var unxipExperiment: (URL) -> AnyPublisher<ProcessOutput, Error> = { url in
|
||||
let unxipPath = Path(url: Bundle.main.url(forAuxiliaryExecutable: "unxip")!)!
|
||||
return Process.run(unxipPath.url, workingDirectory: url.deletingLastPathComponent(), ["\(url.path)"])
|
||||
}
|
||||
|
||||
public var downloadRuntime: (String, String, String?) -> AsyncThrowingStream<Progress, Error> = { platform, version, architecture 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
|
||||
|
||||
var process = Process()
|
||||
let xcodeBuildPath = Path.root.usr.bin.join("xcodebuild").url
|
||||
|
||||
process.executableURL = xcodeBuildPath
|
||||
process.arguments = [
|
||||
"-downloadPlatform",
|
||||
"\(platform)",
|
||||
"-buildVersion",
|
||||
"\(version)"
|
||||
]
|
||||
|
||||
if let architecture {
|
||||
process.arguments?.append(contentsOf: [
|
||||
"-architectureVariant",
|
||||
"\(architecture)"
|
||||
])
|
||||
}
|
||||
|
||||
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 {
|
||||
|
|
@ -166,18 +323,24 @@ public struct Files {
|
|||
public var installedXcodes = _installedXcodes
|
||||
|
||||
public func installedXcode(destination: Path) -> InstalledXcode? {
|
||||
if Entry.isAppBundle(kind: destination.isDirectory ? .directory : .file, path: destination) && Entry.infoPlist(kind: destination.isDirectory ? .directory : .file, path: destination)?.bundleID == "com.apple.dt.Xcode" {
|
||||
if Path.isAppBundle(path: destination) && Path.infoPlist(path: destination)?.bundleID == "com.apple.dt.Xcode" {
|
||||
return InstalledXcode.init(path: destination)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public var write: (Data, URL) throws -> Void = { try $0.write(to: $1) }
|
||||
|
||||
public func write(_ data: Data, to url: URL) throws {
|
||||
try write(data, url)
|
||||
}
|
||||
}
|
||||
|
||||
private func _installedXcodes(destination: Path) -> [InstalledXcode] {
|
||||
((try? destination.ls()) ?? [])
|
||||
destination.ls()
|
||||
.filter { $0.isAppBundle && $0.infoPlist?.bundleID == "com.apple.dt.Xcode" }
|
||||
.map { $0.path }
|
||||
.map { $0 }
|
||||
.compactMap(InstalledXcode.init)
|
||||
}
|
||||
|
||||
|
|
@ -189,10 +352,15 @@ public struct Network {
|
|||
.mapError { $0 as Error }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
public func dataTask(with request: URLRequest) -> AnyPublisher<URLSession.DataTaskPublisher.Output, Error> {
|
||||
dataTask(request)
|
||||
}
|
||||
|
||||
|
||||
public func dataTaskAsync(with request: URLRequest) async throws -> (Data, URLResponse) {
|
||||
return try await AppleAPI.Current.network.session.data(for: request)
|
||||
}
|
||||
|
||||
public var downloadTask: (URL, URL, Data?) -> (Progress, AnyPublisher<(saveLocation: URL, response: URLResponse), Error>) = { AppleAPI.Current.network.session.downloadTask(with: $0, to: $1, resumingWith: $2) }
|
||||
|
||||
public func downloadTask(with url: URL, to saveLocation: URL, resumingWith resumeData: Data?) -> (progress: Progress, publisher: AnyPublisher<(saveLocation: URL, response: URLResponse), Error>) {
|
||||
|
|
|
|||
28
Xcodes/Backend/Hardware.swift
Normal file
28
Xcodes/Backend/Hardware.swift
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import Foundation
|
||||
|
||||
|
||||
struct Hardware {
|
||||
|
||||
///
|
||||
/// Determines the architecture of the Mac on which we're running. Returns `arm64` for Apple Silicon
|
||||
/// and `x86_64` for Intel-based Macs or `nil` if the system call fails.
|
||||
static func getMachineHardwareName() -> String?
|
||||
{
|
||||
var sysInfo = utsname()
|
||||
let retVal = uname(&sysInfo)
|
||||
var finalString: String? = nil
|
||||
|
||||
if retVal == EXIT_SUCCESS
|
||||
{
|
||||
let bytes = Data(bytes: &sysInfo.machine, count: Int(_SYS_NAMELEN))
|
||||
finalString = String(data: bytes, encoding: .utf8)
|
||||
}
|
||||
|
||||
// _SYS_NAMELEN will include a billion null-terminators. Clear those out so string comparisons work as you expect.
|
||||
return finalString?.trimmingCharacters(in: CharacterSet(charactersIn: "\0"))
|
||||
}
|
||||
|
||||
static func isAppleSilicon() -> Bool {
|
||||
return Hardware.getMachineHardwareName() == "arm64"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
import Foundation
|
||||
|
||||
/// A numbered step
|
||||
enum InstallationStep: Equatable, CustomStringConvertible {
|
||||
case downloading(progress: Progress)
|
||||
case unarchiving
|
||||
case moving(destination: String)
|
||||
case trashingArchive
|
||||
case checkingSecurity
|
||||
case finishing
|
||||
|
||||
var description: String {
|
||||
"(\(stepNumber)/\(stepCount)) \(message)"
|
||||
}
|
||||
|
||||
var message: String {
|
||||
switch self {
|
||||
case .downloading:
|
||||
return localizeString("Downloading")
|
||||
case .unarchiving:
|
||||
return localizeString("Unarchiving")
|
||||
case .moving(let destination):
|
||||
return String(format: localizeString("Moving"), destination)
|
||||
case .trashingArchive:
|
||||
return localizeString("TrashingArchive")
|
||||
case .checkingSecurity:
|
||||
return localizeString("CheckingSecurity")
|
||||
case .finishing:
|
||||
return localizeString("Finishing")
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
var stepCount: Int { 6 }
|
||||
}
|
||||
|
|
@ -1,12 +1,17 @@
|
|||
import Foundation
|
||||
import Version
|
||||
import Path
|
||||
import XcodesKit
|
||||
|
||||
/// A version of Xcode that's already installed
|
||||
public struct InstalledXcode: Equatable {
|
||||
public let path: Path
|
||||
public let xcodeID: XcodeID
|
||||
|
||||
/// Composed of the bundle short version from Info.plist and the product build version from version.plist
|
||||
public let version: Version
|
||||
public var version: Version {
|
||||
return xcodeID.version
|
||||
}
|
||||
|
||||
public init?(path: Path) {
|
||||
self.path = path
|
||||
|
|
@ -31,12 +36,21 @@ public struct InstalledXcode: Equatable {
|
|||
else if infoPlist.bundleIconName == "XcodeBeta", !prereleaseIdentifiers.contains("beta") {
|
||||
prereleaseIdentifiers = ["beta"]
|
||||
}
|
||||
|
||||
self.version = Version(major: bundleVersion.major,
|
||||
|
||||
let archsString = try? XcodesKit.Current.shell.archs(path.url.appending(path: "Contents/MacOS/Xcode")).out
|
||||
|
||||
let architectures = archsString?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
.split(separator: " ")
|
||||
.compactMap { Architecture(rawValue: String($0)) }
|
||||
|
||||
let version = Version(major: bundleVersion.major,
|
||||
minor: bundleVersion.minor,
|
||||
patch: bundleVersion.patch,
|
||||
prereleaseIdentifiers: prereleaseIdentifiers,
|
||||
buildMetadataIdentifiers: [versionPlist.productBuildVersion].compactMap { $0 })
|
||||
|
||||
self.xcodeID = XcodeID(version: version, architectures: architectures)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
import Foundation
|
||||
import os.log
|
||||
|
||||
extension Logger {
|
||||
private static var subsystem = Bundle.main.bundleIdentifier!
|
||||
|
||||
static let appState = Logger(subsystem: subsystem, category: "appState")
|
||||
static let helperClient = Logger(subsystem: subsystem, category: "helperClient")
|
||||
static let subprocess = Logger(subsystem: subsystem, category: "subprocess")
|
||||
}
|
||||
|
|
@ -28,4 +28,19 @@ extension Path {
|
|||
}
|
||||
return path
|
||||
}
|
||||
|
||||
static var runtimeCacheFile: Path {
|
||||
return xcodesApplicationSupport/"downloadable-runtimes.json"
|
||||
}
|
||||
|
||||
static var xcodesCaches: Path {
|
||||
return caches/"com.xcodesorg.xcodesapp"
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func setCurrentUserAsOwner() -> Path {
|
||||
let user = ProcessInfo.processInfo.environment["SUDO_USER"] ?? NSUserName()
|
||||
try? FileManager.default.setAttributes([.ownerAccountName: user], ofItemAtPath: string)
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,12 +2,13 @@ import Combine
|
|||
import Foundation
|
||||
import os.log
|
||||
import Path
|
||||
import XcodesKit
|
||||
|
||||
public typealias ProcessOutput = (status: Int32, out: String, err: String)
|
||||
|
||||
extension Process {
|
||||
@discardableResult
|
||||
static func run(_ executable: Path, workingDirectory: URL? = nil, input: String? = nil, _ arguments: String...) -> AnyPublisher<ProcessOutput, Error> {
|
||||
static func run(_ executable: any Pathish, workingDirectory: URL? = nil, input: String? = nil, _ arguments: String...) -> AnyPublisher<ProcessOutput, Error> {
|
||||
return run(executable.url, workingDirectory: workingDirectory, input: input, arguments)
|
||||
}
|
||||
|
||||
|
|
@ -67,9 +68,3 @@ extension Process {
|
|||
.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
|
||||
struct ProcessExecutionError: Error {
|
||||
let process: Process
|
||||
let standardOutput: String
|
||||
let standardError: String
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
57
Xcodes/Backend/SDKs+Xcode.swift
Normal file
57
Xcodes/Backend/SDKs+Xcode.swift
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
//
|
||||
// SDKs+Xcode.swift
|
||||
// Xcodes
|
||||
//
|
||||
// Created by Matt Kiazyk on 2023-06-05.
|
||||
// Copyright © 2023 Robots and Pencils. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import XcodesKit
|
||||
import SwiftUI
|
||||
|
||||
extension SDKs {
|
||||
/// Loops through all SDK's and returns an array of buildNumbers (to be used to correlate runtimes)
|
||||
func allBuilds() -> [String] {
|
||||
var buildNumbers: [String] = []
|
||||
|
||||
if let iOS = self.iOS?.compactMap({ $0.build }) {
|
||||
buildNumbers += iOS
|
||||
}
|
||||
if let tvOS = self.tvOS?.compactMap({ $0.build }) {
|
||||
buildNumbers += tvOS
|
||||
}
|
||||
if let macOS = self.macOS?.compactMap({ $0.build }) {
|
||||
buildNumbers += macOS
|
||||
}
|
||||
if let watchOS = self.watchOS?.compactMap({ $0.build }) {
|
||||
buildNumbers += watchOS
|
||||
}
|
||||
if let visionOS = self.visionOS?.compactMap({ $0.build }) {
|
||||
buildNumbers += visionOS
|
||||
}
|
||||
|
||||
return buildNumbers
|
||||
}
|
||||
}
|
||||
|
||||
extension DownloadableRuntime {
|
||||
func icon() -> Image {
|
||||
switch self.platform {
|
||||
case .iOS:
|
||||
return Image(systemName: "iphone")
|
||||
case .macOS:
|
||||
return Image(systemName: "macwindow")
|
||||
case .watchOS:
|
||||
return Image(systemName: "applewatch")
|
||||
case .tvOS:
|
||||
return Image(systemName: "appletv")
|
||||
case .visionOS:
|
||||
if #available(macOS 14, *) {
|
||||
return Image(systemName: "visionpro")
|
||||
} else {
|
||||
return Image(systemName: "eyeglasses")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
import Version
|
||||
import struct XCModel.Xcode
|
||||
import XcodesKit
|
||||
|
||||
extension Version {
|
||||
/// Initialize a Version from an XcodeReleases' XCModel.Xcode
|
||||
///
|
||||
/// This is kinda quick-and-dirty, and it would probably be better for us to adopt something closer to XCModel.Xcode under the hood and map the scraped data to it instead.
|
||||
init?(xcReleasesXcode: XCModel.Xcode) {
|
||||
init?(xcReleasesXcode: XcodeRelease) {
|
||||
var versionString = xcReleasesXcode.version.number ?? ""
|
||||
|
||||
// Append trailing ".0" in order to get a fully-specified version string
|
||||
|
|
|
|||
|
|
@ -1,14 +1,30 @@
|
|||
import AppKit
|
||||
import Foundation
|
||||
import Version
|
||||
import struct XCModel.SDKs
|
||||
import struct XCModel.Compilers
|
||||
import Path
|
||||
import XcodesKit
|
||||
|
||||
public struct XcodeID: Codable, Hashable, Identifiable {
|
||||
public let version: Version
|
||||
public let architectures: [Architecture]?
|
||||
|
||||
public var id: String {
|
||||
let architectures = architectures?.map { $0.rawValue}.joined() ?? ""
|
||||
return version.description + architectures
|
||||
}
|
||||
|
||||
public init(version: Version, architectures: [Architecture]? = nil) {
|
||||
self.version = version
|
||||
self.architectures = architectures
|
||||
}
|
||||
}
|
||||
|
||||
struct Xcode: Identifiable, CustomStringConvertible {
|
||||
let version: Version
|
||||
var version: Version {
|
||||
return id.version
|
||||
}
|
||||
/// Other Xcode versions that have the same build identifier
|
||||
let identicalBuilds: [Version]
|
||||
let identicalBuilds: [XcodeID]
|
||||
var installState: XcodeInstallState
|
||||
let selected: Bool
|
||||
let icon: NSImage?
|
||||
|
|
@ -18,10 +34,12 @@ struct Xcode: Identifiable, CustomStringConvertible {
|
|||
let sdks: SDKs?
|
||||
let compilers: Compilers?
|
||||
let downloadFileSize: Int64?
|
||||
let architectures: [Architecture]?
|
||||
let id: XcodeID
|
||||
|
||||
init(
|
||||
version: Version,
|
||||
identicalBuilds: [Version] = [],
|
||||
identicalBuilds: [XcodeID] = [],
|
||||
installState: XcodeInstallState,
|
||||
selected: Bool,
|
||||
icon: NSImage?,
|
||||
|
|
@ -30,9 +48,9 @@ struct Xcode: Identifiable, CustomStringConvertible {
|
|||
releaseDate: Date? = nil,
|
||||
sdks: SDKs? = nil,
|
||||
compilers: Compilers? = nil,
|
||||
downloadFileSize: Int64? = nil
|
||||
downloadFileSize: Int64? = nil,
|
||||
architectures: [Architecture]? = nil
|
||||
) {
|
||||
self.version = version
|
||||
self.identicalBuilds = identicalBuilds
|
||||
self.installState = installState
|
||||
self.selected = selected
|
||||
|
|
@ -43,10 +61,10 @@ struct Xcode: Identifiable, CustomStringConvertible {
|
|||
self.sdks = sdks
|
||||
self.compilers = compilers
|
||||
self.downloadFileSize = downloadFileSize
|
||||
self.architectures = architectures
|
||||
self.id = XcodeID(version: version, architectures: architectures)
|
||||
}
|
||||
|
||||
var id: Version { version }
|
||||
|
||||
var description: String {
|
||||
version.appleDescription
|
||||
}
|
||||
|
|
@ -67,4 +85,5 @@ struct Xcode: Identifiable, CustomStringConvertible {
|
|||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import SwiftUI
|
||||
import XcodesKit
|
||||
|
||||
// MARK: - CommandMenu
|
||||
|
||||
|
|
@ -34,15 +35,18 @@ struct XcodeCommands: Commands {
|
|||
|
||||
struct InstallButton: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
|
||||
let xcode: Xcode?
|
||||
|
||||
|
||||
var body: some View {
|
||||
Button(action: install) {
|
||||
Button {
|
||||
install()
|
||||
} label: {
|
||||
Text("Install")
|
||||
.help("Install")
|
||||
.help("InstallDescription")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func install() {
|
||||
guard let xcode = xcode else { return }
|
||||
appState.checkMinVersionAndInstall(id: xcode.id)
|
||||
|
|
@ -55,9 +59,10 @@ struct CancelInstallButton: View {
|
|||
|
||||
var body: some View {
|
||||
Button(action: cancelInstall) {
|
||||
Text("Cancel")
|
||||
.help(localizeString("StopInstallation"))
|
||||
Label("Cancel", systemImage: "xmark")
|
||||
}
|
||||
.help(localizeString("StopInstallation"))
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
private func cancelInstall() {
|
||||
|
|
@ -66,6 +71,23 @@ struct CancelInstallButton: View {
|
|||
}
|
||||
}
|
||||
|
||||
struct CancelRuntimeInstallButton: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
let runtime: DownloadableRuntime?
|
||||
|
||||
var body: some View {
|
||||
Button(action: cancelInstall) {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
}.help(localizeString("StopInstallation"))
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
private func cancelInstall() {
|
||||
guard let runtime = runtime else { return }
|
||||
appState.presentedAlert = .cancelRuntimeInstall(runtime: runtime)
|
||||
}
|
||||
}
|
||||
|
||||
struct SelectButton: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
let xcode: Xcode?
|
||||
|
|
@ -92,16 +114,34 @@ struct OpenButton: View {
|
|||
@EnvironmentObject var appState: AppState
|
||||
let xcode: Xcode?
|
||||
|
||||
var openInRosetta: Bool {
|
||||
appState.showOpenInRosettaOption && Hardware.isAppleSilicon()
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Button(action: open) {
|
||||
Text("Open")
|
||||
if openInRosetta {
|
||||
Menu("Open") {
|
||||
Button(action: open) {
|
||||
Text("Open")
|
||||
}
|
||||
.help("Open")
|
||||
Button(action: open) {
|
||||
Text("Open In Rosetta")
|
||||
}
|
||||
.help("Open In Rosetta")
|
||||
}
|
||||
} else {
|
||||
Button(action: open) {
|
||||
Text("Open")
|
||||
}
|
||||
.help("Open")
|
||||
}
|
||||
.help("Open")
|
||||
|
||||
}
|
||||
|
||||
private func open() {
|
||||
guard let xcode = xcode else { return }
|
||||
appState.open(xcode: xcode)
|
||||
appState.open(xcode: xcode, openInRosetta: openInRosetta)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -133,7 +173,7 @@ struct RevealButton: View {
|
|||
|
||||
private func reveal() {
|
||||
guard let xcode = xcode else { return }
|
||||
appState.reveal(xcode: xcode)
|
||||
appState.reveal(xcode.installedPath)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -155,8 +195,9 @@ struct CopyPathButton: View {
|
|||
}
|
||||
|
||||
struct CopyReleaseNoteButton: View {
|
||||
let url: URL?
|
||||
|
||||
@EnvironmentObject var appState: AppState
|
||||
let xcode: Xcode?
|
||||
|
||||
var body: some View {
|
||||
Button(action: copyReleaseNote) {
|
||||
|
|
@ -166,8 +207,8 @@ struct CopyReleaseNoteButton: View {
|
|||
}
|
||||
|
||||
private func copyReleaseNote() {
|
||||
guard let xcode = xcode else { return }
|
||||
appState.copyReleaseNote(xcode: xcode)
|
||||
guard let url = url else { return }
|
||||
appState.copyReleaseNote(from: url)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -189,6 +230,23 @@ struct CreateSymbolicLinkButton: View {
|
|||
}
|
||||
}
|
||||
|
||||
struct DownloadRuntimeButton: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
let runtime: DownloadableRuntime?
|
||||
|
||||
var body: some View {
|
||||
Button(action: install) {
|
||||
Text("Install")
|
||||
.help("Install")
|
||||
}
|
||||
}
|
||||
|
||||
private func install() {
|
||||
guard let runtime = runtime else { return }
|
||||
appState.downloadRuntime(runtime: runtime)
|
||||
}
|
||||
}
|
||||
|
||||
struct CreateSymbolicBetaLinkButton: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
let xcode: Xcode?
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import Foundation
|
||||
import Path
|
||||
import XcodesKit
|
||||
|
||||
enum XcodeInstallState: Equatable {
|
||||
case notInstalled
|
||||
case installing(InstallationStep)
|
||||
case installing(XcodeInstallationStep)
|
||||
case installed(Path)
|
||||
|
||||
var notInstalled: Bool {
|
||||
|
|
|
|||
|
|
@ -45,9 +45,18 @@ struct AboutView: View {
|
|||
}
|
||||
.buttonStyle(LinkButtonStyle())
|
||||
}
|
||||
|
||||
Text(Bundle.main.humanReadableCopyright!)
|
||||
.font(.footnote)
|
||||
HStack {
|
||||
Text(Bundle.main.humanReadableCopyright!)
|
||||
.font(.footnote)
|
||||
Button(action: {
|
||||
openURL(URL(string: "https://opencollective.com/xcodesapp")!)
|
||||
}) {
|
||||
HStack {
|
||||
Image(systemName: "heart.circle")
|
||||
Text("Support.Xcodes")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ struct AcknowledgmentsView: View {
|
|||
)!
|
||||
.addingAttribute(.foregroundColor, value: NSColor.labelColor)
|
||||
)
|
||||
.frame(minWidth: 500, minHeight: 500)
|
||||
.frame(minWidth: 600, minHeight: 500)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
35
Xcodes/Frontend/Common/NavigationSplitViewWrapper.swift
Normal file
35
Xcodes/Frontend/Common/NavigationSplitViewWrapper.swift
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
//
|
||||
// NavigationSplitViewWrapper.swift
|
||||
// Xcodes
|
||||
//
|
||||
// Created by Matt Kiazyk on 2023-12-12.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct NavigationSplitViewWrapper<Sidebar, Detail>: View where Sidebar: View, Detail: View {
|
||||
private var sidebar: Sidebar
|
||||
private var detail: Detail
|
||||
|
||||
init(
|
||||
@ViewBuilder sidebar: () -> Sidebar,
|
||||
@ViewBuilder detail: () -> Detail
|
||||
) {
|
||||
self.sidebar = sidebar()
|
||||
self.detail = detail()
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationSplitView {
|
||||
if #available(macOS 14, *) {
|
||||
sidebar
|
||||
.navigationSplitViewColumnWidth(min: 290, ideal: 290)
|
||||
} else {
|
||||
sidebar
|
||||
}
|
||||
} 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
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
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() }
|
||||
}
|
||||
|
|
@ -1,10 +1,13 @@
|
|||
import Foundation
|
||||
import XcodesKit
|
||||
|
||||
enum XcodesAlert: Identifiable {
|
||||
case cancelInstall(xcode: Xcode)
|
||||
case cancelRuntimeInstall(runtime: DownloadableRuntime)
|
||||
case privilegedHelper
|
||||
case generic(title: String, message: String)
|
||||
case checkMinSupportedVersion(xcode: AvailableXcode, macOS: String)
|
||||
case unauthenticated
|
||||
|
||||
var id: Int {
|
||||
switch self {
|
||||
|
|
@ -12,6 +15,22 @@ enum XcodesAlert: Identifiable {
|
|||
case .privilegedHelper: return 2
|
||||
case .generic: return 3
|
||||
case .checkMinSupportedVersion: return 4
|
||||
case .cancelRuntimeInstall: return 5
|
||||
case .unauthenticated: return 6
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Splitting out alerts that are shown on the preference screen as by default we are showing on the MainWindow()
|
||||
// and users awkwardly switch screens, sometimes losing the preference screen
|
||||
enum XcodesPreferencesAlert: Identifiable {
|
||||
case deletePlatform(runtime: DownloadableRuntime)
|
||||
case generic(title: String, message: String)
|
||||
|
||||
var id: Int {
|
||||
switch self {
|
||||
case .deletePlatform: return 1
|
||||
case .generic: return 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
43
Xcodes/Frontend/InfoPane/CompatibilityView.swift
Normal file
43
Xcodes/Frontend/InfoPane/CompatibilityView.swift
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
//
|
||||
// CompatibilityView.swift
|
||||
// Xcodes
|
||||
//
|
||||
// Created by Duong Thai on 13/10/2023.
|
||||
// Copyright © 2023 Robots and Pencils. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct CompatibilityView: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
|
||||
let requiredMacOSVersion: String?
|
||||
|
||||
var body: some View {
|
||||
if let requiredMacOSVersion = requiredMacOSVersion {
|
||||
HStack(alignment: .top){
|
||||
VStack(alignment: .leading) {
|
||||
Text("Compatibility")
|
||||
.font(.headline)
|
||||
Text(String(format: localizeString("MacOSRequirement"), requiredMacOSVersion))
|
||||
.font(.subheadline)
|
||||
.foregroundColor(appState.hasMinSupportedOS(requiredMacOSVersion: requiredMacOSVersion) ? .red : .primary)
|
||||
}
|
||||
Spacer()
|
||||
if appState.hasMinSupportedOS(requiredMacOSVersion: requiredMacOSVersion) {
|
||||
Image(systemName: "exclamationmark.triangle.fill")
|
||||
.font(.largeTitle)
|
||||
}
|
||||
}
|
||||
.xcodesBackground()
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
CompatibilityView(requiredMacOSVersion: "10.15.4")
|
||||
.padding()
|
||||
.environmentObject(AppState())
|
||||
}
|
||||
60
Xcodes/Frontend/InfoPane/CompilersView.swift
Normal file
60
Xcodes/Frontend/InfoPane/CompilersView.swift
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
//
|
||||
// CompilersView.swift
|
||||
// Xcodes
|
||||
//
|
||||
// Created by Duong Thai on 13/10/2023.
|
||||
// Copyright © 2023 Robots and Pencils. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import XcodesKit
|
||||
|
||||
struct CompilersView: View {
|
||||
let compilers: Compilers?
|
||||
|
||||
var body: some View {
|
||||
if let compilers = compilers {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Compilers").font(.headline)
|
||||
Text(Self.content(from: compilers))
|
||||
.font(.subheadline)
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
|
||||
static func content(from compilers: Compilers) -> String {
|
||||
[ ("Swift", compilers.swift),
|
||||
("Clang", compilers.clang),
|
||||
("LLVM", compilers.llvm),
|
||||
("LLVM GCC", compilers.llvm_gcc),
|
||||
("GCC", compilers.gcc)
|
||||
].compactMap { // remove nil compiler
|
||||
guard $0.1 != nil, // has version array
|
||||
!$0.1!.isEmpty // has at least 1 version
|
||||
else { return nil }
|
||||
|
||||
let numbers = $0.1!.compactMap { $0.number } // remove nil number
|
||||
guard !numbers.isEmpty // has at least 1 number
|
||||
else { return nil }
|
||||
|
||||
// description for each type of compilers
|
||||
return "\($0.0): \(numbers.joined(separator: ", "))"
|
||||
}.joined(separator: "\n")
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let compilers = Compilers(
|
||||
gcc: .init(number: "4"),
|
||||
llvm_gcc: .init(number: "213"),
|
||||
llvm: .init(number: "2.3"),
|
||||
clang: .init(number: "7.3"),
|
||||
swift: .init(number: "5.3.2")
|
||||
)
|
||||
|
||||
return CompilersView(compilers: compilers)
|
||||
.padding()
|
||||
}
|
||||
35
Xcodes/Frontend/InfoPane/CornerRadiusModifier.swift
Normal file
35
Xcodes/Frontend/InfoPane/CornerRadiusModifier.swift
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
//
|
||||
// CornerRadiusModifier.swift
|
||||
// Xcodes
|
||||
//
|
||||
// Created by Matt Kiazyk on 2023-12-19.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct CornerRadiusModifier: ViewModifier {
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding()
|
||||
.background(.background)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 5, style: .continuous))
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
func xcodesBackground() -> some View {
|
||||
self.modifier(
|
||||
CornerRadiusModifier()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct Previews_CornerRadius_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
HStack {
|
||||
Text(verbatim: "XCODES RULES!")
|
||||
}.xcodesBackground()
|
||||
}
|
||||
}
|
||||
44
Xcodes/Frontend/InfoPane/IconView.swift
Normal file
44
Xcodes/Frontend/InfoPane/IconView.swift
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
//
|
||||
// IconView.swift
|
||||
// Xcodes
|
||||
//
|
||||
// Created by Duong Thai on 11/10/2023.
|
||||
// Copyright © 2023 Robots and Pencils. All rights reserved.
|
||||
//
|
||||
|
||||
import Path
|
||||
import SwiftUI
|
||||
import Version
|
||||
|
||||
struct IconView: View {
|
||||
let xcode: Xcode
|
||||
|
||||
var body: some View {
|
||||
if case let .installed(path) = xcode.installState {
|
||||
Image(nsImage: NSWorkspace.shared.icon(forFile: path.string))
|
||||
} else {
|
||||
Image(xcode.version.isPrerelease ? "xcode-beta" : "xcode")
|
||||
.resizable()
|
||||
.frame(width: 32, height: 32)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview("Installed") {
|
||||
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(xcode: Xcode(version: Version("12.0.0-1234A")!, installState: .notInstalled, selected: false, icon: nil))
|
||||
.frame(width: 300, height: 100)
|
||||
.padding()
|
||||
}
|
||||
63
Xcodes/Frontend/InfoPane/IdenticalBuildView.swift
Normal file
63
Xcodes/Frontend/InfoPane/IdenticalBuildView.swift
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
//
|
||||
// IdenticalBuildView.swift
|
||||
// Xcodes
|
||||
//
|
||||
// Created by Duong Thai on 11/10/2023.
|
||||
// Copyright © 2023 Robots and Pencils. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Version
|
||||
|
||||
struct IdenticalBuildsView: View {
|
||||
let builds: [Version]
|
||||
private let isEmpty: Bool
|
||||
private let accessibilityDescription: String
|
||||
|
||||
var body: some View {
|
||||
if isEmpty {
|
||||
EmptyView()
|
||||
} else {
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
Text("IdenticalBuilds")
|
||||
Image(systemName: "square.fill.on.square.fill")
|
||||
.foregroundColor(.secondary)
|
||||
.accessibility(hidden: true)
|
||||
.help("IdenticalBuilds.help")
|
||||
}
|
||||
.font(.headline)
|
||||
|
||||
ForEach(builds, id: \.description) { version in
|
||||
Text(verbatim: "• \(version.appleDescription)")
|
||||
.font(.subheadline)
|
||||
}
|
||||
}
|
||||
.xcodesBackground()
|
||||
.accessibilityElement()
|
||||
.accessibility(label: Text("IdenticalBuilds"))
|
||||
.accessibility(value: Text(accessibilityDescription))
|
||||
.accessibility(hint: Text("IdenticalBuilds.help"))
|
||||
}
|
||||
}
|
||||
|
||||
init(builds: [Version]) {
|
||||
self.builds = builds
|
||||
self.isEmpty = builds.isEmpty
|
||||
self.accessibilityDescription = builds
|
||||
.map(\.appleDescription)
|
||||
.joined(separator: ", ")
|
||||
}
|
||||
}
|
||||
|
||||
let builds: [Version] = [.init(xcodeVersion: "15.0")!, .init(xcodeVersion: "15.1")!]
|
||||
|
||||
#Preview("Has Some Builds") {
|
||||
IdenticalBuildsView(builds: builds)
|
||||
.padding()
|
||||
}
|
||||
|
||||
#Preview("No Build") {
|
||||
IdenticalBuildsView(builds: [])
|
||||
.padding()
|
||||
}
|
||||
|
|
@ -1,372 +1,210 @@
|
|||
import AppKit
|
||||
import XcodesKit
|
||||
import Path
|
||||
import SwiftUI
|
||||
import Version
|
||||
import struct XCModel.SDKs
|
||||
import struct XCModel.Compilers
|
||||
|
||||
struct InfoPane: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
let selectedXcodeID: Xcode.ID?
|
||||
@SwiftUI.Environment(\.openURL) var openURL: OpenURLAction
|
||||
|
||||
let xcode: Xcode
|
||||
var body: some View {
|
||||
if let xcode = appState.allXcodes.first(where: { $0.id == selectedXcodeID }) {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
icon(for: xcode)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
|
||||
Text(verbatim: "Xcode \(xcode.description) \(xcode.version.buildMetadataIdentifiersDisplay)")
|
||||
.font(.title)
|
||||
|
||||
switch xcode.installState {
|
||||
case .notInstalled:
|
||||
InstallButton(xcode: xcode)
|
||||
downloadFileSize(for: xcode)
|
||||
case .installing(let installationStep):
|
||||
InstallationStepDetailView(installationStep: installationStep)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
CancelInstallButton(xcode: xcode)
|
||||
case let .installed(path):
|
||||
HStack {
|
||||
Text(path.string)
|
||||
Button(action: { appState.reveal(xcode: xcode) }) {
|
||||
Image(systemName: "arrow.right.circle.fill")
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
.help("RevealInFinder")
|
||||
}
|
||||
|
||||
HStack {
|
||||
SelectButton(xcode: xcode)
|
||||
.disabled(xcode.selected)
|
||||
.help("Selected")
|
||||
|
||||
OpenButton(xcode: xcode)
|
||||
.help("Open")
|
||||
|
||||
Spacer()
|
||||
UninstallButton(xcode: xcode)
|
||||
}
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
Group{
|
||||
releaseNotes(for: xcode)
|
||||
releaseDate(for: xcode)
|
||||
identicalBuilds(for: xcode)
|
||||
compatibility(for: xcode)
|
||||
sdks(for: xcode)
|
||||
compilers(for: xcode)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
if #available(macOS 14.0, *) {
|
||||
mainContent
|
||||
.contentMargins(10, for: .scrollContent)
|
||||
} else {
|
||||
mainContent
|
||||
.padding()
|
||||
}
|
||||
.frame(minWidth: 200, maxWidth: .infinity)
|
||||
} else {
|
||||
empty
|
||||
.frame(minWidth: 200, maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func icon(for xcode: Xcode) -> some View {
|
||||
if case let .installed(path) = xcode.installState {
|
||||
Image(nsImage: NSWorkspace.shared.icon(forFile: path.string))
|
||||
} else {
|
||||
Image(systemName: "app.fill")
|
||||
.resizable()
|
||||
.frame(width: 32, height: 32)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func identicalBuilds(for xcode: Xcode) -> some View {
|
||||
if !xcode.identicalBuilds.isEmpty {
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
Text("IdenticalBuilds")
|
||||
Image(systemName: "square.fill.on.square.fill")
|
||||
.foregroundColor(.secondary)
|
||||
.accessibility(hidden: true)
|
||||
.help("IdenticalBuilds.help")
|
||||
}
|
||||
.font(.headline)
|
||||
|
||||
ForEach(xcode.identicalBuilds, id: \.description) { version in
|
||||
Text("• \(version.appleDescription)")
|
||||
.font(.subheadline)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.accessibilityElement()
|
||||
.accessibility(label: Text("IdenticalBuilds"))
|
||||
.accessibility(value: Text(xcode.identicalBuilds.map(\.appleDescription).joined(separator: ", ")))
|
||||
.accessibility(hint: Text("IdenticalBuilds.help"))
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func releaseDate(for xcode: Xcode) -> some View {
|
||||
if let releaseDate = xcode.releaseDate {
|
||||
VStack(alignment: .leading) {
|
||||
Text("ReleaseDate")
|
||||
.font(.headline)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
Text("\(releaseDate, style: .date)")
|
||||
.font(.subheadline)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func releaseNotes(for xcode: Xcode) -> some View {
|
||||
if let releaseNotesURL = xcode.releaseNotesURL {
|
||||
Button(action: { openURL(releaseNotesURL) }) {
|
||||
Label("ReleaseNotes", systemImage: "link")
|
||||
}
|
||||
.buttonStyle(LinkButtonStyle())
|
||||
.contextMenu(menuItems: {
|
||||
releaseNotesMenu(for: xcode)
|
||||
})
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.help("ReleaseNotes.help")
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func releaseNotesMenu(for xcode: Xcode) -> some View {
|
||||
CopyReleaseNoteButton(xcode: xcode)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func compatibility(for xcode: Xcode) -> some View {
|
||||
if let requiredMacOSVersion = xcode.requiredMacOSVersion {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Compatibility")
|
||||
.font(.headline)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
Text(String(format: localizeString("MacOSRequirement"), requiredMacOSVersion))
|
||||
.font(.subheadline)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func sdks(for xcode: Xcode) -> some View {
|
||||
if let sdks = xcode.sdks {
|
||||
VStack(alignment: .leading) {
|
||||
Text("SDKs")
|
||||
.font(.headline)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
ForEach([
|
||||
("macOS", \SDKs.macOS),
|
||||
("iOS", \.iOS),
|
||||
("watchOS", \.watchOS),
|
||||
("tvOS", \.tvOS),
|
||||
], id: \.0) { row in
|
||||
if let sdk = sdks[keyPath: row.1] {
|
||||
Text("\(row.0): \(sdk.compactMap { $0.number }.joined(separator: ", "))")
|
||||
.font(.subheadline)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
private var mainContent: some View {
|
||||
ScrollView(.vertical) {
|
||||
HStack(alignment: .top) {
|
||||
VStack {
|
||||
VStack(spacing: 5) {
|
||||
HStack {
|
||||
IconView(xcode: xcode)
|
||||
|
||||
Text(verbatim: "Xcode \(xcode.description) \(xcode.version.buildMetadataIdentifiersDisplay)")
|
||||
.font(.title)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
InfoPaneControls(xcode: xcode)
|
||||
}
|
||||
.xcodesBackground()
|
||||
|
||||
PlatformsView(xcode: xcode)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func compilers(for xcode: Xcode) -> some View {
|
||||
if let compilers = xcode.compilers {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Compilers")
|
||||
.font(.headline)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.frame(minWidth: 380)
|
||||
|
||||
ForEach([
|
||||
("Swift", \Compilers.swift),
|
||||
("Clang", \.clang),
|
||||
("LLVM", \.llvm),
|
||||
("LLVM GCC", \.llvm_gcc),
|
||||
("GCC", \.gcc),
|
||||
], id: \.0) { row in
|
||||
if let sdk = compilers[keyPath: row.1] {
|
||||
Text("\(row.0): \(sdk.compactMap { $0.number }.joined(separator: ", "))")
|
||||
.font(.subheadline)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
VStack(alignment: .leading) {
|
||||
ReleaseDateView(date: xcode.releaseDate, url: xcode.releaseNotesURL)
|
||||
CompatibilityView(requiredMacOSVersion: xcode.requiredMacOSVersion)
|
||||
IdenticalBuildsView(builds: xcode.identicalBuilds.map { $0.version })
|
||||
SDKandCompilers
|
||||
}
|
||||
.frame(width: 200)
|
||||
|
||||
}
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func downloadFileSize(for xcode: Xcode) -> some View {
|
||||
// if we've downloaded it no need to show the download size
|
||||
if let downloadFileSize = xcode.downloadFileSizeString, case .notInstalled = xcode.installState {
|
||||
VStack(alignment: .leading) {
|
||||
Text("DownloadSize")
|
||||
.font(.headline)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
Text("\(downloadFileSize)")
|
||||
.font(.subheadline)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
} else {
|
||||
EmptyView()
|
||||
var SDKandCompilers: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
SDKsView(sdks: xcode.sdks)
|
||||
CompilersView(compilers: xcode.compilers)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var empty: some View {
|
||||
Text("NoXcodeSelected")
|
||||
.font(.title)
|
||||
.foregroundColor(.secondary)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding()
|
||||
.background(.background)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 5, style: .continuous))
|
||||
}
|
||||
}
|
||||
|
||||
struct InfoPane_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
InfoPane(selectedXcodeID: Version(major: 12, minor: 3, patch: 0))
|
||||
.environmentObject(configure(AppState()) {
|
||||
$0.allXcodes = [
|
||||
.init(
|
||||
version: Version(major: 12, minor: 3, patch: 0),
|
||||
installState: .installed(Path("/Applications/Xcode-12.3.0.app")!),
|
||||
selected: true,
|
||||
icon: NSWorkspace.shared.icon(forFile: "/Applications/Xcode-12.3.0.app"),
|
||||
requiredMacOSVersion: "10.15.4",
|
||||
releaseNotesURL: URL(string: "https://developer.apple.com/documentation/xcode-release-notes/xcode-12_3-release-notes/")!,
|
||||
releaseDate: Date(),
|
||||
sdks: SDKs(
|
||||
macOS: .init(number: "11.1"),
|
||||
iOS: .init(number: "14.3"),
|
||||
watchOS: .init(number: "7.3"),
|
||||
tvOS: .init(number: "14.3")
|
||||
),
|
||||
compilers: Compilers(
|
||||
gcc: .init(number: "4"),
|
||||
llvm_gcc: .init(number: "213"),
|
||||
llvm: .init(number: "2.3"),
|
||||
clang: .init(number: "7.3"),
|
||||
swift: .init(number: "5.3.2")
|
||||
),
|
||||
downloadFileSize: 242342424
|
||||
)
|
||||
]
|
||||
})
|
||||
.previewDisplayName("Populated, Installed, Selected")
|
||||
#Preview(XcodePreviewName.allCases[0].rawValue) { makePreviewContent(for: 0) }
|
||||
#Preview(XcodePreviewName.allCases[1].rawValue) { makePreviewContent(for: 1) }
|
||||
#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) }
|
||||
|
||||
InfoPane(selectedXcodeID: Version(major: 12, minor: 3, patch: 0))
|
||||
.environmentObject(configure(AppState()) {
|
||||
$0.allXcodes = [
|
||||
.init(
|
||||
version: Version(major: 12, minor: 3, patch: 0),
|
||||
installState: .installed(Path("/Applications/Xcode-12.3.0.app")!),
|
||||
selected: false,
|
||||
icon: NSWorkspace.shared.icon(forFile: "/Applications/Xcode-12.3.0.app"),
|
||||
sdks: SDKs(
|
||||
macOS: .init(number: "11.1"),
|
||||
iOS: .init(number: "14.3"),
|
||||
watchOS: .init(number: "7.3"),
|
||||
tvOS: .init(number: "14.3")
|
||||
),
|
||||
compilers: Compilers(
|
||||
gcc: .init(number: "4"),
|
||||
llvm_gcc: .init(number: "213"),
|
||||
llvm: .init(number: "2.3"),
|
||||
clang: .init(number: "7.3"),
|
||||
swift: .init(number: "5.3.2")
|
||||
),
|
||||
downloadFileSize: 242342424)
|
||||
]
|
||||
})
|
||||
.previewDisplayName("Populated, Installed, Unselected")
|
||||
|
||||
InfoPane(selectedXcodeID: Version(major: 12, minor: 3, patch: 0))
|
||||
.environmentObject(configure(AppState()) {
|
||||
$0.allXcodes = [
|
||||
.init(
|
||||
version: Version(major: 12, minor: 3, patch: 0),
|
||||
installState: .notInstalled,
|
||||
selected: false,
|
||||
icon: nil,
|
||||
sdks: SDKs(
|
||||
macOS: .init(number: "11.1"),
|
||||
iOS: .init(number: "14.3"),
|
||||
watchOS: .init(number: "7.3"),
|
||||
tvOS: .init(number: "14.3")
|
||||
),
|
||||
compilers: Compilers(
|
||||
gcc: .init(number: "4"),
|
||||
llvm_gcc: .init(number: "213"),
|
||||
llvm: .init(number: "2.3"),
|
||||
clang: .init(number: "7.3"),
|
||||
swift: .init(number: "5.3.2")
|
||||
),
|
||||
downloadFileSize: 242342424)
|
||||
]
|
||||
})
|
||||
.previewDisplayName("Populated, Uninstalled")
|
||||
|
||||
InfoPane(selectedXcodeID: Version(major: 12, minor: 3, patch: 1, buildMetadataIdentifiers: ["1234A"]))
|
||||
.environmentObject(configure(AppState()) {
|
||||
$0.allXcodes = [
|
||||
.init(
|
||||
version: Version(major: 12, minor: 3, patch: 1, buildMetadataIdentifiers: ["1234A"]),
|
||||
installState: .installed(Path("/Applications/Xcode-12.3.0.app")!),
|
||||
selected: false,
|
||||
icon: nil,
|
||||
sdks: nil,
|
||||
compilers: nil)
|
||||
]
|
||||
})
|
||||
.previewDisplayName("Basic, installed")
|
||||
|
||||
InfoPane(selectedXcodeID: Version(major: 12, minor: 3, patch: 1, buildMetadataIdentifiers: ["1234A"]))
|
||||
.environmentObject(configure(AppState()) {
|
||||
$0.allXcodes = [
|
||||
.init(
|
||||
version: Version(major: 12, minor: 3, patch: 1, buildMetadataIdentifiers: ["1234A"]),
|
||||
installState: .installing(.downloading(progress: configure(Progress(totalUnitCount: 100)) { $0.completedUnitCount = 40; $0.throughput = 232323232; $0.fileCompletedCount = 2323004; $0.fileTotalCount = 1193939393 })),
|
||||
selected: false,
|
||||
icon: nil,
|
||||
sdks: nil,
|
||||
compilers: nil)
|
||||
]
|
||||
})
|
||||
.previewDisplayName("Basic, installing")
|
||||
|
||||
InfoPane(selectedXcodeID: nil)
|
||||
.environmentObject(configure(AppState()) {
|
||||
$0.allXcodes = [
|
||||
]
|
||||
})
|
||||
.previewDisplayName("Empty")
|
||||
}
|
||||
.frame(maxWidth: 300)
|
||||
}
|
||||
private func makePreviewContent(for index: Int) -> some View {
|
||||
let name = XcodePreviewName.allCases[index]
|
||||
return InfoPane(xcode: xcodeDict[name]!)
|
||||
.environmentObject(configure(AppState()) {
|
||||
$0.allXcodes = [xcodeDict[name]!]
|
||||
})
|
||||
.frame(width: 600, height: 400)
|
||||
.padding()
|
||||
}
|
||||
|
||||
enum XcodePreviewName: String, CaseIterable, Identifiable {
|
||||
case Populated_Installed_Selected
|
||||
case Populated_Installed_Unselected
|
||||
case Populated_Uninstalled
|
||||
case Basic_Installed
|
||||
case Basic_Installing
|
||||
case Basic_Unarchiving
|
||||
|
||||
var id: XcodePreviewName { self }
|
||||
}
|
||||
|
||||
var xcodeDict: [XcodePreviewName: Xcode] = [
|
||||
.Populated_Installed_Selected: .init(
|
||||
version: _versionNoMeta,
|
||||
installState: .installed(Path(_path)!),
|
||||
selected: true,
|
||||
icon: NSWorkspace.shared.icon(forFile: _path),
|
||||
requiredMacOSVersion: _requiredMacOSVersion,
|
||||
releaseNotesURL: URL(string: "https://developer.apple.com/documentation/xcode-release-notes/xcode-12_3-release-notes/")!,
|
||||
releaseDate: Date(),
|
||||
sdks: _sdks,
|
||||
compilers: _compilers,
|
||||
downloadFileSize: _downloadFileSize
|
||||
),
|
||||
.Populated_Installed_Unselected: .init(
|
||||
version: _versionNoMeta,
|
||||
installState: .installed(Path(_path)!),
|
||||
selected: false,
|
||||
icon: NSWorkspace.shared.icon(forFile: _path),
|
||||
sdks: _sdks,
|
||||
compilers: _compilers,
|
||||
downloadFileSize: _downloadFileSize
|
||||
),
|
||||
.Populated_Uninstalled: .init(
|
||||
version: Version(major: 12, minor: 3, patch: 0),
|
||||
installState: .notInstalled,
|
||||
selected: false,
|
||||
icon: nil,
|
||||
sdks: _sdks,
|
||||
compilers: _compilers,
|
||||
downloadFileSize: _downloadFileSize
|
||||
),
|
||||
.Basic_Installed: .init(
|
||||
version: _versionWithMeta,
|
||||
installState: .installed(Path(_path)!),
|
||||
selected: false,
|
||||
icon: nil,
|
||||
sdks: nil,
|
||||
compilers: nil
|
||||
),
|
||||
.Basic_Installing: .init(
|
||||
version: _versionWithMeta,
|
||||
installState: .installing(.downloading(
|
||||
progress: configure(Progress()) {
|
||||
$0.kind = .file
|
||||
$0.fileOperationKind = .downloading
|
||||
$0.estimatedTimeRemaining = 123
|
||||
$0.totalUnitCount = 11_944_848_484
|
||||
$0.completedUnitCount = 848_444_920
|
||||
$0.throughput = 9_211_681
|
||||
}
|
||||
)),
|
||||
selected: false,
|
||||
icon: nil,
|
||||
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 = 0//runtimes.firstIndex { $0.sdkBuildUpdate.contains == "19E239" }!
|
||||
var iOSRuntime = runtimes[iOSIndex]
|
||||
iOSRuntime.installState = .installed
|
||||
runtimes[iOSIndex] = iOSRuntime
|
||||
|
||||
let watchOSIndex = 0//runtimes.firstIndex { $0.sdkBuildUpdate.first == "20R362" }!
|
||||
var runtime = runtimes[watchOSIndex]
|
||||
runtime.installState = .installing(
|
||||
RuntimeInstallationStep.downloading(
|
||||
progress:configure(Progress()) {
|
||||
$0.kind = .file
|
||||
$0.fileOperationKind = .downloading
|
||||
$0.estimatedTimeRemaining = 123
|
||||
$0.totalUnitCount = 11_944_848_484
|
||||
$0.completedUnitCount = 848_444_920
|
||||
$0.throughput = 9_211_681
|
||||
}
|
||||
)
|
||||
)
|
||||
runtimes[watchOSIndex] = runtime
|
||||
|
||||
return runtimes
|
||||
}()
|
||||
|
||||
var installedRuntimes: [CoreSimulatorImage] = {
|
||||
[CoreSimulatorImage(uuid: "85B22F5B-048B-4331-B6E2-F4196D8B7475", path: ["relative" : "file:///Library/Developer/CoreSimulator/Images/85B22F5B-048B-4331-B6E2-F4196D8B7475.dmg"], runtimeInfo: CoreSimulatorRuntimeInfo(build: "19E240")),
|
||||
CoreSimulatorImage(uuid: "85B22F5B-048B-4331-B6E2-F4196D8B7473", path: ["relative" : "file:///Library/Developer/CoreSimulator/Images/85B22F5B-048B-4331-B6E2-F4196D8B7475.dmg"], runtimeInfo: CoreSimulatorRuntimeInfo(build: "21N5233f"))]
|
||||
}()
|
||||
|
||||
|
||||
private let _versionNoMeta = Version(major: 12, minor: 3, patch: 0)
|
||||
private let _versionWithMeta = Version(major: 12, minor: 3, patch: 1, buildMetadataIdentifiers: ["1234A"])
|
||||
private let _path = "/Applications/Xcode-12.3.0.app"
|
||||
private let _requiredMacOSVersion = "10.15.4"
|
||||
private let _sdks = SDKs(
|
||||
macOS: .init(number: "11.1"),
|
||||
iOS: .init(number: "15.4", "19E239"),
|
||||
watchOS: .init(number: "7.3", "20R362"),
|
||||
tvOS: .init(number: "14.3", "20K67"),
|
||||
visionOS: .init(number: "1.0", "21N5233e")
|
||||
)
|
||||
private let _compilers = Compilers(
|
||||
gcc: .init(number: "4"),
|
||||
llvm_gcc: .init(number: "213"),
|
||||
llvm: .init(number: "2.3"),
|
||||
clang: .init(number: "7.3"),
|
||||
swift: .init(number: "5.3.2")
|
||||
)
|
||||
private let _downloadFileSize: Int64 = 242_342_424
|
||||
|
|
|
|||
54
Xcodes/Frontend/InfoPane/InfoPaneControls.swift
Normal file
54
Xcodes/Frontend/InfoPane/InfoPaneControls.swift
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
//
|
||||
// InfoPaneControls.swift
|
||||
// Xcodes
|
||||
//
|
||||
// Created by Duong Thai on 14/10/2023.
|
||||
// Copyright © 2023 Robots and Pencils. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct InfoPaneControls: View {
|
||||
let xcode: Xcode
|
||||
|
||||
var body: some View {
|
||||
VStack (alignment: .leading) {
|
||||
switch xcode.installState {
|
||||
case .notInstalled:
|
||||
HStack {
|
||||
Spacer()
|
||||
NotInstalledStateButtons(
|
||||
downloadFileSizeString: xcode.downloadFileSizeString,
|
||||
id: xcode.id)
|
||||
}
|
||||
|
||||
case .installing(let installationStep):
|
||||
HStack(alignment: .top) {
|
||||
InstallationStepDetailView(installationStep: installationStep)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
CancelInstallButton(xcode: xcode)
|
||||
}
|
||||
case .installed(_):
|
||||
InstalledStateButtons(xcode: xcode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview(XcodePreviewName.allCases[0].rawValue) { makePreviewContent(for: 0) }
|
||||
#Preview(XcodePreviewName.allCases[1].rawValue) { makePreviewContent(for: 1) }
|
||||
#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]
|
||||
|
||||
return InfoPaneControls(xcode: xcodeDict[name]!)
|
||||
.environmentObject(configure(AppState()) {
|
||||
$0.allXcodes = [xcodeDict[name]!]
|
||||
})
|
||||
.frame(width: 500)
|
||||
.padding()
|
||||
}
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
import SwiftUI
|
||||
import XcodesKit
|
||||
|
||||
struct InstallationStepDetailView: View {
|
||||
let installationStep: InstallationStep
|
||||
let installationStep: XcodeInstallationStep
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
|
|
@ -16,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)
|
||||
}
|
||||
|
|
@ -24,25 +25,25 @@ struct InstallationStepDetailView: View {
|
|||
}
|
||||
}
|
||||
|
||||
struct InstallDetailView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
InstallationStepDetailView(
|
||||
installationStep: .downloading(
|
||||
progress: configure(Progress()) {
|
||||
$0.kind = .file
|
||||
$0.fileOperationKind = .downloading
|
||||
$0.estimatedTimeRemaining = 123
|
||||
$0.totalUnitCount = 11944848484
|
||||
$0.completedUnitCount = 848444920
|
||||
$0.throughput = 9211681
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
InstallationStepDetailView(
|
||||
installationStep: .unarchiving
|
||||
)
|
||||
}
|
||||
}
|
||||
#Preview("Downloading") {
|
||||
InstallationStepDetailView(
|
||||
installationStep: .downloading(
|
||||
progress: configure(Progress()) {
|
||||
$0.kind = .file
|
||||
$0.fileOperationKind = .downloading
|
||||
$0.estimatedTimeRemaining = 123
|
||||
$0.totalUnitCount = 11944848484
|
||||
$0.completedUnitCount = 848444920
|
||||
$0.throughput = 9211681
|
||||
}
|
||||
)
|
||||
)
|
||||
.padding()
|
||||
}
|
||||
|
||||
#Preview("Unarchiving") {
|
||||
InstallationStepDetailView(
|
||||
installationStep: .unarchiving
|
||||
)
|
||||
.padding()
|
||||
}
|
||||
|
|
|
|||
76
Xcodes/Frontend/InfoPane/InstalledStateButtons.swift
Normal file
76
Xcodes/Frontend/InfoPane/InstalledStateButtons.swift
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
//
|
||||
// InstallingStateButtons.swift
|
||||
// Xcodes
|
||||
//
|
||||
// Created by Duong Thai on 13/10/2023.
|
||||
// Copyright © 2023 Robots and Pencils. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Version
|
||||
import XcodesKit
|
||||
import Path
|
||||
|
||||
struct InstalledStateButtons: View {
|
||||
let xcode: Xcode
|
||||
|
||||
@EnvironmentObject var appState: AppState
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
Text(xcode.installedPath?.string ?? "")
|
||||
Button(action: { appState.reveal(xcode.installedPath) }) {
|
||||
Image(systemName: "arrow.right.circle.fill")
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
.help("RevealInFinder")
|
||||
}
|
||||
|
||||
HStack {
|
||||
SelectButton(xcode: xcode)
|
||||
.disabled(xcode.selected)
|
||||
.help("Selected")
|
||||
|
||||
OpenButton(xcode: xcode)
|
||||
.help("Open")
|
||||
|
||||
Spacer()
|
||||
UninstallButton(xcode: xcode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
InstalledStateButtons(xcode: xcode)
|
||||
.environmentObject(configure(AppState()) {
|
||||
$0.allXcodes = [xcode]
|
||||
})
|
||||
.padding()
|
||||
.frame(width: 300)
|
||||
}
|
||||
|
||||
private let xcode = Xcode(
|
||||
version: Version(major: 12, minor: 3, patch: 0),
|
||||
installState: .installed(Path("/Applications/Xcode-12.3.0.app")!),
|
||||
selected: true,
|
||||
icon: NSWorkspace.shared.icon(forFile: "/Applications/Xcode-12.3.0.app"),
|
||||
requiredMacOSVersion: "10.15.4",
|
||||
releaseNotesURL: URL(string: "https://developer.apple.com/documentation/xcode-release-notes/xcode-12_3-release-notes/")!,
|
||||
releaseDate: Date(),
|
||||
sdks: SDKs(
|
||||
macOS: .init(number: "11.1"),
|
||||
iOS: .init(number: "14.3"),
|
||||
watchOS: .init(number: "7.3"),
|
||||
tvOS: .init(number: "14.3")
|
||||
),
|
||||
compilers: Compilers(
|
||||
gcc: .init(number: "4"),
|
||||
llvm_gcc: .init(number: "213"),
|
||||
llvm: .init(number: "2.3"),
|
||||
clang: .init(number: "7.3"),
|
||||
swift: .init(number: "5.3.2")
|
||||
),
|
||||
downloadFileSize: 242342424
|
||||
)
|
||||
48
Xcodes/Frontend/InfoPane/NotInstalledStateButtons.swift
Normal file
48
Xcodes/Frontend/InfoPane/NotInstalledStateButtons.swift
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
//
|
||||
// NotInstalledStateButtonsView.swift
|
||||
// Xcodes
|
||||
//
|
||||
// Created by Duong Thai on 13/10/2023.
|
||||
// Copyright © 2023 Robots and Pencils. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Version
|
||||
|
||||
struct NotInstalledStateButtons: View {
|
||||
let downloadFileSizeString: String?
|
||||
let id: XcodeID
|
||||
|
||||
@EnvironmentObject var appState: AppState
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Button {
|
||||
appState.checkMinVersionAndInstall(id: id)
|
||||
} label: {
|
||||
if id.architectures?.isAppleSilicon ?? false {
|
||||
Text("Install Apple Silicon").help("Install")
|
||||
} else {
|
||||
Text("Install Universal").help("Install")
|
||||
}
|
||||
}
|
||||
|
||||
if let size = downloadFileSizeString {
|
||||
Text("DownloadSize")
|
||||
.font(.headline)
|
||||
Text(size)
|
||||
.font(.subheadline)
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
NotInstalledStateButtons(
|
||||
downloadFileSizeString: "1,19 GB",
|
||||
id: XcodeID(version: Version(major: 12, minor: 3, patch: 0), architectures: nil)
|
||||
)
|
||||
.padding()
|
||||
}
|
||||
142
Xcodes/Frontend/InfoPane/PlatformsView.swift
Normal file
142
Xcodes/Frontend/InfoPane/PlatformsView.swift
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
//
|
||||
// PlatformsView.swift
|
||||
// Xcodes
|
||||
//
|
||||
// Created by Matt Kiazyk on 2023-12-18.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import XcodesKit
|
||||
|
||||
struct PlatformsView: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
@AppStorage("selectedRuntimeArchitecture") private var selectedVariant: ArchitectureVariant = .universal
|
||||
|
||||
let xcode: Xcode
|
||||
|
||||
var body: some View {
|
||||
|
||||
let builds = xcode.sdks?.allBuilds()
|
||||
let runtimes = builds?.flatMap { sdkBuild in
|
||||
appState.downloadableRuntimes.filter {
|
||||
$0.sdkBuildUpdate?.contains(sdkBuild) ?? false &&
|
||||
($0.architectures?.isEmpty ?? true ||
|
||||
($0.architectures?.isUniversal ?? false && selectedVariant == .universal) ||
|
||||
($0.architectures?.isAppleSilicon ?? false && selectedVariant == .appleSilicon)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let architectures = Set((runtimes ?? []).flatMap { $0.architectures ?? [] })
|
||||
|
||||
VStack {
|
||||
HStack {
|
||||
Text("Platforms")
|
||||
.font(.title3)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
if !architectures.isEmpty {
|
||||
Spacer()
|
||||
Picker("Architecture", selection: $selectedVariant) {
|
||||
ForEach(ArchitectureVariant.allCases, id: \.self) { arch in
|
||||
Label(arch.displayString, systemImage: arch.iconName)
|
||||
.tag(arch)
|
||||
}
|
||||
.labelStyle(.trailingIcon)
|
||||
}
|
||||
.pickerStyle(.menu)
|
||||
.menuStyle(.button)
|
||||
.buttonStyle(.borderless)
|
||||
.fixedSize()
|
||||
.labelsHidden()
|
||||
}
|
||||
}
|
||||
|
||||
ForEach(runtimes ?? [], id: \.identifier) { runtime in
|
||||
runtimeView(runtime: runtime)
|
||||
.frame(minWidth: 200)
|
||||
.padding()
|
||||
.background(.quinary)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 5, style: .continuous))
|
||||
}
|
||||
}
|
||||
.xcodesBackground()
|
||||
|
||||
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func runtimeView(runtime: DownloadableRuntime) -> some View {
|
||||
VStack(spacing: 10) {
|
||||
HStack {
|
||||
runtime.icon()
|
||||
Text("\(runtime.visibleIdentifier)")
|
||||
.font(.headline)
|
||||
ForEach(runtime.architectures ?? [], id: \.self) { architecture in
|
||||
TagView(text: architecture.displayString)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
if case let .installing(installationStep) = runtime.installState {
|
||||
HStack(alignment: .top, spacing: 5){
|
||||
RuntimeInstallationStepDetailView(installationStep: installationStep)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
Spacer()
|
||||
CancelRuntimeInstallButton(runtime: runtime)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func pathIfAvailable(xcode: Xcode, runtime: DownloadableRuntime) -> some View {
|
||||
if let path = appState.runtimeInstallPath(xcode: xcode, runtime: runtime) {
|
||||
Button(action: { appState.reveal(path: path.string) }) {
|
||||
Image(systemName: "arrow.right.circle.fill")
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
.help("RevealInFinder")
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview(XcodePreviewName.allCases[0].rawValue) { makePreviewContent(for: 0) }
|
||||
|
||||
private func makePreviewContent(for index: Int) -> some View {
|
||||
let name = XcodePreviewName.allCases[index]
|
||||
let runtimes = downloadableRuntimes
|
||||
|
||||
return PlatformsView(xcode: xcodeDict[name]!)
|
||||
.environmentObject({ () -> AppState in
|
||||
let a = AppState()
|
||||
a.allXcodes = [xcodeDict[name]!]
|
||||
a.installedRuntimes = installedRuntimes
|
||||
a.downloadableRuntimes = runtimes
|
||||
|
||||
return a
|
||||
|
||||
}())
|
||||
.frame(width: 300)
|
||||
.padding()
|
||||
}
|
||||
46
Xcodes/Frontend/InfoPane/ReleaseDateView.swift
Normal file
46
Xcodes/Frontend/InfoPane/ReleaseDateView.swift
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
//
|
||||
// ReleaseDateView.swift
|
||||
// Xcodes
|
||||
//
|
||||
// Created by Duong Thai on 11/10/2023.
|
||||
// Copyright © 2023 Robots and Pencils. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ReleaseDateView: View {
|
||||
let date: Date?
|
||||
let url: URL?
|
||||
var body: some View {
|
||||
if let date = date {
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
Text("ReleaseDate")
|
||||
.font(.headline)
|
||||
Spacer()
|
||||
if let url {
|
||||
ReleaseNotesView(url: url)
|
||||
}
|
||||
}
|
||||
|
||||
Text("\(date, style: .date)")
|
||||
.font(.subheadline)
|
||||
|
||||
}
|
||||
|
||||
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding()
|
||||
.background(.background)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 5, style: .continuous))
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ReleaseDateView(date: Date(), url: URL(string: "https://www.xcodes.app")!)
|
||||
.padding()
|
||||
}
|
||||
38
Xcodes/Frontend/InfoPane/ReleaseNotesView.swift
Normal file
38
Xcodes/Frontend/InfoPane/ReleaseNotesView.swift
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
//
|
||||
// ReleaseNotesView.swift
|
||||
// Xcodes
|
||||
//
|
||||
// Created by Duong Thai on 13/10/2023.
|
||||
// Copyright © 2023 Robots and Pencils. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ReleaseNotesView: View {
|
||||
let url: URL?
|
||||
|
||||
@SwiftUI.Environment(\.openURL) var openURL: OpenURLAction
|
||||
|
||||
var body: some View {
|
||||
if let url = url {
|
||||
Button(action: { openURL(url) }) {
|
||||
Image(systemName: "link.circle.fill")
|
||||
.font(.title)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.contextMenu(menuItems: {
|
||||
CopyReleaseNoteButton(url: url)
|
||||
})
|
||||
.help("ReleaseNotes.help")
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let url = URL(string: "https://developer.apple.com/documentation/xcode-release-notes/xcode-12_3-release-notes/")!
|
||||
|
||||
return ReleaseNotesView(url: url)
|
||||
.padding()
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
//
|
||||
// RuntimeInstallationStepDetailView.swift
|
||||
// Xcodes
|
||||
//
|
||||
// Created by Matt Kiazyk on 2023-11-23.
|
||||
// Copyright © 2023 Robots and Pencils. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import XcodesKit
|
||||
|
||||
struct RuntimeInstallationStepDetailView: View {
|
||||
let installationStep: RuntimeInstallationStep
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
Text(String(format: localizeString("InstallationStepDescription"), installationStep.stepNumber, installationStep.stepCount, installationStep.message))
|
||||
|
||||
switch installationStep {
|
||||
case let .downloading(progress):
|
||||
ObservingProgressIndicator(
|
||||
progress,
|
||||
controlSize: .regular,
|
||||
style: .bar,
|
||||
showsAdditionalDescription: true
|
||||
)
|
||||
|
||||
case .installing, .trashingArchive:
|
||||
ObservingProgressIndicator(
|
||||
Progress(),
|
||||
controlSize: .regular,
|
||||
style: .bar,
|
||||
showsAdditionalDescription: false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview("Downloading") {
|
||||
RuntimeInstallationStepDetailView(
|
||||
installationStep: .downloading(
|
||||
progress: configure(Progress()) {
|
||||
$0.kind = .file
|
||||
$0.fileOperationKind = .downloading
|
||||
$0.estimatedTimeRemaining = 123
|
||||
$0.totalUnitCount = 11944848484
|
||||
$0.completedUnitCount = 848444920
|
||||
$0.throughput = 9211681
|
||||
}
|
||||
))
|
||||
}
|
||||
#Preview("Installing") {
|
||||
RuntimeInstallationStepDetailView(
|
||||
installationStep: .installing
|
||||
)
|
||||
}
|
||||
70
Xcodes/Frontend/InfoPane/SDKsView.swift
Normal file
70
Xcodes/Frontend/InfoPane/SDKsView.swift
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
//
|
||||
// SDKsView.swift
|
||||
// Xcodes
|
||||
//
|
||||
// Created by Duong Thai on 13/10/2023.
|
||||
// Copyright © 2023 Robots and Pencils. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import XcodesKit
|
||||
|
||||
struct SDKsView: View {
|
||||
let content: String
|
||||
|
||||
var body: some View {
|
||||
if content.isEmpty {
|
||||
EmptyView()
|
||||
} else {
|
||||
VStack(alignment: .leading) {
|
||||
Text("SDKs").font(.headline)
|
||||
Text(content)
|
||||
.font(.subheadline)
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init(sdks: SDKs?) {
|
||||
guard let sdks = sdks else {
|
||||
self.content = ""
|
||||
return
|
||||
}
|
||||
let content = Self.content(from: sdks)
|
||||
self.content = content
|
||||
}
|
||||
|
||||
static private func content(from sdks: SDKs) -> String {
|
||||
let content: String = [
|
||||
("macOS", sdks.macOS),
|
||||
("iOS", sdks.iOS),
|
||||
("watchOS", sdks.watchOS),
|
||||
("tvOS", sdks.tvOS)
|
||||
].compactMap { // remove nil compiler
|
||||
guard $0.1 != nil, // has version array
|
||||
!$0.1!.isEmpty // has at least 1 version
|
||||
else { return nil }
|
||||
|
||||
let numbers = $0.1!.compactMap { $0.number } // remove nil number
|
||||
guard !numbers.isEmpty // has at least 1 number
|
||||
else { return nil }
|
||||
|
||||
// description for each type of compilers
|
||||
return "\($0.0): \(numbers.joined(separator: ", "))"
|
||||
}.joined(separator: "\n")
|
||||
.trimmingCharacters(in: .whitespaces)
|
||||
|
||||
return content
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let sdks = SDKs(
|
||||
macOS: .init(number: "11.1"),
|
||||
iOS: .init(number: "14.3"),
|
||||
watchOS: .init(number: "7.3"),
|
||||
tvOS: .init(number: "14.3"))
|
||||
|
||||
return SDKsView(sdks: sdks)
|
||||
.padding()
|
||||
}
|
||||
26
Xcodes/Frontend/InfoPane/UnselectedView.swift
Normal file
26
Xcodes/Frontend/InfoPane/UnselectedView.swift
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
//
|
||||
// UnselectedView.swift
|
||||
// Xcodes
|
||||
//
|
||||
// Created by Duong Thai on 13/10/2023.
|
||||
// Copyright © 2023 Robots and Pencils. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct UnselectedView: View {
|
||||
var body: some View {
|
||||
VStack {
|
||||
Spacer()
|
||||
Text("NoXcodeSelected")
|
||||
.font(.title)
|
||||
.foregroundColor(.secondary)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
UnselectedView()
|
||||
.padding()
|
||||
}
|
||||
|
|
@ -1,5 +1,8 @@
|
|||
import ErrorHandling
|
||||
import SwiftUI
|
||||
import XcodesKit
|
||||
import Path
|
||||
import Version
|
||||
|
||||
struct MainWindow: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
|
|
@ -12,12 +15,12 @@ struct MainWindow: View {
|
|||
// FB8979533 SceneStorage doesn't restore value after app is quit by user
|
||||
@AppStorage("isShowingInfoPane") private var isShowingInfoPane = false
|
||||
@AppStorage("xcodeListCategory") private var category: XcodeListCategory = .all
|
||||
@AppStorage("xcodeListArchitecture") private var architecture: XcodeListArchitecture = .universal
|
||||
@AppStorage("isInstalledOnly") private var isInstalledOnly = false
|
||||
|
||||
|
||||
var body: some View {
|
||||
HSplitView {
|
||||
XcodeListView(selectedXcodeID: $selectedXcodeID, searchText: searchText, category: category, isInstalledOnly: isInstalledOnly)
|
||||
.frame(minWidth: 300)
|
||||
NavigationSplitViewWrapper {
|
||||
XcodeListView(selectedXcodeID: $selectedXcodeID, searchText: searchText, category: category, isInstalledOnly: isInstalledOnly, architecture: architecture)
|
||||
.layoutPriority(1)
|
||||
.alert(item: $appState.xcodeBeingConfirmedForUninstallation) { xcode in
|
||||
Alert(title: Text(String(format: localizeString("Alert.Uninstall.Title"), xcode.description)),
|
||||
|
|
@ -25,18 +28,43 @@ struct MainWindow: View {
|
|||
primaryButton: .destructive(Text("Uninstall"), action: { self.appState.uninstall(xcode: xcode) }),
|
||||
secondaryButton: .cancel(Text("Cancel")))
|
||||
}
|
||||
|
||||
if isShowingInfoPane {
|
||||
InfoPane(selectedXcodeID: selectedXcodeID)
|
||||
.frame(minWidth: 300, maxWidth: .infinity)
|
||||
.searchable(text: $searchText, placement: .sidebar)
|
||||
.mainToolbar(
|
||||
category: $category,
|
||||
isInstalledOnly: $isInstalledOnly,
|
||||
isShowingInfoPane: $isShowingInfoPane,
|
||||
architecture: $architecture
|
||||
)
|
||||
} detail: {
|
||||
Group {
|
||||
if let xcode = xcode {
|
||||
InfoPane(xcode: xcode)
|
||||
} else {
|
||||
UnselectedView()
|
||||
}
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItemGroup {
|
||||
Button(action: { appState.presentedSheet = .signIn }, label: {
|
||||
Label("Login", systemImage: "person.circle")
|
||||
})
|
||||
.help("LoginDescription")
|
||||
if #available(macOS 14, *) {
|
||||
SettingsLink(label: {
|
||||
Label("Preferences", systemImage: "gearshape")
|
||||
})
|
||||
.help("PreferencesDescription")
|
||||
} else {
|
||||
Button(action: {
|
||||
NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil)
|
||||
}, label: {
|
||||
Label("Preferences", systemImage: "gearshape")
|
||||
})
|
||||
.help("PreferencesDescription")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.mainToolbar(
|
||||
category: $category,
|
||||
isInstalledOnly: $isInstalledOnly,
|
||||
isShowingInfoPane: $isShowingInfoPane,
|
||||
searchText: $searchText
|
||||
)
|
||||
.bottomStatusBar()
|
||||
.padding([.top], 0)
|
||||
.navigationSubtitle(subtitleText)
|
||||
|
|
@ -50,6 +78,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
|
||||
|
|
@ -59,7 +90,11 @@ struct MainWindow: View {
|
|||
// FB8954571 focusedValue(_:_:) on List row doesn't propagate value to @FocusedValue
|
||||
.focusedValue(\.selectedXcode, SelectedXcode(appState.allXcodes.first { $0.id == selectedXcodeID }))
|
||||
}
|
||||
|
||||
|
||||
private var xcode: Xcode? {
|
||||
appState.allXcodes.first(where: { $0.id == selectedXcodeID })
|
||||
}
|
||||
|
||||
private var subtitleText: Text {
|
||||
if let lastUpdated = lastUpdated.map(Date.init(timeIntervalSince1970:)) {
|
||||
return Text("\(localizeString("UpdatedAt")) \(lastUpdated, style: .date) \(lastUpdated, style: .time)")
|
||||
|
|
@ -77,6 +112,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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -153,24 +190,62 @@ struct MainWindow: View {
|
|||
action: { appState.presentedAlert = nil }
|
||||
)
|
||||
)
|
||||
case .unauthenticated:
|
||||
return Alert(
|
||||
title: Text("Alert.Install.Error.Title"),
|
||||
message: Text("Alert.Install.AuthError.Message"),
|
||||
primaryButton: .default(
|
||||
Text("OK"),
|
||||
action: {
|
||||
appState.presentedSheet = .signIn
|
||||
}
|
||||
),
|
||||
secondaryButton: .cancel(
|
||||
Text("Cancel")
|
||||
)
|
||||
)
|
||||
case let .checkMinSupportedVersion(xcode, deviceVersion):
|
||||
return Alert(
|
||||
title: Text("Alert.MinSupported.Title"),
|
||||
message: Text(String(format: localizeString("Alert.MinSupported.Message"), xcode.version.descriptionWithoutBuildMetadata, xcode.requiredMacOSVersion ?? "", deviceVersion)),
|
||||
message: Text(String(format: localizeString("Alert.MinSupported.Message"), xcode.xcodeID.version.descriptionWithoutBuildMetadata, xcode.requiredMacOSVersion ?? "", deviceVersion)),
|
||||
primaryButton: .default(
|
||||
Text("Install"),
|
||||
action: {
|
||||
self.appState.install(id: xcode.version)
|
||||
self.appState.install(id: xcode.xcodeID)
|
||||
}
|
||||
),
|
||||
secondaryButton: .cancel(Text("Cancel"))
|
||||
)
|
||||
|
||||
case let .cancelRuntimeInstall(runtime):
|
||||
return Alert(
|
||||
title: Text(String(format: localizeString("Alert.CancelInstall.Runtimes.Title"), runtime.name)),
|
||||
message: Text("Alert.CancelInstall.Message"),
|
||||
primaryButton: .destructive(
|
||||
Text("Alert.CancelInstall.PrimaryButton"),
|
||||
action: {
|
||||
self.appState.cancelRuntimeInstall(runtime: runtime)
|
||||
}
|
||||
),
|
||||
secondaryButton: .cancel(Text("Cancel"))
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
struct MainWindow_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
MainWindow()
|
||||
MainWindow().environmentObject({ () -> AppState in
|
||||
let a = AppState()
|
||||
a.allXcodes = [
|
||||
Xcode(version: Version("12.0.0+1234A")!, identicalBuilds: [XcodeID(version: Version("12.0.0+1234A")!), XcodeID(version: Version("12.0.0-RC+1234A")!)], installState: .installed(Path("/Applications/Xcode-12.3.0.app")!), selected: false, icon: nil),
|
||||
Xcode(version: Version("12.3.0")!, installState: .installed(Path("/Applications/Xcode-12.3.0.app")!), selected: true, icon: nil),
|
||||
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),
|
||||
]
|
||||
return a
|
||||
}())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,12 +108,24 @@ struct AdvancedPreferencePane: View {
|
|||
.disabled(appState.createSymLinkOnSelectDisabled)
|
||||
Text("AutomaticallyCreateSymbolicLinkDescription")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
.groupBoxStyle(PreferencesGroupBoxStyle())
|
||||
|
||||
if Hardware.isAppleSilicon() {
|
||||
GroupBox(label: Text("Apple Silicon")) {
|
||||
Toggle("ShowOpenInRosetta", isOn: $appState.showOpenInRosettaOption)
|
||||
.disabled(appState.createSymLinkOnSelectDisabled)
|
||||
Text("ShowOpenInRosettaDescription")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
.groupBoxStyle(PreferencesGroupBoxStyle())
|
||||
}
|
||||
|
||||
GroupBox(label: Text("PrivilegedHelper")) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
|
|
@ -116,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()
|
||||
|
|
@ -141,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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -151,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
88
Xcodes/Frontend/Preferences/PlatformsListView.swift
Normal file
88
Xcodes/Frontend/Preferences/PlatformsListView.swift
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
//
|
||||
// PlatformsListView.swift
|
||||
// Xcodes
|
||||
//
|
||||
// Created by Matt Kiazyk on 2023-12-20.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import Path
|
||||
import XcodesKit
|
||||
import OrderedCollections
|
||||
|
||||
struct PlatformsListView: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
@State private var runtimes: OrderedDictionary<DownloadableRuntime.Platform, [DownloadableRuntime]> = [:]
|
||||
@State private var selectedRuntime: DownloadableRuntime?
|
||||
|
||||
var body: some View {
|
||||
List(selection: $selectedRuntime) {
|
||||
Text("PlatformsList.Title")
|
||||
.font(.body)
|
||||
ForEach(runtimes.elements.sorted(\.key.order), id: \.key) { platform, runtimeList in
|
||||
Section {
|
||||
ForEach(runtimeList, id: \.self) { runtime in
|
||||
HStack {
|
||||
Text(runtime.name)
|
||||
Spacer()
|
||||
Text(runtime.downloadFileSizeString)
|
||||
Button {
|
||||
deleteRuntime(runtime: runtime)
|
||||
} label: {
|
||||
Image(systemName: "trash")
|
||||
}
|
||||
.foregroundStyle(.red)
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
.frame(height: 30)
|
||||
}
|
||||
|
||||
} header: {
|
||||
HStack {
|
||||
runtimeList.first!.icon()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 20)
|
||||
Text(platform.shortName)
|
||||
.font(.headline)
|
||||
}
|
||||
} footer: {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
.listStyle(.inset(alternatesRowBackgrounds: true))
|
||||
.task {
|
||||
loadRuntimes()
|
||||
}
|
||||
.onChange(of: appState.installedRuntimes) { _ in
|
||||
loadRuntimes()
|
||||
}
|
||||
}
|
||||
|
||||
func loadRuntimes() {
|
||||
let filteredRuntimes = appState.downloadableRuntimes.filter { runtime in
|
||||
appState.installedRuntimes.contains { $0.runtimeInfo.build == runtime.simulatorVersion.buildUpdate
|
||||
}
|
||||
}
|
||||
runtimes = OrderedDictionary(grouping: filteredRuntimes, by: { $0.platform })
|
||||
}
|
||||
|
||||
func deleteRuntime(runtime: DownloadableRuntime) {
|
||||
appState.presentedPreferenceAlert = .deletePlatform(runtime: runtime)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#Preview {
|
||||
PlatformsListView()
|
||||
.environmentObject({ () -> AppState in
|
||||
let a = AppState()
|
||||
|
||||
a.installedRuntimes = installedRuntimes
|
||||
a.downloadableRuntimes = downloadableRuntimes
|
||||
|
||||
return a
|
||||
|
||||
}())
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -69,7 +76,8 @@ struct UpdatesPreferencePane: View {
|
|||
|
||||
class ObservableUpdater: ObservableObject {
|
||||
private let updater: SPUUpdater
|
||||
|
||||
private let updaterDelegate = UpdaterDelegate()
|
||||
|
||||
@Published var automaticallyChecksForUpdates = false {
|
||||
didSet {
|
||||
updater.automaticallyChecksForUpdates = automaticallyChecksForUpdates
|
||||
|
|
@ -80,18 +88,24 @@ class ObservableUpdater: ObservableObject {
|
|||
private var lastUpdateCheckDateObservation: NSKeyValueObservation?
|
||||
@Published var includePrereleaseVersions = false {
|
||||
didSet {
|
||||
UserDefaults.standard.setValue(includePrereleaseVersions, forKey: "includePrereleaseVersions")
|
||||
Current.defaults.set(includePrereleaseVersions, forKey: "includePrereleaseVersions")
|
||||
|
||||
if includePrereleaseVersions {
|
||||
updater.setFeedURL(.prereleaseAppcast)
|
||||
} else {
|
||||
updater.setFeedURL(.appcast)
|
||||
}
|
||||
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: nil, userDriverDelegate: nil).updater
|
||||
updater = SPUStandardUpdaterController(startingUpdater: true, updaterDelegate: updaterDelegate, userDriverDelegate: nil).updater
|
||||
|
||||
// upgrade from an old sparkle version which set feeds via the updater
|
||||
// now it uses the `updaterDelegate`
|
||||
updater.clearFeedURLFromUserDefaults()
|
||||
|
||||
automaticallyChecksForUpdatesObservation = updater.observe(
|
||||
\.automaticallyChecksForUpdates,
|
||||
|
|
@ -108,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() {
|
||||
|
|
@ -116,9 +130,22 @@ class ObservableUpdater: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
extension URL {
|
||||
static let appcast = URL(string: "https://robotsandpencils.github.io/XcodesApp/appcast.xml")!
|
||||
static let prereleaseAppcast = URL(string: "https://robotsandpencils.github.io/XcodesApp/appcast_pre.xml")!
|
||||
class UpdaterDelegate: NSObject, SPUUpdaterDelegate {
|
||||
var includePrereleaseVersions: Bool = false
|
||||
|
||||
func feedURLString(for updater: SPUUpdater) -> String? {
|
||||
if includePrereleaseVersions {
|
||||
return .prereleaseAppcast
|
||||
} else {
|
||||
return .appcast
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension String {
|
||||
static let appcast = "https://www.xcodes.app/appcast.xml"
|
||||
static let prereleaseAppcast = "https://www.xcodes.app/appcast_pre.xml"
|
||||
}
|
||||
|
||||
struct UpdatesPreferencePane_Previews: PreviewProvider {
|
||||
|
|
@ -126,7 +153,9 @@ struct UpdatesPreferencePane_Previews: PreviewProvider {
|
|||
Group {
|
||||
UpdatesPreferencePane()
|
||||
.environmentObject(AppState())
|
||||
.frame(maxWidth: 500)
|
||||
.environmentObject(ObservableUpdater())
|
||||
.frame(maxWidth: 600)
|
||||
.frame(minHeight: 300)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,10 +6,12 @@ struct PinCodeTextField: NSViewRepresentable {
|
|||
|
||||
@Binding var code: String
|
||||
let numberOfDigits: Int
|
||||
let complete: (String) -> Void
|
||||
|
||||
func makeNSView(context: Context) -> NSViewType {
|
||||
let view = PinCodeTextView(numberOfDigits: numberOfDigits, itemSpacing: 10)
|
||||
view.codeDidChange = { c in code = c }
|
||||
view.codeDidChange = { c in code = c }
|
||||
view.codeDidComplete = { complete($0) }
|
||||
return view
|
||||
}
|
||||
|
||||
|
|
@ -29,8 +31,9 @@ struct PinCodeTextField_Previews: PreviewProvider {
|
|||
struct PreviewContainer: View {
|
||||
@State private var code = "1234567890"
|
||||
var body: some View {
|
||||
PinCodeTextField(code: $code, numberOfDigits: 11)
|
||||
.padding()
|
||||
PinCodeTextField(code: $code, numberOfDigits: 11) {
|
||||
print("Input is complete \($0)")
|
||||
}.padding()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -52,10 +55,16 @@ class PinCodeTextView: NSControl, NSTextFieldDelegate {
|
|||
handler(String(code.compactMap { $0 }))
|
||||
}
|
||||
updateText()
|
||||
|
||||
if code.compactMap({ $0 }).count == numberOfDigits,
|
||||
let handler = codeDidComplete {
|
||||
handler(String(code.compactMap { $0 }))
|
||||
}
|
||||
}
|
||||
}
|
||||
var codeDidChange: ((String) -> Void)? = nil
|
||||
|
||||
var codeDidComplete: ((String) -> Void)? = nil
|
||||
|
||||
private let numberOfDigits: Int
|
||||
private let stackView: NSStackView = .init(frame: .zero)
|
||||
private var characterViews: [PinCodeCharacterTextField] = []
|
||||
|
|
|
|||
|
|
@ -10,12 +10,14 @@ 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()
|
||||
}
|
||||
.padding()
|
||||
|
|
@ -30,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,29 +7,33 @@ 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)
|
||||
}
|
||||
.onAppear {
|
||||
if phoneNumbers.count == 1 {
|
||||
selectedPhoneNumberID = phoneNumbers.first?.id
|
||||
}
|
||||
}
|
||||
} 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)
|
||||
|
|
@ -49,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())
|
||||
|
|
@ -59,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,13 @@ 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()
|
||||
}
|
||||
.padding()
|
||||
|
|
@ -29,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
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
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)
|
||||
|
|
|
|||
|
|
@ -9,27 +9,40 @@
|
|||
import Foundation
|
||||
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 {
|
||||
content
|
||||
VStack(spacing: 0) {
|
||||
Divider()
|
||||
HStack {
|
||||
Text(appState.bottomStatusBarMessage)
|
||||
.font(.subheadline)
|
||||
Spacer()
|
||||
Text(Bundle.main.shortVersion!)
|
||||
.font(.subheadline)
|
||||
content
|
||||
VStack(spacing: 0) {
|
||||
Divider()
|
||||
HStack {
|
||||
Text(appState.bottomStatusBarMessage)
|
||||
.font(.subheadline)
|
||||
Spacer()
|
||||
if !hideSupportXcodes {
|
||||
Button(action: {
|
||||
openURL(URL(string: "https://opencollective.com/xcodesapp")!)
|
||||
}) {
|
||||
HStack {
|
||||
Image(systemName: "heart.circle")
|
||||
Text("Support.Xcodes")
|
||||
}
|
||||
}
|
||||
}
|
||||
Text(verbatim: "\(Bundle.main.shortVersion!) (\(Bundle.main.version!))")
|
||||
.font(.subheadline)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: 30, alignment: .leading)
|
||||
.padding([.leading, .trailing], 10)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: 30, alignment: .leading)
|
||||
.padding([.leading, .trailing], 10)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: 30, alignment: .leading)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension View {
|
||||
|
|
@ -42,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
|
||||
}())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import SwiftUI
|
||||
import XcodesKit
|
||||
|
||||
struct InstallationStepRowView: View {
|
||||
let installationStep: InstallationStep
|
||||
let installationStep: XcodeInstallationStep
|
||||
let highlighted: Bool
|
||||
let cancel: () -> Void
|
||||
|
||||
|
|
@ -17,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,22 +5,17 @@ struct MainToolbarModifier: ViewModifier {
|
|||
@Binding var category: XcodeListCategory
|
||||
@Binding var isInstalledOnly: Bool
|
||||
@Binding var isShowingInfoPane: Bool
|
||||
@Binding var searchText: String
|
||||
|
||||
@Binding var architectures: XcodeListArchitecture
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.toolbar { toolbar }
|
||||
}
|
||||
|
||||
private var toolbar: some ToolbarContent {
|
||||
ToolbarItemGroup(placement: .status) {
|
||||
Button(action: { appState.presentedSheet = .signIn }, label: {
|
||||
Label("Login", systemImage: "person.circle")
|
||||
})
|
||||
.help("LoginDescription")
|
||||
|
||||
ToolbarItemGroup {
|
||||
ProgressButton(
|
||||
isInProgress: appState.isUpdating,
|
||||
isInProgress: appState.isUpdating,
|
||||
action: appState.update
|
||||
) {
|
||||
Label("Refresh", systemImage: "arrow.clockwise")
|
||||
|
|
@ -28,79 +23,43 @@ struct MainToolbarModifier: ViewModifier {
|
|||
.keyboardShortcut(KeyEquivalent("r"))
|
||||
.help("RefreshDescription")
|
||||
|
||||
Button(action: {
|
||||
switch category {
|
||||
case .all: category = .release
|
||||
case .release: category = .beta
|
||||
case .beta: category = .all
|
||||
Spacer()
|
||||
|
||||
let isFiltering = isInstalledOnly || category != .all || architectures != .universal
|
||||
Menu("Filter", systemImage: "line.horizontal.3.decrease.circle") {
|
||||
Section {
|
||||
Toggle("Installed Only", systemImage: "arrow.down.app", isOn: $isInstalledOnly) .labelStyle(.titleAndIcon)
|
||||
}
|
||||
}) {
|
||||
switch category {
|
||||
case .all:
|
||||
Label("All", systemImage: "line.horizontal.3.decrease.circle")
|
||||
case .release:
|
||||
if #available(macOS 11.3, *) {
|
||||
.help("FilterInstalledDescription")
|
||||
|
||||
Section {
|
||||
Picker("Category", selection: $category) {
|
||||
Label("All", systemImage: "line.horizontal.3.decrease.circle")
|
||||
.tag(XcodeListCategory.all)
|
||||
Label("ReleaseOnly", systemImage: "line.horizontal.3.decrease.circle.fill")
|
||||
.labelStyle(TitleAndIconLabelStyle())
|
||||
.foregroundColor(.accentColor)
|
||||
} else {
|
||||
Label("ReleaseOnly", systemImage: "line.horizontal.3.decrease.circle.fill")
|
||||
.labelStyle(TitleOnlyLabelStyle())
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
case .beta:
|
||||
if #available(macOS 11.3, *) {
|
||||
.tag(XcodeListCategory.release)
|
||||
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)
|
||||
.tag(XcodeListCategory.beta)
|
||||
}
|
||||
}
|
||||
}
|
||||
.help("FilterAvailableDescription")
|
||||
|
||||
Button(action: {
|
||||
isInstalledOnly.toggle()
|
||||
}) {
|
||||
if isInstalledOnly {
|
||||
Label("Filter", systemImage: "arrow.down.app.fill")
|
||||
.foregroundColor(.accentColor)
|
||||
} else {
|
||||
Label("Filter", systemImage: "arrow.down.app")
|
||||
|
||||
.help("FilterAvailableDescription")
|
||||
.disabled(category.isManaged)
|
||||
|
||||
Section {
|
||||
Picker("Architecture", selection: $architectures) {
|
||||
Label("Universal", systemImage: "cpu.fill")
|
||||
.tag(XcodeListArchitecture.universal)
|
||||
Label("Apple Silicon", systemImage: "m4.button.horizontal")
|
||||
.foregroundColor(.accentColor)
|
||||
.tag(XcodeListArchitecture.appleSilicon)
|
||||
}
|
||||
.help("FilterArchitecturesDescription")
|
||||
.disabled(architectures.isManaged)
|
||||
}
|
||||
.labelStyle(.titleAndIcon)
|
||||
}
|
||||
.help("FilterInstalledDescription")
|
||||
|
||||
Button(action: { isShowingInfoPane.toggle() }) {
|
||||
if isShowingInfoPane {
|
||||
Label("Info", systemImage: "info.circle.fill")
|
||||
.foregroundColor(.accentColor)
|
||||
} else {
|
||||
Label("Info", systemImage: "info.circle")
|
||||
}
|
||||
}
|
||||
.keyboardShortcut(KeyboardShortcut("i", modifiers: [.command, .option]))
|
||||
.help("InfoDescription")
|
||||
|
||||
Button(action: {
|
||||
if #available(macOS 13, *) {
|
||||
NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil)
|
||||
} else {
|
||||
NSApp.sendAction(Selector(("showPreferencesWindow:")), to: nil, from: nil)
|
||||
}
|
||||
}, label: {
|
||||
Label("Preferences", systemImage: "gearshape")
|
||||
})
|
||||
.help("PreferencesDescription")
|
||||
|
||||
TextField("Search", text: $searchText)
|
||||
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||
.frame(width: 200)
|
||||
.help("SearchDescription")
|
||||
.pickerStyle(.inline)
|
||||
.symbolVariant(isFiltering ? .fill : .none)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -110,14 +69,14 @@ extension View {
|
|||
category: Binding<XcodeListCategory>,
|
||||
isInstalledOnly: Binding<Bool>,
|
||||
isShowingInfoPane: Binding<Bool>,
|
||||
searchText: Binding<String>
|
||||
architecture: Binding<XcodeListArchitecture>
|
||||
) -> some View {
|
||||
self.modifier(
|
||||
modifier(
|
||||
MainToolbarModifier(
|
||||
category: category,
|
||||
isInstalledOnly: isInstalledOnly,
|
||||
isShowingInfoPane: isShowingInfoPane,
|
||||
searchText: searchText
|
||||
architectures: architecture
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import Foundation
|
||||
import XcodesKit
|
||||
|
||||
enum XcodeListCategory: String, CaseIterable, Identifiable, CustomStringConvertible {
|
||||
case all
|
||||
|
|
@ -14,4 +15,22 @@ enum XcodeListCategory: String, CaseIterable, Identifiable, CustomStringConverti
|
|||
case .beta: return localizeString("Beta")
|
||||
}
|
||||
}
|
||||
|
||||
var isManaged: Bool { PreferenceKey.xcodeListCategory.isManaged() }
|
||||
}
|
||||
|
||||
enum XcodeListArchitecture: String, CaseIterable, Identifiable, CustomStringConvertible {
|
||||
case universal
|
||||
case appleSilicon
|
||||
|
||||
var id: Self { self }
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .universal: return localizeString("Universal")
|
||||
case .appleSilicon: return localizeString("Apple Silicon")
|
||||
}
|
||||
}
|
||||
|
||||
var isManaged: Bool { PreferenceKey.xcodeListCategory.isManaged() }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,13 +7,16 @@ struct XcodeListView: View {
|
|||
@Binding var selectedXcodeID: Xcode.ID?
|
||||
private let searchText: String
|
||||
private let category: XcodeListCategory
|
||||
private let architecture: XcodeListArchitecture
|
||||
private let isInstalledOnly: Bool
|
||||
|
||||
init(selectedXcodeID: Binding<Xcode.ID?>, searchText: String, category: XcodeListCategory, isInstalledOnly: Bool) {
|
||||
@AppStorage(PreferenceKey.allowedMajorVersions.rawValue) private var allowedMajorVersions = Int.max
|
||||
|
||||
init(selectedXcodeID: Binding<Xcode.ID?>, searchText: String, category: XcodeListCategory, isInstalledOnly: Bool, architecture: XcodeListArchitecture) {
|
||||
self._selectedXcodeID = selectedXcodeID
|
||||
self.searchText = searchText
|
||||
self.category = category
|
||||
self.isInstalledOnly = isInstalledOnly
|
||||
self.architecture = architecture
|
||||
}
|
||||
|
||||
var visibleXcodes: [Xcode] {
|
||||
|
|
@ -27,6 +30,27 @@ struct XcodeListView: View {
|
|||
xcodes = appState.allXcodes.filter { $0.version.isPrerelease }
|
||||
}
|
||||
|
||||
if architecture == .appleSilicon {
|
||||
xcodes = xcodes.filter { $0.architectures == [.arm64] }
|
||||
}
|
||||
|
||||
|
||||
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) }
|
||||
}
|
||||
|
|
@ -42,21 +66,65 @@ struct XcodeListView: View {
|
|||
List(visibleXcodes, selection: $selectedXcodeID) { xcode in
|
||||
XcodeListViewRow(xcode: xcode, selected: selectedXcodeID == xcode.id, appState: appState)
|
||||
}
|
||||
.listStyle(.sidebar)
|
||||
.safeAreaInset(edge: .bottom, spacing: 0) {
|
||||
PlatformsPocket()
|
||||
.padding(.horizontal)
|
||||
.padding(.vertical, 8)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PlatformsPocket: View {
|
||||
@SwiftUI.Environment(\.openWindow) private var openWindow
|
||||
|
||||
var body: some View {
|
||||
Button(action: {
|
||||
openWindow(id: "platforms")
|
||||
}
|
||||
) {
|
||||
if #available(macOS 26.0, *) {
|
||||
platformsLabel
|
||||
.glassEffect(in: .rect(cornerRadius: 8, style: .continuous))
|
||||
} else {
|
||||
platformsLabel
|
||||
.background(.quaternary.opacity(0.75))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
|
||||
}
|
||||
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
var platformsLabel: some View {
|
||||
HStack(spacing: 5) {
|
||||
Image(systemName: "square.3.layers.3d")
|
||||
.font(.title3.weight(.medium))
|
||||
Text("PlatformsDescription")
|
||||
Spacer()
|
||||
}
|
||||
.font(.body.weight(.medium))
|
||||
.padding(.horizontal)
|
||||
.padding(.vertical, 12)
|
||||
}
|
||||
}
|
||||
|
||||
struct XcodeListView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
XcodeListView(selectedXcodeID: .constant(nil), searchText: "", category: .all, isInstalledOnly: false)
|
||||
XcodeListView(selectedXcodeID: .constant(nil), searchText: "", category: .all, isInstalledOnly: false, architecture: .appleSilicon)
|
||||
.environmentObject({ () -> AppState in
|
||||
let a = AppState()
|
||||
a.allXcodes = [
|
||||
Xcode(version: Version("12.0.0+1234A")!, identicalBuilds: [Version("12.0.0+1234A")!, Version("12.0.0-RC+1234A")!], installState: .installed(Path("/Applications/Xcode-12.3.0.app")!), selected: false, icon: nil),
|
||||
Xcode(version: Version("12.0.0+1234A")!, identicalBuilds: [XcodeID(version: Version("12.0.0+1234A")!), XcodeID(version: Version("12.0.0-RC+1234A")!)], installState: .installed(Path("/Applications/Xcode-12.3.0.app")!), selected: false, icon: nil),
|
||||
Xcode(version: Version("12.3.0")!, installState: .installed(Path("/Applications/Xcode-12.3.0.app")!), selected: true, icon: nil),
|
||||
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
|
||||
}())
|
||||
|
|
|
|||
|
|
@ -21,18 +21,23 @@ struct XcodeListViewRow: View {
|
|||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
.accessibility(label: Text("IdenticalBuilds"))
|
||||
.accessibility(value: Text(xcode.identicalBuilds.map(\.appleDescription).joined(separator: ", ")))
|
||||
.accessibility(value: Text(xcode.identicalBuilds.map(\.version.appleDescription).joined(separator: ", ")))
|
||||
.help("IdenticalBuilds.help")
|
||||
}
|
||||
|
||||
if xcode.architectures?.isAppleSilicon ?? false {
|
||||
Image(systemName: "m4.button.horizontal")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
.accessibility(label: Text("Apple Silicon"))
|
||||
.help("Apple Silicon")
|
||||
}
|
||||
}
|
||||
|
||||
if case let .installed(path) = xcode.installState {
|
||||
Text(verbatim: path.string)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
} else {
|
||||
Text(verbatim: "")
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -42,6 +47,7 @@ struct XcodeListViewRow: View {
|
|||
.padding(.trailing, 16)
|
||||
installControl(for: xcode)
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
.contextMenu {
|
||||
switch xcode.installState {
|
||||
case .notInstalled:
|
||||
|
|
@ -75,9 +81,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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -110,10 +117,9 @@ struct XcodeListViewRow: View {
|
|||
.buttonStyle(AppStoreButtonStyle(primary: true, highlighted: selected))
|
||||
.help("OpenDescription")
|
||||
case .notInstalled:
|
||||
Button("Install") { appState.checkMinVersionAndInstall(id: xcode.id) }
|
||||
InstallButton(xcode: xcode)
|
||||
.textCase(.uppercase)
|
||||
.buttonStyle(AppStoreButtonStyle(primary: false, highlighted: selected))
|
||||
.help("InstallDescription")
|
||||
.buttonStyle(AppStoreButtonStyle(primary: false, highlighted: false))
|
||||
case let .installing(installationStep):
|
||||
InstallationStepRowView(
|
||||
installationStep: installationStep,
|
||||
|
|
@ -158,7 +164,7 @@ struct XcodeListViewRow_Previews: PreviewProvider {
|
|||
)
|
||||
|
||||
XcodeListViewRow(
|
||||
xcode: Xcode(version: Version("12.0.0+1234A")!, identicalBuilds: [Version("12.0.0-RC+1234A")!], installState: .installed(Path("/Applications/Xcode-12.3.0.app")!), selected: false, icon: nil),
|
||||
xcode: Xcode(version: Version("12.0.0+1234A")!, identicalBuilds: [XcodeID(version: Version("12.0.0-RC+1234A")!)], installState: .installed(Path("/Applications/Xcode-12.3.0.app")!), selected: false, icon: nil),
|
||||
selected: false,
|
||||
appState: AppState()
|
||||
)
|
||||
|
|
|
|||
6
Xcodes/Resources/Assets.xcassets/Icons/Contents.json
Normal file
6
Xcodes/Resources/Assets.xcassets/Icons/Contents.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
12
Xcodes/Resources/Assets.xcassets/xcode-beta.imageset/Contents.json
vendored
Normal file
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
BIN
Xcodes/Resources/Assets.xcassets/xcode-beta.imageset/Image.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 82 KiB |
12
Xcodes/Resources/Assets.xcassets/xcode.imageset/Contents.json
vendored
Normal file
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
BIN
Xcodes/Resources/Assets.xcassets/xcode.imageset/xcode.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
|
|
@ -25,7 +25,7 @@
|
|||
<key>LSMinimumSystemVersion</key>
|
||||
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2022 Robots and Pencils.</string>
|
||||
<string>Copyright © 2024 XcodesOrg</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
<key>NSSupportsAutomaticTermination</key>
|
||||
|
|
@ -34,11 +34,11 @@
|
|||
<true/>
|
||||
<key>SMPrivilegedExecutables</key>
|
||||
<dict>
|
||||
<key>com.robotsandpencils.XcodesApp.Helper</key>
|
||||
<string>identifier "com.robotsandpencils.XcodesApp.Helper" and info [CFBundleShortVersionString] >= "1.0.0" and anchor apple generic and certificate leaf[subject.OU] = "$(CODE_SIGNING_SUBJECT_ORGANIZATIONAL_UNIT)"</string>
|
||||
<key>com.xcodesorg.xcodesapp.Helper</key>
|
||||
<string>identifier "com.xcodesorg.xcodesapp.Helper" and info [CFBundleShortVersionString] >= "1.0.0" and anchor apple generic and certificate leaf[subject.OU] = "$(CODE_SIGNING_SUBJECT_ORGANIZATIONAL_UNIT)"</string>
|
||||
</dict>
|
||||
<key>SUFeedURL</key>
|
||||
<string>https://robotsandpencils.github.io/XcodesApp/appcast.xml</string>
|
||||
<string>https://www.xcodes.app/appcast.xml</string>
|
||||
<key>SUPublicEDKey</key>
|
||||
<string>SEcz0vgUSeBTOoAXYe+64zea95G6lIf5NgzFs3InYJQ=</string>
|
||||
</dict>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,37 @@
|
|||
{\rtf1\ansi\ansicpg1252\cocoartf2639
|
||||
{\rtf1\ansi\ansicpg1252\cocoartf2865
|
||||
\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset0 .SFNS-Regular;}
|
||||
{\colortbl;\red255\green255\blue255;}
|
||||
{\*\expandedcolortbl;;}
|
||||
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\partightenfactor0
|
||||
|
||||
\f0\fs34 \cf0 SwiftSoup\
|
||||
\f0\fs34 \cf0 AsyncHTTPNetworkService\
|
||||
\
|
||||
|
||||
\fs26 MIT License\
|
||||
\
|
||||
Copyright (c) 2022 Robots and Pencils\
|
||||
\
|
||||
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 SwiftSoup\
|
||||
\
|
||||
|
||||
\fs26 MIT License\
|
||||
|
|
@ -31,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\
|
||||
\
|
||||
|
||||
|
|
@ -59,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\
|
||||
\
|
||||
|
||||
|
|
@ -308,6 +597,445 @@ For more information, please refer to <<http://unlicense.org/>>\
|
|||
otherwise be required by Sections 4(a), 4(b) and 4(d) of the License.\
|
||||
\
|
||||
|
||||
\fs34 swift-collections\
|
||||
\
|
||||
|
||||
\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.\
|
||||
\
|
||||
\
|
||||
\
|
||||
## Runtime Library Exception to the Apache 2.0 License: ##\
|
||||
\
|
||||
\
|
||||
As an exception, if you use this Software to compile your source code and\
|
||||
portions of this Software are embedded into the binary product as a result,\
|
||||
you may redistribute such product without providing attribution as would\
|
||||
otherwise be required by Sections 4(a), 4(b) and 4(d) of the License.\
|
||||
\
|
||||
\
|
||||
|
||||
\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> (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:\
|
||||
\
|
||||
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 KeychainAccess\
|
||||
\
|
||||
|
||||
|
|
@ -397,13 +1125,86 @@ EXTERNAL LICENSES\
|
|||
=================\
|
||||
\
|
||||
bspatch.c and bsdiff.c, from bsdiff 4.3 <http://www.daemonology.net/bsdiff/>:\
|
||||
Copyright (c) 2003-2005 Colin Percival.\
|
||||
\
|
||||
Copyright 2003-2005 Colin Percival\
|
||||
All rights reserved\
|
||||
\
|
||||
Redistribution and use in source and binary forms, with or without\
|
||||
modification, are permitted providing that the following conditions \
|
||||
are met:\
|
||||
1. Redistributions of source code must retain the above copyright\
|
||||
notice, this list of conditions and the following disclaimer.\
|
||||
2. Redistributions in binary form must reproduce the above copyright\
|
||||
notice, this list of conditions and the following disclaimer in the\
|
||||
documentation and/or other materials provided with the distribution.\
|
||||
\
|
||||
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR\
|
||||
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY\
|
||||
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS\
|
||||
OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)\
|
||||
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,\
|
||||
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING\
|
||||
IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\
|
||||
POSSIBILITY OF SUCH DAMAGE.\
|
||||
\
|
||||
--\
|
||||
\
|
||||
sais.c and sais.c, from sais-lite (2010/08/07) <https://sites.google.com/site/yuta256/sais>:\
|
||||
Copyright (c) 2008-2010 Yuta Mori.\
|
||||
\
|
||||
The sais-lite copyright is as follows:\
|
||||
\
|
||||
Copyright (c) 2008-2010 Yuta Mori All Rights Reserved.\
|
||||
\
|
||||
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.\
|
||||
\
|
||||
--\
|
||||
\
|
||||
Portable C implementation of Ed25519, from https://github.com/orlp/ed25519\
|
||||
\
|
||||
Copyright (c) 2015 Orson Peters <orsonpeters@gmail.com>\
|
||||
\
|
||||
This software is provided 'as-is', without any express or implied warranty. In no event will the\
|
||||
authors be held liable for any damages arising from the use of this software.\
|
||||
\
|
||||
Permission is granted to anyone to use this software for any purpose, including commercial\
|
||||
applications, and to alter it and redistribute it freely, subject to the following restrictions:\
|
||||
\
|
||||
1. The origin of this software must not be misrepresented; you must not claim that you wrote the\
|
||||
original software. If you use this software in a product, an acknowledgment in the product\
|
||||
documentation would be appreciated but is not required.\
|
||||
\
|
||||
2. Altered source versions must be plainly marked as such, and must not be misrepresented as\
|
||||
being the original software.\
|
||||
\
|
||||
3. This notice may not be removed or altered from any source distribution.\
|
||||
\
|
||||
--\
|
||||
\
|
||||
SUSignatureVerifier.m:\
|
||||
Copyright (c) 2011 Mark Hamlin.\
|
||||
\
|
||||
Copyright (c) 2011 Mark Hamlin.\
|
||||
\
|
||||
All rights reserved.\
|
||||
\
|
||||
|
|
|
|||
24980
Xcodes/Resources/Localizable.xcstrings
Normal file
24980
Xcodes/Resources/Localizable.xcstrings
Normal file
File diff suppressed because it is too large
Load diff
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue