diff --git a/Xcodes/Backend/AppState.swift b/Xcodes/Backend/AppState.swift index 42b91b8..4e4c1fc 100644 --- a/Xcodes/Backend/AppState.swift +++ b/Xcodes/Backend/AppState.swift @@ -4,7 +4,7 @@ import Combine import Path import LegibleError import KeychainAccess -import SwiftUI +import Path import Version class AppState: ObservableObject { @@ -12,6 +12,7 @@ class AppState: ObservableObject { private let helperClient = HelperClient() private var cancellables = Set() private var selectPublisher: AnyCancellable? + private var uninstallPublisher: AnyCancellable? @Published var authenticationState: AuthenticationState = .unauthenticated @Published var availableXcodes: [AvailableXcode] = [] { @@ -200,14 +201,32 @@ class AppState: ObservableObject { .store(in: &cancellables) } - // MARK: - + // MARK: - Install func install(id: Xcode.ID) { // TODO: } + // MARK: - Uninstall func uninstall(id: Xcode.ID) { - // TODO: + guard + let installedXcode = Current.files.installedXcodes(Path.root/"Applications").first(where: { $0.version == id }), + uninstallPublisher == nil + else { return } + + uninstallPublisher = uninstallXcode(path: installedXcode.path) + .flatMap { [unowned self] _ in + self.updateSelectedXcodePath() + } + .sink( + receiveCompletion: { [unowned self] completion in + if case let .failure(error) = completion { + self.error = AlertContent(title: "Error uninstalling Xcode", message: error.legibleLocalizedDescription) + } + self.uninstallPublisher = nil + }, + receiveValue: { _ in } + ) } func reveal(id: Xcode.ID) { @@ -293,6 +312,20 @@ class AppState: ObservableObject { } } + + private func uninstallXcode(path: Path) -> AnyPublisher { + return Deferred { + Future { promise in + do { + try Current.files.trashItem(at: path.url) + promise(.success(())) + } catch { + promise(.failure(error)) + } + } + } + .eraseToAnyPublisher() + } // MARK: - Nested Types diff --git a/Xcodes/Backend/XcodeCommands.swift b/Xcodes/Backend/XcodeCommands.swift index 9a4b1c3..4cbee0b 100644 --- a/Xcodes/Backend/XcodeCommands.swift +++ b/Xcodes/Backend/XcodeCommands.swift @@ -9,6 +9,7 @@ struct XcodeCommands: Commands { var body: some Commands { CommandMenu("Xcode") { Group { + InstallCommand() Divider() @@ -17,6 +18,10 @@ struct XcodeCommands: Commands { OpenCommand() RevealCommand() CopyPathCommand() + + Divider() + + UninstallCommand() } .environmentObject(appState) } @@ -31,24 +36,15 @@ struct InstallButton: View { let xcode: Xcode? var body: some View { - Button(action: uninstallOrInstall) { - if let xcode = xcode { - Text(xcode.installed == true ? "Uninstall" : "Install") - .help(xcode.installed == true ? "Uninstall" : "Install") - } else { - Text("Install") - .help("Install") - } + Button(action: install) { + Text("Install") + .help("Install") } } - private func uninstallOrInstall() { + private func install() { guard let xcode = xcode else { return } - if xcode.installed { - appState.xcodeBeingConfirmedForUninstallation = xcode - } else { - appState.install(id: xcode.id) - } + appState.install(id: xcode.id) } } @@ -91,6 +87,21 @@ struct OpenButton: View { } } +struct UninstallButton: View { + @EnvironmentObject var appState: AppState + let xcode: Xcode? + + var body: some View { + Button(action: { + appState.xcodeBeingConfirmedForUninstallation = xcode + }) { + Text("Uninstall") + } + .foregroundColor(.red) + .help("Uninstall") + } +} + struct RevealButton: View { @EnvironmentObject var appState: AppState let xcode: Xcode? @@ -133,8 +144,8 @@ struct InstallCommand: View { var body: some View { InstallButton(xcode: selectedXcode.unwrapped) - .keyboardShortcut(selectedXcode.unwrapped?.installed == true ? "u" : "i", modifiers: [.command, .option]) - .disabled(selectedXcode.unwrapped == nil) + .keyboardShortcut("i", modifiers: [.command, .option]) + .disabled(selectedXcode.unwrapped?.installed == true) } } @@ -181,3 +192,14 @@ struct CopyPathCommand: View { .disabled(selectedXcode.unwrapped?.installed != true) } } + +struct UninstallCommand: View { + @EnvironmentObject var appState: AppState + @FocusedValue(\.selectedXcode) private var selectedXcode: SelectedXcode? + + var body: some View { + UninstallButton(xcode: selectedXcode.unwrapped) + .keyboardShortcut("u", modifiers: [.command, .option]) + .disabled(selectedXcode.unwrapped?.installed != true) + } +} diff --git a/Xcodes/Frontend/MainWindow.swift b/Xcodes/Frontend/MainWindow.swift index fe78e1b..5052e77 100644 --- a/Xcodes/Frontend/MainWindow.swift +++ b/Xcodes/Frontend/MainWindow.swift @@ -7,13 +7,18 @@ struct MainWindow: View { @AppStorage("lastUpdated") private var lastUpdated: Double? @SceneStorage("isShowingInfoPane") private var isShowingInfoPane = false @SceneStorage("xcodeListCategory") private var category: XcodeListCategory = .all - + var body: some View { HSplitView { XcodeListView(selectedXcodeID: $selectedXcodeID, searchText: searchText, category: category) .frame(minWidth: 300) .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."), + primaryButton: .destructive(Text("Uninstall"), action: { self.appState.uninstall(id: xcode.id) }), + secondaryButton: .cancel(Text("Cancel"))) + } InfoPane(selectedXcodeID: selectedXcodeID) .frame(minWidth: 300, maxWidth: .infinity) .frame(width: isShowingInfoPane ? nil : 0) @@ -31,15 +36,6 @@ struct MainWindow: View { message: Text(verbatim: error.message), dismissButton: .default(Text("OK"))) } - /* - Removing this for now, because it's overriding the error alert that's being worked on above. - .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."), - primaryButton: .destructive(Text("Uninstall"), action: { self.appState.uninstall(id: xcode.id) }), - secondaryButton: .cancel(Text("Cancel"))) - } - **/ .sheet(isPresented: $appState.secondFactorData.isNotNil) { secondFactorView(appState.secondFactorData!) .environmentObject(appState) diff --git a/Xcodes/Frontend/XcodeList/InfoPane.swift b/Xcodes/Frontend/XcodeList/InfoPane.swift index 9395fab..0c2f29b 100644 --- a/Xcodes/Frontend/XcodeList/InfoPane.swift +++ b/Xcodes/Frontend/XcodeList/InfoPane.swift @@ -37,6 +37,9 @@ struct InfoPane: View { OpenButton(xcode: xcode) .help("Open") + + Spacer() + UninstallButton(xcode: xcode) } } else { InstallButton(xcode: xcode) @@ -49,7 +52,7 @@ struct InfoPane: View { compatibility(for: xcode) sdks(for: xcode) compilers(for: xcode) - + Spacer() } } else { @@ -250,6 +253,21 @@ struct InfoPane_Previews: PreviewProvider { }) .previewDisplayName("Populated, Uninstalled") + 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, + selected: false, + path: "/Applications/Xcode-12.3.0.app", + icon: nil, + sdks: nil, + compilers: nil) + ] + }) + .previewDisplayName("Basic, installed") + InfoPane(selectedXcodeID: nil) .environmentObject(configure(AppState()) { $0.allXcodes = [ diff --git a/Xcodes/Frontend/XcodeList/XcodeListViewRow.swift b/Xcodes/Frontend/XcodeList/XcodeListViewRow.swift index 1b1b16c..ad8e2ec 100644 --- a/Xcodes/Frontend/XcodeList/XcodeListViewRow.swift +++ b/Xcodes/Frontend/XcodeList/XcodeListViewRow.swift @@ -26,15 +26,15 @@ struct XcodeListViewRow: View { installControl(for: xcode) } .contextMenu { - InstallButton(xcode: xcode) - - Divider() - if xcode.installed { SelectButton(xcode: xcode) OpenButton(xcode: xcode) RevealButton(xcode: xcode) CopyPathButton(xcode: xcode) + Divider() + UninstallButton(xcode: xcode) + } else { + InstallButton(xcode: xcode) } } }