Compare commits

...

622 commits

Author SHA1 Message Date
Matt Kiazyk
1a0d3353b9 update readme with v3 changes 2025-09-19 23:53:54 -05:00
Matt Kiazyk
da8d22c6d3 bad package.resolved 2025-09-19 19:56:41 -05:00
Matt Kiazyk
4f9b7f8f6a remove old libfido2swift fix 2025-09-19 19:31:16 -05:00
Matt Kiazyk
f34e291433 3.0.2 2025-09-19 19:20:34 -05:00
Matt Kiazyk
789eedcdb7 add some glass effect... why not? 2025-09-19 19:19:58 -05:00
Matt Kiazyk
f9262d9a05 fix for codesign issues with libfido2swift 2025-09-19 19:19:48 -05:00
Matt Kiazyk
4776af75ee 3.0.1 2025-09-17 23:01:03 -05:00
Matt Kiazyk
cab5533b49
Merge pull request #756 from neetrath/main
Add Thai translations
2025-09-17 22:55:26 -05:00
Matt Kiazyk
fc50db1cee fix how platforms are architecture filtered 2025-09-17 22:37:19 -05:00
Ruttanachai Auitragool
576585b7de feat: Add Thai translations 2025-09-16 23:22:44 +07:00
Matt Kiazyk
6cf9647840
Merge pull request #755 from XcodesOrg/newIcon
New Icon - who dis?
2025-09-16 10:43:43 -05:00
Matt Kiazyk
90dccd8180 new fancy app icon! 2025-09-15 23:08:51 -05:00
Matt Kiazyk
372d5b1f4e
Merge pull request #753 from XcodesOrg/matt/FixXcoded26Check
fix: xcode 26 check for Apple Silicon installs
2025-09-15 22:59:56 -05:00
Matt Kiazyk
4063796833
Merge pull request #727 from megabitsenmzq/main
Update zh_Hans translations.
2025-09-15 22:42:13 -05:00
Matt Kiazyk
be2eea0532
Merge pull request #746 from LiYanan2004/filter-ux
Improve filtering UI/UX on Xcode List
2025-09-15 22:41:32 -05:00
Matt Kiazyk
a6dec75603 fix libfido2 errors on Xcode26 thanks @anciltech 2025-09-15 22:34:19 -05:00
Matt Kiazyk
9527fb2961 fix: xcode 26 check for Apple Silicon installs 2025-09-15 22:08:49 -05:00
Matt Kiazyk
084bd8f4a1
Merge pull request #744 from leschlogl/feature/portuguese-translations
Fixes Brazilian Portuguese translations
2025-09-15 22:06:46 -05:00
Lucas Eduardo Schlögl
f93da1c502 small translation improvements 2025-09-10 14:43:20 +02:00
LiYanan2004
79ef494504 Add icon for "Installed Only" filter 2025-09-02 22:22:55 +08:00
LiYanan2004
cdf3675a5e Improve filtering UI 2025-09-02 14:40:35 +08:00
Jinyu Meng
5974a85800 Update zh_Hans translations.
# Conflicts:
#	Xcodes/Resources/Localizable.xcstrings
2025-08-29 11:45:06 +09:00
Matt Kiazyk
4ccb6e7f06
Merge pull request #741 from r1b2ns/feat/redirect-new-user-to-login
feat: improve user experience with a redirect window to sign-in
2025-08-27 08:37:19 -05:00
Rubens
22e4991f9a
chore: add new localized string in all languages 2025-08-27 10:32:20 -03:00
Lucas Eduardo Schlögl
059c51b5dd Fixes Brazilian Portuguese translations 2025-08-27 13:30:47 +02:00
Matt Kiazyk
00dcf3816e v3.0.0 2025-08-26 22:22:11 -05:00
Matt Kiazyk
08738d6912
Merge pull request #743 from XcodesOrg/XcodeArchitectures
Support Showing and Downloading Multiple Xcode Architectures.
2025-08-26 20:49:45 -05:00
Matt Kiazyk
472e36ed0f adjust names 2025-08-25 22:59:28 -05:00
Matt Kiazyk
bfb8c2cbb8 fix tests 2025-08-25 22:45:21 -05:00
Matt Kiazyk
a7b3f78813 clean up localizable strings 2025-08-25 22:37:51 -05:00
Matt Kiazyk
2e2b16e759 warning if users download silicon runtime without xcode 26 selected 2025-08-25 22:26:41 -05:00
Matt Kiazyk
0bc8e42a9b have both arch versions as the same version on list 2025-08-23 21:51:13 -06:00
Matt Kiazyk
ceae881d9a show installed xcodes specific with architectures 2025-08-23 20:11:57 -06:00
Matt Kiazyk
debc41f688 support downloading individual xcode architecture versions 2025-08-23 14:56:40 -06:00
Matt Kiazyk
4b9d86b22e Support showing multiple architectures 2025-08-23 13:35:49 -06:00
Rubens
bbec397613
feat: add button to redirect user when this try to install a xcode version without signed 2025-08-20 08:38:16 -03:00
Matt Kiazyk
a434d26921
Merge pull request #730 from XcodesOrg/matt/multipleArch
Support Xcode 26 multiple Architectures
2025-07-09 21:46:04 -05:00
Matt Kiazyk
78615418b5 update ci image 2025-07-09 21:40:33 -05:00
Matt Kiazyk
54cca28dbe clean up localizations 2025-07-09 21:36:23 -05:00
Matt Kiazyk
10ce2b8511 support filtering 2025-07-07 23:03:20 -05:00
Matt Kiazyk
14b2b3da10 support displaying multiple architectures in runtimes 2025-06-25 15:31:51 -05:00
Matt Kiazyk
f3f3bf0e4f remove cache 2025-06-09 23:04:33 -05:00
Matt Kiazyk
3621601658 update jekyll gem 2025-06-09 23:02:54 -05:00
Matt Kiazyk
9630a68224 update jekyll 2025-06-09 22:59:06 -05:00
Matt Kiazyk
227f42af37 v2.4.2b31 2025-06-09 22:21:44 -05:00
Matt Kiazyk
8fbf9eef8a
Merge pull request #678 from mahadshahib/patch-1
Update AppState.swift
2025-06-09 22:18:41 -05:00
Matt Kiazyk
b302365454
Merge pull request #655 from MultiColourPixel/main
Support FIDO2 authentication with devices that don’t have a PIN code
2025-06-09 22:10:12 -05:00
Mohammad ShahibZadeh
a61338804c
Update AppState.swift
Fix the issue where entering an email address with capital letters causes the login to fail.
2024-12-24 12:31:56 +03:30
Edgar Story
259ad0789a
Improve the support for PIN-less FIDO2 devices
- We now check if in the handling of two factor option, the option to be used is a SecurityKey. If so, check if a FIDO2 device is attached and if it needs a PIN.
- When a PIN is not required, we can just move straight onto assertation, the code for which will present the touch key UI.
- Otherwise we fallback to the original flow.
2024-11-12 09:20:38 +00:00
Edgar Story
cc03660576
Push the setting of authError to happen on MainActor
- The Xcode “Run Time Issue” breakpoint was being hit whenever an error was being set, complaining about this being set outside of the main thread.
2024-11-12 09:14:51 +00:00
Edgar Story
a43bf63aab
Add function to check if a FIDO2 device is even connected 2024-11-12 09:12:44 +00:00
Edgar Story
cfef2879b5
Add function to check if fido2 device needs a PIN 2024-11-12 09:12:21 +00:00
Edgar Story
36424a78e0
Make fido2 property a lazy var
- This object was being kept around after being created and as we need it in some other functions it made sense to make it lazy and keep it around that way.
- Arguably the FIDO2 instance could be removed after each time it’s been used, but as the FIDO2 class doesn’t have any state stored in it, it seems benign keeping it about for now.
2024-11-12 09:08:26 +00:00
Edgar Story
3d9cf73fc1
Require LibFido2Swift to be 0.1.4 at least
- This has the new functionality for checking if a device is attached.
2024-11-12 09:02:45 +00:00
Edgar Story
2dc1bcdcbb
Update requirement for LibFido2Swift library to be at least 0.1.3 2024-11-10 22:06:14 +00:00
Edgar Story
afa6dff0b9
Support FIDO authentication with devices that don’t have a PIN code set 2024-11-10 12:16:17 +00:00
Matt Kiazyk
17f3d365b8 v2.4.1b30 2024-11-04 21:47:10 -06:00
Matt Kiazyk
7b154501b6
Merge pull request #650 from kabiroberai/kabir/legacy-srp
Fix handling of s2k_fo
2024-11-04 21:41:13 -06:00
Kabir Oberai
8654756d67 comment 2024-11-04 21:21:12 -05:00
Kabir Oberai
85c9bdba55 Fix handling of s2k_fo 2024-11-04 21:14:48 -05:00
Matt Kiazyk
0a02b29a4b v2.4.0b29 2024-10-28 22:28:29 -05:00
Matt Kiazyk
3a8722c07d
Merge pull request #640 from XcodesOrg/matt/SRPLogin
Support SRP Login
2024-10-28 22:22:21 -05:00
Matt Kiazyk
29bf77007a
Merge pull request #638 from abiligiri/SRPLogin_fix
SRP Login works now
2024-10-28 22:02:04 -05:00
Matt Kiazyk
a75c54f2f6
Merge pull request #632 from abiligiri/feature/more_managed_preferences
Restrict allowed versions & hide 'Support Xcodes'
2024-10-28 22:01:37 -05:00
Matt Kiazyk
42c2c6bfc0 move to xcodesOrg/swift-srp fork 2024-10-28 21:45:07 -05:00
Matt Kiazyk
69e667cf87 some cleanup of extra prints and old code 2024-10-28 21:34:41 -05:00
Anand Biligiri
dc22b913f3 Use swift-srp from remote source
- Use from https://github.com/abiligiri/swift-srp, version 1.1.0
  This is based on latest from upstream with changes required
- Remove local copy of swift-srp
2024-10-28 16:52:25 -07:00
Anand Biligiri
9b107ec98c SRP Login works now
- Switch to use https://github.com/adam-fowler/swift-srp with some modifications
  that are local
  - Pad g value to equal size of N while calculating clientProof
- Use SHA256(plain-text-password) while computing key using PBKDF2
- Added a unit test with some sample values
2024-10-28 13:25:17 -07:00
Matt Kiazyk
2ed84ef792 clean up srp client, still not working 2024-10-24 19:34:26 -05:00
Matt Kiazyk
e04ed029de (wip) SRP Login implementation 2024-10-22 23:35:59 -05:00
Anand Biligiri
e3f996da6e Restrict allowed versions & hide 'Support Xcodes'
- Introduced a new preference keys allowedMajorVersions, hideSupportXcodes
- allowedMajorVersions defaults to Int.max (ie allow all versions till date)
- allowedMajorVersions is used to limit the number of major versions to as many as
    value set for this key. Eg: A value of 1 would allow the latest GA version and one major version before
    A value of 0 would allow only the latest GA version
    A value of 2 would allow the latest GA and previous two major versions
- allowedMajorVersions does not have preference UI
    $ defaults write com.xcodesorg.xcodesapp allowedMajorVersions 2 #to limit to current GA and previous major
    $ defaults delete com.xcodesorg.xcodesapp allowedMajorVersions  #to remove limits
- Display buildNumber in bottom status bar
2024-10-20 12:51:14 -07:00
Matt Kiazyk
aca4e0ac89 update readme 2024-10-18 09:55:36 -05:00
Matt Kiazyk
dd9a348298 Bump version 2.3 2024-10-18 08:54:38 -05:00
Matt Kiazyk
d6efd8fa74
Merge pull request #629 from XcodesOrg/dependabot/github_actions/ruby/setup-ruby-1.197.0
Bump ruby/setup-ruby from 1.190.0 to 1.197.0
2024-10-18 08:38:20 -05:00
Matt Kiazyk
b81eab0cc4
Merge pull request #625 from XcodesOrg/dependabot/github_actions/actions/cache-4.1.1
Bump actions/cache from 4.0.2 to 4.1.1
2024-10-18 08:38:09 -05:00
Matt Kiazyk
dc31fe884c
Merge pull request #628 from XcodesOrg/matt/runtimeDownloadCleanup
allow cancelling of runtime downloads, cleanup
2024-10-18 08:37:50 -05:00
Matt Kiazyk
283c1a4739
Merge pull request #621 from Kyle-Ye/bugfix/windows
Terminate Xcodes app after last window closed
2024-10-18 08:37:35 -05:00
dependabot[bot]
cac0ebfab7
Bump ruby/setup-ruby from 1.190.0 to 1.197.0
Bumps [ruby/setup-ruby](https://github.com/ruby/setup-ruby) from 1.190.0 to 1.197.0.
- [Release notes](https://github.com/ruby/setup-ruby/releases)
- [Changelog](https://github.com/ruby/setup-ruby/blob/master/release.rb)
- [Commits](https://github.com/ruby/setup-ruby/compare/v1.190.0...v1.197.0)

---
updated-dependencies:
- dependency-name: ruby/setup-ruby
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-18 10:50:01 +00:00
Kyle
59331bcb38
Adjust TerminateAfterLastWindowClosed toggle location
Move from advanced pane into general
2024-10-17 00:32:56 +08:00
Kyle
3d6908f526
Fix Xcode warning for Localizable string 2024-10-16 21:50:16 +08:00
Kyle
b91a7e53ad
Update the missing Localizable string 2024-10-16 21:46:04 +08:00
Kyle
f8970f44d5
Add TerminateAfterLastWindowClosed toggle support in Misc 2024-10-16 14:02:55 +08:00
Matt Kiazyk
6050c78613 allow cancelling of runtime downloads, cleanup 2024-10-15 22:07:53 -05:00
Matt Kiazyk
cf85e2fc5a
Merge pull request #569 from abiligiri/feature/managed_preferences
Disallow changes to managed preferences
2024-10-15 21:38:49 -05:00
Matt Kiazyk
15630109d5
Merge pull request #598 from duffpod/main
Update Localizable.xcstrings for Russian language (3)
2024-10-15 21:36:41 -05:00
dependabot[bot]
7171b90c88
Bump actions/cache from 4.0.2 to 4.1.1
Bumps [actions/cache](https://github.com/actions/cache) from 4.0.2 to 4.1.1.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v4.0.2...v4.1.1)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-14 10:16:07 +00:00
Kyle
3d5fdd528b
Replace WIndowGroup with Window to fix multi window and terminate issue 2024-10-13 12:11:50 +08:00
Matt Kiazyk
06558610e9
Merge pull request #593 from pan93412/l10n/update-zhtw-localization
Update translation in zh-Hant
2024-10-12 22:53:58 -05:00
Matt Kiazyk
5e5fdf0d3a
Merge pull request #609 from megabitsenmzq/main
Fix one Greek localization. Add one zh_Hans localization.
2024-10-12 22:53:08 -05:00
Matt Kiazyk
8277554048
Merge pull request #617 from kinoroy/security-key-auth
Implement Security Key Auth
2024-10-12 22:50:16 -05:00
Matt Kiazyk
ba11d4129e
Merge pull request #601 from XcodesOrg/dependabot/github_actions/ruby/setup-ruby-1.190.0
Bump ruby/setup-ruby from 1.177.1 to 1.190.0
2024-10-12 22:36:12 -05:00
Matt Kiazyk
389d8d2a63
Merge pull request #603 from LiYanan2004/context-menu-fine-tune
Update label for Cancel Install Button
2024-10-12 22:34:37 -05:00
Kino Roy
f4567bdf1e Merge branch 'main' into security-key-auth
# Conflicts:
#	Xcodes.xcodeproj/project.pbxproj
2024-10-12 20:30:34 -07:00
Kino Roy
27bbf6f2a3 Add missing localizations 2024-10-12 20:26:19 -07:00
Matt Kiazyk
c1670b250a
Merge pull request #624 from XcodesOrg/matt/updateSparkle
chore: update sparkle framework
2024-10-12 22:25:00 -05:00
Matt Kiazyk
8e78c1c8c5
Merge pull request #622 from XcodesOrg/matt/cryptexRuntimeDownloads
feat: support downloading of cryptex (ex iOS 18+) runtimes
2024-10-12 22:24:48 -05:00
Matt Kiazyk
c31a1ef6fa Merge branch 'main' into matt/cryptexRuntimeDownloads 2024-10-12 21:16:32 -05:00
Matt Kiazyk
37a86643b9 chore: update sparkle framework 2024-10-12 21:16:00 -05:00
Matt Kiazyk
c245a1e69f
Merge pull request #613 from chuganzy/ganzy/rollback-loading
Add `authenticating` to the `XcodeInstallationStep`
2024-10-12 21:15:07 -05:00
Matt Kiazyk
0f5e42b88e
Merge pull request #623 from XcodesOrg/matt/Xcode16
chore: set CI to MacOS 14, Xcode 16
2024-10-12 21:03:17 -05:00
Matt Kiazyk
dd46112769 chore: set CI to MacOS 14, Xcode 16 2024-10-12 20:57:39 -05:00
Kino Roy
c3e568e234 Add missing English localization 2024-10-12 14:13:02 -07:00
Kino Roy
f269dcbc98 Fix typo 2024-10-12 14:12:50 -07:00
Kino Roy
604358bf6c Update LibFido2Swift with Xcode 15 support 2024-10-12 14:06:48 -07:00
Takeru Chuganji
1a3ca60f46
Temporarily add all translations 2024-10-12 10:08:59 +09:00
Kyle
27d5b03321
Terminate Xcodes app after last window closed 2024-10-12 00:09:20 +08:00
Matt Kiazyk
20b9833b67 feat: support downloading of cryptex runtimes 2024-10-11 10:57:16 -05:00
Kino Roy
e855a1fb62 Implement security key auth 2024-09-28 16:25:11 -07:00
Takeru Chuganji
908e7ba3ea
Add authenticating step 2024-09-22 18:06:29 +09:00
Jinyu Meng
7daa3e600e Fix one Greek localization. Add one zh_Hans localization. 2024-09-10 18:09:59 +09:00
LiYanan2004
a936bf84b2 Update label of CancelInstallButton to better describe the intent 2024-08-17 11:37:05 +08:00
dependabot[bot]
897f449b7c
Bump ruby/setup-ruby from 1.177.1 to 1.190.0
Bumps [ruby/setup-ruby](https://github.com/ruby/setup-ruby) from 1.177.1 to 1.190.0.
- [Release notes](https://github.com/ruby/setup-ruby/releases)
- [Changelog](https://github.com/ruby/setup-ruby/blob/master/release.rb)
- [Commits](https://github.com/ruby/setup-ruby/compare/v1.177.1...v1.190.0)

---
updated-dependencies:
- dependency-name: ruby/setup-ruby
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-29 10:27:15 +00:00
duffpod
5ebe24060f Update Localizable.strings for Russian language (3) 2024-07-17 21:16:04 +03:00
pan93412
13496d8a7d
Update translation
Co-Authored-By: itszero <itszero@gmail.com>
2024-07-09 09:23:56 +08:00
Yi-Jyun Pan
4b6616fdea
Update translation in zh-Hant 2024-07-08 01:52:38 +08:00
Matt Kiazyk
74237f8fc1 v 2.2.0 2024-06-28 10:38:20 -05:00
Matt Kiazyk
c94c2c1979
Merge pull request #568 from floydkim/korean-localisation
Update Korean localisation
2024-06-28 10:36:12 -05:00
Matt Kiazyk
b36165100b
Merge pull request #585 from XcodesOrg/matt/iOS18Runtimes
Fix runtime downloading
2024-06-28 10:35:34 -05:00
Matt Kiazyk
8a6f5a7deb
Merge pull request #581 from Person2099/patch-1
fix: Update GitHub URLs in help menu
2024-06-28 10:33:32 -05:00
Matt Kiazyk
600d4cbafa
Merge pull request #576 from MohamediosDev/feature/add_arabic_localization_to_app
ADD: Added Arabic Localizations To Xcodes
2024-06-28 10:26:24 -05:00
Matt Kiazyk
8811a6d0d8
Merge pull request #564 from LePips/left-align-unarchiving-pane
Left Align Unarchiving Pane
2024-06-28 10:19:52 -05:00
Matt Kiazyk
f973e7d744
Merge pull request #545 from Kyle-Ye/bugfix/dock
Fix broken DockProgress Bar
2024-06-28 10:19:32 -05:00
Anand Biligiri
6e64db26fb Disallow changes to managed preferences
- Define enumerations for preferences that can be managed in an enterprise environment using MDM
- Add methods in AppState to check for managed preferences
- Update Advanced, Download, Experiments and Update preference panes to disable controls
  to modify any of the managed preferences
- Update Xcode category list button to be disabled if preference is managed
2024-06-21 07:17:18 +05:30
Seb
97bf958301
Update XcodesApp.swift
Tiny fix in help menu to use updated GitHub URLs. Changed "https://github.com/RobotsAndPencils/XcodesApp" to "https://github.com/XcodesOrg/XcodesApp"
2024-06-15 00:28:06 +10:00
MohamediosDev
dc5f637886 ADD: Added Arabic Localizations To Xcodes 2024-06-11 19:44:04 +03:00
Matt Kiazyk
a587d53798 don't fail downloading old runtime images 2024-06-10 16:18:36 -05:00
floydkim
a433bc4f72 Update Korean localisation 2024-06-03 21:16:56 +09:00
Ethan Pippin
66deeb0d57 left align unarchiving pane 2024-05-28 11:47:50 -06:00
Matt Kiazyk
4a4b469e3f
Merge pull request #522 from LiYanan2004/swift-5.10
Resolve concurrency check warning on Swift 5.10
2024-05-24 20:39:28 -05:00
Matt Kiazyk
7628f85ba8
Merge pull request #563 from XcodesOrg/dependabot/github_actions/ruby/setup-ruby-1.177.1
Bump ruby/setup-ruby from 1.173.0 to 1.177.1
2024-05-24 20:36:45 -05:00
Matt Kiazyk
f64e97f7a2
Merge pull request #560 from mikeirvingweb/png-compression
chore: lossless png compression
2024-05-24 20:36:35 -05:00
Matt Kiazyk
1152bbfb28
Merge pull request #548 from alladinian/alladinian-readme-greek
Update README.md
2024-05-24 20:34:42 -05:00
Matt Kiazyk
33dc6347f2 after last step reset dock progress 2024-05-24 20:23:03 -05:00
Matt Kiazyk
7599178042
Merge pull request #559 from mikeirvingweb/product-name-casings
fix: product name casings
2024-05-24 16:14:37 -05:00
Matt Kiazyk
6fda52ed37
Merge pull request #547 from roddymunro/add-swiftpolyglot-version
Set explicit SwiftPolyglot tag and update clone path
2024-05-24 16:13:39 -05:00
dependabot[bot]
8f1f6e9363
Bump ruby/setup-ruby from 1.173.0 to 1.177.1
Bumps [ruby/setup-ruby](https://github.com/ruby/setup-ruby) from 1.173.0 to 1.177.1.
- [Release notes](https://github.com/ruby/setup-ruby/releases)
- [Commits](https://github.com/ruby/setup-ruby/compare/v1.173.0...v1.177.1)

---
updated-dependencies:
- dependency-name: ruby/setup-ruby
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-23 10:30:04 +00:00
Mike Irving
848ce1d362 fix: rollback icon.png 2024-05-22 21:58:33 +01:00
Mike Irving
38ae157be5 fix: rollback A&P, App Icon, Beta Icon 2024-05-22 21:57:16 +01:00
Mike Irving
7f3e8125c0 chore: lossless png compression 2024-05-14 22:40:07 +01:00
Mike Irving
7f14a4b8b5
fix: product name casings 2024-05-14 22:18:30 +01:00
Roddy Munro
cd80053be6 Move branch argument before url 2024-05-09 19:03:56 -03:00
Vasilis Akoinoglou
021d0aa745
Update README.md
Added Greek localization to the language support table
2024-04-05 10:26:05 +03:00
Roddy Munro
180dd52eee Set explicit SwiftPolyglot tag and update clone path 2024-04-04 21:27:15 -03:00
Kyle
348c89871e
Fix broken DockProgress Bar
Update DockProgress and adopt the new Concurrency Model
2024-03-31 01:44:34 +08:00
Matt Kiazyk
21c26c9793
Merge pull request #542 from XcodesOrg/dependabot/github_actions/ruby/setup-ruby-1.173.0
Bump ruby/setup-ruby from 1.172.0 to 1.173.0
2024-03-22 10:15:28 -05:00
Matt Kiazyk
4e177b5fbf
Merge pull request #543 from XcodesOrg/dependabot/github_actions/actions/cache-4.0.2
Bump actions/cache from 4.0.1 to 4.0.2
2024-03-22 10:15:18 -05:00
Matt Kiazyk
411f76788c
Merge pull request #533 from XcodesOrg/matt/testLocalizationValidation
adds localization validation on PR's
2024-03-22 10:15:05 -05:00
Matt Kiazyk
88b15b989a Update missing translations. 2024-03-22 10:09:47 -05:00
Matt Kiazyk
45626e2789
Merge pull request #540 from marlonjames71/InfoPaneTweaks
InfoPane UI Tweaks
2024-03-22 09:57:20 -05:00
dependabot[bot]
1411bd30b3
Bump actions/cache from 4.0.1 to 4.0.2
Bumps [actions/cache](https://github.com/actions/cache) from 4.0.1 to 4.0.2.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v4.0.1...v4.0.2)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-20 10:42:25 +00:00
dependabot[bot]
f3472d8cc6
Bump ruby/setup-ruby from 1.172.0 to 1.173.0
Bumps [ruby/setup-ruby](https://github.com/ruby/setup-ruby) from 1.172.0 to 1.173.0.
- [Release notes](https://github.com/ruby/setup-ruby/releases)
- [Commits](https://github.com/ruby/setup-ruby/compare/v1.172.0...v1.173.0)

---
updated-dependencies:
- dependency-name: ruby/setup-ruby
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-20 10:42:22 +00:00
Marlon
b9d37054ea
Reduces size of screenshots even more 2024-03-20 02:06:52 -04:00
Marlon
48df320aa8
Reduces size of screenshots 2024-03-20 02:02:49 -04:00
Marlon
feac373350
Updates screenshots for README 2024-03-19 23:35:45 -04:00
Marlon
9e1eff06a3
InfoPane now responsible for its padding
- Uses content margins instead of padding when it can to avoid content clipping during scrolling
- Also fixes a few indentation inconsistencies
- Adds min width for platforms section
2024-03-19 22:51:57 -04:00
Marlon
35314ce36f
Adjusts layout of platform views 2024-03-19 22:45:51 -04:00
Matt Kiazyk
9d73cd2091 v 2.1.2 2024-03-17 09:14:19 -05:00
Matt Kiazyk
91b24fc162
Merge pull request #538 from vvisionnn/main
fix: xcode list row text hidden on macOS 14.4
2024-03-17 09:13:03 -05:00
Ydna
b3fa08b43e fix: xcode list row text hidden 2024-03-17 20:41:33 +08:00
Matt Kiazyk
c417978976
Update xcstrings.yml 2024-03-16 21:18:32 -05:00
Matt Kiazyk
aa34f3b10d allow workflow dispatch 2024-03-16 21:16:44 -05:00
Matt Kiazyk
c595eba5df Test localizable.xcstrings Validation action 2024-03-16 21:12:46 -05:00
Matt Kiazyk
61bee6c84f 2.1.1 - Rebuild because of bad public key 2024-03-16 20:39:04 -05:00
Matt Kiazyk
0aefecf9bf Make sure SUDPublicKey is the right one! 2024-03-16 20:38:20 -05:00
Matt Kiazyk
acaafaeef5 bump version 2.1.0 2024-03-16 19:33:35 -05:00
Matt Kiazyk
7bdb227f3f
Merge pull request #532 from XcodesOrg/matt/xcstrings-cleanup
chore: cleanup some localizeable.xcstrings
2024-03-16 19:25:26 -05:00
Matt Kiazyk
7cf2c6f244 chore: cleanup some localizeable.xcstrings 2024-03-16 19:25:10 -05:00
Matt Kiazyk
b17fbf56a8
Merge pull request #531 from huihuisang/fix/AttributedText-layout-issue
Replace AttributedText with Text and markdown to fix the layout issues
2024-03-16 19:02:41 -05:00
Matt Kiazyk
dbf30000ca
Merge pull request #530 from huihuisang/feature/replace-default-icon-in-info
replace default icon in info page
2024-03-16 18:52:11 -05:00
Matt Kiazyk
7359ae19ae
Merge pull request #527 from chickdan/remove_outdated_version_checks
Remove outdated version checks
2024-03-16 18:41:33 -05:00
Matt Kiazyk
c09b56086b
Merge pull request #519 from Schr0eder/update-german-localization
Updated german localization
2024-03-16 18:40:51 -05:00
huihuisang
b5e50c6eed replace AttributedText with Text to fix layout issue in preference 2024-03-16 22:42:13 +08:00
huihuisang
f4c9a6d683 replace default icon in info page 2024-03-16 19:21:52 +08:00
LiYanan2004
9a5e127387 Resolve warning on Swift 5.10 2024-03-16 10:28:16 +08:00
Daniel Chick
238f974ce3 Merge branch 'main' into remove_outdated_version_checks
# Conflicts:
#	Xcodes/Frontend/Common/NavigationSplitViewWrapper.swift
2024-03-15 13:36:24 -05:00
Matt Kiazyk
b89fc21b8b
Merge pull request #526 from chickdan/trailing_icon_release_filter
Trailing Icon for Filter Button
2024-03-15 11:44:10 -05:00
Matt Kiazyk
bf0dc9d855
Merge pull request #504 from huihuisang/main
Improve the layout of the sidebar
2024-03-15 11:23:00 -05:00
Daniel Chick
74d8f395a5 Remove comment 2024-03-11 21:54:20 -05:00
Daniel Chick
4394c48e11 Remove outdated version checks 2024-03-11 21:51:35 -05:00
Daniel Chick
f62c5139f0 Correct inconsistency in minimum supported version 2024-03-11 21:51:12 -05:00
Daniel Chick
6cd8584058 Replace label style with new trailing icon 2024-03-11 21:30:39 -05:00
Daniel Chick
96e4682c56 Add Label style with trailing icon 2024-03-11 21:29:11 -05:00
Schr0eder
f58fced154 merge conflict adaptions 2024-03-11 19:04:29 +01:00
Schr0eder
fd6baa81c4 Merge remote-tracking branch 'upstream/main' into update-german-localization 2024-03-11 18:57:44 +01:00
huihuisang
9a866acf53 Place PlatformsPocket back to XcodeListView 2024-03-10 15:35:37 +08:00
Matt Kiazyk
6f432eef7f
Merge pull request #509 from XcodesOrg/dependabot/github_actions/ruby/setup-ruby-1.172.0
Bump ruby/setup-ruby from 1.171.0 to 1.172.0
2024-03-08 14:49:14 -06:00
Matt Kiazyk
5632820780
Merge pull request #515 from XcodesOrg/dependabot/github_actions/actions/cache-4.0.1
Bump actions/cache from 4.0.0 to 4.0.1
2024-03-08 14:49:05 -06:00
Matt Kiazyk
5993385a73
Merge pull request #521 from LiYanan2004/main
Improve signing experience when using non-english keyboard
2024-03-08 14:48:50 -06:00
Matt Kiazyk
238ac71517
Merge pull request #523 from XcodesOrg/fix-missing-runtime
fix runtime not appearing when multiple runtimes exist for same build
2024-03-08 14:46:45 -06:00
Matt Kiazyk
e9a95a3e63
Merge pull request #502 from pan93412/l10n/zh-TW/230210
l10n(zh-Hant): Update translation for 2.0.2
2024-03-08 14:26:50 -06:00
Matt Kiazyk
21e2a10eec
Merge pull request #506 from alladinian/feature/Settings-window-layout
Improvements in the layout of the Settings window
2024-03-08 14:21:56 -06:00
Matt Kiazyk
bf4650edcb
Merge pull request #510 from kikiwora/update-ukrainian-localisation-for-2.0.2
Update Ukrainian localisation
2024-03-08 14:07:42 -06:00
Matt Kiazyk
14f00a0e09
Merge pull request #505 from alladinian/feature/Greek-localization
Greek language localization
2024-03-08 13:51:15 -06:00
Matt Kiazyk
d7dc67c046
Merge pull request #503 from clementpadovani/clementpadovani/info_pane/allow_selection
Allow selecting text for version info
2024-03-08 13:34:25 -06:00
Matt Kiazyk
8534e2850b fix runtime not appearing when multiple runtimes exist for same build 2024-03-08 12:16:20 -06:00
LiYanan2004
5054ead2ca Improve signing experience when using non-english keyboard 2024-03-08 12:07:06 +08:00
Schr0eder
dc691ad38c grammatical correction 2024-03-07 19:54:37 +01:00
Schr0eder
4ba5d31427 updated german localization 2024-03-07 19:46:23 +01:00
dependabot[bot]
791ae598a3
Bump actions/cache from 4.0.0 to 4.0.1
Bumps [actions/cache](https://github.com/actions/cache) from 4.0.0 to 4.0.1.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v4.0.0...v4.0.1)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-01 10:56:02 +00:00
huihuisang
950045b427 add icon for Xcode beta version 2024-02-29 09:04:28 +08:00
Roman Suvorov
34d7f6834e
Update Ukrainian localisation 2024-02-21 21:18:08 +02:00
dependabot[bot]
cc9d5e9e16
Bump ruby/setup-ruby from 1.171.0 to 1.172.0
Bumps [ruby/setup-ruby](https://github.com/ruby/setup-ruby) from 1.171.0 to 1.172.0.
- [Release notes](https://github.com/ruby/setup-ruby/releases)
- [Commits](https://github.com/ruby/setup-ruby/compare/v1.171.0...v1.172.0)

---
updated-dependencies:
- dependency-name: ruby/setup-ruby
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-21 10:58:22 +00:00
Vasilis Akoinoglou
b4e598fc49 Layout adjustments for the Settings window 2024-02-20 13:14:20 +02:00
Vasilis Akoinoglou
6b6088d58d Greek language localization 2024-02-20 12:06:15 +02:00
huihuisang
e5c4fd431a Update NavigationSplitViewWrapper.swift
update mini width of sidebar
2024-02-18 19:21:27 +08:00
huihuisang
ae55e0fb15
Merge branch 'main' into main 2024-02-18 19:13:18 +08:00
pan93412
c7f9e54796
Merge branch 'main' into l10n/zh-TW/230210 2024-02-18 03:53:41 -06:00
huihuisang
e520a96021 Improve the layout of the sidebar 2024-02-18 13:52:32 +08:00
Clément Padovani
53ec9156ff allow selecting text from the compilers 2024-02-17 18:39:10 +01:00
Clément Padovani
8d7412fd18 allow selecting text from the SDKs 2024-02-17 18:39:04 +01:00
Clément Padovani
5aed44f969 Allow selecting text from the info panel 2024-02-17 18:37:32 +01:00
Matt Kiazyk
91f75843da
Merge pull request #499 from XcodesOrg/matt/498-fix-nav-close-stucks
fix: adds back in sidebar toggle button so users don't get stuck
2024-02-16 09:09:01 -06:00
Matt Kiazyk
6f0cdc72ec
Merge pull request #482 from megabitsenmzq/main
Update zh_Hans Localizations. And more.
2024-02-16 09:08:25 -06:00
Jinyu Meng
49eb2f4cf2 Update localizations to remove old items and add "Support Xcodes". 2024-02-10 15:41:29 +09:00
Jinyu Meng
8a7597e70b Update more files. 2024-02-10 15:36:07 +09:00
Jinyu Meng
144c591a41 Use Text(verbatim:) instead. 2024-02-10 15:36:07 +09:00
Jinyu Meng
0e76a18168 Remove more useless localizations. 2024-02-10 15:36:07 +09:00
Jinyu Meng
a078b8f7d0 Update zh_Hans localizations. Remove old or useless items. 2024-02-10 15:36:07 +09:00
Jinyu Meng
a00dae9b0a Create an extension for hiding strings from localizations. 2024-02-10 15:34:31 +09:00
pan93412
c4bd1d2789
l10n(zh-Hant): Update translation for 2.0.2 2024-02-10 00:25:47 +08:00
Matt Kiazyk
ab1ebe016f
Merge pull request #497 from XcodesOrg/dependabot/github_actions/release-drafter/release-drafter-6
Bump release-drafter/release-drafter from 5 to 6
2024-02-06 22:08:03 -06:00
Matt Kiazyk
9cb4f243b0 fix: adds back in sidebar toggle button so users don't get stuck 2024-02-06 22:05:54 -06:00
dependabot[bot]
c534d2494d
Bump release-drafter/release-drafter from 5 to 6
Bumps [release-drafter/release-drafter](https://github.com/release-drafter/release-drafter) from 5 to 6.
- [Release notes](https://github.com/release-drafter/release-drafter/releases)
- [Commits](https://github.com/release-drafter/release-drafter/compare/v5...v6)

---
updated-dependencies:
- dependency-name: release-drafter/release-drafter
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-02 10:07:35 +00:00
Matt Kiazyk
19808c58e4
Merge pull request #495 from XcodesOrg/dependabot/github_actions/ruby/setup-ruby-1.171.0
Bump ruby/setup-ruby from 1.169.0 to 1.171.0
2024-02-01 21:49:52 -06:00
dependabot[bot]
b1c986af3e
Bump ruby/setup-ruby from 1.169.0 to 1.171.0
Bumps [ruby/setup-ruby](https://github.com/ruby/setup-ruby) from 1.169.0 to 1.171.0.
- [Release notes](https://github.com/ruby/setup-ruby/releases)
- [Commits](https://github.com/ruby/setup-ruby/compare/v1.169.0...v1.171.0)

---
updated-dependencies:
- dependency-name: ruby/setup-ruby
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-01 10:19:27 +00:00
Matt Kiazyk
2a9227a0d5
Add new openCollective link to readme 2024-01-26 10:24:27 -06:00
Matt Kiazyk
71ed9a3617 Update readme 2024-01-25 23:27:26 -06:00
Matt Kiazyk
da6bfc775c update to 2.0.2 2024-01-25 22:50:56 -06:00
Matt Kiazyk
66a60fcf19 missed code sign identity? 2024-01-25 22:50:23 -06:00
Matt Kiazyk
0ad800a5db Guess we have to set the code sign identity now? 2024-01-25 22:10:52 -06:00
Matt Kiazyk
9d7f122d11 Bump version: 2.0 Release! 2024-01-25 22:04:11 -06:00
Matt Kiazyk
0252bf1fe0 clean up localization for support link 2024-01-25 22:01:40 -06:00
Matt Kiazyk
36be5fd743 Bump version 2024-01-23 22:55:59 -06:00
Matt Kiazyk
2418d22f71
Merge pull request #472 from robinkunde/rk/fixDerivedDataPath
Remove newline from derived data path returned by defaults
2024-01-23 22:12:41 -06:00
Matt Kiazyk
fb7e1f65c1
Merge pull request #480 from XcodesOrg/matt/privilegedHelperUpdate
Update privilege helper to xcodesOrg and Team
2024-01-23 22:04:07 -06:00
Matt Kiazyk
8a0595adc9
Merge pull request #484 from XcodesOrg/dependabot/github_actions/ruby/setup-ruby-1.169.0
Bump ruby/setup-ruby from 1.165.1 to 1.169.0
2024-01-23 22:00:49 -06:00
Matt Kiazyk
7686620b1e
Merge pull request #481 from XcodesOrg/matt/sponsorLink
Add Sponsor/Donate Button
2024-01-23 21:58:45 -06:00
dependabot[bot]
f1604538f3
Bump ruby/setup-ruby from 1.165.1 to 1.169.0
Bumps [ruby/setup-ruby](https://github.com/ruby/setup-ruby) from 1.165.1 to 1.169.0.
- [Release notes](https://github.com/ruby/setup-ruby/releases)
- [Commits](https://github.com/ruby/setup-ruby/compare/v1.165.1...v1.169.0)

---
updated-dependencies:
- dependency-name: ruby/setup-ruby
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-22 10:37:03 +00:00
Matt Kiazyk
e4d21ac18b
Merge pull request #477 from McNight/mcnight/new_french_translations
Add latest missing french translations
2024-01-19 16:17:38 -06:00
Matt Kiazyk
99c4b49f28 Add Sponsor Button 2024-01-19 15:36:18 -06:00
Matt Kiazyk
9ec90a3ca8 Update privilege helper to xcodesOrg and Team 2024-01-19 14:28:12 -06:00
Matt Kiazyk
159f1caa5c
Merge pull request #475 from XcodesOrg/dependabot/github_actions/actions/cache-4.0.0
Bump actions/cache from 3.3.2 to 4.0.0
2024-01-19 13:00:40 -06:00
Matt Kiazyk
0dc13d8eee
Merge pull request #470 from chickdan/fix_missing_platform_list_alert
Handle preference alerts from the PlatformsListView
2024-01-19 12:58:44 -06:00
Matt Kiazyk
dede435725 update ruby version for appcast action 2024-01-19 12:04:41 -06:00
Matt Kiazyk
4acd676f69 Bump and fix AppCast action 2024-01-19 11:59:19 -06:00
Matt Kiazyk
dada7f2716 2.0.0 beta 2024-01-19 11:18:45 -06:00
McNight
e1ea64c647 Add latest missing french translations 2024-01-17 22:48:35 +01:00
dependabot[bot]
1cf4381c3a
Bump actions/cache from 3.3.2 to 4.0.0
Bumps [actions/cache](https://github.com/actions/cache) from 3.3.2 to 4.0.0.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v3.3.2...v4.0.0)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-17 10:39:41 +00:00
Robin Kunde
111f08fdcf Remove newline from derived data path returned by defaults 2024-01-10 13:44:02 -05:00
Daniel Chick
4d95a270ae Handle preference alerts from the PlatformsListView 2024-01-04 22:01:18 -06:00
Matt Kiazyk
2184c872d3
Merge pull request #469 from XcodesOrg/matt/SparkleUpdate
update Sparkle to 2.5.2
2024-01-02 22:27:51 -06:00
Matt Kiazyk
7aaf0563e6 update Sparkle to 2.5.2 2024-01-02 22:19:31 -06:00
Matt Kiazyk
efcf6508f5
Merge pull request #463 from XcodesOrg/matt/platformsList
Add new Platforms List Window
2024-01-01 20:01:33 -06:00
Matt Kiazyk
39f39212f6
Merge pull request #468 from XcodesOrg/dependabot/github_actions/ruby/setup-ruby-1.165.1
Bump ruby/setup-ruby from 1.162.0 to 1.165.1
2024-01-01 20:01:17 -06:00
Matt Kiazyk
7167c40072 Tests to MacOS 13 2024-01-01 19:55:45 -06:00
Matt Kiazyk
79c1f67811 small adjustments 2024-01-01 19:52:34 -06:00
Matt Kiazyk
805f754bc8 Move platforms list view to it's own window, outside of settings 2024-01-01 19:46:27 -06:00
dependabot[bot]
48e9b1568e
Bump ruby/setup-ruby from 1.162.0 to 1.165.1
Bumps [ruby/setup-ruby](https://github.com/ruby/setup-ruby) from 1.162.0 to 1.165.1.
- [Release notes](https://github.com/ruby/setup-ruby/releases)
- [Commits](https://github.com/ruby/setup-ruby/compare/v1.162.0...v1.165.1)

---
updated-dependencies:
- dependency-name: ruby/setup-ruby
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-29 10:39:05 +00:00
Matt Kiazyk
b968149235 Add new platforms list in settings 2023-12-20 12:10:13 -06:00
Matt Kiazyk
3c5f86023e
Merge pull request #461 from XcodesOrg/matt/redesign
Redesign the main window to better suit more data
2023-12-20 08:56:45 -06:00
Matt Kiazyk
c85b3c2b6c
Merge pull request #462 from redryerye/feat/preselect-phonenumber
Add default phone number selection when there is only one
2023-12-20 08:28:57 -06:00
Yuki Yamamoto
36a9bd267b
add default phone number selection when there is only one 2023-12-20 00:39:03 +01:00
Matt Kiazyk
cd69ddc40e Cleanup 2023-12-19 15:13:11 -06:00
Matt Kiazyk
ede9bd1c46 redesigned infoPane 2023-12-19 14:36:34 -06:00
Matt Kiazyk
7070575a32 Switch to NavigationSplitView 2023-12-18 09:00:10 -06:00
Matt Kiazyk
638543070b
Merge pull request #459 from XcodesOrg/dependabot/github_actions/ruby/setup-ruby-1.162.0
Bump ruby/setup-ruby from 1.161.0 to 1.162.0
2023-12-12 21:30:47 -06:00
Matt Kiazyk
98b986a30d
Merge pull request #456 from chickdan/improve_412_code_entry
Descriptive error for login status code 412
2023-12-12 21:30:17 -06:00
Matt Kiazyk
f25fc3923e
Merge pull request #455 from egesucu/update-readme-xcode-version
Updated README.md
2023-12-12 21:29:25 -06:00
Matt Kiazyk
487bd2d1dd
Merge pull request #454 from egesucu/turkish-translation-improvement
Updated Turkish to 100%
2023-12-12 21:28:53 -06:00
Matt Kiazyk
0884f85359
Merge pull request #437 from chickdan/install_activity_indicator
Activity Indicator on Install Pressed
2023-12-12 21:28:11 -06:00
dependabot[bot]
78a62c4eba
Bump ruby/setup-ruby from 1.161.0 to 1.162.0
Bumps [ruby/setup-ruby](https://github.com/ruby/setup-ruby) from 1.161.0 to 1.162.0.
- [Release notes](https://github.com/ruby/setup-ruby/releases)
- [Commits](https://github.com/ruby/setup-ruby/compare/v1.161.0...v1.162.0)

---
updated-dependencies:
- dependency-name: ruby/setup-ruby
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-12 10:57:26 +00:00
Daniel Chick
c0cff4822c If status code 412 is returned from code entry change error to describe acknowledgement required 2023-12-05 18:12:57 -06:00
Daniel Chick
9622b563b3 Move button styling to not affect the info pane 2023-12-05 17:20:41 -06:00
Ege Sucu
2d3a26374b Updated README.md
Since Strings Catalog has been added, Xcode 15 is required to work with them.
2023-12-03 08:44:00 +03:00
Ege Sucu
eda3e2680b More translations are updated after the rebase 2023-12-03 08:37:18 +03:00
Matt Kiazyk
0d631e6628
Merge pull request #453 from XcodesOrg/matt/cancelRuntimeDownload
Add ability to cancel runtime downloads
2023-12-02 22:32:55 -06:00
Matt Kiazyk
1f032a5719 Fix localization from rebase 2023-12-02 09:30:38 -06:00
Matt Kiazyk
c81a638e35 Merge branch 'main' into matt/cancelRuntimeDownload 2023-12-02 09:27:52 -06:00
Matt Kiazyk
b3e91fad7a
Merge pull request #449 from egesucu/string-catalog-conversion
String Catalog Conversion
2023-12-02 09:26:32 -06:00
Matt Kiazyk
57bf6d8a80 add ability to cancel runtime downloads 2023-12-02 09:24:54 -06:00
Ege Sucu
7690c19115 Localization file updated 2023-12-01 20:29:13 +03:00
Ege Sucu
ad749c1cb0 Converted Localization into String Catalog 2023-12-01 20:29:13 +03:00
Matt Kiazyk
c5ada02a31
Merge pull request #448 from XcodesOrg/matt/runtimeDownload
Support Runtime/Platforms Downloading and Install 🚀
2023-12-01 10:06:45 -06:00
Matt Kiazyk
c1836a78c4 PR Cleanup. 2023-12-01 10:05:36 -06:00
Daniel Chick
4649ba0c8a Reuse ProgressButton 2023-11-25 19:19:59 -06:00
Matt Kiazyk
c153a93b1b adjustments to update runtimes 2023-11-23 14:41:56 -06:00
Matt Kiazyk
f470986e2b Merge branch 'main' into matt/runtimeDownload 2023-11-23 14:16:43 -06:00
Matt Kiazyk
b650261ddd
Merge pull request #436 from thai-d-v/fix-app-state
Clean up code in `InfoPane` to be more robust
2023-11-23 14:07:03 -06:00
Matt Kiazyk
ec4dc2b3d0 runtime clean up 2023-11-23 13:30:59 -06:00
Duong Thai
a596e5ff6c refactor preview code 2023-11-24 00:15:41 +07:00
Matt Kiazyk
6ffce23616 more WIP 2023-11-23 10:37:41 -06:00
Duong Thai
2fdc06031c fix SDKView logic, hide the view when the content is empty 2023-11-23 23:04:16 +07:00
Duong Thai
2f7d45c67a move the control section to InfoPaneControls, fix the layout to make it self-contain 2023-11-23 23:04:14 +07:00
Duong Thai
a2379583ef fix data dependency, preview performance and layout out of InfoPane
- I move `UnselectedView` out to:
        1. narrow data dependency
        2. `InfoPane` should only care about displaying data, not whether an `Xcode` is selected
2023-11-23 23:03:01 +07:00
Duong Thai
4e55aac679 clean up code 2023-11-23 23:01:03 +07:00
Duong Thai
d406972e81 move and fix the InstalledStateButtons 2023-11-23 23:00:10 +07:00
Duong Thai
9e787f98d8 fix layout of UnselectedView 2023-11-23 22:58:53 +07:00
Duong Thai
d172df68e3 move and fix the NotInstalledStateButtonsView 2023-11-23 22:58:49 +07:00
Duong Thai
d08d2d38e4 move and fix the UnselectedView
rebase main
2023-11-23 22:56:18 +07:00
Duong Thai
112829b553 rebase main 2023-11-23 22:53:15 +07:00
Duong Thai
05f5c2cebf rebase main 2023-11-23 22:49:10 +07:00
Duong Thai
cecceea62f remove openURL in InfoPane because ReleaseNotesView already has it 2023-11-23 22:46:22 +07:00
Duong Thai
77ca8a2571 move the compatibility view to CompatibilityView.swift 2023-11-23 22:46:22 +07:00
Duong Thai
f1fea6365c move release notes to ReleaseNotesView.swift
- narrow down interfaces of some functions
2023-11-23 22:46:22 +07:00
Duong Thai
03eb166c3d move icon to IconView.swift 2023-11-23 22:46:22 +07:00
Duong Thai
e56d83dbe3 move identical builds view to IdenticalBuildView.swift 2023-11-23 22:46:22 +07:00
Duong Thai
7079e062d2 move release date view to ReleaseDateView.swift
- I moved outside 2 lines of `.frame(maxWidth: .infinity, alignment: .leading)` because this layout job should belong to `InfoPane`
    - I added preview blocks for testing
2023-11-23 22:46:22 +07:00
Matt Kiazyk
487cbb0045 Merge branch 'main' into matt/runtimeDownload 2023-11-22 13:26:45 -06:00
Matt Kiazyk
5b18a90f81
Merge pull request #444 from XcodesOrg/matt/SettingsLinkSonoma
fix: settings link on Sonoma not working
2023-11-22 08:49:27 -06:00
Matt Kiazyk
b9dc7bf757 update runner to MacOS13, Xcode 15 2023-11-22 08:38:10 -06:00
Matt Kiazyk
1bd5017350
Merge pull request #447 from XcodesOrg/dependabot/github_actions/actions/cache-3.3.2
Bump actions/cache from 3.3.1 to 3.3.2
2023-11-22 08:33:54 -06:00
Matt Kiazyk
ab4a3444ed
Merge pull request #446 from XcodesOrg/dependabot/github_actions/ruby/setup-ruby-1.161.0
Bump ruby/setup-ruby from 1.152.0 to 1.161.0
2023-11-22 08:33:45 -06:00
Matt Kiazyk
8ff65381e6
Merge pull request #445 from XcodesOrg/dependabot/github_actions/actions/checkout-4
Bump actions/checkout from 3 to 4
2023-11-22 08:33:35 -06:00
dependabot[bot]
ca363e1398
Bump actions/cache from 3.3.1 to 3.3.2
Bumps [actions/cache](https://github.com/actions/cache) from 3.3.1 to 3.3.2.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v3.3.1...v3.3.2)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-22 10:43:13 +00:00
dependabot[bot]
d9917ce2c4
Bump ruby/setup-ruby from 1.152.0 to 1.161.0
Bumps [ruby/setup-ruby](https://github.com/ruby/setup-ruby) from 1.152.0 to 1.161.0.
- [Release notes](https://github.com/ruby/setup-ruby/releases)
- [Commits](https://github.com/ruby/setup-ruby/compare/v1.152.0...v1.161.0)

---
updated-dependencies:
- dependency-name: ruby/setup-ruby
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-22 10:43:08 +00:00
dependabot[bot]
44a85d63db
Bump actions/checkout from 3 to 4
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-22 10:43:03 +00:00
Matt Kiazyk
19f1fd876d fix: settings link on Sonoma not working 2023-11-21 23:50:25 -06:00
Matt Kiazyk
75ae0ab456
Merge pull request #443 from XcodesOrg/matt/projectTeamCleanup
cleans up project team so it builds
2023-11-21 23:47:04 -06:00
Matt Kiazyk
2f749deb8d cleans up project team so it builds 2023-11-21 23:46:23 -06:00
Matt Kiazyk
6fcb4aa857
Merge pull request #428 from ElonPark/bug/elon/fix-send-sms-crash
Fix SMS Enter Digit Code Crash
2023-11-21 23:15:21 -06:00
Matt Kiazyk
5edeb60835
Merge pull request #423 from senmu/sam/dockProgress
Add installation progress bar to dock icon
2023-11-21 23:14:12 -06:00
Matt Kiazyk
47710b6bed
Merge pull request #400 from LavareX/support_visionOS
SDK column in the info panel supports displaying visionOS
2023-11-21 23:00:15 -06:00
Matt Kiazyk
330f590d8e
Merge pull request #398 from Kyle-Ye/feature/autocomplete
Add PinCodeTextField autocomplete support
2023-11-21 22:51:16 -06:00
Matt Kiazyk
b8c6a7cda8
Merge pull request #397 from XcodesOrg/dependabot/github_actions/ruby/setup-ruby-1.152.0
Bump ruby/setup-ruby from 1.149.0 to 1.152.0
2023-11-21 22:37:20 -06:00
Matt Kiazyk
cf57fde101
Merge pull request #393 from qnotna/main
Update DE localization
2023-11-21 22:36:43 -06:00
Matt Kiazyk
74ef10b697
Merge pull request #391 from megabitsenmzq/main
Update zh_CN localizations.
2023-11-21 22:35:47 -06:00
Matt Kiazyk
5b95ef2af6
Merge pull request #297 from ferranabello/main
Add catalan translation
2023-11-21 22:35:31 -06:00
Matt Kiazyk
965b87002f
Merge pull request #395 from jmg-duarte/patch-1
Fix typo in README.md
2023-11-21 22:34:36 -06:00
Daniel Chick
5b1cc4a93d Swap button to InstallButton 2023-10-17 18:15:18 -05:00
Daniel Chick
13715930af Update InstallButton with progress view and styling from list row 2023-10-17 18:14:51 -05:00
Elon Park
35fb72db68 Fix SMS Enter Digit Code Crash 2023-09-24 02:47:37 +09:00
Matt Kiazyk
dc5a8b03b6 WIP download runtime, refactor 2023-09-22 15:35:26 -05:00
Sam Lu
b6e654c6f2
Update CI workflow to use Xcode 14.2 2023-09-19 12:16:06 -06:00
Sam Lu
622fac605c
Set up overall progress object
to track both downloading and unarchiving and reflect this in dock progress
2023-09-18 10:39:40 -06:00
Sam Lu
de35bed9fa
Add dock progress indicator
in .bar style
2023-09-13 12:48:14 -06:00
Sam Lu
9dd91a2ce8
Add DockProgress swift package 2023-09-13 12:48:13 -06:00
lavare
5977210252 Panel parsing visionOS field 2023-06-26 16:44:25 +08:00
lavare
8654a2075a Update lastest xcodereleases/data 2023-06-26 16:41:34 +08:00
Matt Kiazyk
7325502853 more runtime download work 2023-06-23 14:45:13 -05:00
Kyle
fc35950885
Add PinCodeTextField autocomplete support 2023-06-20 18:04:31 +08:00
dependabot[bot]
415d86718c
Bump ruby/setup-ruby from 1.149.0 to 1.152.0
Bumps [ruby/setup-ruby](https://github.com/ruby/setup-ruby) from 1.149.0 to 1.152.0.
- [Release notes](https://github.com/ruby/setup-ruby/releases)
- [Commits](https://github.com/ruby/setup-ruby/compare/v1.149.0...v1.152.0)

---
updated-dependencies:
- dependency-name: ruby/setup-ruby
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-14 11:13:04 +00:00
José Duarte
581af76948
Fix typo in README.md 2023-06-11 10:42:19 +01:00
Anton Quietzsch
fb42bfc013 Fix Mac strings 2023-06-09 11:03:52 +02:00
Anton Quietzsch
2de476b101 Add missing localization 2023-06-09 10:54:29 +02:00
Anton Quietzsch
a556d95ee9 Fix typos in DE localization 2023-06-09 10:53:52 +02:00
Matt Kiazyk
4f25905f4c start moving over to XcodesKit package. Runtimes searching 2023-06-06 23:29:09 -05:00
Jinyu Meng
8169ee0a22 Update zh_CN localizations. 2023-06-06 16:11:02 +09:00
Ferran Abelló
927f842c61
Merge branch 'main' into main 2023-05-26 10:49:31 +02:00
Matt Kiazyk
9cf10125ca
Merge pull request #386 from 0x7067/improve-portuguese-translation
Improve Brazilian Portuguese translation
2023-05-19 17:42:04 -05:00
Pedro Guimarães
3353eb1112
Translate missing strings 2023-05-19 19:32:37 -03:00
Pedro Guimarães
9dd569c605
Fix issue where app was crashing on opening the Advanced tab in Settings 2023-05-19 19:32:18 -03:00
Matt Kiazyk
507fa37bb5
Merge pull request #384 from XcodesOrg/dependabot/github_actions/ruby/setup-ruby-1.149.0
Bump ruby/setup-ruby from 1.144.2 to 1.149.0
2023-05-19 16:50:06 -05:00
Matt Kiazyk
907e503f71
Merge pull request #371 from Kyle-Ye/feature/kyle/upgrade_unxip
Update unxip binary to v2.1
2023-05-19 16:49:53 -05:00
Matt Kiazyk
4ecc245db6
Merge pull request #375 from jakex7/locale/pl
Introduce Polish translations
2023-05-19 16:38:44 -05:00
Matt Kiazyk
84702ad63b
Merge pull request #376 from LinXunFeng/main
Fix zh_Hans localizations
2023-05-19 16:26:50 -05:00
Matt Kiazyk
0607f57077
Merge pull request #385 from KyLeggiero/feature/Update-images
Updated readme
2023-05-19 16:24:34 -05:00
Ky
9b6a479798
Updated readme
- Fixed broken CI badge
- Updated screenshots for current version
- Clearer wording for installation instruction titles (now it's clear these are separate/alternative processes rather than stages in the same process)
    - Pulled brew instructions out of the codeblock so that it wraps. Rephrased for the new position
2023-05-16 11:35:38 -06:00
dependabot[bot]
e7f0adf69f
Bump ruby/setup-ruby from 1.144.2 to 1.149.0
Bumps [ruby/setup-ruby](https://github.com/ruby/setup-ruby) from 1.144.2 to 1.149.0.
- [Release notes](https://github.com/ruby/setup-ruby/releases)
- [Commits](https://github.com/ruby/setup-ruby/compare/v1.144.2...v1.149.0)

---
updated-dependencies:
- dependency-name: ruby/setup-ruby
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-15 11:11:14 +00:00
Matt Kiazyk
d8e1069a92
Update some robots references on README.md 2023-04-21 14:00:46 -05:00
Matt Kiazyk
60582094af
Update README.md about next steps 2023-04-21 12:53:02 -05:00
Matt Kiazyk
cc4165252e push nextstep.md 2023-04-21 12:47:50 -05:00
Kyle
3529b19295
Update unxip binary to v2.1 2023-04-18 13:46:21 +08:00
LinXunFeng
771cb3242c Fix zh_Hans localizations 2023-04-17 13:37:49 +08:00
Jakub Grzywacz
7245d54a7c
Docs: Update README.md 2023-04-16 12:50:55 +02:00
Jakub Grzywacz
2d22210d5b Locale: introduce Polish translations 2023-04-16 12:46:30 +02:00
Matt Kiazyk
3b1254a48e
Merge pull request #369 from RobotsAndPencils/dependabot/github_actions/ruby/setup-ruby-1.144.2
Bump ruby/setup-ruby from 1.144.1 to 1.144.2
2023-03-21 22:25:34 -05:00
dependabot[bot]
95c9776788
Bump ruby/setup-ruby from 1.144.1 to 1.144.2
Bumps [ruby/setup-ruby](https://github.com/ruby/setup-ruby) from 1.144.1 to 1.144.2.
- [Release notes](https://github.com/ruby/setup-ruby/releases)
- [Commits](https://github.com/ruby/setup-ruby/compare/v1.144.1...v1.144.2)

---
updated-dependencies:
- dependency-name: ruby/setup-ruby
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-21 10:57:04 +00:00
Matt Kiazyk
93f34dd31b
Merge pull request #368 from RobotsAndPencils/dependabot/github_actions/ruby/setup-ruby-1.144.1
Bump ruby/setup-ruby from 1.144.0 to 1.144.1
2023-03-18 09:50:02 -05:00
dependabot[bot]
ce906c5ce9
Bump ruby/setup-ruby from 1.144.0 to 1.144.1
Bumps [ruby/setup-ruby](https://github.com/ruby/setup-ruby) from 1.144.0 to 1.144.1.
- [Release notes](https://github.com/ruby/setup-ruby/releases)
- [Commits](https://github.com/ruby/setup-ruby/compare/v1.144.0...v1.144.1)

---
updated-dependencies:
- dependency-name: ruby/setup-ruby
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-17 10:57:07 +00:00
Matt Kiazyk
d3e07242b1
Merge pull request #366 from RobotsAndPencils/dependabot/github_actions/actions/cache-3.3.1
Bump actions/cache from 3.2.6 to 3.3.1
2023-03-13 21:30:36 -05:00
Matt Kiazyk
5423f20c26
Merge pull request #365 from RobotsAndPencils/dependabot/github_actions/ruby/setup-ruby-1.144.0
Bump ruby/setup-ruby from 1.143.0 to 1.144.0
2023-03-13 21:30:21 -05:00
dependabot[bot]
74a870d4ff
Bump actions/cache from 3.2.6 to 3.3.1
Bumps [actions/cache](https://github.com/actions/cache) from 3.2.6 to 3.3.1.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v3.2.6...v3.3.1)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-13 10:57:22 +00:00
dependabot[bot]
48b5611e0b
Bump ruby/setup-ruby from 1.143.0 to 1.144.0
Bumps [ruby/setup-ruby](https://github.com/ruby/setup-ruby) from 1.143.0 to 1.144.0.
- [Release notes](https://github.com/ruby/setup-ruby/releases)
- [Commits](https://github.com/ruby/setup-ruby/compare/v1.143.0...v1.144.0)

---
updated-dependencies:
- dependency-name: ruby/setup-ruby
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-10 10:57:06 +00:00
Matt Kiazyk
371c94e3fa
Merge pull request #363 from RobotsAndPencils/dependabot/github_actions/ruby/setup-ruby-1.143.0
Bump ruby/setup-ruby from 1.139.0 to 1.143.0
2023-03-06 21:56:43 -06:00
Matt Kiazyk
9d681927d2
Merge pull request #358 from RobotsAndPencils/dependabot/github_actions/actions/cache-3.2.6
Bump actions/cache from 3.2.5 to 3.2.6
2023-03-06 21:56:31 -06:00
Matt Kiazyk
ba031403d6
Merge pull request #362 from egesucu/turkish-translation-improvements
Turkish Localization Update
2023-03-06 21:56:13 -06:00
dependabot[bot]
846fbc0635
Bump ruby/setup-ruby from 1.139.0 to 1.143.0
Bumps [ruby/setup-ruby](https://github.com/ruby/setup-ruby) from 1.139.0 to 1.143.0.
- [Release notes](https://github.com/ruby/setup-ruby/releases)
- [Commits](https://github.com/ruby/setup-ruby/compare/v1.139.0...v1.143.0)

---
updated-dependencies:
- dependency-name: ruby/setup-ruby
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-06 10:57:48 +00:00
Ege Sucu
0bc8edf75e Updated Turkish Localization with New Content 2023-03-04 16:57:29 +03:00
Matt Kiazyk
b439eb555b
Update appcast ruby action 2023-02-28 19:29:25 -06:00
Matt Kiazyk
d955ec8bd2
v1.10.0 2023-02-28 18:58:53 -06:00
Matt Kiazyk
c2c67f1269
Merge pull request #361 from RobotsAndPencils/hashcash
Implement hashcash for Apple ID Authentication
2023-02-28 18:50:46 -06:00
Matt Kiazyk
76bb3fbae0
Adds hashcash implementation 2023-02-27 12:11:12 -06:00
Matt Kiazyk
29503ad9cf
Testing Hashcash 2023-02-23 23:04:31 -06:00
dependabot[bot]
a0efd467c6
Bump actions/cache from 3.2.5 to 3.2.6
Bumps [actions/cache](https://github.com/actions/cache) from 3.2.5 to 3.2.6.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v3.2.5...v3.2.6)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-21 10:57:00 +00:00
Matt Kiazyk
2f37ae0e41
Merge pull request #356 from RobotsAndPencils/unxip_1.1.3
Update unxip binary to 1.1.3
2023-02-16 21:14:02 -06:00
Matt Kiazyk
9452db4749
Update unxip binary to 1.1.3 2023-02-14 23:07:16 -06:00
Matt Kiazyk
b5550d0557
Merge pull request #354 from RobotsAndPencils/dependabot/github_actions/actions/cache-3.2.5
Bump actions/cache from 3.2.4 to 3.2.5
2023-02-12 16:01:11 -06:00
dependabot[bot]
7e459f8933
Bump actions/cache from 3.2.4 to 3.2.5
Bumps [actions/cache](https://github.com/actions/cache) from 3.2.4 to 3.2.5.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v3.2.4...v3.2.5)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-10 10:56:59 +00:00
Matt Kiazyk
7263ff3f67
Merge pull request #348 from egesucu/improvement/new-username
Updating the username
2023-02-01 23:03:53 -06:00
Matt Kiazyk
16bd85c918
Merge pull request #299 from jrsaruo/fix/missing-ja-localization
Update Japanese localization
2023-02-01 22:59:02 -06:00
Matt Kiazyk
87cdbd7ec0
Merge pull request #349 from FelixHerrmann/main
Ventura UI fixes
2023-02-01 22:51:57 -06:00
Matt Kiazyk
4a48d55ac7
Merge pull request #351 from RobotsAndPencils/dependabot/github_actions/actions/cache-3.2.4
Bump actions/cache from 3.2.2 to 3.2.4
2023-01-31 19:01:10 -06:00
dependabot[bot]
f20ba75384
Bump actions/cache from 3.2.2 to 3.2.4
Bumps [actions/cache](https://github.com/actions/cache) from 3.2.2 to 3.2.4.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v3.2.2...v3.2.4)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-31 10:01:12 +00:00
Felix Herrmann
c496238fb9 fix Ventura status bar spacing 2023-01-16 17:53:19 +01:00
Felix Herrmann
7eb5a8f073 fix Ventura toolbar placement 2023-01-16 17:52:26 +01:00
Ege Sucu
834dd9807a
Updating the username 2023-01-10 00:43:23 +03:00
Matt Kiazyk
e0be252899
Merge pull request #342 from RobotsAndPencils/dependabot/github_actions/actions/cache-3.2.2
Bump actions/cache from 3.0.11 to 3.2.2
2022-12-29 10:01:04 -06:00
Matt Kiazyk
5b43677f30
Merge pull request #336 from yossizahn/main
Fix #329
2022-12-29 10:00:41 -06:00
dependabot[bot]
816c00460d
Bump actions/cache from 3.0.11 to 3.2.2
Bumps [actions/cache](https://github.com/actions/cache) from 3.0.11 to 3.2.2.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v3.0.11...v3.2.2)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-28 10:01:42 +00:00
Yossi Zahn
550d917b33 Fix #329 2022-12-18 00:29:33 +02:00
Ferran Abelló
45ea417a98
Merge branch 'main' into main 2022-12-14 14:45:35 +01:00
Yusaku Nishi
b56a73c8fe
Add missing localizations for Japanese 2022-12-10 23:10:27 +09:00
Yusaku Nishi
495dfedf46
Merge branch 'main' into fix/missing-ja-localization 2022-12-10 22:53:37 +09:00
Matt Kiazyk
0b6ae5d7ec
Merge pull request #333 from RobotsAndPencils/matt/OpenInRosetta
Adds open in Rosetta option for Apple Silicon machines
2022-12-06 18:42:57 -06:00
Matt Kiazyk
b4a4f8e329
Adds open in Rosetta option for Apple Silicon machines 2022-12-05 23:08:44 -06:00
Matt Kiazyk
f8ec0a37fe
Bump version 1.9.0 2022-11-22 21:48:47 -06:00
Matt Kiazyk
34817a6916
Merge pull request #322 from DannyJJK/patch-1
Improve Spanish localization in Preferences
2022-11-22 21:34:23 -06:00
Matt Kiazyk
9a61a94239
Merge pull request #304 from leon-wolf/feature/281-add-beta-symlink
Adds an option to save beta versions with `-beta` symlink
2022-11-22 21:33:24 -06:00
Matt Kiazyk
ce19069353
Merge pull request #260 from brunomunizaf/brazilian-localization
Add brazilian portuguese localization
2022-11-22 21:14:37 -06:00
Matt Kiazyk
c373068f15
Merge pull request #328 from RobotsAndPencils/matt/xcodeReleasesAppleAuth
Switch Xcode releases back to using Apple Auth :(
2022-11-18 08:55:39 -06:00
Matt Kiazyk
a0a74ed5bf
Switch Xcode releases back to using Apple Auth :( 2022-11-17 22:39:38 -06:00
Bruno Muniz
ebafb0fb3d
Merge branch 'main' into brazilian-localization 2022-11-14 16:57:57 -03:00
Danny Kirkham
2d748a1a2a
Improve Spanish strings in Preferences
I've not added all the missing ones but this fills some of them in, including some corrections
2022-11-11 19:05:01 +00:00
Matt Kiazyk
4049e43ce5
Merge pull request #307 from jfversluis/main
Add Dutch localization
2022-11-11 11:33:23 -06:00
Matt Kiazyk
35ae6b7672
Merge pull request #306 from leon-wolf/feature/144-add-error-on-uninstall
add error message when trying to uninstall Xcode if file not found
2022-11-11 11:27:22 -06:00
Leon Wolf
0e45884608 Merge branch 'main' into feature/144-add-error-on-uninstall
# Conflicts:
#	Xcodes.xcodeproj/project.pbxproj
2022-11-11 17:24:14 +01:00
Matt Kiazyk
b9ba988c30
Merge pull request #305 from leon-wolf/feature/291-cleanup-preferences
move download settings to own PreferencePane
2022-11-11 09:34:12 -06:00
Leon Wolf
d31065e55e add FileError.swift to project 2022-11-11 10:44:40 +01:00
Leon Wolf
e533012ea0 adding DownloadPreferencePane to project 2022-11-11 10:40:03 +01:00
Bruno Muniz Azevedo Filho
5be971cc8e updated readme 2022-11-09 23:24:59 -03:00
Bruno Muniz
513d39d45e Add files via upload 2022-11-09 23:19:52 -03:00
Bruno Muniz
7150bced63 Update project.pbxproj 2022-11-09 23:19:51 -03:00
Matt Kiazyk
25460b9f80
Merge pull request #310 from RobotsAndPencils/dependabot/github_actions/actions/cache-3.0.11
Bump actions/cache from 3.0.8 to 3.0.11
2022-11-08 22:50:39 -06:00
Matt Kiazyk
7fda5e7a9a
Merge pull request #309 from tt37/fix-typo
Fix localization on UnsupportedFileFormat
2022-11-08 22:50:24 -06:00
Matt Kiazyk
ae5b606e52
Merge pull request #308 from jfversluis/remove-localized-link
Remove localization from apple.com links
2022-11-08 22:43:48 -06:00
Matt Kiazyk
cf03241320
Merge pull request #303 from chedabob/FixPreferencesButtonVentura
fix: Preference button on toolbar not working on Mac OS 13 Ventura
2022-11-03 23:08:09 -05:00
Matt Kiazyk
27abbb18fd
Merge pull request #318 from RobotsAndPencils/contributingguide
Add new contributing.md
2022-10-31 12:35:16 -05:00
Matt Kiazyk
093e44c21f
Add new contributing.md 2022-10-29 22:51:11 -05:00
dependabot[bot]
502559e7a4
Bump actions/cache from 3.0.8 to 3.0.11
Bumps [actions/cache](https://github.com/actions/cache) from 3.0.8 to 3.0.11.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v3.0.8...v3.0.11)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-14 10:04:21 +00:00
tt37
6e8dbbc6de
Fix typo 2022-10-13 11:13:28 +02:00
Gerald Versluis
a02cc71ac6
Remove localization from apple.com links 2022-10-07 11:18:02 +02:00
Gerald Versluis
651fe480a0
Update README.md 2022-10-07 11:14:53 +02:00
Gerald Versluis
90dbf4f010
Update project.pbxproj 2022-10-07 11:12:11 +02:00
Gerald Versluis
64d6b5d5dc
Update Localizable.strings 2022-10-07 11:07:23 +02:00
Gerald Versluis
5e0eb827d3
Create Localizable.strings 2022-10-06 16:43:04 +02:00
Leon Wolf
ed2e5bfcaa add error message when trying to uninstall Xcode if file not found 2022-10-06 08:42:40 +02:00
Leon Wolf
2bad04fec7 fix localization 2022-10-05 21:50:49 +02:00
Leon Wolf
ea3264a2ce add localizations 2022-10-05 21:44:48 +02:00
Leon Wolf
0e7deed1c6 move download settings to own PreferencePane 2022-10-05 21:06:02 +02:00
Leon Wolf
7ae956e44d add Xcode-Beta.app Symlink and localizations 2022-10-05 20:34:35 +02:00
Leon Wolf
78c9207877 add .idea to gitignore 2022-10-05 20:31:58 +02:00
chedabob
bb8676b620 fix: Preference button on toolbar not working on Mac OS 13 Ventura 2022-10-05 00:27:08 +01:00
Yusaku Nishi
cb1120624b Update Japanese localization 2022-09-30 14:59:51 +09:00
Yusaku Nishi
da93339a3d Add missing localization keys for Japanese 2022-09-29 21:48:13 +09:00
Matt Kiazyk
13e829ca88
Merge pull request #296 from nklasio/patch-1
Typo in German Localization File
2022-09-28 23:14:52 -05:00
Ferran
beca9b4187 Update Readme file 2022-09-28 19:07:54 +02:00
Ferran
2030ef2d5a Added catalan localization strings 2022-09-28 19:07:37 +02:00
Niklas Stambor
998a8982f8
Typo in German Localization File
Aktalisiert  --> Aktualisiert
2022-09-25 21:54:38 +02:00
Matt Kiazyk
dc6124f25c
Bump version
Signed-off-by: Matt Kiazyk <mattkiazyk@gmail.com>
2022-09-16 22:46:49 -05:00
Matt Kiazyk
bb66552812
Add localization category for Release drafter 2022-09-16 22:36:36 -05:00
Matt Kiazyk
c27b28868b
Merge pull request #290 from RobotsAndPencils/NoLoginDownload
Add ability to download Xcode without logging in using XcodeReleases
2022-09-16 22:34:08 -05:00
Matt Kiazyk
e987104502
Merge pull request #288 from florentmorin/fr-translation-fixes
Fix French Translation
2022-09-16 21:41:02 -05:00
Matt Kiazyk
8b43903b42
Add ability to download Xcode without logging in using XcodeRelease 2022-09-16 21:34:14 -05:00
Matt Kiazyk
3574a3e781
Merge pull request #285 from drct/feature/german_translation
German translation added
2022-09-13 21:51:13 -05:00
Florent Morin
0f3d92b70f
Update French Localization 2022-09-10 10:41:09 +02:00
Florent Morin
e2f49bee23
Fix French Localization 2022-09-10 10:33:26 +02:00
Dietrich Tegge
c045592aea German translation added
German translation added for install and renaming features
2022-09-08 08:42:09 +02:00
Matt Kiazyk
197197982f
Merge pull request #277 from egrimo/turkish-text-translations
Added Turkish missing translations in 1.7.0
2022-09-07 18:34:49 -05:00
Matt Kiazyk
3e970f5b56
Merge pull request #284 from marcusziade/fix-read-me-finish-typo
Fix Finnish language name typo in README
2022-09-07 18:33:55 -05:00
Matt Kiazyk
33b0356622
Merge pull request #280 from swiftty/fix-jp-localization
Fix "Unarchiving" localization in Japanese
2022-09-07 18:32:31 -05:00
Marcus Ziadé
bb0e4d489f Fix Finnish language name typo in README 2022-09-07 23:37:52 +03:00
swiftty
410e1e5ae3 Fix "Unarchiving" localization in Japanese 2022-08-30 04:12:31 +09:00
Ege Sucu
c85f48ae3c Added missing translations in 1.7.0 2022-08-24 21:16:31 +03:00
Matt Kiazyk
28456c4e8e
Merge pull request #274 from ymhuang0808/fix-traditional-chinese-flag
Update readme with Taiwan flag
2022-08-22 20:43:16 -05:00
Matt Kiazyk
17d94cac67
Merge pull request #276 from RobotsAndPencils/dependabot/github_actions/actions/cache-3.0.8
Bump actions/cache from 3.0.7 to 3.0.8
2022-08-22 20:42:59 -05:00
dependabot[bot]
b74c567a5c
Bump actions/cache from 3.0.7 to 3.0.8
Bumps [actions/cache](https://github.com/actions/cache) from 3.0.7 to 3.0.8.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v3.0.7...v3.0.8)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-08-22 10:04:41 +00:00
John Huang
ac3bb5a330 Update readme with Taiwan flag 2022-08-17 13:58:13 +08:00
Matt Kiazyk
653f1ae59e
Update readme with more localization contributors 2022-08-11 22:54:59 -05:00
Matt Kiazyk
61464cd812
Merge pull request #272 from RobotsAndPencils/dependabot/github_actions/actions/cache-3.0.7
Bump actions/cache from 3.0.6 to 3.0.7
2022-08-11 19:40:35 -05:00
Matt Kiazyk
739193c3cd
Merge pull request #271 from duffpod/patch-2
Update Localizable.strings for Russian language (2)
2022-08-11 19:39:44 -05:00
Matt Kiazyk
8fb1564fdd
Merge pull request #270 from megabitsenmzq/main
Update zh_Hans localizations.
2022-08-11 19:10:47 -05:00
dependabot[bot]
dcb9c0e860
Bump actions/cache from 3.0.6 to 3.0.7
Bumps [actions/cache](https://github.com/actions/cache) from 3.0.6 to 3.0.7.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v3.0.6...v3.0.7)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-08-11 10:06:03 +00:00
Paul Semionov
b2c949d68a
Update Localizable.strings for Russian language (2)
Fixed typos here and there, made casing more consistent, rewrote some sentences to make more sense, localized new strings from en.lproj.
2022-08-10 19:09:16 +03:00
Jinyu Meng
0296f14632 Update zh_Hans localizations. 2022-08-09 13:55:30 +09:00
Matt Kiazyk
48b1b19185
bump version - 1.7.0 2022-08-08 20:24:17 -05:00
Matt Kiazyk
a5486aa6ac
Merge pull request #268 from RobotsAndPencils/customInstallDirectory
Ability to change the installation directory
2022-08-08 19:53:57 -05:00
Matt Kiazyk
fb6a858491
Merge pull request #269 from RobotsAndPencils/dependabot/github_actions/actions/cache-3.0.6
Bump actions/cache from 3.0.5 to 3.0.6
2022-08-08 19:53:19 -05:00
dependabot[bot]
a0b45f45be
Bump actions/cache from 3.0.5 to 3.0.6
Bumps [actions/cache](https://github.com/actions/cache) from 3.0.5 to 3.0.6.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v3.0.5...v3.0.6)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-08-05 10:05:49 +00:00
Matt Kiazyk
38b7fe77dd
Allow ability to change the installation directory 2022-07-28 19:51:13 -05:00
Matt Kiazyk
ee9ee59fc4
Merge pull request #267 from RobotsAndPencils/renameOnSelect
add option to rename Xcode to Xcode.app on select.
2022-07-28 18:19:31 -05:00
Matt Kiazyk
b64af2fc44
add option to rename Xcode to Xcode.app on select. 2022-07-24 15:27:34 -05:00
Matt Kiazyk
e34f1a207a
Merge pull request #258 from GetToSet/ethanwong/ventura-settings-crash-fix
Fix macOS Ventura crash on Settings View
2022-07-24 13:40:20 -05:00
Matt Kiazyk
491e810a3c
Merge pull request #266 from RobotsAndPencils/dependabot/bundler/AppCast/tzinfo-1.2.10
Bump tzinfo from 1.2.9 to 1.2.10 in /AppCast
2022-07-24 13:03:17 -05:00
Matt Kiazyk
2ecd1398fb
Updates AcknowledgementsGenerator for SPM 5.4 2022-07-24 12:59:09 -05:00
Matt Kiazyk
155b57f202
Merge pull request #257 from swiftty/fix-sms-on-cancel
Fix crash on sign in sheet cancel
2022-07-24 12:36:29 -05:00
dependabot[bot]
672efcbca4
Bump tzinfo from 1.2.9 to 1.2.10 in /AppCast
Bumps [tzinfo](https://github.com/tzinfo/tzinfo) from 1.2.9 to 1.2.10.
- [Release notes](https://github.com/tzinfo/tzinfo/releases)
- [Changelog](https://github.com/tzinfo/tzinfo/blob/master/CHANGES.md)
- [Commits](https://github.com/tzinfo/tzinfo/compare/v1.2.9...v1.2.10)

---
updated-dependencies:
- dependency-name: tzinfo
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-24 17:33:49 +00:00
Matt Kiazyk
dc1d89dfc9
Merge pull request #264 from gredman/main
Use monospaced digit font for download progress
2022-07-23 15:44:48 -05:00
Matt Kiazyk
286e69bbaf
Merge pull request #262 from marcusziade/add-finnish-localizations
Add Finnish localization
2022-07-23 15:32:56 -05:00
Matt Kiazyk
bc0f44bc9a
Merge pull request #263 from RobotsAndPencils/dependabot/github_actions/actions/cache-3.0.5
Bump actions/cache from 3.0.4 to 3.0.5
2022-07-23 15:22:05 -05:00
Marcus Ziadé
60bbe49ea7 Add Finnish localizations 2022-07-18 10:15:31 +03:00
Gareth Redman
45f034565b Use monospaced digit font for download progress 2022-07-15 10:15:48 +12:00
dependabot[bot]
43d604aa6b
Bump actions/cache from 3.0.4 to 3.0.5
Bumps [actions/cache](https://github.com/actions/cache) from 3.0.4 to 3.0.5.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v3.0.4...v3.0.5)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-13 10:07:54 +00:00
Matt Kiazyk
b93fa01d07
Merge pull request #259 from gelosi/main
Adding Ukrainian Localization
2022-07-12 23:30:03 -05:00
Matt Kiazyk
5f0b722799
Merge pull request #255 from d-date/copy-release-note
Adds Copy Release Notes URL context menu
2022-07-12 22:58:56 -05:00
Oleg Shanyuk
8f3f992460
Adding Ukrainian Localization 2022-06-26 13:17:46 +02:00
Ethan Wong
7945f94bf8
Fix crash on macOS Ventura by moving pane .frame(width:) to its parents. 2022-06-25 14:46:28 +08:00
swiftty
a70069ce87 Fix sign in sheet cancel behavior 2022-06-24 12:52:38 +09:00
Daiki Matsudate
575a7269df Add copy release note url menu 2022-06-17 13:57:12 +09:00
Matt Kiazyk
1baf1f8515
Merge pull request #251 from d-date/dateformat
Update Release Date format on InfoPane for better localization
2022-06-15 22:02:57 -05:00
Matt Kiazyk
bbbd0322eb
Merge pull request #249 from tatsuz0u/feature/jpn-translation
Add Japanese Translation
2022-06-15 21:50:40 -05:00
Matt Kiazyk
8831d5f992
Merge pull request #243 from drct/feature/german_translation
German translation added
2022-06-15 21:38:17 -05:00
Matt Kiazyk
ffec3d5157
Merge pull request #253 from RobotsAndPencils/matt/bugfix/minVersionLocalization
Fix minimum version supported localization alert message
2022-06-15 21:22:53 -05:00
Daiki Matsudate
564ca96149 Use string interpolation 2022-06-15 19:25:23 +09:00
Dietrich T
c9f7c3b7d1
Data source name changed to Xcode Releases
Co-authored-by: Matt Kiazyk <mattkiazyk@gmail.com>
2022-06-14 08:50:06 +02:00
Matt Kiazyk
d351068684
Fix Minimum version supported alert to show proper message 2022-06-13 19:37:36 -05:00
Dietrich Tegge
b2143744b3 German localization added including XcodeProj 2022-06-13 17:48:00 +02:00
Dietrich Tegge
c7049c908c Merge branch 'main' into feature/german_translation 2022-06-13 17:40:54 +02:00
Tatsuzou Araki
9483a0c785
Merge branch 'main' into feature/jpn-translation 2022-06-12 16:31:22 +09:00
Matt Kiazyk
b3e1b58a0c
Merge pull request #240 from itszero/feature/traditional-chinese-localization
Add Traditional Chinese localization
2022-06-11 22:51:46 -05:00
Matt Kiazyk
e8e9c8bbd9
Merge pull request #241 from RobotsAndPencils/dependabot/github_actions/actions/cache-3.0.4
Bump actions/cache from 3.0.3 to 3.0.4
2022-06-11 22:39:28 -05:00
Matt Kiazyk
b240d0f77b
Merge pull request #248 from duffpod/patch-1
Fixed wrong localization key for Github Repo in AboutView
2022-06-11 22:39:05 -05:00
Matt Kiazyk
57521e5e65
Merge pull request #246 from egrimo/improvement/Turkish-WWDC-Text
Turkish Language Improvements & WWDC Message Localization
2022-06-11 22:37:49 -05:00
たつぞう
9a926af7ee Apply suggestions from code review 2022-06-12 12:28:31 +09:00
Tatsuzou Araki
961e4e9d5b
Apply suggestions from code review
Co-authored-by: Daiki Matsudate <d.matsudate@gmail.com>
2022-06-12 12:25:54 +09:00
Tatsuzou Araki
f2a7c155eb
Apply suggestions from code review
Co-authored-by: Daiki Matsudate <d.matsudate@gmail.com>
2022-06-12 10:51:33 +09:00
Tatsuzou Araki
d461721221
Apply suggestions from code review
Co-authored-by: Daiki Matsudate <d.matsudate@gmail.com>
2022-06-12 10:49:44 +09:00
Tatsuzou Araki
71760ca6e3
Apply suggestions from code review
Co-authored-by: Daiki Matsudate <d.matsudate@gmail.com>
2022-06-12 10:45:34 +09:00
Tatsuzou Araki
a1923ea29a
Apply suggestions from code review
Co-authored-by: Daiki Matsudate <d.matsudate@gmail.com>
2022-06-12 10:42:51 +09:00
たつぞう
65f79d9313 Add Japanese Translation (the rest) 2022-06-11 18:46:36 +09:00
たつぞう
8185b1054f Add Japanese Translation (List, Setting, InfoPane) 2022-06-11 15:18:59 +09:00
Paul Semionov
3213b3ef33
Fixed wrong localization key for Github Repo in AboutView 2022-06-09 23:03:03 +03:00
Ege Sucu
25bbe1cb54 added Turkish Language improvements & Localized WWDC Message. 2022-06-09 14:57:12 +03:00
Dietrich Tegge
75a9047492 German translation added 2022-06-08 14:49:15 +02:00
dependabot[bot]
6d6b9db70e
Bump actions/cache from 3.0.3 to 3.0.4
Bumps [actions/cache](https://github.com/actions/cache) from 3.0.3 to 3.0.4.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v3.0.3...v3.0.4)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-07 10:05:48 +00:00
Zero Cho
a9c7e524ad finish up TC localization 2022-06-06 16:04:25 -07:00
Ethan Huang
2f40821244 Delete the extra colon 2022-06-06 14:59:10 -07:00
Ethan Huang
bce40c95ae Add Traditional Chinese localization 2022-06-06 14:59:09 -07:00
Matt Kiazyk
5fa3152918
Update Readme with new localization contributors 2022-06-04 22:34:38 -05:00
Matt Kiazyk
a106835e15
Bump version number 2022-06-04 21:41:54 -05:00
Matt Kiazyk
f3e97f54f5
Updated unxip binary 2022-06-04 21:37:38 -05:00
Matt Kiazyk
9f013ccdf2
Adds a bottom status bar to the window 2022-06-04 21:30:39 -05:00
Matt Kiazyk
b9140036e2
Merge pull request #236 from RobotsAndPencils/dependabot/github_actions/actions/cache-3.0.3
Bump actions/cache from 3.0.2 to 3.0.3
2022-06-01 08:14:27 -05:00
Matt Kiazyk
f82136b8a0
Merge pull request #229 from gualtierofrigerio/main
Adding Italian localization
2022-06-01 08:14:06 -05:00
Gualtiero
4a2dc8b603 added italian localization 2022-06-01 11:33:25 +02:00
Gualtiero
311014323a added Italian localization 2022-06-01 11:28:54 +02:00
dependabot[bot]
5106ccead4
Bump actions/cache from 3.0.2 to 3.0.3
Bumps [actions/cache](https://github.com/actions/cache) from 3.0.2 to 3.0.3.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v3.0.2...v3.0.3)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-05-31 10:05:20 +00:00
Matt Kiazyk
ae35153fdf
Merge pull request #222 from megabitsenmzq/main
Adding Chinese-Simplified localizations.
2022-05-26 22:24:51 -05:00
Matt Kiazyk
f699ab6c2f
Merge branch 'main' into main 2022-05-26 22:06:15 -05:00
Matt Kiazyk
3db3fdbb54
Merge pull request #234 from duffpod/patch-1
Update Localizable.strings for Russian language
2022-05-25 14:28:33 -05:00
Paul Semionov
e59bf08e54
Update Localizable.strings for Russian language 2022-05-25 20:40:14 +03:00
Matt Kiazyk
cea8548872
Merge pull request #233 from ryan-son/feature/KoreanLocalization
Add Korean Localization
2022-05-24 22:11:30 -05:00
ryan-son
7ae8f4211d Add Korean Localization 2022-05-22 09:40:50 +09:00
Jinyu Meng
37c8d17716 Fix same typo in other languages. 2022-05-21 21:31:12 +09:00
Matt Kiazyk
37e4a185e5
Merge pull request #219 from egrimo/main
Adding Turkish Language
2022-05-19 21:51:15 -05:00
Jinyu Meng
3a0e06ded2 Chinese-Simplified localization changes. 2022-05-16 01:40:08 +09:00
Jinyu Meng
2abae033ea Fix a small problem in English strings. 2022-05-16 01:39:08 +09:00
Jinyu Meng
4204b42044 Adding Chinese-Simplified localizations.
Chinese-Simplified localization changes.

Chinese-Simplified localization changes.

Removed all spaces between Chinese and English. Because the OS will add these spaces automatically.

Chinese-Simplified localization changes.
2022-05-16 01:39:08 +09:00
Ege Sucu
a4349ee540 added turkish language as an option 2022-05-13 19:26:56 +03:00
Matt Kiazyk
8b9a21ec74
Bump version 1.5
Signed-off-by: Matt Kiazyk <mattkiazyk@gmail.com>
2022-05-12 13:34:08 -05:00
Matt Kiazyk
7440f5715d
Merge pull request #218 from RobotsAndPencils/SparkUpdate
Update Sparkle to 2.1
2022-05-12 13:08:37 -05:00
Matt Kiazyk
5386b630e2
Merge pull request #212 from RobotsAndPencils/Feature/Hindi_localizableFile
Hindi Localization
2022-05-10 14:05:06 -05:00
Gurpreet Kaur
0b03ee98ef Updated Hindi localizable.strings file
Added $ to retain positional specifier in Hindi localizable.strings file
2022-05-10 11:29:14 -07:00
Gurpreet Kaur
050cf98660 Updated Hindi Localizable.strings file
Updated installation steps translation to retain positional specifier
2022-05-10 10:24:26 -07:00
Matt Kiazyk
becc545efd
Update Sparkle to 2.1 2022-05-09 21:57:48 -05:00
Matt Kiazyk
dd99b9c611
Merge from main 2022-05-09 21:00:21 -05:00
Matt Kiazyk
755c2656fd
Merge branch 'main' into Feature/Hindi_localizableFile 2022-05-09 20:59:39 -05:00
Matt Kiazyk
8ed69a97a1
Update InstallationStepDescription using positional specifiers 2022-05-09 20:59:21 -05:00
Gurpreet Kaur
37c67b7c16 Merge branch 'main' into Feature/Hindi_localizableFile 2022-04-25 10:00:22 -07:00
Gurpreet Kaur
048aac2a8b Updated Localizable.strings
Updated Localizable(Hindi) file
2022-04-25 09:50:24 -07:00
Gurpreet Kaur
e3555f9868 Added Localisable (Hindi) file
Added Localisable (Hindi) file to support multiple languages
2022-04-24 22:47:34 -07:00
Matt Kiazyk
c64ff9f6ed
Update readme 2022-04-23 22:18:30 -05:00
Matt Kiazyk
3116a6857e
Merge pull request #210 from RobotsAndPencils/feature/dom/FrenchLocalization
French Localization
2022-04-23 20:08:19 -05:00
Matt Kiazyk
23521d2180
Merge branch 'main' into feature/dom/FrenchLocalization 2022-04-23 20:02:06 -05:00
Matt Kiazyk
dbdc7cf76d
Add OK to spanish 2022-04-23 19:59:42 -05:00
Matt Kiazyk
11f7cae14f
Add "OK" to localizable strings 2022-04-23 19:59:42 -05:00
Matt Kiazyk
8786b403cb
Fixes that were on the english translation, moved over 2022-04-23 19:59:42 -05:00
César Trujillo
857c232fb0
Leaving a word in english due to an attributed text to a URL 2022-04-23 19:59:42 -05:00
César Trujillo
74f54c8128
Spanish Localization
- Localizable ES file added.
- Localizing all strings.
2022-04-23 19:59:42 -05:00
Dominic Pepin
5d680633e0 No need to fr-ca localization file. fr will be used if not present. 2022-04-23 08:35:04 -06:00
Dominic Pepin
4af1fca7c7 Fix issue where French/Longer strings were getting truncated 2022-04-23 08:03:36 -06:00
Dominic Pepin
fad96f497d Fix issue where strings were not getting called in French 2022-04-23 07:56:14 -06:00
Dominic Pepin
d1bee98abb Initial French Localization Strings File 2022-04-23 07:55:46 -06:00
Matt Kiazyk
0821834fed
Add "OK" to localizable strings 2022-04-22 09:30:18 -05:00
Alex Maslov
795de95d53 update DataSourceDescription to refer to first xcode releas instance in english 2022-04-22 08:24:29 -04:00
Alex Maslov
d01013d8b8 reinstate macOS 12 code 2022-04-21 15:17:41 -04:00
Alex Maslov
dc85ab87f7 apply formatter fixes from main 2022-04-21 15:09:42 -04:00
Alex Maslov
0ab62517dd Merge remote-tracking branch 'origin/main' into alex/feature/localization-rus 2022-04-21 15:06:32 -04:00
Alex Maslov
7a82620a8d fix auto-translate errors 2022-04-21 15:06:03 -04:00
Matt Kiazyk
21c6ef2d47
Merge pull request #206 from RobotsAndPencils/localization_fixes_english
Fixes some localization formatting from string to ints
2022-04-21 10:18:23 -05:00
Matt Kiazyk
764a242096
Fixes some localization formatting from string to ints 2022-04-21 09:49:39 -05:00
Alex Maslov
77ab5b9f59 add rus 2022-04-21 10:39:45 -04:00
Matt Kiazyk
aad0f54ed3
Merge pull request #203 from RobotsAndPencils/localization
Support Localization
2022-04-19 08:31:41 -05:00
Matt Kiazyk
cd8104ca69
Merge pull request #202 from RobotsAndPencils/symlinkPreferenceFix
Fix: Load create SymLink preference on start
2022-04-18 19:06:38 -05:00
Matt Kiazyk
c8f4de47ea
Update CI to MacOS 12 + 13.2.1 2022-04-18 09:42:54 -05:00
Matt Kiazyk
1d22be649a
convert strings to support Localization 2022-04-17 21:29:51 -05:00
Matt Kiazyk
abf8dbf7b2
Fix: Load create SymLink preference on start 2022-04-15 08:29:41 -05:00
Matt Kiazyk
03a136385f
Bump build v1.4.0 2022-04-14 15:15:48 -05:00
Matt Kiazyk
44bef1a5f4
Merge pull request #200 from RobotsAndPencils/MakeSymLinkOnSelect
Add Preference to make Symbolic Link Automatic on Xcode Select
2022-04-14 14:41:38 -05:00
Matt Kiazyk
f7b4c5e2bb
Make Symbolic Link on Xcode Select 2022-04-14 14:34:44 -05:00
Matt Kiazyk
b36ef479a7
Merge pull request #199 from RobotsAndPencils/MinOSVersionCheck
Add check for min MacOS version when installing
2022-04-13 21:03:59 -05:00
Matt Kiazyk
e2f0c9dbc5
Merge pull request #198 from RobotsAndPencils/SymbolicLink
Adds option to create symbolic link at Path Xcode.app
2022-04-13 21:03:46 -05:00
Matt Kiazyk
20e7b57ad4
Add check for min MacOS version when installing 2022-04-12 20:11:42 -05:00
Matt Kiazyk
4cee0f2f15
Adds option to create symbolic link at Path Xcode.app 2022-04-11 19:37:11 -05:00
Matt Kiazyk
e628d04beb
Merge pull request #196 from RobotsAndPencils/andrew/DateModifiedFormatterLocale
Use a fixed locale for API response date parsing
2022-04-11 18:32:31 -05:00
Matt Kiazyk
c281abe7d2
Merge pull request #197 from RobotsAndPencils/dependabot/github_actions/actions/cache-3.0.2
Bump actions/cache from 3.0.1 to 3.0.2
2022-04-11 18:31:22 -05:00
dependabot[bot]
555843327b
Bump actions/cache from 3.0.1 to 3.0.2
Bumps [actions/cache](https://github.com/actions/cache) from 3.0.1 to 3.0.2.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v3.0.1...v3.0.2)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-11 10:07:09 +00:00
Andrew Erickson
c2259cf078 use fixed locale for date parsing of api responses 2022-04-09 12:12:16 -06:00
Matt Kiazyk
9a62be6376
Merge pull request #195 from RobotsAndPencils/dependabot/github_actions/actions/cache-3.0.1
Bump actions/cache from 2.1.7 to 3.0.1
2022-04-07 18:13:30 -05:00
dependabot[bot]
34dabd73dc
Bump actions/cache from 2.1.7 to 3.0.1
Bumps [actions/cache](https://github.com/actions/cache) from 2.1.7 to 3.0.1.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v2.1.7...v3.0.1)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-31 10:05:47 +00:00
Matt Kiazyk
e92434e9ba
increment build 1.3.1 2022-03-28 21:48:09 -05:00
Matt Kiazyk
70d4372f51
Merge remote-tracking branch 'origin/main' into main 2022-03-18 09:05:33 -05:00
Matt Kiazyk
f665054c7f
Updates unxip to be universal. Updates experiment icon to support OS11 2022-03-18 09:05:20 -05:00
Matt Kiazyk
6f71cce405
Merge pull request #186 from RobotsAndPencils/dependabot/github_actions/actions/checkout-3
Bump actions/checkout from 2.4.0 to 3
2022-03-16 21:56:56 -05:00
Matt Kiazyk
227768f6ae
Merge pull request #191 from Fleuronic/fix-typo
Fix typo
2022-03-16 21:56:38 -05:00
Jordan Kay
1bd83870a2 Fix typo 2022-03-15 18:09:36 -04:00
dependabot[bot]
8e7063e538
Bump actions/checkout from 2.4.0 to 3
Bumps [actions/checkout](https://github.com/actions/checkout) from 2.4.0 to 3.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2.4.0...v3)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-02 10:05:30 +00:00
Matt Kiazyk
9fc2c7aaf6
codesign unxip binary 2022-02-07 15:24:32 -06:00
Matt Kiazyk
f75bf2d5bb
Bump version number 2022-02-06 13:53:18 -06:00
Matt Kiazyk
e2ac15eed4
Merge pull request #179 from RobotsAndPencils/unxipExperiment
add a unxip experiment for faster unxipping
2022-02-06 13:46:07 -06:00
Matt Kiazyk
ce001c8e68
add a unxip experiment for faster unxipping 2022-02-06 13:25:23 -06:00
Matt Kiazyk
c0974edc98
Increment build number
Signed-off-by: Matt Kiazyk <mattkiazyk@gmail.com>
2022-01-06 21:38:22 -06:00
Matt Kiazyk
49746e4b81
Merge pull request #168 from RobotsAndPencils/matt/nonPaidAppleDeveloper
Allow Non-Paid Apple Developer to download versions.
2021-11-26 13:51:16 -06:00
Matt Kiazyk
a394f1df20
Merge pull request #170 from RobotsAndPencils/dependabot/github_actions/actions/cache-2.1.7
Bump actions/cache from 2.1.6 to 2.1.7
2021-11-24 13:05:52 -06:00
Matt Kiazyk
9de2684f2f
Update readme for dark/light mode 2021-11-24 12:22:07 -06:00
dependabot[bot]
61f43849b5
Bump actions/cache from 2.1.6 to 2.1.7
Bumps [actions/cache](https://github.com/actions/cache) from 2.1.6 to 2.1.7.
- [Release notes](https://github.com/actions/cache/releases)
- [Commits](https://github.com/actions/cache/compare/v2.1.6...v2.1.7)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-23 10:04:06 +00:00
Matt Kiazyk
709af9e97a
Update download task to check for resultCode !=0 2021-11-09 16:25:26 -06:00
Matt Kiazyk
239aeabf08
Merge pull request #166 from RobotsAndPencils/dependabot/github_actions/actions/checkout-2.4.0
Bump actions/checkout from 2.3.5 to 2.4.0
2021-11-04 22:06:17 -05:00
Matt Kiazyk
4db8346c2f
Merge branch 'main' into matt/nonPaidAppleDeveloper 2021-11-04 21:49:32 -05:00
Matt Kiazyk
f769f69f87
Merge pull request #165 from qyzdo/notAuthorized
Show a message when user is not authorized yet
2021-11-04 21:48:30 -05:00
dependabot[bot]
a3c541cb39
Bump actions/checkout from 2.3.5 to 2.4.0
Bumps [actions/checkout](https://github.com/actions/checkout) from 2.3.5 to 2.4.0.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2.3.5...v2.4.0)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-03 10:03:42 +00:00
Oskar Figiel
04d81ed9e4 Show a message when user is not authorized yet 2021-11-01 17:30:39 +01:00
Matt Kiazyk
e8fd83032b
fix: Allow non-paid Apple Developers to download 2021-10-27 08:30:43 -05:00
170 changed files with 32119 additions and 1218 deletions

View file

@ -5,6 +5,9 @@ categories:
- title: '🐛 Bug Fixes'
labels:
- 'bugfix'
- title: '🌎 Localization'
labels:
- 'localization'
- title: '🧰 Maintenance'
label:
- 'chore'

View file

@ -9,23 +9,23 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout 🛎
uses: actions/checkout@v2.3.5
uses: actions/checkout@v4
with:
# If you're using actions/checkout@v2.3.5 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@v2.1.6
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

View file

@ -8,10 +8,10 @@ on:
jobs:
test:
runs-on: macOS-11.0
runs-on: macos-15
steps:
- uses: actions/checkout@v2.3.5
- uses: actions/checkout@v4
- name: Run tests
env:
DEVELOPER_DIR: /Applications/Xcode_12.5.1.app
DEVELOPER_DIR: /Applications/Xcode_16.4.app
run: xcodebuild test -scheme Xcodes

View file

@ -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
View 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

3
.gitignore vendored
View file

@ -36,6 +36,9 @@ Product/*
timeline.xctimeline
playground.xcworkspace
# Jetbrains IDEA
.idea
# Swift Package Manager
#
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.

View file

@ -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

View file

@ -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.9)
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

43
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,43 @@
# Contributing to Xcodes
We love your input! We want to make contributing to this project as easy and transparent as possible, whether it's:
- Reporting a bug
- Discussing the current state of the code
- Submitting a fix
- 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.
## 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:
1. Fork the repo and create your branch from `main`.
2. If you've added code that should be tested, add tests.
3. If you've added new functionality, add documentation
4. Ensure the test suite passes.
5. Make sure your code lints.
6. Issue that pull request!
## 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 [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
**Great Bug Reports** tend to have:
- A quick summary and/or background
- Steps to reproduce
- Be specific!
- What you expected would happen
- What actually happens
- Notes (possibly including why you think this might be happening, or stuff you tried that didn't work)
People *love* thorough bug reports.
## License
By contributing, you agree that your contributions will be licensed under its MIT License.

View file

@ -73,4 +73,4 @@ Based on [this blog post](https://yiqiu.me/2015/11/19/sparkle-update-on-github/)
We're deliberately not capturing system profile data with Sparkle right now, because we don't want it and because it would require additional infrastructure.
We also considered https://github.com/mxcl/AppUpdater, but decided aganist it because it seemed less battle-tested than Sparkle and currently lacks an open source license.
We also considered https://github.com/mxcl/AppUpdater, but decided against it because it seemed less battle-tested than Sparkle and currently lacks an open source license.

View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
IconMono.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

123
README.md
View file

@ -1,47 +1,116 @@
<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)._
![CI](https://github.com/RobotsAndPencils/Xcodes.app/workflows/CI/badge.svg)
![CI](https://github.com/XcodesOrg/XcodesApp/workflows/CI/badge.svg)
![](screenshot.png)
![](screenshot_light.png#gh-light-mode-only)
![](screenshot_dark.png#gh-dark-mode-only)
### :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.
- Install any Xcode version, fully automated from start to finish. Xcodes uses [`aria2`](https://aria2.github.io), which uses up to 16 connections to download 3-5x faster than URLSession.
- Install any Xcode version, **fully automated** from start to finish. Xcodes uses [`aria2`](https://aria2.github.io), which uses up to 16 connections to download 3-5x faster than URLSession.
- Automatically resumes installs if network errors.
- Apple ID required to download Xcode versions.
- 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.
![](experiment_light.png#gh-light-mode-only)
![](experiment_dark.png#gh-dark-mode-only)
## Localization
Xcodes supports localization in several languages.
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 🇹🇷 |[@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)|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.
## 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 11 Big Sur and Xcode 12 in order to build and run Xcodes.app.
You'll need macOS 15.6 Ventura and Xcode 26 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.
`Unxip` and `aria2` must be compiled as a universal binary
```
# compile for Intel
swiftc -parse-as-library -O -target x86_64-apple-macos11 unxip.swift
# compile for M1
swiftc -parse-as-library -O -target arm64-apple-macos11 unxip.swift
# combine for universal binary
lipo -create -output unxip unxip_intel unxip_m1
# check it
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.
<details>
<summary>Releasing a new version</summary>
@ -50,9 +119,9 @@ Follow the steps below to build and release a new version of Xcodes.app. For any
```sh
# 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.
# Question: Did anything in XPCHelper change?
# - 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.
@ -77,17 +146,20 @@ scripts/package_release.sh
# Notarize the app
# Do this from the Product directory so the app is zipped without being nested inside Product
# Create a app specific password on appleid.apple.com if you haven't already
# % xcrun altool --store-password-in-keychain-item "AC_PASSWORD" -u "<appleiduseremail>" -p <app_specific_secret>
# xcrun notarytool store-credentials "AC_PASSWORD" \
# --apple-id "test@example.com" \
# --team-id "teamid" \
# --password "app specific password"
pushd Product
../scripts/notarize.sh "test@example.com" "@keychain:AC_PASSWORD" <MyOrg> Xcodes.zip
../scripts/notarize.sh Xcodes.zip <MYORG>
# Sign the .zip for Sparkle, note the signature in the output for later
# If you're warned about the signing key not being found, see the Xcodes 1Password vault for the key and installation instructions.
../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
@ -98,14 +170,13 @@ 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>
## Contact
## Maintainers
<a href="http://www.robotsandpencils.com"><img src="R&PLogo.png" width="153" height="74" /></a>
[Matt Kiazyk](https://github.com/mattkiazyk) - [Twitter](https://www.twitter.com/mattkiazyk)
Made with ❤️ by [Robots & Pencils](http://www.robotsandpencils.com)
[Twitter](https://twitter.com/robotsNpencils) | [GitHub](https://github.com/robotsandpencils)
[Twitter](https://twitter.com/xcodesApp) | [GitHub](https://github.com/xcodesOrg) | [Mastadon](https://iosdev.space/@XcodesApp) |

View 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

View file

@ -17,46 +17,39 @@
# https://developer.apple.com/documentation/xcode/notarizing_macos_software_before_distribution/customizing_the_notarization_workflow
# Adapted from https://github.com/keybase/client/blob/46f5df0aa64ff19198ba7b044bbb7cd907c0be9f/packaging/desktop/package_darwin.sh
username="$1"
password="$2"
asc_provider="$3"
file="$4"
file="$1"
team_id="$2"
echo "Uploading to notarization service"
uuid=$(xcrun altool \
--notarize-app \
--primary-bundle-id "com.robotsandpencils.XcodesApp.zip" \
--username "$username" \
--password "$password" \
--asc-provider "$asc_provider" \
--file "$file" 2>&1 | \
grep 'RequestUUID' | \
awk '{ print $3 }')
result=$(xcrun notarytool submit "$file" \
--keychain-profile "AC_PASSWORD" \
--team-id "$team_id" \
--wait)
# echo "done1"
echo $result
# My grep/awk is bad and I can't figure out how to get the UUID out properly
# uuid=$("$result" | \
# grep 'id:' | tail -n1 | \
# cut -d":" -f2-)
echo "Successfully uploaded to notarization service, polling for result: $uuid"
sleep 15
while :
do
fullstatus=$(xcrun altool \
--notarization-info "$uuid" \
--username "$username" \
--password "$password" \
--asc-provider "$asc_provider" 2>&1)
status=$(echo "$fullstatus" | grep 'Status\:' | awk '{ print $2 }')
if [ "$status" = "success" ]; then
echo "Notarization success"
exit 0
elif [ "$status" = "in" ]; then
echo "Notarization still in progress, sleeping for 15 seconds and trying again"
sleep 15
else
echo "Notarization failed, full status below"
echo "$fullstatus"
exit 1
fi
done
# we should check here using the info (or notarytool log) to check the results and log
#
# fullstatus=$(xcrun notarytool info "$uuid" \
# --keychain-profile "AC_PASSWORD" 2>&1)
# status=$(echo "$fullstatus" | grep 'status\:' | awk '{ print $2 }')
# if [ "$status" = "Accepted" ]; then
# echo "Notarization success"
# exit 0
# else
# echo "Notarization failed, full status below"
# echo "$fullstatus"
# exit 1
# fi
# Remove .zip
rm $file
@ -66,4 +59,4 @@ app_path="$(basename -s ".zip" "$file").app"
xcrun stapler staple "$app_path"
# Zip the stapled app for distribution
ditto -c -k --sequesterRsrc --keepParent "$file" "$app_path"
ditto -c -k --sequesterRsrc --keepParent "$app_path" "$file"

View file

@ -10,6 +10,9 @@ PROJECT_DIR=$(pwd)/$PROJECT_NAME/Resources
SCRIPTS_DIR=$(pwd)/Scripts
INFOPLIST_FILE="Info.plist"
# If needed ensure that the unxip binary is signed with a hardened runtime so we can notarize
# codesign --force --options runtime --sign "Developer ID Application: Robots and Pencils Inc." $PROJECT_DIR/unxip
# Ensure a clean build
rm -rf Archive/*
rm -rf Product/*

View file

@ -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

View file

@ -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": "891afd44c7075e699924ed9b81d8dc94a5111dfd",
"version": "1.24.0-spm"
"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
}
},
{

View file

@ -44,6 +44,7 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = "en"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"

View file

@ -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>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View file

@ -1,4 +1,4 @@
// swift-tools-version:5.3
// swift-tools-version:5.4
import PackageDescription
@ -14,7 +14,7 @@ let package = Package(
dependencies: [
],
targets: [
.target(
.executableTarget(
name: "AcknowledgementsGenerator",
dependencies: []
),

View file

@ -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
}
}

View file

@ -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"]),

View file

@ -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,17 +238,54 @@ 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
public func validateSession() -> AnyPublisher<Void, Error> {
return Current.network.dataTask(with: URLRequest.olympusSession)
.map(\.data)
.tryMap { result -> Data in
let httpResponse = result.response as! HTTPURLResponse
if httpResponse.statusCode == 401 {
throw AuthenticationError.notAuthorized
}
return result.data
}
.decode(type: AppleSession.self, decoder: JSONDecoder())
.tryMap { session in
if session.provider == nil {
throw AuthenticationError.notDeveloperAppleId
}
// A user that is a non-paid Apple Developer will have a provider == nil
// Those users can still download Xcode.
// Non Apple Developers will get caught in the download as invalid
// if session.provider == nil {
// throw AuthenticationError.notDeveloperAppleId
// }
}
.eraseToAnyPublisher()
}
@ -167,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
@ -180,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?)
@ -189,11 +365,16 @@ public enum AuthenticationError: Swift.Error, LocalizedError, Equatable {
case accountLocked(String)
case badStatusCode(statusCode: Int, data: Data, response: HTTPURLResponse)
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:
@ -217,6 +398,12 @@ public enum AuthenticationError: Swift.Error, LocalizedError, Equatable {
return "Received an unexpected status code: \(statusCode). If you continue to have problems, please submit a bug report in the Help menu."
case .notDeveloperAppleId:
return "You are not registered as an Apple Developer. Please visit Apple Developer Registration. https://developer.apple.com/register/"
case .notAuthorized:
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"
}
}
}
@ -257,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 {
@ -285,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
}
@ -347,7 +546,7 @@ public struct AuthOptionsResponse: Equatable, Decodable {
}
public enum Kind: Equatable {
case twoStep, twoFactor, unknown
case twoStep, twoFactor, securityKey, unknown
}
}
@ -384,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()
}
}

View 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
}
}

View file

@ -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
}

View file

@ -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),
]
}

View file

@ -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
@ -24,10 +26,10 @@ extension AppState {
if autoInstallType == .newestBeta {
Logger.appState.info("Auto installing newest Xcode Beta")
// install it, as user doesn't have it installed and it's either latest beta or latest release
install(id: newestXcode.id)
checkMinVersionAndInstall(id: newestXcode.id)
} else if autoInstallType == .newestVersion && newestXcode.version.isNotPrerelease {
Logger.appState.info("Auto installing newest Xcode")
install(id: newestXcode.id)
checkMinVersionAndInstall(id: newestXcode.id)
} else {
Logger.appState.info("No new Xcodes version found to auto install")
}
@ -40,11 +42,11 @@ extension AppState {
}
private func install(_ installationType: InstallationType, downloader: Downloader, attemptNumber: Int) -> AnyPublisher<InstalledXcode, Error> {
// We need to check if the Apple ID that is logged in is an Apple Developer
// Since users can use xcodereleases, we don't check for Apple ID on a xcode list refresh
// If the Apple Id is not a developer, the download action will try and download a xip that is invalid, causing a `xcode13.0.xip is damaged and can't be expanded.`
Logger.appState.info("Using \(downloader) downloader")
setupDockProgress()
return validateSession()
.flatMap { _ in
self.getXcodeArchive(installationType, downloader: downloader)
@ -53,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() }
@ -88,22 +92,24 @@ extension AppState {
private func getXcodeArchive(_ installationType: InstallationType, downloader: Downloader) -> AnyPublisher<(AvailableXcode, URL), Error> {
switch installationType {
case .version(let availableXcode):
if let installedXcode = Current.files.installedXcodes(Path.root/"Applications").first(where: { $0.version.isEquivalent(to: availableXcode.version) }) {
if let installedXcode = Current.files.installedXcodes(Path.installDirectory).first(where: { $0.version.isEquivalent(to: availableXcode.version) }) {
return Fail(error: InstallationError.versionAlreadyInstalled(installedXcode))
.eraseToAnyPublisher()
}
return downloadXcode(availableXcode: availableXcode, downloader: downloader)
}
}
private func downloadXcode(availableXcode: AvailableXcode, downloader: Downloader) -> AnyPublisher<(AvailableXcode, URL), Error> {
downloadOrUseExistingArchive(for: availableXcode, downloader: downloader, progressChanged: { [unowned self] progress in
DispatchQueue.main.async {
self.setInstallationStep(of: availableXcode.version, to: .downloading(progress: progress))
}
})
.map { return (availableXcode, $0) }
.eraseToAnyPublisher()
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) }
.eraseToAnyPublisher()
}
public func downloadOrUseExistingArchive(for availableXcode: AvailableXcode, downloader: Downloader, progressChanged: @escaping (Progress) -> Void) -> AnyPublisher<URL, Error> {
@ -152,6 +158,7 @@ extension AppState {
cookies
)
progressChanged(progress)
return publisher
.map { _ in destination.url }
.eraseToAnyPublisher()
@ -166,6 +173,7 @@ extension AppState {
to: destination.url,
resumingWith: resumeData ?? persistedResumeData)
progressChanged(progress)
return publisher
.map { $0.saveLocation }
.eraseToAnyPublisher()
@ -177,8 +185,11 @@ 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.root.join("Applications").join("Xcode-\(availableXcode.version.descriptionWithoutBuildMetadata).app").url
let destinationURL = Path.installDirectory.join("Xcode-\(availableXcode.version.descriptionWithoutBuildMetadata).app").url
switch archiveURL.pathExtension {
case "xip":
return unarchiveAndMoveXIP(availableXcode: availableXcode, at: archiveURL, to: destinationURL)
@ -214,8 +225,9 @@ extension AppState {
.handleEvents(receiveCompletion: { [unowned self] completion in
if case let .failure(error) = completion {
self.error = error
self.presentedAlert = .generic(title: "Unable to install archived Xcode", message: error.legibleLocalizedDescription)
self.presentedAlert = .generic(title: localizeString("Alert.InstallArchive.Error.Title"), message: error.legibleLocalizedDescription)
}
resetDockProgressTracking()
})
.catch { _ in
Just(installedXcode)
@ -238,8 +250,8 @@ extension AppState {
func unarchiveAndMoveXIP(availableXcode: AvailableXcode, at source: URL, to destination: URL) -> AnyPublisher<URL, Swift.Error> {
self.setInstallationStep(of: availableXcode.version, to: .unarchiving)
return Current.shell.unxip(source)
return unxipOrUnxipExperiment(source)
.catch { error -> AnyPublisher<ProcessOutput, Swift.Error> in
if let executionError = error as? ProcessExecutionError {
if executionError.standardError.contains("damaged and cant be expanded") {
@ -278,6 +290,16 @@ extension AppState {
})
.eraseToAnyPublisher()
}
func unxipOrUnxipExperiment(_ source: URL) -> AnyPublisher<ProcessOutput, Error> {
if unxipExperiment {
// All hard work done by https://github.com/saagarjha/unxip
// Compiled to binary with `swiftc -parse-as-library -O unxip.swift`
return Current.shell.unxipExperiment(source)
} else {
return Current.shell.unxip(source)
}
}
public func verifySecurityAssessment(of xcode: InstalledXcode) -> AnyPublisher<Void, Error> {
return Current.shell.spctlAssess(xcode.path.url)
@ -351,7 +373,7 @@ extension AppState {
receiveCompletion: { completion in
if case let .failure(error) = completion {
self.error = error
self.presentedAlert = .generic(title: "Unable to perform post install steps", message: error.legibleLocalizedDescription)
self.presentedAlert = .generic(title: localizeString("Alert.PostInstall.Title"), message: error.legibleLocalizedDescription)
}
},
receiveValue: {}
@ -406,6 +428,9 @@ extension AppState {
}
self.presentedAlert = .privilegedHelper
}
unxipProgress.completedUnitCount = AppState.totalProgressUnits
resetDockProgressTracking()
return helperInstallConsentSubject
.flatMap {
@ -446,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)
}
}
}
}
@ -492,60 +550,41 @@ public enum InstallationError: LocalizedError, Equatable {
public var errorDescription: String? {
switch self {
case .damagedXIP(let url):
return "The archive \"\(url.lastPathComponent)\" is damaged and can't be expanded."
return String(format: localizeString("InstallationError.DamagedXIP"), url.lastPathComponent)
case let .notEnoughFreeSpaceToExpandArchive(archivePath, version):
return """
The archive \(archivePath.basename()) cant be expanded because the current volume doesnt have enough free space.
Make more space available to expand the archive and then install Xcode \(version.appleDescription) again to start installation from where it left off.
"""
return String(format: localizeString("InstallationError.NotEnoughFreeSpaceToExpandArchive"), archivePath.basename(), version.appleDescription)
case .failedToMoveXcodeToApplications:
return "Failed to move Xcode to the /Applications directory."
return String(format: localizeString("InstallationError.FailedToMoveXcodeToApplications"), Path.installDirectory.string)
case .failedSecurityAssessment(let xcode, let output):
return """
Xcode \(xcode.version) failed its security assessment with the following output:
\(output)
It remains installed at \(xcode.path) if you wish to use it anyways.
"""
return String(format: localizeString("InstallationError.FailedSecurityAssessment"), String(xcode.version), output, xcode.path.string)
case .codesignVerifyFailed(let output):
return """
The downloaded Xcode failed code signing verification with the following output:
\(output)
"""
return String(format: localizeString("InstallationError.CodesignVerifyFailed"), output)
case .unexpectedCodeSigningIdentity(let identity, let certificateAuthority):
return """
The downloaded Xcode doesn't have the expected code signing identity.
Got:
\(identity)
\(certificateAuthority)
Expected:
\(XcodeTeamIdentifier)
\(XcodeCertificateAuthority)
"""
return String(format: localizeString("InstallationError.UnexpectedCodeSigningIdentity"), identity, certificateAuthority, XcodeTeamIdentifier, XcodeCertificateAuthority)
case .unsupportedFileFormat(let fileExtension):
return "xcodes doesn't (yet) support installing Xcode from the \(fileExtension) file format."
return String(format: localizeString("InstallationError.UnsupportedFileFormat"), fileExtension)
case .missingSudoerPassword:
return "Missing password. Please try again."
return localizeString("InstallationError.MissingSudoerPassword")
case let .unavailableVersion(version):
return "Could not find version \(version.appleDescription)."
return String(format: localizeString("InstallationError.UnavailableVersion"), version.appleDescription)
case .noNonPrereleaseVersionAvailable:
return "No non-prerelease versions available."
return localizeString("InstallationError.NoNonPrereleaseVersionAvailable")
case .noPrereleaseVersionAvailable:
return "No prerelease versions available."
return localizeString("InstallationError.NoPrereleaseVersionAvailable")
case .missingUsernameOrPassword:
return "Missing username or a password. Please try again."
return localizeString("InstallationError.MissingUsernameOrPassword")
case let .versionAlreadyInstalled(installedXcode):
return "\(installedXcode.version.appleDescription) is already installed at \(installedXcode.path)"
return String(format: localizeString("InstallationError.VersionAlreadyInstalled"), installedXcode.version.appleDescription, installedXcode.path.string)
case let .invalidVersion(version):
return "\(version) is not a valid version number."
return String(format: localizeString("InstallationError.InvalidVersion"), version)
case let .versionNotInstalled(version):
return "\(version.appleDescription) is not installed."
return String(format: localizeString("InstallationError.VersionNotInstalled"), version.appleDescription)
case let .postInstallStepsNotPerformed(version, helperInstallState):
switch helperInstallState {
case .installed:
return "Installation was completed, but some post-install steps weren't performed automatically. These will be performed when you first launch Xcode \(version.appleDescription)."
return String(format: localizeString("InstallationError.PostInstallStepsNotPerformed.Installed"), version.appleDescription)
case .notInstalled, .unknown:
return "Installation was completed, but some post-install steps weren't performed automatically. Xcodes performs these steps with a privileged helper, which appears to not be installed. You can install it from Preferences > Advanced.\n\nThese steps will be performed when you first launch Xcode \(version.appleDescription)."
return String(format: localizeString("InstallationError.PostInstallStepsNotPerformed.NotInstalled"), version.appleDescription)
}
}
}

View 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() }
}
}
}

View file

@ -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)
@ -47,7 +49,7 @@ extension AppState {
// 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 {
self.error = error
self.presentedAlert = .generic(title: "Unable to update selected Xcode", message: error.legibleLocalizedDescription)
self.presentedAlert = .generic(title: localizeString("Alert.Update.Error.Title"), message: error.legibleLocalizedDescription)
}
case .finished:
Current.defaults.setDate(Current.date(), forKey: "lastUpdated")
@ -125,20 +127,40 @@ 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 {
// MARK: - Apple
private func releasedXcodes() -> AnyPublisher<[AvailableXcode], Error> {
private func releasedXcodes() -> AnyPublisher<[AvailableXcode], Swift.Error> {
Current.network.dataTask(with: URLRequest.downloads)
.map(\.data)
.decode(type: Downloads.self, decoder: configure(JSONDecoder()) {
$0.dateDecodingStrategy = .formatted(.downloadsDateModified)
})
.map { downloads -> [AvailableXcode] in
let xcodes = downloads
.downloads
.tryMap { downloads -> [AvailableXcode] in
if downloads.hasError {
throw AuthenticationError.invalidResult(resultString: downloads.resultsString)
}
guard let downloadList = downloads.downloads else {
throw AuthenticationError.invalidResult(resultString: localizeString("DownloadingError"))
}
let xcodes = downloadList
.filter { $0.name.range(of: "^Xcode [0-9]", options: .regularExpression) != nil }
.compactMap { download -> AvailableXcode? in
let urlPrefix = URL(string: "https://download.developer.apple.com/")!
@ -188,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,
@ -210,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

View file

@ -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
@ -17,11 +41,11 @@ class AppState: ObservableObject {
@Published var availableXcodes: [AvailableXcode] = [] {
willSet {
if newValue.count > availableXcodes.count && availableXcodes.count != 0 {
Current.notificationManager.scheduleNotification(title: "New Xcode versions", body: "New Xcode versions are available to download.", category: .normal)
Current.notificationManager.scheduleNotification(title: localizeString("Notification.NewXcodeVersion.Title"), body: localizeString("Notification.NewXcodeVersion.Body"), category: .normal)
}
updateAllXcodes(
availableXcodes: newValue,
installedXcodes: Current.files.installedXcodes(Path.root/"Applications"),
installedXcodes: Current.files.installedXcodes(Path.installDirectory),
selectedXcodePath: selectedXcodePath
)
}
@ -34,7 +58,7 @@ class AppState: ObservableObject {
willSet {
updateAllXcodes(
availableXcodes: availableXcodes,
installedXcodes: Current.files.installedXcodes(Path.root/"Applications"),
installedXcodes: Current.files.installedXcodes(Path.installDirectory),
selectedXcodePath: newValue
)
}
@ -43,9 +67,9 @@ class AppState: ObservableObject {
var isUpdating: Bool { updatePublisher != nil }
@Published var presentedSheet: XcodesSheet? = nil
@Published var isProcessingAuthRequest = false
@Published var secondFactorData: SecondFactorData?
@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,14 +86,85 @@ class AppState: ObservableObject {
Current.defaults.set(localPath, forKey: "localPath")
}
}
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")
}
}
var createSymLinkOnSelectDisabled: Bool {
return onSelectActionType == .rename || PreferenceKey.createSymLinkOnSelect.isManaged()
}
@Published var onSelectActionType = SelectedActionType.none {
didSet {
Current.defaults.set(onSelectActionType.rawValue, forKey: "onSelectActionType")
if onSelectActionType == .rename {
createSymLinkOnSelect = false
}
}
}
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 {
@ -84,11 +179,23 @@ class AppState: ObservableObject {
savedUsername != nil
}
var bottomStatusBarMessage: String {
let formatter = DateFormatter()
formatter.dateFormat = "dd/MM/yyyy"
let finishDate = formatter.date(from: "11/06/2022")
if Date().compare(finishDate!) == .orderedAscending {
return String(format: localizeString("WWDC.Message"), "2022")
}
return ""
}
// MARK: - Init
init() {
guard !isTesting else { return }
try? loadCachedAvailableXcodes()
try? loadCacheDownloadableRuntimes()
checkIfHelperIsInstalled()
setupAutoInstallTimer()
setupDefaults()
@ -96,13 +203,19 @@ class AppState: ObservableObject {
func setupDefaults() {
localPath = Current.defaults.string(forKey: "localPath") ?? Path.defaultXcodesApplicationSupport.string
unxipExperiment = Current.defaults.bool(forKey: "unxipExperiment") ?? false
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
@ -111,7 +224,28 @@ class AppState: ObservableObject {
}
// MARK: - Authentication
func validateADCSession(path: String) -> AnyPublisher<Void, Error> {
return Current.network.dataTask(with: URLRequest.downloadADCAuth(path: path))
.receive(on: DispatchQueue.main)
.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()
.receive(on: DispatchQueue.main)
.handleEvents(receiveCompletion: { completion in
@ -144,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 }
@ -157,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
@ -172,12 +306,17 @@ class AppState: ObservableObject {
}
func handleTwoFactorOption(_ option: TwoFactorOption, authOptions: AuthOptionsResponse, serviceKey: String, sessionID: String, scnt: String) {
self.secondFactorData = SecondFactorData(
option: option,
authOptions: authOptions,
sessionData: AppleSessionData(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt)
)
self.presentedSheet = .twoFactor
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) {
@ -200,7 +339,11 @@ class AppState: ObservableObject {
}
func choosePhoneNumberForSMS(authOptions: AuthOptionsResponse, sessionData: AppleSessionData) {
secondFactorData = SecondFactorData(option: .smsPendingChoice, authOptions: authOptions, sessionData: sessionData)
self.presentedSheet = .twoFactor(.init(
option: .smsPendingChoice,
authOptions: authOptions,
sessionData: sessionData
))
}
func submitSecurityCode(_ code: SecurityCode, sessionData: AppleSessionData) {
@ -219,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):
@ -230,7 +450,6 @@ class AppState: ObservableObject {
switch self.authenticationState {
case .authenticated, .unauthenticated, .notAppleDeveloper:
self.presentedSheet = nil
self.secondFactorData = nil
case let .waitingForSecondFactor(option, authOptions, sessionData):
self.handleTwoFactorOption(option, authOptions: authOptions, serviceKey: sessionData.serviceKey, sessionID: sessionData.sessionID, scnt: sessionData.scnt)
}
@ -270,7 +489,7 @@ class AppState: ObservableObject {
receiveCompletion: { [unowned self] completion in
if case let .failure(error) = completion {
self.error = error
self.presentedAlert = .generic(title: "Unable to install helper", message: error.legibleLocalizedDescription)
self.presentedAlert = .generic(title: localizeString("Alert.PrivilegedHelper.Error.Title"), message: error.legibleLocalizedDescription)
}
},
receiveValue: {}
@ -305,10 +524,42 @@ class AppState: ObservableObject {
// MARK: - Install
func install(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
if let requiredMacOSVersion = availableXcode.requiredMacOSVersion {
if hasMinSupportedOS(requiredMacOSVersion: requiredMacOSVersion) {
// prompt
self.presentedAlert = .checkMinSupportedVersion(xcode: availableXcode, macOS: ProcessInfo.processInfo.operatingSystemVersion.versionString())
return
}
}
switch self.dataSource {
case .apple:
install(id: id)
case .xcodeReleases:
install(id: id)
}
}
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,
@ -337,12 +588,22 @@ class AppState: ObservableObject {
// We need the cookies from its response in order to download Xcodes though,
// so perform it here first just to be sure.
Current.network.dataTask(with: URLRequest.downloads)
.receive(on: DispatchQueue.main)
.map { _ in Void() }
.map(\.data)
.decode(type: Downloads.self, decoder: configure(JSONDecoder()) {
$0.dateDecodingStrategy = .formatted(.downloadsDateModified)
})
.tryMap { downloads -> Void in
if downloads.hasError {
throw AuthenticationError.invalidResult(resultString: downloads.resultsString)
}
if downloads.downloads == nil {
throw AuthenticationError.invalidResult(resultString: localizeString("DownloadingError"))
}
}
.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(
@ -350,9 +611,13 @@ 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 = .generic(title: "Unable to install Xcode", message: error.legibleLocalizedDescription)
self.presentedAlert = .unauthenticated
} else if error as? AuthenticationError != .invalidSession {
self.error = error
self.presentedAlert = .generic(title: localizeString("Alert.Install.Error.Title"), message: error.legibleLocalizedDescription)
}
if let index = self.allXcodes.firstIndex(where: { $0.id == id }) {
self.allXcodes[index].installState = .notInstalled
@ -363,11 +628,38 @@ 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.xcodeID == id }) else { return }
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
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 {
self.error = error
self.presentedAlert = .generic(title: localizeString("Alert.Install.Error.Title"), message: error.legibleLocalizedDescription)
}
if let index = self.allXcodes.firstIndex(where: { $0.id == id }) {
self.allXcodes[index].installState = .notInstalled
}
}
},
receiveValue: { _ in }
)
}
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.
@ -396,7 +688,7 @@ class AppState: ObservableObject {
receiveCompletion: { [unowned self] completion in
if case let .failure(error) = completion {
self.error = error
self.presentedAlert = .generic(title: "Unable to uninstall Xcode", message: error.legibleLocalizedDescription)
self.presentedAlert = .generic(title: localizeString("Alert.Uninstall.Error.Title"), message: error.legibleLocalizedDescription)
}
self.uninstallPublisher = nil
},
@ -404,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) {
@ -438,10 +730,15 @@ class AppState: ObservableObject {
}
guard
let installedXcodePath = xcode.installedPath,
var installedXcodePath = xcode.installedPath,
selectPublisher == nil
else { return }
if onSelectActionType == .rename {
guard let newDestinationXcodePath = renameToXcode(xcode: xcode) else { return }
installedXcodePath = newDestinationXcodePath
}
selectPublisher = installHelperIfNecessary()
.flatMap {
Current.helper.switchXcodePath(installedXcodePath.string)
@ -453,7 +750,11 @@ class AppState: ObservableObject {
receiveCompletion: { [unowned self] completion in
if case let .failure(error) = completion {
self.error = error
self.presentedAlert = .generic(title: "Unable to select Xcode", message: error.legibleLocalizedDescription)
self.presentedAlert = .generic(title: localizeString("Alert.Select.Error.Title"), message: error.legibleLocalizedDescription)
} else {
if self.createSymLinkOnSelect {
createSymbolicLink(xcode: xcode)
}
}
self.selectPublisher = nil
},
@ -461,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
}
}
@ -479,6 +785,79 @@ class AppState: ObservableObject {
NSPasteboard.general.setString(installedXcodePath.string, forType: .string)
}
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)
}
func createSymbolicLink(xcode: Xcode, isBeta: Bool = false) {
guard let installedXcodePath = xcode.installedPath else { return }
let destinationPath: Path = Path.installDirectory/"Xcode\(isBeta ? "-Beta" : "").app"
// does an Xcode.app file exist?
if FileManager.default.fileExists(atPath: destinationPath.string) {
do {
// if it's not a symlink, error because we don't want to delete an actual xcode.app file
let attributes: [FileAttributeKey : Any]? = try? FileManager.default.attributesOfItem(atPath: destinationPath.string)
if attributes?[.type] as? FileAttributeType == FileAttributeType.typeSymbolicLink {
try FileManager.default.removeItem(atPath: destinationPath.string)
Logger.appState.info("Successfully deleted old symlink")
} else {
self.presentedAlert = .generic(title: localizeString("Alert.SymLink.Title"), message: localizeString("Alert.SymLink.Message"))
return
}
} catch {
self.presentedAlert = .generic(title: localizeString("Alert.SymLink.Title"), message: error.localizedDescription)
}
}
do {
try FileManager.default.createSymbolicLink(atPath: destinationPath.string, withDestinationPath: installedXcodePath.string)
Logger.appState.info("Successfully created symbolic link with Xcode\(isBeta ? "-Beta": "").app")
} catch {
Logger.appState.error("Unable to create symbolic Link")
self.error = error
self.presentedAlert = .generic(title: localizeString("Alert.SymLink.Title"), message: error.legibleLocalizedDescription)
}
}
func renameToXcode(xcode: Xcode) -> Path? {
guard let installedXcodePath = xcode.installedPath else { return nil }
let destinationPath: Path = Path.installDirectory/"Xcode.app"
// rename any old named `Xcode.app` to the Xcodes versioned named files
if FileManager.default.fileExists(atPath: destinationPath.string) {
if let originalXcode = Current.files.installedXcode(destination: destinationPath) {
let newName = "Xcode-\(originalXcode.version.descriptionWithoutBuildMetadata).app"
Logger.appState.debug("Found Xcode.app - renaming back to \(newName)")
do {
try destinationPath.rename(to: newName)
} catch {
Logger.appState.error("Unable to create rename Xcode.app back to original")
self.error = error
// TODO UPDATE MY ERROR STRING
self.presentedAlert = .generic(title: localizeString("Alert.SymLink.Title"), message: error.legibleLocalizedDescription)
}
}
}
// rename passed in xcode to xcode.app
Logger.appState.debug("Found Xcode.app - renaming back to Xcode.app")
do {
return try installedXcodePath.rename(to: "Xcode.app")
} catch {
Logger.appState.error("Unable to create rename Xcode.app back to original")
self.error = error
// TODO UPDATE MY ERROR STRING
self.presentedAlert = .generic(title: localizeString("Alert.SymLink.Title"), message: error.legibleLocalizedDescription)
}
return nil
}
func updateAllXcodes(availableXcodes: [AvailableXcode], installedXcodes: [InstalledXcode], selectedXcodePath: String?) {
var adjustedAvailableXcodes = availableXcodes
@ -489,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
@ -497,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
}
}
}
@ -514,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 &&
@ -531,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
@ -552,7 +938,8 @@ class AppState: ObservableObject {
releaseDate: availableXcode.releaseDate,
sdks: availableXcode.sdks,
compilers: availableXcode.compilers,
downloadFileSize: availableXcode.fileSize
downloadFileSize: availableXcode.fileSize,
architectures: availableXcode.architectures
)
}
@ -574,6 +961,7 @@ class AppState: ObservableObject {
self.allXcodes = newAllXcodes.sorted { $0.version > $1.version }
}
// MARK: - Private
@ -607,10 +995,10 @@ class AppState: ObservableObject {
var message: String
var id: String { title + message }
}
}
struct SecondFactorData {
let option: TwoFactorOption
let authOptions: AuthOptionsResponse
let sessionData: AppleSessionData
extension OperatingSystemVersion {
func versionString() -> String {
return String(majorVersion) + "." + String(minorVersion) + "." + String(patchVersion)
}
}

View file

@ -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,6 +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,
@ -24,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
@ -35,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)
}
}

View file

@ -0,0 +1,17 @@
//
// Collection+.swift
// Xcodes
//
// Created by Matt Kiazyk on 2022-04-11.
// Copyright © 2022 Robots and Pencils. All rights reserved.
//
import Foundation
public extension Collection {
/// Returns the element at the specified index iff it is within bounds, otherwise nil.
subscript (safe index: Index) -> Element? {
return indices.contains(index) ? self[index] : nil
}
}

View file

@ -14,4 +14,6 @@ public enum DataSource: String, CaseIterable, Identifiable, CustomStringConverti
case .xcodeReleases: return "Xcode Releases"
}
}
var isManaged: Bool { PreferenceKey.dataSource.isManaged() }
}

View file

@ -5,6 +5,8 @@ extension DateFormatter {
static let downloadsDateModified: 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
}()

View file

@ -13,4 +13,6 @@ public enum Downloader: String, CaseIterable, Identifiable, CustomStringConverti
case .aria2: return "aria2"
}
}
var isManaged: Bool { PreferenceKey.downloader.isManaged() }
}

View file

@ -3,7 +3,13 @@ import Path
import Version
struct Downloads: Codable {
let downloads: [Download]
let resultCode: Int
let resultsString: String?
let downloads: [Download]?
var hasError: Bool {
return resultCode != 0
}
}
// Set to Int64 as ByteCountFormatter uses it.

View file

@ -1,14 +1,13 @@
import Foundation
import Path
extension Entry {
var isAppBundle: Bool {
kind == .directory &&
extension Path {
static func isAppBundle(path: Path) -> Bool {
path.isDirectory &&
path.extension == "app" &&
!path.isSymlink
}
var infoPlist: InfoPlist? {
static func infoPlist(path: Path) -> InfoPlist? {
let infoPlistPath = path.join("Contents").join("Info.plist")
guard
let infoPlistData = try? Data(contentsOf: infoPlistPath.url),
@ -17,4 +16,12 @@ extension Entry {
return infoPlist
}
var isAppBundle: Bool {
Path.isAppBundle(path: self)
}
var infoPlist: InfoPlist? {
Path.infoPlist(path: self)
}
}

View file

@ -3,7 +3,7 @@ import Foundation
import Path
import AppleAPI
import KeychainAccess
import XcodesKit
/**
Lightweight dependency injection using global mutable state :P
@ -43,7 +43,7 @@ public struct Shell {
"--max-connection-per-server=16",
"--split=16",
"--summary-interval=1",
"--stop-with-process=\(ProcessInfo.processInfo.processIdentifier)",
"--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
@ -111,6 +111,169 @@ 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 {
@ -158,11 +321,26 @@ public struct Files {
}
public var installedXcodes = _installedXcodes
public func installedXcode(destination: Path) -> InstalledXcode? {
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)
}
@ -174,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>) {
@ -238,6 +421,11 @@ public struct Defaults {
public func get(forKey key: String) -> Any? {
get(key)
}
public var bool: (String) -> Bool? = { UserDefaults.standard.bool(forKey: $0) }
public func bool(forKey key: String) -> Bool? {
bool(key)
}
}
private let helperClient = HelperClient()

View file

@ -0,0 +1,23 @@
//
// FileError.swift
// Xcodes
//
// Created by Leon Wolf on 06.10.22.
// Copyright © 2022 Robots and Pencils. All rights reserved.
//
import Foundation
import LegibleError
enum FileError: LocalizedError{
case fileNotFound(_ fileName: String)
}
extension FileError {
var errorDescription: String? {
switch self {
case .fileNotFound(let fileName):
return String(format: localizeString("Alert.Uninstall.Error.Message.FileNotFound"), fileName)
}
}
}

View file

@ -11,7 +11,11 @@ extension FileManager {
@discardableResult
func trashItem(at url: URL) throws -> URL {
var resultingItemURL: NSURL!
try trashItem(at: url, resultingItemURL: &resultingItemURL)
if fileExists(atPath: url.path) {
try trashItem(at: url, resultingItemURL: &resultingItemURL)
} else {
throw FileError.fileNotFound(url.lastPathComponent)
}
return resultingItemURL as URL
}
}
}

View 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"
}
}

View file

@ -361,7 +361,7 @@ enum HelperClientError: LocalizedError {
var errorDescription: String? {
switch self {
case .failedToCreateRemoteObjectProxy:
return "Unable to communicate with privileged helper."
return localizeString("HelperClient.error")
case let .message(message):
return message
}

View file

@ -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 "Downloading"
case .unarchiving:
return "Unarchiving (This can take a while)"
case .moving(let destination):
return "Moving to \(destination)"
case .trashingArchive:
return "Moving archive to the Trash"
case .checkingSecurity:
return "Security verification"
case .finishing:
return "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 }
}

View file

@ -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)
}
}

View file

@ -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")
}

View file

@ -22,9 +22,9 @@ public enum XcodesNotificationType: String, Identifiable, CaseIterable, CustomSt
public var description: String {
switch self {
case .newVersionAvailable:
return "New version is available"
return localizeString("Notification.NewVersionAvailable")
case .finishedInstalling:
return "Finished installing"
return localizeString("Notification.FinishedInstalling")
}
}
}

View file

@ -16,4 +16,31 @@ extension Path {
static var cacheFile: Path {
return xcodesApplicationSupport/"available-xcodes.json"
}
static let defaultInstallDirectory = Path.root/"Applications"
static var installDirectory: Path {
guard let savedInstallDirectory = Current.defaults.string(forKey: "installPath") else {
return defaultInstallDirectory
}
guard let path = Path(savedInstallDirectory) else {
return defaultInstallDirectory
}
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
}
}

View file

@ -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
}

View file

@ -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")
}
}
}

View 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")
}
}
}
}

View file

@ -0,0 +1,31 @@
//
// SelectedActionType.swift
// Xcodes
//
// Created by Matt Kiazyk on 2022-07-24.
// Copyright © 2022 Robots and Pencils. All rights reserved.
//
import Foundation
public enum SelectedActionType: String, CaseIterable, Identifiable, CustomStringConvertible {
case none
case rename
public var id: Self { self }
public static var `default` = SelectedActionType.none
public var description: String {
switch self {
case .none: return localizeString("OnSelectDoNothing")
case .rename: return localizeString("OnSelectRenameXcode")
}
}
public var detailedDescription: String {
switch self {
case .none: return localizeString("OnSelectDoNothingDescription")
case .rename: return localizeString("OnSelectRenameXcodeDescription")
}
}
}

View file

@ -4,6 +4,7 @@ extension URL {
static let download = URL(string: "https://developer.apple.com/download")!
static let downloads = URL(string: "https://developer.apple.com/services-account/QH65B2/downloadws/listDownloads.action")!
static let downloadXcode = URL(string: "https://developer.apple.com/devcenter/download.action")!
static let downloadADCAuth = URL(string: "https://developerservices2.apple.com/services/download")!
}
extension URLRequest {
@ -25,4 +26,13 @@ extension URLRequest {
request.allHTTPHeaderFields?["Accept"] = "*/*"
return request
}
static func downloadADCAuth(path: String) -> URLRequest {
var components = URLComponents(url: .downloadADCAuth, resolvingAgainstBaseURL: false)!
components.queryItems = [URLQueryItem(name: "path", value: path)]
var request = URLRequest(url: components.url!)
request.allHTTPHeaderFields = request.allHTTPHeaderFields ?? [:]
request.allHTTPHeaderFields?["Accept"] = "*/*"
return request
}
}

View file

@ -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

View file

@ -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
}
}
}

View file

@ -1,4 +1,5 @@
import SwiftUI
import XcodesKit
// MARK: - CommandMenu
@ -18,6 +19,7 @@ struct XcodeCommands: Commands {
OpenCommand()
RevealCommand()
CopyPathCommand()
CreateSymbolicLinkCommand()
Divider()
@ -33,18 +35,21 @@ 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.install(id: xcode.id)
appState.checkMinVersionAndInstall(id: xcode.id)
}
}
@ -54,9 +59,10 @@ struct CancelInstallButton: View {
var body: some View {
Button(action: cancelInstall) {
Text("Cancel")
.help("Stop installation")
Label("Cancel", systemImage: "xmark")
}
.help(localizeString("StopInstallation"))
.buttonStyle(.plain)
}
private func cancelInstall() {
@ -65,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?
@ -74,7 +97,7 @@ struct SelectButton: View {
if xcode?.selected == true {
Text("Active")
} else {
Text("Make active")
Text("MakeActive")
}
}
.disabled(xcode?.selected != false)
@ -91,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)
}
}
@ -125,14 +166,14 @@ struct RevealButton: View {
var body: some View {
Button(action: reveal) {
Text("Reveal in Finder")
Text("RevealInFinder")
}
.help("Reveal in Finder")
.help("RevealInFinder")
}
private func reveal() {
guard let xcode = xcode else { return }
appState.reveal(xcode: xcode)
appState.reveal(xcode.installedPath)
}
}
@ -142,9 +183,9 @@ struct CopyPathButton: View {
var body: some View {
Button(action: copyPath) {
Text("Copy Path")
Text("CopyPath")
}
.help("Copy path")
.help("CopyPath")
}
private func copyPath() {
@ -153,6 +194,76 @@ struct CopyPathButton: View {
}
}
struct CopyReleaseNoteButton: View {
let url: URL?
@EnvironmentObject var appState: AppState
var body: some View {
Button(action: copyReleaseNote) {
Text("CopyReleaseNoteURL")
}
.help("CopyReleaseNoteURL")
}
private func copyReleaseNote() {
guard let url = url else { return }
appState.copyReleaseNote(from: url)
}
}
struct CreateSymbolicLinkButton: View {
@EnvironmentObject var appState: AppState
let xcode: Xcode?
var body: some View {
Button(action: createSymbolicLink) {
Text("CreateSymLink")
}
.help("CreateSymLink")
}
private func createSymbolicLink() {
guard let xcode = xcode else { return }
appState.createSymbolicLink(xcode: xcode)
}
}
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?
var body: some View {
Button(action: createSymbolicBetaLink) {
Text("CreateSymLinkBeta")
}
.help("CreateSymLinkBeta")
}
private func createSymbolicBetaLink() {
guard let xcode = xcode else { return }
appState.createSymbolicLink(xcode: xcode, isBeta: true)
}
}
// MARK: - Commands
struct InstallCommand: View {
@ -225,3 +336,15 @@ struct UninstallCommand: View {
.disabled(selectedXcode.unwrapped?.installState.installed != true)
}
}
struct CreateSymbolicLinkCommand: View {
@EnvironmentObject var appState: AppState
@FocusedValue(\.selectedXcode) private var selectedXcode: SelectedXcode?
var body: some View {
CreateSymbolicLinkButton(xcode: selectedXcode.unwrapped)
.keyboardShortcut("s", modifiers: [.command, .option])
.disabled(selectedXcode.unwrapped?.installState.installed != true)
}
}

View file

@ -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 {

View file

@ -12,16 +12,13 @@ struct AboutView: View {
Text(Bundle.main.bundleName!)
.font(.largeTitle)
Text("Version \(Bundle.main.shortVersion!) (\(Bundle.main.version!))")
Color.clear
.frame(width: 300, height: 16)
Text(String(format: localizeString("VersionWithBuild"), Bundle.main.shortVersion!, Bundle.main.version!))
HStack(spacing: 32) {
Button(action: {
openURL(URL(string: "https://github.com/RobotsAndPencils/XcodesApp/")!)
}) {
Label("GitHub Repo", systemImage: "link")
Label("GithubRepo", systemImage: "link")
}
.buttonStyle(LinkButtonStyle())
@ -30,9 +27,36 @@ struct AboutView: View {
}
.buttonStyle(LinkButtonStyle())
}
Text(Bundle.main.humanReadableCopyright!)
.font(.footnote)
Color.clear
.frame(width: 300, height: 0)
Label("UnxipExperiment", systemImage: "lightbulb")
HStack(spacing: 32) {
Button(action: {
openURL(URL(string: "https://github.com/saagarjha/unxip/")!)
}) {
Label("GithubRepo", systemImage: "link")
}
.buttonStyle(LinkButtonStyle())
Button(action: {
openURL(URL(string: "https://github.com/saagarjha/unxip/blob/main/LICENSE")!)
}) {
Label("License", systemImage: "link")
}
.buttonStyle(LinkButtonStyle())
}
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()

View file

@ -10,7 +10,7 @@ struct AcknowledgmentsView: View {
)!
.addingAttribute(.foregroundColor, value: NSColor.labelColor)
)
.frame(minWidth: 500, minHeight: 500)
.frame(minWidth: 600, minHeight: 500)
}
}

View 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)
}
}

View file

@ -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() }
}
@ -46,11 +47,11 @@ public struct ObservingProgressIndicator: View {
isIndeterminate: progress.progress.isIndeterminate,
style: style
)
.help("Downloading: \(Int((progress.progress.fractionCompleted * 100)))% complete")
.help(String(format: localizeString("DownloadingPercentDescription"), Int((progress.progress.fractionCompleted * 100))))
if showsAdditionalDescription, progress.progress.xcodesLocalizedDescription.isEmpty == false {
Text(progress.progress.xcodesLocalizedDescription)
.font(.subheadline)
.font(.subheadline.monospacedDigit())
.foregroundColor(.secondary)
}
}
@ -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)
}

View file

@ -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)
}
}

View 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)
)
}
}

View 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() }
}

View file

@ -1,15 +1,36 @@
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 {
case .cancelInstall: return 1
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
}
}
}

View file

@ -1,8 +1,43 @@
import Foundation
import AppleAPI
enum XcodesSheet: Identifiable {
case signIn
case twoFactor
case twoFactor(SecondFactorData)
case securityKeyTouchToConfirm
var id: Int { hashValue }
var id: Int { Kind(self).hashValue }
struct SecondFactorData {
let option: TwoFactorOption
let authOptions: AuthOptionsResponse
let sessionData: AppleSessionData
}
}
extension XcodesSheet {
private enum Kind: Hashable {
case signIn, twoFactor(TwoFactorOption), securityKeyTouchToConfirm
enum TwoFactorOption {
case smsSent
case codeSent
case smsPendingChoice
case securityKeyPin
}
init(_ sheet: XcodesSheet) {
switch sheet {
case .signIn: self = .signIn
case .twoFactor(let data):
switch data.option {
case .smsSent: self = .twoFactor(.smsSent)
case .smsPendingChoice: self = .twoFactor(.smsPendingChoice)
case .codeSent: self = .twoFactor(.codeSent)
case .securityKey: self = .twoFactor(.securityKeyPin)
}
case .securityKeyTouchToConfirm: self = .securityKeyTouchToConfirm
}
}
}
}

View 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())
}

View 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()
}

View 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()
}
}

View 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()
}

View 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()
}

View file

@ -1,364 +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("Reveal in Finder")
}
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("Identical Builds")
Image(systemName: "square.fill.on.square.fill")
.foregroundColor(.secondary)
.accessibility(hidden: true)
.help("Sometimes a prerelease and release version are the exact same build. Xcodes will automatically display these versions together.")
}
.font(.headline)
ForEach(xcode.identicalBuilds, id: \.description) { version in
Text("\(version.appleDescription)")
.font(.subheadline)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.accessibilityElement()
.accessibility(label: Text("Identical Builds"))
.accessibility(value: Text(xcode.identicalBuilds.map(\.appleDescription).joined(separator: ", ")))
.accessibility(hint: Text("Sometimes a prerelease and release version are the exact same build. Xcodes will automatically display these versions together."))
} else {
EmptyView()
}
}
@ViewBuilder
private func releaseDate(for xcode: Xcode) -> some View {
if let releaseDate = xcode.releaseDate {
VStack(alignment: .leading) {
Text("Release Date")
.font(.headline)
.frame(maxWidth: .infinity, alignment: .leading)
Text(DateFormatter.downloadsReleaseDate.string(from: releaseDate))
.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("Release Notes", systemImage: "link")
}
.buttonStyle(LinkButtonStyle())
.frame(maxWidth: .infinity, alignment: .leading)
.help("View Release Notes")
} else {
EmptyView()
}
}
@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("Requires macOS \(requiredMacOSVersion) or later")
.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("Download Size")
.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("No Xcode Selected")
.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

View 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()
}

View file

@ -1,11 +1,12 @@
import SwiftUI
import XcodesKit
struct InstallationStepDetailView: View {
let installationStep: InstallationStep
let installationStep: XcodeInstallationStep
var body: some View {
VStack(alignment: .leading, spacing: 0) {
Text("Step \(installationStep.stepNumber) of \(installationStep.stepCount): \(installationStep.message)")
Text(String(format: localizeString("InstallationStepDescription"), installationStep.stepNumber, installationStep.stepCount, installationStep.message))
switch installationStep {
case let .downloading(progress):
@ -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()
}

View 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
)

View 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()
}

View 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()
}

View 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()
}

View 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()
}

View file

@ -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
)
}

View 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()
}

View 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()
}

View file

@ -1,5 +1,8 @@
import ErrorHandling
import SwiftUI
import XcodesKit
import Path
import Version
struct MainWindow: View {
@EnvironmentObject var appState: AppState
@ -12,31 +15,58 @@ 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("Uninstall Xcode \(xcode.description)?"),
message: Text("It will be moved to the Trash, but won't be emptied."),
Alert(title: Text(String(format: localizeString("Alert.Uninstall.Title"), xcode.description)),
message: Text("Alert.Uninstall.Message"),
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)
.frame(minWidth: 600, maxWidth: .infinity, minHeight: 300, maxHeight: .infinity)
.emittingError($appState.error, recoveryHandler: { _ in })
@ -45,8 +75,11 @@ struct MainWindow: View {
case .signIn:
signInView()
.environmentObject(appState)
case .twoFactor:
secondFactorView(appState.secondFactorData!)
case .twoFactor(let secondFactorData):
secondFactorView(secondFactorData)
.environmentObject(appState)
case .securityKeyTouchToConfirm:
SignInSecurityKeyTouchView(isPresented: $appState.presentedSheet.isNotNil)
.environmentObject(appState)
}
}
@ -57,24 +90,30 @@ 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("Updated at \(lastUpdated, style: .date) \(lastUpdated, style: .time)")
return Text("\(localizeString("UpdatedAt")) \(lastUpdated, style: .date) \(lastUpdated, style: .time)")
} else {
return Text("")
}
}
@ViewBuilder
private func secondFactorView(_ secondFactorData: AppState.SecondFactorData) -> some View {
private func secondFactorView(_ secondFactorData: XcodesSheet.SecondFactorData) -> some View {
switch secondFactorData.option {
case .codeSent:
SignIn2FAView(isPresented: $appState.secondFactorData.isNotNil, authOptions: secondFactorData.authOptions, sessionData: secondFactorData.sessionData)
SignIn2FAView(isPresented: $appState.presentedSheet.isNotNil, authOptions: secondFactorData.authOptions, sessionData: secondFactorData.sessionData)
case .smsSent(let trustedPhoneNumber):
SignInSMSView(isPresented: $appState.secondFactorData.isNotNil, trustedPhoneNumber: trustedPhoneNumber, authOptions: secondFactorData.authOptions, sessionData: secondFactorData.sessionData)
SignInSMSView(isPresented: $appState.presentedSheet.isNotNil, trustedPhoneNumber: trustedPhoneNumber, authOptions: secondFactorData.authOptions, sessionData: secondFactorData.sessionData)
case .smsPendingChoice:
SignInPhoneListView(isPresented: $appState.secondFactorData.isNotNil, authOptions: secondFactorData.authOptions, sessionData: secondFactorData.sessionData)
SignInPhoneListView(isPresented: $appState.presentedSheet.isNotNil, authOptions: secondFactorData.authOptions, sessionData: secondFactorData.sessionData)
case .securityKey:
SignInSecurityKeyPinView(isPresented: $appState.presentedSheet.isNotNil, authOptions: secondFactorData.authOptions, sessionData: secondFactorData.sessionData)
}
}
@ -101,10 +140,10 @@ struct MainWindow: View {
switch alertType {
case let .cancelInstall(xcode):
return Alert(
title: Text("Are you sure you want to stop the installation of Xcode \(xcode.description)?"),
message: Text("Any progress will be discarded."),
title: Text(String(format: localizeString("Alert.CancelInstall.Title"), xcode.description)),
message: Text("Alert.CancelInstall.Message"),
primaryButton: .destructive(
Text("Stop Installation"),
Text("Alert.CancelInstall.PrimaryButton"),
action: {
self.appState.cancelInstall(id: xcode.id)
}
@ -113,8 +152,8 @@ struct MainWindow: View {
)
case .privilegedHelper:
return Alert(
title: Text("Privileged Helper"),
message: Text("Xcodes uses a separate privileged helper to perform tasks as root. These are things that would require sudo on the command line, including post-install steps and switching Xcode versions with xcode-select.\n\nYou'll be prompted for your macOS account password to install it."),
title: Text("Alert.PrivilegedHelper.Title"),
message: Text("Alert.PrivilegedHelper.Message"),
primaryButton: .default(Text("Install"), action: {
// The isPreparingUserForActionRequiringHelper closure is set to nil by the alert's binding when its dismissed.
// We need to capture it to be invoked after that happens.
@ -147,16 +186,66 @@ struct MainWindow: View {
title: Text(title),
message: Text(message),
dismissButton: .default(
Text("Ok"),
Text("OK"),
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.xcodeID.version.descriptionWithoutBuildMetadata, xcode.requiredMacOSVersion ?? "", deviceVersion)),
primaryButton: .default(
Text("Install"),
action: {
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
}())
}
}

View file

@ -4,24 +4,58 @@ import Path
struct AdvancedPreferencePane: View {
@EnvironmentObject var appState: AppState
@AppStorage("dataSource") var dataSource: DataSource = .xcodeReleases
@AppStorage("downloader") var downloader: Downloader = .aria2
var body: some View {
VStack(alignment: .leading, spacing: 20) {
GroupBox(label: Text("Local Cache Path")) {
GroupBox(label: Text("InstallDirectory")) {
VStack(alignment: .leading) {
HStack(alignment: .top, spacing: 5) {
Text(appState.installPath).font(.footnote)
.fixedSize(horizontal: false, vertical: true)
.lineLimit(2)
Button(action: { appState.reveal(path: appState.installPath) }) {
Image(systemName: "arrow.right.circle.fill")
}
.buttonStyle(PlainButtonStyle())
.help("RevealInFinder")
.fixedSize()
}
Button("Change") {
let panel = NSOpenPanel()
panel.allowsMultipleSelection = false
panel.canChooseDirectories = true
panel.canChooseFiles = false
panel.canCreateDirectories = true
panel.allowedContentTypes = [.folder]
panel.directoryURL = URL(fileURLWithPath: appState.installPath)
if panel.runModal() == .OK {
guard let pathURL = panel.url, let path = Path(url: pathURL) else { return }
self.appState.installPath = path.string
}
}
.disabled(appState.disableInstallPathChange)
Text("InstallPathDescription")
.font(.footnote)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
}
.groupBoxStyle(PreferencesGroupBoxStyle())
GroupBox(label: Text("LocalCachePath")) {
VStack(alignment: .leading) {
HStack(alignment: .top, spacing: 5) {
Text(appState.localPath).font(.footnote)
.fixedSize(horizontal: false, vertical: true)
.lineLimit(2)
.fixedSize(horizontal: false, vertical: true)
.lineLimit(2)
Button(action: { appState.reveal(path: appState.localPath) }) {
Image(systemName: "arrow.right.circle.fill")
}
.buttonStyle(PlainButtonStyle())
.help("Reveal in Finder")
.help("RevealInFinder")
.fixedSize()
}
Button("Change") {
@ -34,70 +68,86 @@ struct AdvancedPreferencePane: View {
panel.directoryURL = URL(fileURLWithPath: appState.localPath)
if panel.runModal() == .OK {
guard let pathURL = panel.url, let path = Path(url: pathURL) else { return }
self.appState.localPath = path.string
}
}
Text("Xcodes caches available Xcode versions and temporary downloads new versions to a directory")
.disabled(appState.disableLocalPathChange)
Text("LocalCachePathDescription")
.font(.footnote)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
}
.groupBoxStyle(PreferencesGroupBoxStyle())
GroupBox(label: Text("Data Source")) {
GroupBox(label: Text("Active/Select")) {
VStack(alignment: .leading) {
Picker("Data Source", selection: $dataSource) {
ForEach(DataSource.allCases) { dataSource in
Text(dataSource.description)
.tag(dataSource)
}
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)
AttributedText(dataSourceFootnote)
Toggle("AutomaticallyCreateSymbolicLink", isOn: $appState.createSymLinkOnSelect)
.disabled(appState.createSymLinkOnSelectDisabled)
Text("AutomaticallyCreateSymbolicLinkDescription")
.font(.footnote)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
.fixedSize(horizontal: false, vertical: true)
}
.groupBoxStyle(PreferencesGroupBoxStyle())
GroupBox(label: Text("Downloader")) {
VStack(alignment: .leading) {
Picker("Downloader", selection: $downloader) {
ForEach(Downloader.allCases) { downloader in
Text(downloader.description)
.tag(downloader)
}
}
.labelsHidden()
AttributedText(downloaderFootnote)
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())
}
.groupBoxStyle(PreferencesGroupBoxStyle())
GroupBox(label: Text("Privileged Helper")) {
GroupBox(label: Text("PrivilegedHelper")) {
VStack(alignment: .leading, spacing: 8) {
switch appState.helperInstallState {
case .unknown:
ProgressView()
.scaleEffect(0.5, anchor: .center)
case .installed:
Text("Helper is installed")
Text("HelperInstalled")
case .notInstalled:
HStack {
Text("Helper is not installed")
Button("Install helper") {
VStack(alignment: .leading) {
Button("InstallHelper") {
appState.installHelperIfNecessary()
}
Text("HelperNotInstalled")
.font(.footnote)
}
}
Text("Xcodes uses a separate privileged helper to perform tasks as root. These are things that would require sudo on the command line, including post-install steps and switching Xcode versions with xcode-select.\n\nYou'll be prompted for your macOS account password to install it.")
Text("PrivilegedHelperDescription")
.font(.footnote)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
Spacer()
@ -105,41 +155,6 @@ struct AdvancedPreferencePane: View {
}
.groupBoxStyle(PreferencesGroupBoxStyle())
}
.frame(width: 500)
}
private var dataSourceFootnote: NSAttributedString {
let string = """
The Apple data source scrapes the Apple Developer website. It will always show the latest releases that are available, but is more fragile.
Xcode Releases is an unofficial list of Xcode releases. It's provided as well-formed data, contains extra information that is not readily available from Apple, and is less likely to break if Apple redesigns their developer website.
"""
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 = """
aria2 uses up to 16 connections to download Xcode 3-5x faster than URLSession. It's bundled as an executable along with its source code within Xcodes to comply with its GPLv2 license.
URLSession is the default Apple API for making URL requests.
"""
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
}
}
@ -148,7 +163,9 @@ struct AdvancedPreferencePane_Previews: PreviewProvider {
Group {
AdvancedPreferencePane()
.environmentObject(AppState())
.frame(maxWidth: 600)
}
.frame(width: 600, height: 700, alignment: .center)
}
}
@ -156,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

View file

@ -0,0 +1,64 @@
import AppleAPI
import SwiftUI
struct DownloadPreferencePane: View {
@EnvironmentObject var appState: AppState
@AppStorage("dataSource") var dataSource: DataSource = .xcodeReleases
@AppStorage("downloader") var downloader: Downloader = .aria2
var body: some View {
VStack(alignment: .leading) {
GroupBox(label: Text("DataSource")) {
VStack(alignment: .leading) {
Picker("DataSource", selection: $dataSource) {
ForEach(DataSource.allCases) { dataSource in
Text(dataSource.description)
.tag(dataSource)
}
}
.labelsHidden()
.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) {
ForEach(Downloader.allCases) { downloader in
Text(downloader.description)
.tag(downloader)
}
}
.labelsHidden()
.fixedSize()
Text("DownloaderDescription")
.font(.footnote)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
}
.groupBoxStyle(PreferencesGroupBoxStyle())
.disabled(downloader.isManaged)
}
}
}
struct DownloadPreferencePane_Previews: PreviewProvider {
static var previews: some View {
Group {
DownloadPreferencePane()
.environmentObject(AppState())
.frame(maxWidth: 600)
.frame(minHeight: 300)
}
}
}

View file

@ -0,0 +1,37 @@
import AppleAPI
import Path
import SwiftUI
struct ExperimentsPreferencePane: View {
@EnvironmentObject var appState: AppState
var body: some View {
VStack(alignment: .leading, spacing: 20) {
GroupBox(label: Text("FasterUnxip")) {
VStack(alignment: .leading) {
Toggle(
"UseUnxipExperiment",
isOn: $appState.unxipExperiment
)
.disabled(appState.disableUnxipExperiment)
Text("FasterUnxipDescription")
.font(.footnote)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
.fixedSize(horizontal: false, vertical: true)
}
.groupBoxStyle(PreferencesGroupBoxStyle())
}
}
}
struct ExperimentsPreferencePane_Previews: PreviewProvider {
static var previews: some View {
Group {
ExperimentsPreferencePane()
.environmentObject(AppState())
.frame(maxWidth: 600)
}
}
}

View file

@ -6,11 +6,11 @@ struct GeneralPreferencePane: View {
var body: some View {
VStack(alignment: .leading) {
GroupBox(label: Text("Apple ID")) {
GroupBox(label: Text("AppleID")) {
if appState.authenticationState == .authenticated {
SignedInView()
} else {
Button("Sign In", action: { self.appState.presentedSheet = .signIn })
Button("SignIn", action: { self.appState.presentedSheet = .signIn })
}
}
.groupBoxStyle(PreferencesGroupBoxStyle())
@ -20,8 +20,13 @@ struct GeneralPreferencePane: View {
NotificationsView().environmentObject(appState)
}
.groupBoxStyle(PreferencesGroupBoxStyle())
Divider()
GroupBox(label: Text("Misc")) {
Toggle("TerminateAfterLastWindowClosed", isOn: $appState.terminateAfterLastWindowClosed)
}
.groupBoxStyle(PreferencesGroupBoxStyle())
}
.frame(width: 500)
}
}
@ -30,6 +35,7 @@ struct GeneralPreferencePane_Previews: PreviewProvider {
Group {
GeneralPreferencePane()
.environmentObject(AppState())
.frame(maxWidth: 600)
}
}
}

View file

@ -8,17 +8,17 @@ struct NotificationsView: View {
switch Current.notificationManager.notificationStatus {
case .shownAndAccepted:
Text("Access Granted. You will receive notifications from Xcodes.")
Text("AccessGranted")
.fixedSize(horizontal: false, vertical: true)
case .shownAndDenied:
Text("⚠️ Access Denied ⚠️\n\nPlease open your Notification Settings and select Xcodes if you wish to allow access.")
Text("AccessDenied")
.fixedSize(horizontal: false, vertical: true)
Button("Notification Settings", action: {
Button("NotificationSettings", action: {
NSWorkspace.shared.open(URL(string: "x-apple.systempreferences:com.apple.preference.notifications")!)
})
default:
Button("Enable Notifications", action: {
Button("EnableNotifications", action: {
Current.notificationManager.requestAccess()
})
}

View 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
}())
}

View file

@ -2,9 +2,10 @@ import SwiftUI
struct PreferencesView: View {
private enum Tabs: Hashable {
case general, updates, advanced
case general, updates, advanced, experiment
}
@EnvironmentObject var appState: AppState
@EnvironmentObject var updater: ObservableUpdater
var body: some View {
TabView {
@ -15,17 +16,29 @@ struct PreferencesView: View {
}
.tag(Tabs.general)
UpdatesPreferencePane()
.environmentObject(updater)
.tabItem {
Label("Updates", systemImage: "arrow.triangle.2.circlepath.circle")
}
.tag(Tabs.updates)
DownloadPreferencePane()
.environmentObject(appState)
.tabItem {
Label("Downloads", systemImage: "icloud.and.arrow.down")
}
AdvancedPreferencePane()
.environmentObject(appState)
.tabItem {
Label("Advanced", systemImage: "gearshape.2")
}
.tag(Tabs.advanced)
ExperimentsPreferencePane()
.tabItem {
Label("Experiments", systemImage: "lightbulb")
}
.tag(Tabs.experiment)
}
.padding(20)
.frame(width: 600)
}
}

View file

@ -3,7 +3,7 @@ import Sparkle
import SwiftUI
struct UpdatesPreferencePane: View {
@StateObject var updater = ObservableUpdater()
@EnvironmentObject var updater: ObservableUpdater
@AppStorage("autoInstallation") var autoInstallationType: AutoInstallationType = .none
@ -12,14 +12,16 @@ struct UpdatesPreferencePane: View {
GroupBox(label: Text("Versions")) {
VStack(alignment: .leading) {
Toggle(
"Automatically install new versions of Xcode",
"AutomaticInstallNewVersion",
isOn: $autoInstallationType.isAutoInstalling
)
.disabled(updater.disableAutoInstallNewVersions)
Toggle(
"Include prerelease/beta versions",
"IncludePreRelease",
isOn: $autoInstallationType.isAutoInstallingBeta
)
.disabled(updater.disableIncludePrereleaseVersions)
}
.fixedSize(horizontal: false, vertical: true)
}
@ -27,37 +29,42 @@ struct UpdatesPreferencePane: View {
Divider()
GroupBox(label: Text("Xcodes.app Updates")) {
GroupBox(label: Text("AppUpdates")) {
VStack(alignment: .leading) {
Toggle(
"Automatically check for app updates",
"CheckForAppUpdates",
isOn: $updater.automaticallyChecksForUpdates
)
.fixedSize(horizontal: true, vertical: false)
.disabled(updater.disableAutoUpdateXcodesApp)
Toggle(
"Include prerelease app versions",
"IncludePreRelease",
isOn: $updater.includePrereleaseVersions
)
Button("Check Now") {
SUUpdater.shared()?.checkForUpdates(nil)
.disabled(updater.disableAutoUpdateXcodesAppPrereleaseVersions)
Button("CheckNow") {
updater.checkForUpdates()
}
Text("Last checked: \(lastUpdatedString)")
.padding(.top)
.disabled(updater.disableAutoUpdateXcodesApp)
Text(String(format: localizeString("LastChecked"), lastUpdatedString))
.font(.footnote)
.foregroundStyle(.secondary)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
.groupBoxStyle(PreferencesGroupBoxStyle())
}
.frame(width: 500)
}
private var lastUpdatedString: String {
if let lastUpdatedDate = updater.lastUpdateCheckDate {
return Self.formatter.string(from: lastUpdatedDate)
} else {
return "Never"
return localizeString("Never")
}
}
@ -68,9 +75,12 @@ struct UpdatesPreferencePane: View {
}
class ObservableUpdater: ObservableObject {
private let updater: SPUUpdater
private let updaterDelegate = UpdaterDelegate()
@Published var automaticallyChecksForUpdates = false {
didSet {
SUUpdater.shared()?.automaticallyChecksForUpdates = automaticallyChecksForUpdates
updater.automaticallyChecksForUpdates = automaticallyChecksForUpdates
}
}
private var automaticallyChecksForUpdatesObservation: NSKeyValueObservation?
@ -78,18 +88,26 @@ 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 {
SUUpdater.shared()?.feedURL = .prereleaseAppcast
} else {
SUUpdater.shared()?.feedURL = .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() {
automaticallyChecksForUpdatesObservation = SUUpdater.shared()?.observe(
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,
options: [.initial, .new, .old],
changeHandler: { [unowned self] updater, change in
@ -97,20 +115,37 @@ class ObservableUpdater: ObservableObject {
self.automaticallyChecksForUpdates = updater.automaticallyChecksForUpdates
}
)
lastUpdateCheckDateObservation = SUUpdater.shared()?.observe(
lastUpdateCheckDateObservation = updater.observe(
\.lastUpdateCheckDate,
options: [.initial, .new, .old],
changeHandler: { [unowned self] updater, change in
self.lastUpdateCheckDate = updater.lastUpdateCheckDate
}
)
includePrereleaseVersions = UserDefaults.standard.bool(forKey: "includePrereleaseVersions")
includePrereleaseVersions = Current.defaults.bool(forKey: "includePrereleaseVersions") ?? false
}
func checkForUpdates() {
updater.checkForUpdates()
}
}
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 {
@ -118,6 +153,9 @@ struct UpdatesPreferencePane_Previews: PreviewProvider {
Group {
UpdatesPreferencePane()
.environmentObject(AppState())
.environmentObject(ObservableUpdater())
.frame(maxWidth: 600)
.frame(minHeight: 300)
}
}
}

View file

@ -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] = []

View file

@ -10,11 +10,14 @@ struct SignIn2FAView: View {
var body: some View {
VStack(alignment: .leading) {
Text("Enter the \(authOptions.securityCode.length) digit code from one of your trusted devices:")
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()
@ -22,14 +25,14 @@ struct SignIn2FAView: View {
HStack {
Button("Cancel", action: { isPresented = false })
.keyboardShortcut(.cancelAction)
Button("Send SMS", action: { appState.choosePhoneNumberForSMS(authOptions: authOptions, sessionData: sessionData) })
Button("SendSMS", action: { appState.choosePhoneNumberForSMS(authOptions: authOptions, sessionData: sessionData) })
Spacer()
ProgressButton(isInProgress: appState.isProcessingAuthRequest,
action: { appState.submitSecurityCode(.device(code: code), sessionData: sessionData) }) {
Text("Continue")
}
.keyboardShortcut(.defaultAction)
.disabled(code.count != authOptions.securityCode.length)
.disabled(code.count != authOptions.securityCode!.length)
}
.frame(height: 25)
}

View file

@ -1,24 +1,33 @@
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) {
Text("Sign in with your Apple ID.")
Text("SignInWithApple")
.bold()
.padding(.vertical)
HStack {
Text("Apple ID:")
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:")
Text("Password")
.frame(minWidth: 100, alignment: .trailing)
SecureField("Required", text: $password)
.focused($focusedField, equals: .password)
}
if appState.authError != nil {
HStack {

View file

@ -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("Select a trusted phone number to receive a \(authOptions.securityCode.length) digit code via SMS:")
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: "Your account doesn't have any trusted phone numbers, but they're required for two-factor authentication.\n\nSee https://support.apple.com/en-ca/HT204915.")
.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())

View file

@ -11,11 +11,13 @@ struct SignInSMSView: View {
var body: some View {
VStack(alignment: .leading) {
Text("Enter the \(authOptions.securityCode.length) digit code sent to \(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)
}

View 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())
}

Some files were not shown because too many files have changed in this diff Show more