diff --git a/Applite.xcodeproj/project.pbxproj b/Applite.xcodeproj/project.pbxproj index a90b298..3835f16 100644 --- a/Applite.xcodeproj/project.pbxproj +++ b/Applite.xcodeproj/project.pbxproj @@ -106,6 +106,8 @@ 419256A42D25CFE600D9EF10 /* AppMigrationView+ExportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 419256A32D25CFE600D9EF10 /* AppMigrationView+ExportView.swift */; }; 419256A62D25D00200D9EF10 /* AppMigrationView+ImportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 419256A52D25D00200D9EF10 /* AppMigrationView+ImportView.swift */; }; 419256A82D25D10F00D9EF10 /* ExportFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 419256A72D25D10F00D9EF10 /* ExportFile.swift */; }; + 419256AB2D25E19B00D9EF10 /* BrewManagementView+ActionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 419256AA2D25E19B00D9EF10 /* BrewManagementView+ActionsView.swift */; }; + 419256AD2D25E1F100D9EF10 /* BrewManagementView+InfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 419256AC2D25E1F100D9EF10 /* BrewManagementView+InfoView.swift */; }; 419506A42964A27F00FE5802 /* SetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 419506A32964A27F00FE5802 /* SetupView.swift */; }; 419506A62964A5EF00FE5802 /* BrewPathSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 419506A52964A5EF00FE5802 /* BrewPathSelectorView.swift */; }; 4196C8F528F9CB2600EADDDA /* DiscoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4196C8F428F9CB2600EADDDA /* DiscoverView.swift */; }; @@ -216,6 +218,8 @@ 419256A32D25CFE600D9EF10 /* AppMigrationView+ExportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppMigrationView+ExportView.swift"; sourceTree = ""; }; 419256A52D25D00200D9EF10 /* AppMigrationView+ImportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppMigrationView+ImportView.swift"; sourceTree = ""; }; 419256A72D25D10F00D9EF10 /* ExportFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExportFile.swift; sourceTree = ""; }; + 419256AA2D25E19B00D9EF10 /* BrewManagementView+ActionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BrewManagementView+ActionsView.swift"; sourceTree = ""; }; + 419256AC2D25E1F100D9EF10 /* BrewManagementView+InfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BrewManagementView+InfoView.swift"; sourceTree = ""; }; 419506A32964A27F00FE5802 /* SetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupView.swift; sourceTree = ""; }; 419506A52964A5EF00FE5802 /* BrewPathSelectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrewPathSelectorView.swift; sourceTree = ""; }; 419506A729696A5300FE5802 /* Applite-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Applite-Info.plist"; sourceTree = SOURCE_ROOT; }; @@ -612,6 +616,16 @@ path = "App Migration"; sourceTree = ""; }; + 419256A92D25E17900D9EF10 /* Brew Management */ = { + isa = PBXGroup; + children = ( + 418989AC2A33A5C4004AC23B /* BrewManagementView.swift */, + 419256AC2D25E1F100D9EF10 /* BrewManagementView+InfoView.swift */, + 419256AA2D25E19B00D9EF10 /* BrewManagementView+ActionsView.swift */, + ); + path = "Brew Management"; + sourceTree = ""; + }; 4196C8F628F9CB4100EADDDA /* App Views */ = { isa = PBXGroup; children = ( @@ -634,7 +648,7 @@ 41B731382A879353008BF6B9 /* ActiveTasksView.swift */, 419256A22D25CFBF00D9EF10 /* App Migration */, 41857B742912D94A004A1894 /* CategoryView.swift */, - 418989AC2A33A5C4004AC23B /* BrewManagementView.swift */, + 419256A92D25E17900D9EF10 /* Brew Management */, ); path = "Detail Views"; sourceTree = ""; @@ -768,6 +782,7 @@ 419256182D1DEA4400D9EF10 /* AppView+ActionsView.swift in Sources */, 413F77A52972B2E70053349A /* DependencyManager.swift in Sources */, 418989B42A35D67C004AC23B /* isCommandLineToolsInstalled.swift in Sources */, + 419256AB2D25E19B00D9EF10 /* BrewManagementView+ActionsView.swift in Sources */, 419256352D1DF30700D9EF10 /* SettingsView+UpdateSettings.swift in Sources */, 4192563C2D1DF3C900D9EF10 /* ContentView+SidebarViews.swift in Sources */, 4192560F2D1CC09500D9EF10 /* DependencyError.swift in Sources */, @@ -833,6 +848,7 @@ 419256912D23F93E00D9EF10 /* Card.swift in Sources */, 419256252D1DF17F00D9EF10 /* SetupView+Welcome.swift in Sources */, 419256082D1C734600D9EF10 /* Shell.swift in Sources */, + 419256AD2D25E1F100D9EF10 /* BrewManagementView+InfoView.swift in Sources */, 4192563E2D1DF3E900D9EF10 /* ContentView+DetailView.swift in Sources */, 419256162D1DEA0A00D9EF10 /* AppView+IconAndDescriptionView.swift in Sources */, 419256942D24255000D9EF10 /* CaskLoadError.swift in Sources */, diff --git a/Applite/Model/Cask Models/Cask Manager/CaskManager+BrewFunctions.swift b/Applite/Model/Cask Models/Cask Manager/CaskManager+BrewFunctions.swift index c9e6273..0a64e66 100644 --- a/Applite/Model/Cask Models/Cask Manager/CaskManager+BrewFunctions.swift +++ b/Applite/Model/Cask Models/Cask Manager/CaskManager+BrewFunctions.swift @@ -85,7 +85,7 @@ extension CaskManager { runTask(for: cask) { cask.progressState = .busy(withTask: "Uninstalling") - var arguments: [String] = [cask.info.id] + var arguments: [String] = ["uninstall", "--cask", cask.info.id] // Add -- zap argument if zap { @@ -95,7 +95,7 @@ extension CaskManager { var output: String = "" do { - output = try await Shell.runBrewCommand("uninstall", arguments: arguments) + output = try await Shell.runBrewCommand(arguments) } catch { await self.showFailure( for: cask, @@ -127,7 +127,7 @@ extension CaskManager { var output: String = "" do { - output = try await Shell.runBrewCommand("upgrade", arguments: [cask.info.id]) + output = try await Shell.runBrewCommand(["upgrade", "--cask", cask.info.id]) } catch { await self.showFailure( for: cask, @@ -158,7 +158,7 @@ extension CaskManager { var output: String = "" do { - output = try await Shell.runBrewCommand("reinstall", arguments: [cask.info.id]) + output = try await Shell.runBrewCommand(["reinstall", "--cask", cask.info.id]) } catch { await self.showFailure( for: cask, diff --git a/Applite/Model/Cask Models/Cask Manager/CaskManager+LoadData.swift b/Applite/Model/Cask Models/Cask Manager/CaskManager+LoadData.swift index d3ef339..d8039ea 100644 --- a/Applite/Model/Cask Models/Cask Manager/CaskManager+LoadData.swift +++ b/Applite/Model/Cask Models/Cask Manager/CaskManager+LoadData.swift @@ -94,7 +94,7 @@ extension CaskManager { /// - Returns: A list of Cask ID's @Sendable func getInstalledCasks() async throws -> [String] { - let output = try await Shell.runAsync("\(BrewPaths.currentBrewExecutable) list --cask") + let output = try await Shell.runBrewCommand(["list", "--cask"]) if output.isEmpty { await Self.logger.notice("No installed casks were found") diff --git a/Applite/Model/Cask Models/Cask Manager/CaskManager+RefreshOutdated.swift b/Applite/Model/Cask Models/Cask Manager/CaskManager+RefreshOutdated.swift index c26e744..7fe71bb 100644 --- a/Applite/Model/Cask Models/Cask Manager/CaskManager+RefreshOutdated.swift +++ b/Applite/Model/Cask Models/Cask Manager/CaskManager+RefreshOutdated.swift @@ -9,13 +9,13 @@ import Foundation extension CaskManager { func refreshOutdated(greedy: Bool = false) async throws -> Void { - var arguments: [String] = ["-q"] + var arguments: [String] = ["outdated", "--cask", "-q"] if greedy { arguments.append("-g") } - let output = try await Shell.runBrewCommand("outdated", arguments: arguments) + let output = try await Shell.runBrewCommand(arguments) let outdatedCaskIDs = output .trimmingCharacters(in: .whitespacesAndNewlines) diff --git a/Applite/Utilities/App Migration/ExportCasks.swift b/Applite/Utilities/App Migration/ExportCasks.swift index 2f2e662..261c8cc 100644 --- a/Applite/Utilities/App Migration/ExportCasks.swift +++ b/Applite/Utilities/App Migration/ExportCasks.swift @@ -14,7 +14,7 @@ enum CaskImportError: Error { enum AppMigration { static func export() async throws -> ExportFile { - let output = try await Shell.runBrewCommand("list") + let output = try await Shell.runBrewCommand(["list", "--cask"]) let exportedCasks = output.trimmingCharacters(in: .whitespacesAndNewlines) diff --git a/Applite/Utilities/Shell/Shell.swift b/Applite/Utilities/Shell/Shell.swift index 254a6ec..3c90c22 100644 --- a/Applite/Utilities/Shell/Shell.swift +++ b/Applite/Utilities/Shell/Shell.swift @@ -76,8 +76,8 @@ enum Shell { /// /// Using the `pty` option can leave unwanted characters in the output, use only when necessary @discardableResult - static func runBrewCommand(_ brewCommand: String, arguments: [String] = [], pty: Bool = false) async throws -> String { - let command = "\(BrewPaths.currentBrewExecutable) \(brewCommand) --cask \(arguments.joined(separator: " "))" + static func runBrewCommand(_ arguments: [String], pty: Bool = false) async throws -> String { + let command = "\(BrewPaths.currentBrewExecutable) \(arguments.joined(separator: " "))" return try await runAsync(command) } diff --git a/Applite/Views/Content View/ContentView+LoadCasks.swift b/Applite/Views/Content View/ContentView+LoadCasks.swift index 4e8d51f..ab3ad72 100644 --- a/Applite/Views/Content View/ContentView+LoadCasks.swift +++ b/Applite/Views/Content View/ContentView+LoadCasks.swift @@ -13,7 +13,7 @@ extension ContentView { loadAlert.show(title: "Couldn't load app catalog", message: DependencyManager.brokenPathOrIstallMessage) brokenInstall = true - let output = (try? await Shell.runAsync("\(BrewPaths.currentBrewExecutable) --version")) ?? "n/a" + let output = (try? await Shell.runBrewCommand(["--version"])) ?? "n/a" logger.error( """ diff --git a/Applite/Views/Detail Views/Brew Management/BrewManagementView+ActionsView.swift b/Applite/Views/Detail Views/Brew Management/BrewManagementView+ActionsView.swift new file mode 100644 index 0000000..cc92c6e --- /dev/null +++ b/Applite/Views/Detail Views/Brew Management/BrewManagementView+ActionsView.swift @@ -0,0 +1,207 @@ +// +// BrewManagementView+ActionsView.swift +// Applite +// +// Created by Milán Várady on 2025.01.01. +// + +import SwiftUI + +extension BrewManagementView { + struct ActionsView: View { + @Binding var modifyingBrew: Bool + let cardWidth: CGFloat + let cardPadding: CGFloat + let cardHeight: CGFloat = 210 + + @State var updateDone = false + @State var reinstallDone = false + + @State var isAppBrewInstalled = false + + @State var isPresentingReinstallConfirm = false + + @State var updateFailed = false + @State var reinstallFailed = false + + private struct Remark: Identifiable { + let title: LocalizedStringKey + let color: Color + let remark: LocalizedStringKey + + var id = UUID() + } + + var body: some View { + VStack(alignment: .leading) { + Text("Actions") + .font(.appliteSmallTitle) + + HStack { + ActionCard( + cardWidth: cardWidth, + cardHeight: cardHeight, + paddig: cardPadding, + actionSuccessful: $updateDone, + remarks: [ + .init(title: "Warning", color: .orange, remark: "All other app functions will be disabled during the update!") + ] + ) { + updateButton + } + + ActionCard( + cardWidth: cardWidth, + cardHeight: cardHeight, + paddig: cardPadding, + actionSuccessful: $reinstallDone, + remarks: [ + .init(title: "Note", color: .blue, remark: "This will (re)install \(Bundle.main.appName)'s Homebrew installation at: ~/Library/Application Support/\(Bundle.main.appName)/homebrew"), + .init(title: "Warning", color: .orange, remark: "After reinstalling, all currently installed apps will be unlinked from \(Bundle.main.appName). They won't be deleted, but you won't be able to update or uninstall them via \(Bundle.main.appName).") + ] + ) { + reinstallButton + } + } + .padding(.bottom, 10) + + // Progress indicator + if modifyingBrew { + HStack { + Text("In progress...") + .bold() + + SmallProgressView() + } + } + } + .task { + // Check if brew is installed in application support + isAppBrewInstalled = await isBrewPathValid(path: BrewPaths.getBrewExectuablePath(for: .appPath)) + } + } + + private struct ActionCard: View { + let cardWidth: CGFloat + let cardHeight: CGFloat + let paddig: CGFloat + @Binding var actionSuccessful: Bool + let remarks: [Remark] + @ViewBuilder let actionButton: ActionButton + + var body: some View { + Card(cardWidth: cardWidth, cardHeight: cardHeight, paddig: paddig) { + VStack(alignment: .leading) { + HStack { + actionButton + + // Success checkmark + if actionSuccessful { + Image(systemName: "checkmark.circle") + .imageScale(.large) + .foregroundStyle(.green) + } + } + .padding(.bottom, 12) + + VStack(alignment: .leading, spacing: 5) { + ForEach(remarks) { remark in + Text(remark.title) + .foregroundColor(remark.color) + .fontWeight(.bold) + + + Text(": ") + .foregroundColor(remark.color) + .fontWeight(.bold) + + + Text(remark.remark) + } + } + + Spacer() + } + } + } + } + + @MainActor + private var updateButton: some View { + Button { + withAnimation { + modifyingBrew = true + } + + Task { + logger.info("Updating brew started") + + do { + try await Shell.runBrewCommand(["update"]) + } catch { + logger.error("Brew update failed. Error: \(error.localizedDescription)") + updateFailed = true + } + + logger.info("Brew update successful") + + updateDone = true + + withAnimation { + modifyingBrew = false + } + + } + } label: { + Label("Update Homebrew", systemImage: "arrow.uturn.down.circle") + } + .controlSize(.large) + .disabled(modifyingBrew) + .padding(.trailing, 3) + .alert("Update failed", isPresented: $updateFailed, actions: {}) + } + + @MainActor + private var reinstallButton: some View { + Button(role: .destructive) { + isPresentingReinstallConfirm = true + } label: { + Label(isAppBrewInstalled ? "Reinstall Homebrew" : "Install Separate Brew", systemImage: "wrench.and.screwdriver") + } + .controlSize(.large) + .disabled(modifyingBrew) + .confirmationDialog("Are you sure you want to \(isAppBrewInstalled ? "re" : "")install Homebrew?", isPresented: $isPresentingReinstallConfirm) { + Button("Reinstall", role: .destructive) { + withAnimation { + modifyingBrew = true + } + + Task { + do { + try await DependencyManager.installHomebrew() + } catch { + reinstallFailed = true + } + + if !reinstallFailed { + reinstallDone = true + } + + withAnimation { + modifyingBrew = false + } + } + } + + Button("Cancel", role: .cancel) { } + } message: { + if isAppBrewInstalled { + Text("All currently installed apps will be unlinked from \(Bundle.main.appName).") + } else { + Text("A new Homebrew installation will be installed into ~/Library/Application Support/\(Bundle.main.appName)") + } + } + .alert("Reinstall failed", isPresented: $reinstallFailed, actions: { + Button("OK", role: .cancel) { } + }) + } + } +} diff --git a/Applite/Views/Detail Views/Brew Management/BrewManagementView+InfoView.swift b/Applite/Views/Detail Views/Brew Management/BrewManagementView+InfoView.swift new file mode 100644 index 0000000..be4e981 --- /dev/null +++ b/Applite/Views/Detail Views/Brew Management/BrewManagementView+InfoView.swift @@ -0,0 +1,61 @@ +// +// BrewManagementView+InfoView.swift +// Applite +// +// Created by Milán Várady on 2025.01.01. +// + +import SwiftUI + +extension BrewManagementView { + struct InfoView: View { + let cardWidth: CGFloat + let cardPadding: CGFloat + let cardHeight: CGFloat = 120 + + // These will be loaded in asynchronously + @State var homebrewVersion = "loading..." + @State var numberOfCasks = "loading..." + + var body: some View { + VStack(alignment: .leading) { + Text("Info") + .font(.appliteSmallTitle) + + HStack { + infoCard(title: "Homebrew Version", info: homebrewVersion) + + infoCard(title: "Apps Installed", info: numberOfCasks) + } + } + .task { + // Get version + guard let versionOutput = try? await Shell.runBrewCommand(["--version"]), + let version = versionOutput.firstMatch(of: /Homebrew ([\d\.]+)/), + let casksInstalled = try? await Shell.runAsync("\(BrewPaths.currentBrewExecutable) list --cask | wc -w") else { + + homebrewVersion = "Error" + numberOfCasks = "Error" + return + } + + homebrewVersion = String(version.1) + numberOfCasks = casksInstalled.trimmingCharacters(in: .whitespacesAndNewlines) + } + } + + private func infoCard(title: LocalizedStringKey, info: String) -> some View { + Card(cardWidth: cardWidth, cardHeight: cardHeight, paddig: cardPadding) { + VStack { + Text(title) + .font(.system(size: 16, weight: .bold)) + + Text(info) + .font(.system(size: 52, weight: .thin)) + + Spacer() + } + } + } + } +} diff --git a/Applite/Views/Detail Views/Brew Management/BrewManagementView.swift b/Applite/Views/Detail Views/Brew Management/BrewManagementView.swift new file mode 100755 index 0000000..fb580aa --- /dev/null +++ b/Applite/Views/Detail Views/Brew Management/BrewManagementView.swift @@ -0,0 +1,60 @@ +// +// BrewManagementView.swift +// Applite +// +// Created by Milán Várady on 2023. 06. 09.. +// + +import SwiftUI +import OSLog + +/// Displays info and provides tools to manage brew installation +struct BrewManagementView: View { + @Binding var modifyingBrew: Bool + + static let logger = Logger() + + let width: CGFloat = 640 + let columnSpacing: CGFloat = 40 + + var cardWidth: CGFloat { + (width - columnSpacing) / 2 + } + let cardPadding: CGFloat = 16 + + var body: some View { + ScrollView { + VStack { + VStack(alignment: .leading) { + titleAndDescription + + InfoView(cardWidth: cardWidth, cardPadding: cardPadding) + .padding(.vertical, 16) + + ActionsView(modifyingBrew: $modifyingBrew, cardWidth: cardWidth, cardPadding: cardPadding) + + Spacer() + } + .frame(width: width) + .padding(12) + } + .frame(maxWidth: .infinity) + } + } + + var titleAndDescription: some View { + VStack(alignment: .leading) { + Text("Manage Homebrew") + .font(.appliteMediumTitle) + .padding(.bottom, 2) + + Text("This application uses the [Homebrew](https://brew.sh/) (brew for short) package manager to download apps. Homebrew is a free and open source command line utility that can download useful developer tools as well as desktop applications.") + } + } +} + +struct BrewManagementView_Previews: PreviewProvider { + static var previews: some View { + BrewManagementView(modifyingBrew: .constant(false)) + } +} diff --git a/Applite/Views/Detail Views/BrewManagementView.swift b/Applite/Views/Detail Views/BrewManagementView.swift deleted file mode 100755 index 745e687..0000000 --- a/Applite/Views/Detail Views/BrewManagementView.swift +++ /dev/null @@ -1,261 +0,0 @@ -// -// BrewManagementView.swift -// Applite -// -// Created by Milán Várady on 2023. 06. 09.. -// - -import SwiftUI -import OSLog - -/// Displays info and provides tools to manage brew installation -struct BrewManagementView: View { - @Binding var modifyingBrew: Bool - - static let logger = Logger() - - var body: some View { - ScrollView { - VStack { - VStack(alignment: .leading) { - // Title - Text("Manage Homebrew") - .font(.appliteSmallTitle) - - Text("This application uses the [Homebrew](https://brew.sh/) (brew for short) package manager to download apps. Homebrew is a free and open source command line utility that can download useful developer tools as well as desktop applications.") - .padding(.bottom) - - section(title: "Info") { - InfoView() - } - .padding(.bottom) - - section(title: "Actions") { - ActionsView(modifyingBrew: $modifyingBrew) - } - .padding(.bottom) - - Spacer() - } - .frame(maxWidth: 800) - .padding(12) - } - .frame(maxWidth: .infinity) - } - } - - func section(title: LocalizedStringKey, @ViewBuilder content: ()->Content) -> some View { - VStack(alignment: .leading, spacing: 6) { - Text(title) - .font(.title) - - VStack(alignment: .leading) { - content() - } - .frame(maxWidth: .infinity, alignment: .leading) - .padding() - .background(.quinary) - .clipShape(RoundedRectangle(cornerRadius: 10)) - } - } - - private enum NoticeType: String { - case note = "Note" - case warning = "Warning" - } - - static private func notice(type: NoticeType, _ body: LocalizedStringKey) -> some View { - Group { - Text(LocalizedStringKey(type.rawValue)) - .bold() - .foregroundColor(type == .note ? .blue : .orange) - + - Text(": ") - .bold() - .foregroundColor(type == .note ? .blue : .orange) - + - Text(body) - } - } - - struct InfoView: View { - // These will be loaded in asynchronously - @State var homebrewVersion = "loading..." - @State var numberOfCasks = "loading..." - - var body: some View { - // Show Homebrew version and number of installed casks - VStack(alignment: .leading) { - Group { - Text("Homebrew version: ") + - Text(homebrewVersion).fontWeight(.light) - } - - Group { - Text("Number of apps installed: ") + - Text(numberOfCasks).fontWeight(.light) - } - } - .task { - // Get version - guard let versionOutput = try? await Shell.runAsync("\(BrewPaths.currentBrewExecutable) --version"), - let version = versionOutput.firstMatch(of: /Homebrew ([\d\.]+)/), - let casksInstalled = try? await Shell.runAsync("\(BrewPaths.currentBrewExecutable) list --cask | wc -w") else { - homebrewVersion = "N/a" - numberOfCasks = "N/a" - return - } - - homebrewVersion = String(version.1) - numberOfCasks = casksInstalled.trimmingCharacters(in: .whitespacesAndNewlines) - } - } - } - - struct ActionsView: View { - @Binding var modifyingBrew: Bool - - @State var updateDone = false - @State var reinstallDone = false - - @State var isAppBrewInstalled = false - - @State var isPresentingReinstallConfirm = false - - @State var updateFailed = false - @State var reinstallFailed = false - - var body: some View { - HStack { - // Update brew button - updateButton - - // Checkmark if success - if updateDone { - Image(systemName: "checkmark") - .font(.system(size: 18, weight: .bold)) - .foregroundColor(.green) - } - } - - notice(type: .warning, "All other app functions will be disabled during the update!") - - Divider() - .padding(.vertical, 8) - - // Reinstall brew button - HStack { - reinstallButton - .task { - // Check if brew is installed in application support - isAppBrewInstalled = await isBrewPathValid(path: BrewPaths.getBrewExectuablePath(for: .appPath)) - } - - if reinstallDone { - Image(systemName: "checkmark") - .font(.system(size: 18, weight: .bold)) - .foregroundColor(.green) - } - } - - notice(type: .note, "This will (re)install \(Bundle.main.appName)'s Homebrew installation at: ~/Library/Application Support/\(Bundle.main.appName)/homebrew") - .padding(.bottom, 3) - - notice(type: .warning, "After reinstalling, all currently installed apps will be unlinked from \(Bundle.main.appName). They won't be deleted, but you won't be able to update or uninstall them via \(Bundle.main.appName).") - - // Progress indicator - if modifyingBrew { - HStack { - Text("In progress...") - .bold() - SmallProgressView() - } - } - } - - private var updateButton: some View { - Button { - withAnimation { - modifyingBrew = true - } - - Task { - logger.info("Updating brew started") - - do { - try await Shell.runAsync("\(BrewPaths.currentBrewExecutable) update") - } catch { - await MainActor.run { - logger.error("Brew update failed. Error: \(error.localizedDescription)") - updateFailed = true - } - } - - logger.info("Brew update successful") - - await MainActor.run { - updateDone = true - - withAnimation { - modifyingBrew = false - } - } - } - } label: { - Label("Update Homebrew", systemImage: "arrow.uturn.down.circle") - } - .disabled(modifyingBrew) - .padding(.trailing, 3) - .alert("Update failed", isPresented: $updateFailed, actions: {}) - } - - private var reinstallButton: some View { - Button { - isPresentingReinstallConfirm = true - } label: { - Label(isAppBrewInstalled ? "Reinstall Homebrew" : "Install Separate Brew", systemImage: "wrench.and.screwdriver") - } - .disabled(modifyingBrew) - .confirmationDialog("Are you sure you want to \(isAppBrewInstalled ? "re" : "")install Homebrew?", isPresented: $isPresentingReinstallConfirm) { - Button("Yes") { - withAnimation { - modifyingBrew = true - } - - Task { - do { - try await DependencyManager.installHomebrew() - } catch { - reinstallFailed = true - } - - if !reinstallFailed { - reinstallDone = true - } - - withAnimation { - modifyingBrew = false - } - } - } - - Button("Cancel", role: .cancel) { } - } message: { - if isAppBrewInstalled { - Text("All currently installed apps will be unlinked from \(Bundle.main.appName).") - } else { - Text("A new Homebrew installation will be installed into ~/Library/Application Support/\(Bundle.main.appName)") - } - } - .alert("Reinstall failed", isPresented: $reinstallFailed, actions: { - Button("OK", role: .cancel) { } - }) - } - } -} - -struct BrewManagementView_Previews: PreviewProvider { - static var previews: some View { - BrewManagementView(modifyingBrew: .constant(false)) - } -} diff --git a/Localizable.xcstrings b/Localizable.xcstrings index da773a3..4f91e31 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -1036,6 +1036,9 @@ } } } + }, + "Apps Installed" : { + }, "Apps with few downloads are hidden, consider turning off this filter" : { "localizations" : { @@ -2736,8 +2739,12 @@ } } } + }, + "Homebrew Version" : { + }, "Homebrew version: " : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -3674,6 +3681,7 @@ } }, "Number of apps installed: " : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : {