mirror of https://github.com/buresdv/Cork
845 lines
30 KiB
Swift
845 lines
30 KiB
Swift
//
|
|
// ContentView.swift
|
|
// Cork
|
|
//
|
|
// Created by David Bureš on 03.07.2022.
|
|
//
|
|
|
|
// swiftlint:disable file_length
|
|
|
|
import ButtonKit
|
|
import CorkShared
|
|
import Defaults
|
|
import SwiftUI
|
|
|
|
struct ContentView: View, Sendable
|
|
{
|
|
@Default(.sortPackagesBy) var sortPackagesBy: PackageSortingOptions
|
|
@Default(.allowBrewAnalytics) var allowBrewAnalytics
|
|
|
|
@Default(.areNotificationsEnabled) var areNotificationsEnabled
|
|
@Default(.outdatedPackageNotificationType) var outdatedPackageNotificationType
|
|
|
|
@Default(.enableDiscoverability) var enableDiscoverability
|
|
@Default(.discoverabilityDaySpan) var discoverabilityDaySpan: DiscoverabilityDaySpans
|
|
@Default(.sortTopPackagesBy) var sortTopPackagesBy
|
|
|
|
@Default(.customHomebrewPath) var customHomebrewPath: URL?
|
|
|
|
@Environment(\.openWindow) var openWindow: OpenWindowAction
|
|
|
|
@Environment(AppState.self) var appState: AppState
|
|
|
|
@Environment(BrewPackagesTracker.self) var brewPackagesTracker: BrewPackagesTracker
|
|
@Environment(TapTracker.self) var tapTracker: TapTracker
|
|
|
|
@Environment(CachedDownloadsTracker.self) var cachedDownloadsTracker: CachedDownloadsTracker
|
|
|
|
@Environment(TopPackagesTracker.self) var topPackagesTracker: TopPackagesTracker
|
|
|
|
@EnvironmentObject var updateProgressTracker: UpdateProgressTracker
|
|
|
|
@Environment(OutdatedPackagesTracker.self) var outdatedPackagesTracker: OutdatedPackagesTracker
|
|
|
|
@State private var multiSelection: Set<UUID> = .init()
|
|
@State private var columnVisibility: NavigationSplitViewVisibility = .doubleColumn
|
|
|
|
@State fileprivate var corruptedPackage: CorruptedPackage?
|
|
|
|
// MARK: - ViewBuilders
|
|
|
|
@ViewBuilder private var upgradePackagesButton: some View
|
|
{
|
|
Button
|
|
{
|
|
appState.showSheet(ofType: .fullUpdate)
|
|
} label: {
|
|
Label
|
|
{
|
|
Text("navigation.upgrade-packages")
|
|
} icon: {
|
|
Image(systemName: "square.and.arrow.down")
|
|
}
|
|
}
|
|
.help("navigation.upgrade-packages.help")
|
|
.disabled(self.outdatedPackageTracker.isCheckingForPackageUpdates)
|
|
}
|
|
|
|
@ViewBuilder private var addTapButton: some View
|
|
{
|
|
Button
|
|
{
|
|
appState.showSheet(ofType: .tapAddition)
|
|
} label: {
|
|
Label
|
|
{
|
|
Text("navigation.add-tap")
|
|
} icon: {
|
|
Image(systemName: "spigot")
|
|
}
|
|
}
|
|
.help("navigation.add-tap.help")
|
|
}
|
|
|
|
@ViewBuilder private var installPackageButton: some View
|
|
{
|
|
Button
|
|
{
|
|
appState.showSheet(ofType: .packageInstallation)
|
|
} label: {
|
|
Label
|
|
{
|
|
Text("navigation.install-package")
|
|
} icon: {
|
|
Image(systemName: "plus")
|
|
}
|
|
}
|
|
.help("navigation.install-package.help")
|
|
}
|
|
|
|
@ViewBuilder private var performMaintenanceButton: some View
|
|
{
|
|
Button
|
|
{
|
|
appState.showSheet(ofType: .maintenance(fastCacheDeletion: false))
|
|
} label: {
|
|
Label("start-page.open-maintenance", systemImage: "arrow.3.trianglepath")
|
|
}
|
|
.help("navigation.maintenance.help")
|
|
}
|
|
|
|
@ViewBuilder private var manageServicesButton: some View
|
|
{
|
|
Button
|
|
{
|
|
self.openWindow(id: .servicesWindowID)
|
|
} label: {
|
|
Label("navigation.manage-services", systemImage: "square.stack.3d.down.right")
|
|
}
|
|
.help("navigation.manage-services.help")
|
|
}
|
|
|
|
// MARK: - The main view
|
|
|
|
var body: some View
|
|
{
|
|
NavigationSplitView(columnVisibility: self.$columnVisibility)
|
|
{
|
|
SidebarView()
|
|
.navigationDestination(for: BrewPackage.self)
|
|
{ brewPackage in
|
|
PackageDetailView(package: brewPackage)
|
|
.id(brewPackage.id)
|
|
}
|
|
.navigationDestination(for: BrewTap.self)
|
|
{ brewTap in
|
|
TapDetailView(tap: brewTap)
|
|
.id(brewTap.id)
|
|
}
|
|
} detail: {
|
|
NavigationStack
|
|
{
|
|
StartPage()
|
|
.frame(minWidth: 600, minHeight: 500)
|
|
}
|
|
}
|
|
.navigationTitle("app-name")
|
|
.navigationSubtitle("navigation.installed-packages.count-\(self.brewPackagesTracker.numberOfInstalledPackages)")
|
|
.toolbar(id: "PackageActions")
|
|
{
|
|
ToolbarItem(id: "updatePackages", placement: .primaryAction)
|
|
{
|
|
CheckForOutdatedPackagesButton()
|
|
}
|
|
.defaultCustomization(.hidden)
|
|
|
|
ToolbarItem(id: "upgradePackages", placement: .primaryAction)
|
|
{
|
|
self.upgradePackagesButton
|
|
}
|
|
|
|
ToolbarItem(id: "addTap", placement: .primaryAction)
|
|
{
|
|
self.addTapButton
|
|
}
|
|
|
|
ToolbarItem(id: "installPackage", placement: .primaryAction)
|
|
{
|
|
self.installPackageButton
|
|
}
|
|
|
|
ToolbarItem(id: "maintenance", placement: .primaryAction)
|
|
{
|
|
self.performMaintenanceButton
|
|
}
|
|
.defaultCustomization(.hidden)
|
|
|
|
ToolbarItem(id: "manageServices", placement: .primaryAction)
|
|
{
|
|
self.manageServicesButton
|
|
}
|
|
.defaultCustomization(.hidden)
|
|
|
|
ToolbarItem(id: "spacer", placement: .automatic)
|
|
{
|
|
Spacer()
|
|
}
|
|
.defaultCustomization(.hidden)
|
|
|
|
ToolbarItem(id: "divider", placement: .automatic)
|
|
{
|
|
Divider()
|
|
}
|
|
.defaultCustomization(.hidden)
|
|
|
|
// TODO: Implement this button
|
|
/*
|
|
ToolbarItem(id: "installPackageDirectly", placement: .automatic)
|
|
{
|
|
Button
|
|
{
|
|
AppConstants.shared.logger.info("Ahoj")
|
|
} label: {
|
|
Label
|
|
{
|
|
Text("navigation.install-package.direct")
|
|
} icon: {
|
|
Image(systemName: "plus.viewfinder")
|
|
}
|
|
}
|
|
.help("navigation.install-package.direct.help")
|
|
}
|
|
.defaultCustomization(.hidden)
|
|
*/
|
|
}
|
|
.basicsSetup(of: self)
|
|
.packageLoadingTask(of: self)
|
|
.tapLoadingTask(of: self)
|
|
.analyticsSetupTask(of: self)
|
|
.cachedDownloadsCalculationTask(of: self)
|
|
.onChanges(boundToView: self)
|
|
.sheets(of: self)
|
|
.alerts(of: self)
|
|
.confirmationDialogs(of: self)
|
|
.topPackagesLoadingTask(of: self)
|
|
}
|
|
}
|
|
|
|
// MARK: - View extensions
|
|
|
|
private extension View
|
|
{
|
|
func basicsSetup(of view: ContentView) -> some View
|
|
{
|
|
self
|
|
.onAppear
|
|
{
|
|
AppConstants.shared.logger.debug("Brew executable path: \(AppConstants.shared.brewExecutablePath, privacy: .public)")
|
|
|
|
if view.customHomebrewPath != nil && !FileManager.default.fileExists(atPath: AppConstants.shared.brewExecutablePath.path)
|
|
{
|
|
view.appState.showAlert(errorToShow: .customBrewExcutableGotDeleted)
|
|
}
|
|
|
|
AppConstants.shared.logger.debug("Documents directory: \(AppConstants.shared.documentsDirectoryPath.path, privacy: .public)")
|
|
|
|
AppConstants.shared.logger.debug("System version: \(String(describing: AppConstants.shared.osVersionString), privacy: .public)")
|
|
|
|
if !FileManager.default.fileExists(atPath: AppConstants.shared.documentsDirectoryPath.path)
|
|
{
|
|
AppConstants.shared.logger.info("Documents directory does not exist, creating it...")
|
|
do
|
|
{
|
|
try FileManager.default.createDirectory(at: AppConstants.shared.documentsDirectoryPath, withIntermediateDirectories: true)
|
|
}
|
|
catch let documentDirectoryCreationError
|
|
{
|
|
AppConstants.shared.logger.error("Failed while creating document directory: \(documentDirectoryCreationError.localizedDescription)")
|
|
}
|
|
}
|
|
else
|
|
{
|
|
AppConstants.shared.logger.info("Documents directory exists")
|
|
}
|
|
|
|
if !FileManager.default.fileExists(atPath: AppConstants.shared.metadataFilePath.path)
|
|
{
|
|
AppConstants.shared.logger.info("Metadata file does not exist, creating it...")
|
|
|
|
do
|
|
{
|
|
try Data().write(to: AppConstants.shared.metadataFilePath, options: .atomic)
|
|
}
|
|
catch let metadataDirectoryCreationError
|
|
{
|
|
AppConstants.shared.logger.error("Failed while creating metadata directory: \(metadataDirectoryCreationError.localizedDescription)")
|
|
}
|
|
}
|
|
else
|
|
{
|
|
AppConstants.shared.logger.info("Metadata file exists")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private extension View
|
|
{
|
|
func packageLoadingTask(of view: ContentView) -> some View
|
|
{
|
|
self
|
|
.task(priority: .high)
|
|
{
|
|
AppConstants.shared.logger.info("Started Package Load startup action at \(Date())")
|
|
|
|
defer
|
|
{
|
|
view.appState.isLoadingFormulae = false
|
|
view.appState.isLoadingCasks = false
|
|
}
|
|
|
|
async let availableFormulae: BrewPackages? = await view.brewPackagesTracker.loadInstalledPackages(packageTypeToLoad: .formula, appState: view.appState)
|
|
async let availableCasks: BrewPackages? = await view.brewPackagesTracker.loadInstalledPackages(packageTypeToLoad: .cask, appState: view.appState)
|
|
|
|
view.brewPackagesTracker.installedFormulae = await availableFormulae ?? .init()
|
|
view.brewPackagesTracker.installedCasks = await availableCasks ?? .init()
|
|
|
|
view.cachedDownloadsTracker.assignPackageTypeToCachedDownloads(brewPackagesTracker: view.brewPackagesTracker)
|
|
|
|
// MARK: - Getting tagged packages
|
|
do
|
|
{
|
|
view.appState.taggedPackageNames = try loadTaggedIDsFromDisk()
|
|
|
|
AppConstants.shared.logger.info("Tagged packages in appState: \(view.appState.taggedPackageNames)")
|
|
|
|
do
|
|
{
|
|
try await view.brewPackagesTracker.applyTags(appState: view.appState)
|
|
}
|
|
catch let taggedStateApplicationError as NSError
|
|
{
|
|
AppConstants.shared.logger.error("Error while applying tagged state to packages: \(taggedStateApplicationError, privacy: .public)")
|
|
view.appState.showAlert(errorToShow: .couldNotApplyTaggedStateToPackages)
|
|
}
|
|
}
|
|
catch let uuidLoadingError as NSError
|
|
{
|
|
AppConstants.shared.logger.error("Failed while loading UUIDs from file: \(uuidLoadingError, privacy: .public)")
|
|
view.appState.showAlert(errorToShow: .couldNotApplyTaggedStateToPackages)
|
|
}
|
|
|
|
// MARK: - Getting pinned packages
|
|
guard let pinnedPackagesPath: URL = AppConstants.shared.pinnedPackagesPath else
|
|
{
|
|
return
|
|
}
|
|
|
|
let namesOfPinnedPackages: Set<String> = await view.brewData.getNamesOfPinnedPackages(atPinnedPackagesPath: pinnedPackagesPath)
|
|
|
|
AppConstants.shared.logger.debug("Retrieved a list of pinned package names: \(namesOfPinnedPackages.formatted(.list(type: .and)))")
|
|
|
|
await view.brewData.applyPinnedStatus(namesOfPinnedPackages: namesOfPinnedPackages)
|
|
}
|
|
}
|
|
|
|
func tapLoadingTask(of view: ContentView) -> some View
|
|
{
|
|
self
|
|
.task
|
|
{
|
|
defer
|
|
{
|
|
view.appState.isLoadingTaps = false
|
|
}
|
|
|
|
async let tapTracker: [BrewTap] = await view.tapTracker.loadUpTappedTaps()
|
|
|
|
do
|
|
{
|
|
view.tapTracker.addedTaps = try await tapTracker
|
|
}
|
|
catch let tapLoadingError as TapLoadingError
|
|
{
|
|
AppConstants.shared.logger.error("Failed while loading taps: \(tapLoadingError.localizedDescription)")
|
|
|
|
view.appState.failedWhileLoadingTaps = true
|
|
|
|
switch tapLoadingError
|
|
{
|
|
case .couldNotAccessParentTapFolder(let errorDetails):
|
|
view.appState.showAlert(errorToShow: .tapLoadingFailedDueToTapParentLocation(localizedDescription: errorDetails))
|
|
case .couldNotReadTapFolderContents(let errorDetails):
|
|
view.appState.showAlert(errorToShow: .tapLoadingFailedDueToTapItself(localizedDescription: errorDetails))
|
|
}
|
|
}
|
|
catch let unimplementedError
|
|
{
|
|
AppConstants.shared.logger.error("Failed while loading taps: Unimplemented error: \(unimplementedError.localizedDescription)")
|
|
|
|
view.appState.failedWhileLoadingTaps = true
|
|
}
|
|
}
|
|
}
|
|
|
|
func analyticsSetupTask(of view: ContentView) -> some View
|
|
{
|
|
self
|
|
.task
|
|
{
|
|
AppConstants.shared.logger.info("Started Analytics startup action at \(Date())")
|
|
|
|
async let analyticsQueryCommand: TerminalOutput = await shell(AppConstants.shared.brewExecutablePath, ["analytics"])
|
|
|
|
if await analyticsQueryCommand.standardOutput.localizedCaseInsensitiveContains("Analytics are enabled")
|
|
{
|
|
view.allowBrewAnalytics = true
|
|
AppConstants.shared.logger.info("Analytics are ENABLED")
|
|
}
|
|
else
|
|
{
|
|
view.allowBrewAnalytics = false
|
|
AppConstants.shared.logger.info("Analytics are DISABLED")
|
|
}
|
|
}
|
|
}
|
|
|
|
func discoverabilitySetupTask(of view: ContentView) -> some View
|
|
{
|
|
self
|
|
.task
|
|
{
|
|
AppConstants.shared.logger.info("Started Discoverability startup action at \(Date())")
|
|
|
|
if view.enableDiscoverability
|
|
{
|
|
if view.appState.isLoadingFormulae && view.appState.isLoadingCasks || view.tapTracker.addedTaps.isEmpty
|
|
{
|
|
await view.loadTopPackages()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func cachedDownloadsCalculationTask(of view: ContentView) -> some View
|
|
{
|
|
self
|
|
.task
|
|
{
|
|
if view.cachedDownloadsTracker.cachedDownloads.isEmpty
|
|
{
|
|
AppConstants.shared.logger.info("Will calculate cached downloads")
|
|
await view.cachedDownloadsTracker.loadCachedDownloadedPackages(brewPackagesTracker: view.brewPackagesTracker)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private extension View
|
|
{
|
|
func onChanges(boundToView view: ContentView) -> some View
|
|
{
|
|
self
|
|
.onChange(of: view.cachedDownloadsTracker.cachedDownloadsSize)
|
|
{ _ in
|
|
#warning("FIXME: This might fuck up the memory")
|
|
Task
|
|
{
|
|
AppConstants.shared.logger.info("Will recalculate cached downloads")
|
|
await view.cachedDownloadsTracker.loadCachedDownloadedPackages(brewPackagesTracker: view.brewPackagesTracker)
|
|
}
|
|
}
|
|
.onChange(of: view.areNotificationsEnabled, perform: { newValue in
|
|
if newValue == true
|
|
{
|
|
Task
|
|
{
|
|
await view.appState.setupNotifications()
|
|
}
|
|
}
|
|
})
|
|
.onChange(of: view.enableDiscoverability, perform: { newValue in
|
|
if newValue == true
|
|
{
|
|
Task
|
|
{
|
|
await view.loadTopPackages()
|
|
}
|
|
}
|
|
else
|
|
{
|
|
AppConstants.shared.logger.info("Will purge top package trackers")
|
|
/// Clear out the package trackers so they don't take up RAM
|
|
view.topPackagesTracker.topFormulae = .init()
|
|
view.topPackagesTracker.topCasks = .init()
|
|
|
|
AppConstants.shared.logger.info("Package tracker status: \(view.topPackagesTracker.topFormulae) \(view.topPackagesTracker.topCasks)")
|
|
}
|
|
})
|
|
.onChange(of: view.discoverabilityDaySpan, perform: { _ in
|
|
Task
|
|
{
|
|
await view.loadTopPackages()
|
|
}
|
|
})
|
|
.onChange(of: view.customHomebrewPath, perform: { _ in
|
|
restartApp()
|
|
})
|
|
.onChange(of: view.appState.taggedPackageNames)
|
|
{ _ in
|
|
AppConstants.shared.logger.info("Will try to save tagged IDs to disk")
|
|
do
|
|
{
|
|
try saveTaggedIDsToDisk(appState: view.appState)
|
|
}
|
|
catch let dataSavingError as NSError
|
|
{
|
|
AppConstants.shared.logger.error("Failed while trying to save data to disk: \(dataSavingError, privacy: .public)")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private extension View
|
|
{
|
|
/// Various sheets
|
|
func sheets(of view: ContentView) -> some View
|
|
{
|
|
self
|
|
.sheet(item: Bindable(view.appState).sheetToShow)
|
|
{ sheetType in
|
|
switch sheetType
|
|
{
|
|
case .packageInstallation:
|
|
AddFormulaView()
|
|
case .tapAddition:
|
|
AddTapView()
|
|
|
|
case .fullUpdate:
|
|
UpdatePackagesView()
|
|
|
|
case .partialUpdate(let packagesToUpdate):
|
|
UpdateSomePackagesView(packagesToUpdate: packagesToUpdate)
|
|
|
|
case .corruptedPackageFix(let corruptedPackage):
|
|
ReinstallCorruptedPackageView(corruptedPackageToReinstall: corruptedPackage)
|
|
|
|
case .sudoRequiredForPackageRemoval:
|
|
SudoRequiredForRemovalSheet()
|
|
|
|
case .brewfileExport:
|
|
BrewfileExportProgressView()
|
|
|
|
case .brewfileImport:
|
|
BrewfileImportProgressView()
|
|
|
|
case .maintenance(let fastCacheDeletion):
|
|
switch fastCacheDeletion
|
|
{
|
|
case false:
|
|
MaintenanceView()
|
|
case true:
|
|
MaintenanceView(shouldPurgeCache: false, shouldUninstallOrphans: false, shouldPerformHealthCheck: false, forcedOptions: true)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private extension View
|
|
{
|
|
func alerts(of view: ContentView) -> some View
|
|
{
|
|
self
|
|
.alert(isPresented: Bindable(view.appState).isShowingFatalError, error: view.appState.fatalAlertType)
|
|
{ error in
|
|
switch error
|
|
{
|
|
case .couldNotGetContentsOfPackageFolder:
|
|
EmptyView()
|
|
|
|
case .uninstallationNotPossibleDueToDependency:
|
|
EmptyView()
|
|
|
|
case .couldNotLoadAnyPackages:
|
|
RestartCorkButton()
|
|
|
|
case .triedToThreatFolderContainingPackagesAsPackage:
|
|
RestartCorkButton()
|
|
|
|
case .couldNotLoadCertainPackage(let offendingPackage, let offendingPackageURL, _):
|
|
VStack
|
|
{
|
|
Button
|
|
{
|
|
offendingPackageURL.revealInFinder(.openParentDirectoryAndHighlightTarget)
|
|
} label: {
|
|
Text("action.reveal-certain-file-in-finder-\(offendingPackage)")
|
|
}
|
|
RestartCorkButton()
|
|
}
|
|
|
|
case .licenseCheckingFailedDueToAuthorizationComplexNotBeingEncodedProperly:
|
|
EmptyView()
|
|
|
|
case .licenseCheckingFailedDueToNoInternet:
|
|
EmptyView()
|
|
|
|
case .licenseCheckingFailedDueToTimeout:
|
|
EmptyView()
|
|
|
|
case .licenseCheckingFailedForOtherReason:
|
|
EmptyView()
|
|
|
|
case .customBrewExcutableGotDeleted:
|
|
Button
|
|
{
|
|
view.customHomebrewPath = nil
|
|
} label: {
|
|
Text("action.reset-custom-brew-executable")
|
|
}
|
|
|
|
case .couldNotFindPackageUUIDInList:
|
|
EmptyView()
|
|
|
|
case .couldNotApplyTaggedStateToPackages:
|
|
VStack
|
|
{
|
|
Button(role: .destructive)
|
|
{
|
|
if FileManager.default.fileExists(atPath: AppConstants.shared.documentsDirectoryPath.path)
|
|
{
|
|
do
|
|
{
|
|
try FileManager.default.removeItem(atPath: AppConstants.shared.documentsDirectoryPath.path)
|
|
restartApp()
|
|
}
|
|
catch
|
|
{
|
|
view.appState.fatalAlertType = .couldNotClearMetadata
|
|
}
|
|
}
|
|
else
|
|
{
|
|
view.appState.fatalAlertType = .metadataFolderDoesNotExist
|
|
}
|
|
} label: {
|
|
Text("action.clear-metadata")
|
|
}
|
|
|
|
QuitCorkButton()
|
|
}
|
|
|
|
case .couldNotClearMetadata:
|
|
VStack
|
|
{
|
|
Button
|
|
{
|
|
if FileManager.default.fileExists(atPath: AppConstants.shared.documentsDirectoryPath.path)
|
|
{
|
|
AppConstants.shared.documentsDirectoryPath.revealInFinder(.openParentDirectoryAndHighlightTarget)
|
|
}
|
|
else
|
|
{
|
|
view.appState.fatalAlertType = .metadataFolderDoesNotExist
|
|
}
|
|
} label: {
|
|
Text("action.reveal-in-finder")
|
|
}
|
|
|
|
QuitCorkButton()
|
|
}
|
|
|
|
case .metadataFolderDoesNotExist:
|
|
QuitCorkButton()
|
|
|
|
case .couldNotCreateCorkMetadataDirectory:
|
|
RestartCorkButton()
|
|
|
|
case .couldNotCreateCorkMetadataFile:
|
|
RestartCorkButton()
|
|
|
|
case .installedPackageHasNoVersions(let corruptedPackageName):
|
|
Button
|
|
{
|
|
view.corruptedPackage = .init(name: corruptedPackageName)
|
|
} label: {
|
|
Text("action.repair-\(corruptedPackageName)")
|
|
}
|
|
|
|
case .installedPackageIsNotAFolder(let itemName, let itemURL):
|
|
VStack
|
|
{
|
|
Button
|
|
{
|
|
itemURL.revealInFinder(.openParentDirectoryAndHighlightTarget)
|
|
} label: {
|
|
Text("action.reveal-certain-file-in-finder-\(itemName)")
|
|
}
|
|
RestartCorkButton()
|
|
}
|
|
|
|
case .homePathNotSet:
|
|
QuitCorkButton()
|
|
|
|
case .numberOfLoadedPackagesDoesNotMatchNumberOfPackageFolders:
|
|
EmptyView()
|
|
|
|
case .couldNotObtainNotificationPermissions:
|
|
Button
|
|
{
|
|
view.appState.dismissAlert()
|
|
} label: {
|
|
Text("action.use-without-notifications")
|
|
}
|
|
|
|
case .couldNotRemoveTapDueToPackagesFromItStillBeingInstalled:
|
|
EmptyView()
|
|
|
|
case .couldNotParseTopPackages:
|
|
EmptyView()
|
|
|
|
case .receivedInvalidResponseFromBrew:
|
|
Button
|
|
{
|
|
view.appState.dismissAlert()
|
|
view.enableDiscoverability = false
|
|
} label: {
|
|
Text("action.close")
|
|
}
|
|
|
|
case .topPackageArrayFilterCouldNotRetrieveAnyPackages:
|
|
VStack
|
|
{
|
|
Button
|
|
{
|
|
view.appState.dismissAlert()
|
|
} label: {
|
|
Text("action.close")
|
|
}
|
|
RestartCorkButton()
|
|
}
|
|
|
|
case .couldNotAssociateAnyPackageWithProvidedPackageUUID:
|
|
EmptyView()
|
|
|
|
case .couldNotFindPackageInParentDirectory:
|
|
EmptyView()
|
|
|
|
case .fatalPackageInstallationError:
|
|
EmptyView()
|
|
|
|
case .fatalPackageUninstallationError:
|
|
EmptyView()
|
|
|
|
case .couldNotSynchronizePackages:
|
|
RestartCorkButton()
|
|
|
|
case .couldNotGetWorkingDirectory:
|
|
EmptyView()
|
|
|
|
case .couldNotDumpBrewfile:
|
|
EmptyView()
|
|
|
|
case .couldNotReadBrewfile:
|
|
EmptyView()
|
|
|
|
case .couldNotGetBrewfileLocation:
|
|
EmptyView()
|
|
|
|
case .couldNotImportBrewfile:
|
|
EmptyView()
|
|
|
|
case .malformedBrewfile:
|
|
EmptyView()
|
|
|
|
case .tapLoadingFailedDueToTapParentLocation:
|
|
EmptyView()
|
|
|
|
case .tapLoadingFailedDueToTapItself:
|
|
EmptyView()
|
|
case .couldNotDeleteCachedDownloads:
|
|
EmptyView()
|
|
}
|
|
} message: { error in
|
|
if let recoverySuggestion = error.recoverySuggestion
|
|
{
|
|
Text(recoverySuggestion)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private extension View
|
|
{
|
|
func confirmationDialogs(of view: ContentView) -> some View
|
|
{
|
|
self
|
|
.confirmationDialog(view.appState.confirmationDialogType?.title ?? "error.generic", isPresented: Bindable(view.appState).isShowingConfirmationDialog, presenting: view.appState.confirmationDialogType, actions: { dialogType in
|
|
switch dialogType
|
|
{
|
|
case .uninstallPackage(let packageToUninstall):
|
|
AsyncButton
|
|
{
|
|
try await view.brewPackagesTracker.uninstallSelectedPackage(
|
|
package: packageToUninstall,
|
|
cachedDownloadsTracker: view.cachedDownloadsTracker,
|
|
appState: view.appState,
|
|
outdatedPackagesTracker: view.outdatedPackagesTracker,
|
|
shouldRemoveAllAssociatedFiles: false
|
|
)
|
|
} label: {
|
|
Text("action.uninstall-\(packageToUninstall.name)")
|
|
}
|
|
.keyboardShortcut(.defaultAction)
|
|
.asyncButtonStyle(.plainStyle)
|
|
|
|
case .purgePackage(let packageToPurge):
|
|
AsyncButton
|
|
{
|
|
try await view.brewPackagesTracker.uninstallSelectedPackage(
|
|
package: packageToPurge,
|
|
cachedDownloadsTracker: view.cachedDownloadsTracker,
|
|
appState: view.appState,
|
|
outdatedPackagesTracker: view.outdatedPackagesTracker,
|
|
shouldRemoveAllAssociatedFiles: true
|
|
)
|
|
} label: {
|
|
Text("action.purge-\(packageToPurge.name)")
|
|
}
|
|
}
|
|
}, message: { dialogType in
|
|
Text(dialogType.message)
|
|
})
|
|
}
|
|
}
|
|
|
|
private extension View
|
|
{
|
|
func topPackagesLoadingTask(of view: ContentView) -> some View
|
|
{
|
|
self
|
|
.task
|
|
{
|
|
await view.loadTopPackages()
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Functions
|
|
|
|
private extension ContentView
|
|
{
|
|
func loadTopPackages() async
|
|
{
|
|
AppConstants.shared.logger.info("Initial setup finished, time to fetch the top packages")
|
|
|
|
defer
|
|
{
|
|
self.appState.isLoadingTopPackages = false
|
|
}
|
|
|
|
await self.topPackagesTracker.loadTopPackages(numberOfDays: self.discoverabilityDaySpan.rawValue, appState: self.appState)
|
|
}
|
|
}
|