diff --git a/Xcodes.xcodeproj/project.pbxproj b/Xcodes.xcodeproj/project.pbxproj index 078e3507..b57cfd75 100644 --- a/Xcodes.xcodeproj/project.pbxproj +++ b/Xcodes.xcodeproj/project.pbxproj @@ -116,6 +116,8 @@ E832EAF82B0FBCF4001B570D /* RuntimeInstallationStepDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E832EAF72B0FBCF4001B570D /* RuntimeInstallationStepDetailView.swift */; }; E84B7D0D2B296A8900DBDA2B /* NavigationSplitViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = E84B7D0C2B296A8900DBDA2B /* NavigationSplitViewWrapper.swift */; }; E84E4F522B323A5F003F3959 /* CornerRadiusModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E84E4F512B323A5F003F3959 /* CornerRadiusModifier.swift */; }; + E84E4F542B333864003F3959 /* PlatformsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E84E4F532B333864003F3959 /* PlatformsListView.swift */; }; + E84E4F572B335094003F3959 /* OrderedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = E84E4F562B335094003F3959 /* OrderedCollections */; }; E86671272B309D2F0048559A /* PlatformsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E86671262B309D2F0048559A /* PlatformsView.swift */; }; E87AB3C52939B65E00D72F43 /* Hardware.swift in Sources */ = {isa = PBXBuildFile; fileRef = E87AB3C42939B65E00D72F43 /* Hardware.swift */; }; E87DD6EB25D053FA00D86808 /* Progress+.swift in Sources */ = {isa = PBXBuildFile; fileRef = E87DD6EA25D053FA00D86808 /* Progress+.swift */; }; @@ -313,6 +315,7 @@ E832EAF72B0FBCF4001B570D /* RuntimeInstallationStepDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuntimeInstallationStepDetailView.swift; sourceTree = ""; }; E84B7D0C2B296A8900DBDA2B /* NavigationSplitViewWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationSplitViewWrapper.swift; sourceTree = ""; }; E84E4F512B323A5F003F3959 /* CornerRadiusModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CornerRadiusModifier.swift; sourceTree = ""; }; + E84E4F532B333864003F3959 /* PlatformsListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlatformsListView.swift; sourceTree = ""; }; E856BB73291EDD3D00DC438B /* XcodesKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = XcodesKit; path = Xcodes/XcodesKit; sourceTree = ""; }; E86671262B309D2F0048559A /* PlatformsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlatformsView.swift; sourceTree = ""; }; E87AB3C42939B65E00D72F43 /* Hardware.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Hardware.swift; sourceTree = ""; }; @@ -352,6 +355,7 @@ E8FD5727291EE4AC001E004C /* AsyncNetworkService in Frameworks */, CAA1CB2D255A5262003FD669 /* AppleAPI in Frameworks */, CABFA9EE2592F0CC00380FEE /* SwiftSoup in Frameworks */, + E84E4F572B335094003F3959 /* OrderedCollections in Frameworks */, E8F44A1E296B4CD7002D6592 /* Path in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -628,6 +632,7 @@ E8977EA225C11E1500835F80 /* PreferencesView.swift */, E8DA461025FAF7FB002E85EF /* NotificationsView.swift */, E8CBDB8A27AE02FF00B22292 /* ExperiementsPreferencePane.swift */, + E84E4F532B333864003F3959 /* PlatformsListView.swift */, ); path = Preferences; sourceTree = ""; @@ -705,6 +710,7 @@ E8FD5726291EE4AC001E004C /* AsyncNetworkService */, E8C0EB19291EF43E0081528A /* XcodesKit */, E8F44A1D296B4CD7002D6592 /* Path */, + E84E4F562B335094003F3959 /* OrderedCollections */, ); productName = XcodesMac; productReference = CAD2E79E2449574E00113D76 /* Xcodes.app */; @@ -791,6 +797,7 @@ E689540125BE8C64000EBCEA /* XCRemoteSwiftPackageReference "DockProgress" */, E8FD5725291EE4AC001E004C /* XCRemoteSwiftPackageReference "AsyncHTTPNetworkService" */, E8F44A1C296B4CD7002D6592 /* XCRemoteSwiftPackageReference "Path" */, + E84E4F552B335094003F3959 /* XCRemoteSwiftPackageReference "swift-collections" */, ); productRefGroup = CAD2E79F2449574E00113D76 /* Products */; projectDirPath = ""; @@ -914,6 +921,7 @@ CABFA9C12592EEEA00380FEE /* Version+.swift in Sources */, E8D655C0288DD04700A139C2 /* SelectedActionType.swift in Sources */, 36741BFD291E4FDB00A85AAE /* DownloadPreferencePane.swift in Sources */, + E84E4F542B333864003F3959 /* PlatformsListView.swift in Sources */, E86671272B309D2F0048559A /* PlatformsView.swift in Sources */, CA9FF8522595080100E47BAF /* AcknowledgementsView.swift in Sources */, CABFA9CE2592EEEA00380FEE /* Version+Xcode.swift in Sources */, @@ -1501,6 +1509,14 @@ minimumVersion = 3.2.0; }; }; + E84E4F552B335094003F3959 /* XCRemoteSwiftPackageReference "swift-collections" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/apple/swift-collections.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.0.5; + }; + }; E8F44A1C296B4CD7002D6592 /* XCRemoteSwiftPackageReference "Path" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/mxcl/Path.swift"; @@ -1572,6 +1588,11 @@ package = E689540125BE8C64000EBCEA /* XCRemoteSwiftPackageReference "DockProgress" */; productName = DockProgress; }; + E84E4F562B335094003F3959 /* OrderedCollections */ = { + isa = XCSwiftPackageProductDependency; + package = E84E4F552B335094003F3959 /* XCRemoteSwiftPackageReference "swift-collections" */; + productName = OrderedCollections; + }; E8C0EB19291EF43E0081528A /* XcodesKit */ = { isa = XCSwiftPackageProductDependency; productName = XcodesKit; diff --git a/Xcodes.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Xcodes.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index a08a9b21..743059b0 100644 --- a/Xcodes.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Xcodes.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -82,6 +82,15 @@ "version": "2.1.0" } }, + { + "package": "swift-collections", + "repositoryURL": "https://github.com/apple/swift-collections.git", + "state": { + "branch": null, + "revision": "a902f1823a7ff3c9ab2fba0f992396b948eda307", + "version": "1.0.5" + } + }, { "package": "SwiftSoup", "repositoryURL": "https://github.com/scinfu/SwiftSoup", diff --git a/Xcodes/Backend/AppState+Install.swift b/Xcodes/Backend/AppState+Install.swift index ae562d30..325f314f 100644 --- a/Xcodes/Backend/AppState+Install.swift +++ b/Xcodes/Backend/AppState+Install.swift @@ -500,12 +500,13 @@ extension AppState { } } - func setInstallationStep(of runtime: DownloadableRuntime, to step: RuntimeInstallationStep) { + 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) - - Current.notificationManager.scheduleNotification(title: runtime.name, body: step.description, category: .normal) + if postNotification { + Current.notificationManager.scheduleNotification(title: runtime.name, body: step.description, category: .normal) + } } } } diff --git a/Xcodes/Backend/AppState+Runtimes.swift b/Xcodes/Backend/AppState+Runtimes.swift index 9143667b..63a00f1a 100644 --- a/Xcodes/Backend/AppState+Runtimes.swift +++ b/Xcodes/Backend/AppState+Runtimes.swift @@ -35,6 +35,7 @@ extension AppState { func updateInstalledRuntimes() { Task { do { + Logger.appState.info("Loading Installed runtimes") let runtimes = try await self.runtimeService.localInstalledRuntimes() DispatchQueue.main.async { @@ -51,7 +52,7 @@ extension AppState { do { let downloadedURL = try await downloadRunTimeFull(runtime: runtime) if !Task.isCancelled { - Logger.appState.debug("Installing rungtime: \(runtime.name)") + Logger.appState.debug("Installing runtime: \(runtime.name)") DispatchQueue.main.async { self.setInstallationStep(of: runtime, to: .installing) } @@ -110,11 +111,10 @@ extension AppState { let aria2Path = Path(url: Bundle.main.url(forAuxiliaryExecutable: "aria2c")!)! for try await progress in downloadRuntimeWithAria2(runtime, to: expectedRuntimePath, aria2Path: aria2Path) { DispatchQueue.main.async { - Logger.appState.debug("Downloading: \(progress.fractionCompleted)") - self.setInstallationStep(of: runtime, to: .downloading(progress: progress)) + self.setInstallationStep(of: runtime, to: .downloading(progress: progress), postNotification: false) } } - Logger.appState.debug("Done downloading") + Logger.appState.debug("Done downloading runtime") case .urlSession: throw "Downloading runtimes with URLSession is not supported. Please use aria2" @@ -210,6 +210,35 @@ extension AppState { 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 }).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 { diff --git a/Xcodes/Backend/AppState.swift b/Xcodes/Backend/AppState.swift index 998e9f82..7bec2cb5 100644 --- a/Xcodes/Backend/AppState.swift +++ b/Xcodes/Backend/AppState.swift @@ -48,6 +48,7 @@ class AppState: ObservableObject { @Published var isProcessingAuthRequest = false @Published var xcodeBeingConfirmedForUninstallation: Xcode? @Published var presentedAlert: XcodesAlert? + @Published var presentedPreferenceAlert: XcodesPreferencesAlert? @Published var helperInstallState: HelperInstallState = .notInstalled /// Whether the user is being prepared for the helper installation alert with an explanation. /// This closure will be performed after the user chooses whether or not to proceed. @@ -824,19 +825,7 @@ class AppState: ObservableObject { self.allXcodes = newAllXcodes.sorted { $0.version > $1.version } } - - // MARK: Runtimes - func runtimeInstallPath(xcode: Xcode, runtime: DownloadableRuntime) -> Path? { - if let coreSimulatorInfo = installedRuntimes.filter({ $0.runtimeInfo.build == runtime.simulatorVersion.buildUpdate }).first { - 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 - } + // MARK: - Private diff --git a/Xcodes/Frontend/Common/XcodesAlert.swift b/Xcodes/Frontend/Common/XcodesAlert.swift index 38636cb4..2d5e66f1 100644 --- a/Xcodes/Frontend/Common/XcodesAlert.swift +++ b/Xcodes/Frontend/Common/XcodesAlert.swift @@ -18,3 +18,17 @@ enum XcodesAlert: Identifiable { } } } + +// 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 + } + } +} diff --git a/Xcodes/Frontend/InfoPane/InfoPane.swift b/Xcodes/Frontend/InfoPane/InfoPane.swift index 36f33872..95826b73 100644 --- a/Xcodes/Frontend/InfoPane/InfoPane.swift +++ b/Xcodes/Frontend/InfoPane/InfoPane.swift @@ -171,7 +171,8 @@ var downloadableRuntimes: [DownloadableRuntime] = { }() 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"))] // same as iOS in _SDK's + [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"))] }() diff --git a/Xcodes/Frontend/InfoPane/PlatformsView.swift b/Xcodes/Frontend/InfoPane/PlatformsView.swift index 184e597d..93b2d3bb 100644 --- a/Xcodes/Frontend/InfoPane/PlatformsView.swift +++ b/Xcodes/Frontend/InfoPane/PlatformsView.swift @@ -62,6 +62,7 @@ struct PlatformsView: View { HStack(alignment: .top, spacing: 5){ RuntimeInstallationStepDetailView(installationStep: installationStep) .fixedSize(horizontal: false, vertical: true) + Spacer() CancelRuntimeInstallButton(runtime: runtime) } diff --git a/Xcodes/Frontend/Preferences/PlatformsListView.swift b/Xcodes/Frontend/Preferences/PlatformsListView.swift new file mode 100644 index 00000000..855bd7e2 --- /dev/null +++ b/Xcodes/Frontend/Preferences/PlatformsListView.swift @@ -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 = [:] + @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 + + }()) +} diff --git a/Xcodes/Frontend/Preferences/PreferencesView.swift b/Xcodes/Frontend/Preferences/PreferencesView.swift index 83ab7756..5aa5d7d3 100644 --- a/Xcodes/Frontend/Preferences/PreferencesView.swift +++ b/Xcodes/Frontend/Preferences/PreferencesView.swift @@ -2,7 +2,7 @@ import SwiftUI struct PreferencesView: View { private enum Tabs: Hashable { - case general, updates, advanced, experiment + case general, updates, platforms, advanced, experiment } @EnvironmentObject var appState: AppState @EnvironmentObject var updater: ObservableUpdater @@ -26,6 +26,12 @@ struct PreferencesView: View { .tabItem { Label("Downloads", systemImage: "icloud.and.arrow.down") } + PlatformsListView() + .environmentObject(appState) + .tabItem { + Label("Platforms", systemImage: "ipad.and.iphone") + } + .tag(Tabs.platforms) AdvancedPreferencePane() .environmentObject(appState) .tabItem { diff --git a/Xcodes/Resources/Licenses.rtf b/Xcodes/Resources/Licenses.rtf index d79d9738..aa97f20f 100644 --- a/Xcodes/Resources/Licenses.rtf +++ b/Xcodes/Resources/Licenses.rtf @@ -335,30 +335,220 @@ For more information, please refer to <>\ otherwise be required by Sections 4(a), 4(b) and 4(d) of the License.\ \ -\fs34 SwiftUIMasonry\ +\fs34 swift-collections\ \ -\fs26 MIT License\ +\fs26 Apache License\ + Version 2.0, January 2004\ + http://www.apache.org/licenses/\ \ -Copyright (c) 2022 Ciaran O'Brien\ + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\ \ -Permission is hereby granted, free of charge, to any person obtaining a copy\ -of this software and associated documentation files (the "Software"), to deal\ -in the Software without restriction, including without limitation the rights\ -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ -copies of the Software, and to permit persons to whom the Software is\ -furnished to do so, subject to the following conditions:\ + 1. Definitions.\ \ -The above copyright notice and this permission notice shall be included in all\ -copies or substantial portions of the Software.\ + "License" shall mean the terms and conditions for use, reproduction,\ + and distribution as defined by Sections 1 through 9 of this document.\ \ -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\ -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\ -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\ -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\ -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\ -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\ -SOFTWARE.\ + "Licensor" shall mean the copyright owner or entity authorized by\ + the copyright owner that is granting the License.\ +\ + "Legal Entity" shall mean the union of the acting entity and all\ + other entities that control, are controlled by, or are under common\ + control with that entity. For the purposes of this definition,\ + "control" means (i) the power, direct or indirect, to cause the\ + direction or management of such entity, whether by contract or\ + otherwise, or (ii) ownership of fifty percent (50%) or more of the\ + outstanding shares, or (iii) beneficial ownership of such entity.\ +\ + "You" (or "Your") shall mean an individual or Legal Entity\ + exercising permissions granted by this License.\ +\ + "Source" form shall mean the preferred form for making modifications,\ + including but not limited to software source code, documentation\ + source, and configuration files.\ +\ + "Object" form shall mean any form resulting from mechanical\ + transformation or translation of a Source form, including but\ + not limited to compiled object code, generated documentation,\ + and conversions to other media types.\ +\ + "Work" shall mean the work of authorship, whether in Source or\ + Object form, made available under the License, as indicated by a\ + copyright notice that is included in or attached to the work\ + (an example is provided in the Appendix below).\ +\ + "Derivative Works" shall mean any work, whether in Source or Object\ + form, that is based on (or derived from) the Work and for which the\ + editorial revisions, annotations, elaborations, or other modifications\ + represent, as a whole, an original work of authorship. For the purposes\ + of this License, Derivative Works shall not include works that remain\ + separable from, or merely link (or bind by name) to the interfaces of,\ + the Work and Derivative Works thereof.\ +\ + "Contribution" shall mean any work of authorship, including\ + the original version of the Work and any modifications or additions\ + to that Work or Derivative Works thereof, that is intentionally\ + submitted to Licensor for inclusion in the Work by the copyright owner\ + or by an individual or Legal Entity authorized to submit on behalf of\ + the copyright owner. For the purposes of this definition, "submitted"\ + means any form of electronic, verbal, or written communication sent\ + to the Licensor or its representatives, including but not limited to\ + communication on electronic mailing lists, source code control systems,\ + and issue tracking systems that are managed by, or on behalf of, the\ + Licensor for the purpose of discussing and improving the Work, but\ + excluding communication that is conspicuously marked or otherwise\ + designated in writing by the copyright owner as "Not a Contribution."\ +\ + "Contributor" shall mean Licensor and any individual or Legal Entity\ + on behalf of whom a Contribution has been received by Licensor and\ + subsequently incorporated within the Work.\ +\ + 2. Grant of Copyright License. Subject to the terms and conditions of\ + this License, each Contributor hereby grants to You a perpetual,\ + worldwide, non-exclusive, no-charge, royalty-free, irrevocable\ + copyright license to reproduce, prepare Derivative Works of,\ + publicly display, publicly perform, sublicense, and distribute the\ + Work and such Derivative Works in Source or Object form.\ +\ + 3. Grant of Patent License. Subject to the terms and conditions of\ + this License, each Contributor hereby grants to You a perpetual,\ + worldwide, non-exclusive, no-charge, royalty-free, irrevocable\ + (except as stated in this section) patent license to make, have made,\ + use, offer to sell, sell, import, and otherwise transfer the Work,\ + where such license applies only to those patent claims licensable\ + by such Contributor that are necessarily infringed by their\ + Contribution(s) alone or by combination of their Contribution(s)\ + with the Work to which such Contribution(s) was submitted. If You\ + institute patent litigation against any entity (including a\ + cross-claim or counterclaim in a lawsuit) alleging that the Work\ + or a Contribution incorporated within the Work constitutes direct\ + or contributory patent infringement, then any patent licenses\ + granted to You under this License for that Work shall terminate\ + as of the date such litigation is filed.\ +\ + 4. Redistribution. You may reproduce and distribute copies of the\ + Work or Derivative Works thereof in any medium, with or without\ + modifications, and in Source or Object form, provided that You\ + meet the following conditions:\ +\ + (a) You must give any other recipients of the Work or\ + Derivative Works a copy of this License; and\ +\ + (b) You must cause any modified files to carry prominent notices\ + stating that You changed the files; and\ +\ + (c) You must retain, in the Source form of any Derivative Works\ + that You distribute, all copyright, patent, trademark, and\ + attribution notices from the Source form of the Work,\ + excluding those notices that do not pertain to any part of\ + the Derivative Works; and\ +\ + (d) If the Work includes a "NOTICE" text file as part of its\ + distribution, then any Derivative Works that You distribute must\ + include a readable copy of the attribution notices contained\ + within such NOTICE file, excluding those notices that do not\ + pertain to any part of the Derivative Works, in at least one\ + of the following places: within a NOTICE text file distributed\ + as part of the Derivative Works; within the Source form or\ + documentation, if provided along with the Derivative Works; or,\ + within a display generated by the Derivative Works, if and\ + wherever such third-party notices normally appear. The contents\ + of the NOTICE file are for informational purposes only and\ + do not modify the License. You may add Your own attribution\ + notices within Derivative Works that You distribute, alongside\ + or as an addendum to the NOTICE text from the Work, provided\ + that such additional attribution notices cannot be construed\ + as modifying the License.\ +\ + You may add Your own copyright statement to Your modifications and\ + may provide additional or different license terms and conditions\ + for use, reproduction, or distribution of Your modifications, or\ + for any such Derivative Works as a whole, provided Your use,\ + reproduction, and distribution of the Work otherwise complies with\ + the conditions stated in this License.\ +\ + 5. Submission of Contributions. Unless You explicitly state otherwise,\ + any Contribution intentionally submitted for inclusion in the Work\ + by You to the Licensor shall be under the terms and conditions of\ + this License, without any additional terms or conditions.\ + Notwithstanding the above, nothing herein shall supersede or modify\ + the terms of any separate license agreement you may have executed\ + with Licensor regarding such Contributions.\ +\ + 6. Trademarks. This License does not grant permission to use the trade\ + names, trademarks, service marks, or product names of the Licensor,\ + except as required for reasonable and customary use in describing the\ + origin of the Work and reproducing the content of the NOTICE file.\ +\ + 7. Disclaimer of Warranty. Unless required by applicable law or\ + agreed to in writing, Licensor provides the Work (and each\ + Contributor provides its Contributions) on an "AS IS" BASIS,\ + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\ + implied, including, without limitation, any warranties or conditions\ + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\ + PARTICULAR PURPOSE. You are solely responsible for determining the\ + appropriateness of using or redistributing the Work and assume any\ + risks associated with Your exercise of permissions under this License.\ +\ + 8. Limitation of Liability. In no event and under no legal theory,\ + whether in tort (including negligence), contract, or otherwise,\ + unless required by applicable law (such as deliberate and grossly\ + negligent acts) or agreed to in writing, shall any Contributor be\ + liable to You for damages, including any direct, indirect, special,\ + incidental, or consequential damages of any character arising as a\ + result of this License or out of the use or inability to use the\ + Work (including but not limited to damages for loss of goodwill,\ + work stoppage, computer failure or malfunction, or any and all\ + other commercial damages or losses), even if such Contributor\ + has been advised of the possibility of such damages.\ +\ + 9. Accepting Warranty or Additional Liability. While redistributing\ + the Work or Derivative Works thereof, You may choose to offer,\ + and charge a fee for, acceptance of support, warranty, indemnity,\ + or other liability obligations and/or rights consistent with this\ + License. However, in accepting such obligations, You may act only\ + on Your own behalf and on Your sole responsibility, not on behalf\ + of any other Contributor, and only if You agree to indemnify,\ + defend, and hold each Contributor harmless for any liability\ + incurred by, or claims asserted against, such Contributor by reason\ + of your accepting any such warranty or additional liability.\ +\ + END OF TERMS AND CONDITIONS\ +\ + APPENDIX: How to apply the Apache License to your work.\ +\ + To apply the Apache License to your work, attach the following\ + boilerplate notice, with the fields enclosed by brackets "[]"\ + replaced with your own identifying information. (Don't include\ + the brackets!) The text should be enclosed in the appropriate\ + comment syntax for the file format. We also recommend that a\ + file or class name and description of purpose be included on the\ + same "printed page" as the copyright notice for easier\ + identification within third-party archives.\ +\ + Copyright [yyyy] [name of copyright owner]\ +\ + Licensed under the Apache License, Version 2.0 (the "License");\ + you may not use this file except in compliance with the License.\ + You may obtain a copy of the License at\ +\ + http://www.apache.org/licenses/LICENSE-2.0\ +\ + Unless required by applicable law or agreed to in writing, software\ + distributed under the License is distributed on an "AS IS" BASIS,\ + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\ + See the License for the specific language governing permissions and\ + limitations under the License.\ +\ +\ +\ +## Runtime Library Exception to the Apache 2.0 License: ##\ +\ +\ + As an exception, if you use this Software to compile your source code and\ + portions of this Software are embedded into the binary product as a result,\ + you may redistribute such product without providing attribution as would\ + otherwise be required by Sections 4(a), 4(b) and 4(d) of the License.\ \ \ diff --git a/Xcodes/Resources/Localizable.xcstrings b/Xcodes/Resources/Localizable.xcstrings index b7d3d62e..1b0fa25b 100644 --- a/Xcodes/Resources/Localizable.xcstrings +++ b/Xcodes/Resources/Localizable.xcstrings @@ -1166,6 +1166,28 @@ } } }, + "Alert.DeletePlatform.PrimaryButton" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Delete" + } + } + } + }, + "Alert.DeletePlatform.Title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Are you sure you want to delete %@?" + } + } + } + }, "Alert.Install.Error.Title" : { "comment" : "Install", "extractionState" : "manual", @@ -6504,6 +6526,17 @@ } } }, + "Error" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Error" + } + } + } + }, "example@icloud.com" : { "localizations" : { "tr" : { @@ -14581,6 +14614,17 @@ } } }, + "PlatformsList.Title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Below are a list of platforms that are installed on this machine. " + } + } + } + }, "Preferences" : { "localizations" : { "ca" : { diff --git a/Xcodes/XcodesApp.swift b/Xcodes/XcodesApp.swift index 695a9e23..c6eeceb4 100644 --- a/Xcodes/XcodesApp.swift +++ b/Xcodes/XcodesApp.swift @@ -73,9 +73,50 @@ struct XcodesApp: App { PreferencesView() .environmentObject(appState) .environmentObject(updater) + .alert(item: $appState.presentedPreferenceAlert, content: { presentedAlert in + alert(for: presentedAlert) + }) } #endif } + + private func alert(for alertType: XcodesPreferencesAlert) -> Alert { + switch alertType { + case let .deletePlatform(runtime): + return Alert( + title: Text(String(format: localizeString("Alert.DeletePlatform.Title"), runtime.name)), + primaryButton: .destructive( + Text("Alert.DeletePlatform.PrimaryButton"), + action: { + Task { + do { + try await self.appState.deleteRuntime(runtime: runtime) + } catch { + var errorString: String + if let error = error as? String { + errorString = error + } else { + errorString = error.localizedDescription + } + self.appState.presentedPreferenceAlert = .generic(title: "Error", message: errorString) + } + + } + } + ), + secondaryButton: .cancel(Text("Cancel")) + ) + case let .generic(title, message): + return Alert( + title: Text(title), + message: Text(message), + dismissButton: .default( + Text("OK"), + action: { appState.presentedAlert = nil } + ) + ) + } + } } class AppDelegate: NSObject, NSApplicationDelegate { diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/CoreSimulatorImage.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/CoreSimulatorImage.swift index f2ed89b9..cf537d45 100644 --- a/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/CoreSimulatorImage.swift +++ b/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/CoreSimulatorImage.swift @@ -15,7 +15,11 @@ public struct CoreSimulatorPlist: Decodable { } } -public struct CoreSimulatorImage: Decodable { +public struct CoreSimulatorImage: Decodable, Identifiable, Equatable { + public var id: String { + return uuid + } + public let uuid: String public let path: [String: String] public let runtimeInfo: CoreSimulatorRuntimeInfo @@ -25,6 +29,10 @@ public struct CoreSimulatorImage: Decodable { self.path = path self.runtimeInfo = runtimeInfo } + + public static func == (lhs: CoreSimulatorImage, rhs: CoreSimulatorImage) -> Bool { + lhs.id == rhs.id + } } public struct CoreSimulatorRuntimeInfo: Decodable { diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/RuntimeInstallState.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/RuntimeInstallState.swift index 84c4c8d1..f50e4a22 100644 --- a/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/RuntimeInstallState.swift +++ b/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/RuntimeInstallState.swift @@ -8,7 +8,7 @@ import Foundation import Path -public enum RuntimeInstallState: Equatable { +public enum RuntimeInstallState: Equatable, Hashable { case notInstalled case installing(RuntimeInstallationStep) case installed diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/RuntimeInstallationStep.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/RuntimeInstallationStep.swift index 27b4e175..231971f7 100644 --- a/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/RuntimeInstallationStep.swift +++ b/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/RuntimeInstallationStep.swift @@ -7,7 +7,7 @@ import Foundation -public enum RuntimeInstallationStep: Equatable, CustomStringConvertible { +public enum RuntimeInstallationStep: Equatable, CustomStringConvertible, Hashable { case downloading(progress: Progress) case installing case trashingArchive diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/Runtimes.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/Runtimes.swift index 9e9e370b..26289ee8 100644 --- a/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/Runtimes.swift +++ b/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/Runtimes.swift @@ -8,7 +8,7 @@ public struct DownloadableRuntimesResponse: Codable { public let version: String } -public struct DownloadableRuntime: Codable { +public struct DownloadableRuntime: Codable, Identifiable, Hashable { public let category: Category public let simulatorVersion: SimulatorVersion public let source: String @@ -71,6 +71,14 @@ public struct DownloadableRuntime: Codable { public var downloadFileSizeString: String { return ByteCountFormatter.string(fromByteCount: Int64(fileSize), countStyle: .file) } + + public var id: String { + return visibleIdentifier + } + + public static func == (lhs: DownloadableRuntime, rhs: DownloadableRuntime) -> Bool { + return lhs.identifier == rhs.identifier + } } public struct SDKToSeedMapping: Codable { @@ -86,12 +94,12 @@ public struct SDKToSimulatorMapping: Codable { } extension DownloadableRuntime { - public struct SimulatorVersion: Codable { + public struct SimulatorVersion: Codable, Hashable { public let buildUpdate: String public let version: String } - public struct HostRequirements: Codable { + public struct HostRequirements: Codable, Hashable { let maxHostVersion: String? let excludedHostArchitectures: [String]? let minHostVersion: String? @@ -118,7 +126,7 @@ extension DownloadableRuntime { case tvOS = "com.apple.platform.appletvos" case visionOS = "com.apple.platform.xros" - var order: Int { + public var order: Int { switch self { case .iOS: return 1 case .macOS: return 2 @@ -128,7 +136,7 @@ extension DownloadableRuntime { } } - var shortName: String { + public var shortName: String { switch self { case .iOS: return "iOS" case .macOS: return "macOS" @@ -137,6 +145,7 @@ extension DownloadableRuntime { case .visionOS: return "visionOS" } } + } } diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Services/RuntimeService.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Services/RuntimeService.swift index 5ffb33c2..32508019 100644 --- a/Xcodes/XcodesKit/Sources/XcodesKit/Services/RuntimeService.swift +++ b/Xcodes/XcodesKit/Sources/XcodesKit/Services/RuntimeService.swift @@ -84,6 +84,17 @@ public struct RuntimeService { public func installPkg(pkgPath: Path, expandedPkgPath: Path) async throws { _ = try await Current.shell.installPkg(pkgPath.url, expandedPkgPath.url.absoluteString) } + + public func deleteRuntime(identifier: String) async throws { + do { + _ = try await Current.shell.deleteRuntime(identifier) + } catch { + if let executionError = error as? ProcessExecutionError { + throw executionError.standardError + } + throw error + } + } } extension String: Error {} diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Shell/XcodesShell.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Shell/XcodesShell.swift index c5760b3e..4c727951 100644 --- a/Xcodes/XcodesKit/Sources/XcodesKit/Shell/XcodesShell.swift +++ b/Xcodes/XcodesKit/Sources/XcodesKit/Shell/XcodesShell.swift @@ -23,4 +23,7 @@ public struct XcodesShell { public var installRuntimeImage: (URL) async throws -> ProcessOutput = { try await Process.run(Path.root.usr.bin.join("xcrun"), "simctl", "runtime", "add", $0.path) } + public var deleteRuntime: (String) async throws -> ProcessOutput = { + try await Process.run(Path.root.usr.bin.join("xcrun"), "simctl", "runtime", "delete", $0) + } }