mirror of https://github.com/buresdv/Cork
Merge branch 'main.compilation-adjustments' into main
Signed-off-by: David Bureš <buresdv@gmail.com>
This commit is contained in:
commit
93386c2385
|
|
@ -0,0 +1,20 @@
|
|||
name: Test Application
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
jobs:
|
||||
test:
|
||||
runs-on: macos-15
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: jdx/mise-action@v2
|
||||
- name: Select Xcode
|
||||
run: sudo xcode-select -switch /Applications/Xcode_16.1.app
|
||||
- name: Fix plugins
|
||||
run: defaults write com.apple.dt.Xcode IDESkipPackagePluginFingerprintValidatation -bool YES
|
||||
- run: tuist install
|
||||
- run: tuist test Cork
|
||||
|
|
@ -78,7 +78,16 @@ identifier_name:
|
|||
- GlobalAPIKey
|
||||
allowed_symbols: ["_"]
|
||||
reporter: "xcode" # reporter type (xcode, json, csv, checkstyle, codeclimate, junit, html, emoji, sonarqube, markdown, github-actions-logging, summary)
|
||||
#custom_rules:
|
||||
custom_rules:
|
||||
enum_associated_value_let_syntax:
|
||||
name: "Enum Associated Value Let Syntax"
|
||||
regex: 'case\s+\.?\w+\([^)]*:\s*[^)]'
|
||||
message: "Use `let` syntax for enum associated values (e.g., `case .example(let value)` instead of `case .example(value: _)`)"
|
||||
severity: warning
|
||||
match_kinds:
|
||||
- keyword # matches 'case'
|
||||
- identifier # matches enum case names
|
||||
- attribute.builtin # matches associated value declarations
|
||||
# brackets_newline:
|
||||
# name: "Opening Brackets not on Next Line"
|
||||
# message: "Opening brackets should be placed on their own lines"
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ class AppState: ObservableObject
|
|||
// MARK: - Licensing
|
||||
|
||||
@Published var licensingState: LicensingState = .notBoughtOrHasNotActivatedDemo
|
||||
@Published var isShowingLicensingSheet: Bool = false
|
||||
|
||||
// MARK: - Navigation
|
||||
|
||||
|
|
@ -29,24 +28,12 @@ class AppState: ObservableObject
|
|||
@Published var notificationEnabledInSystemSettings: Bool?
|
||||
@Published var notificationAuthStatus: UNAuthorizationStatus = .notDetermined
|
||||
|
||||
// MARK: - Stuff for controlling various sheets from the menu bar
|
||||
|
||||
@Published var isShowingInstallationSheet: Bool = false
|
||||
@Published var isShowingPackageReinstallationSheet: Bool = false
|
||||
@Published var isShowingUninstallationSheet: Bool = false
|
||||
@Published var isShowingMaintenanceSheet: Bool = false
|
||||
@Published var isShowingFastCacheDeletionMaintenanceView: Bool = false
|
||||
@Published var isShowingAddTapSheet: Bool = false
|
||||
@Published var isShowingUpdateSheet: Bool = false
|
||||
|
||||
// MARK: - Stuff for controlling the UI in general
|
||||
|
||||
@Published var isSearchFieldFocused: Bool = false
|
||||
|
||||
// MARK: - Brewfile importing and exporting
|
||||
|
||||
@Published var isShowingBrewfileExportProgress: Bool = false
|
||||
@Published var isShowingBrewfileImportProgress: Bool = false
|
||||
|
||||
@Published var brewfileImportingStage: BrewfileImportStage = .importing
|
||||
|
||||
@Published var isCheckingForPackageUpdates: Bool = true
|
||||
|
|
@ -54,28 +41,37 @@ class AppState: ObservableObject
|
|||
@Published var isShowingUninstallationProgressView: Bool = false
|
||||
@Published var isShowingFatalError: Bool = false
|
||||
@Published var fatalAlertType: DisplayableAlert? = nil
|
||||
|
||||
@Published var sheetToShow: DisplayableSheet? = nil
|
||||
|
||||
@Published var isShowingSudoRequiredForUninstallSheet: Bool = false
|
||||
@Published var packageTryingToBeUninstalledWithSudo: BrewPackage?
|
||||
|
||||
@Published var isShowingRemoveTapFailedAlert: Bool = false
|
||||
|
||||
@Published var isShowingIncrementalUpdateSheet: Bool = false
|
||||
|
||||
// MARK: - Loading of packages and taps
|
||||
@Published var isLoadingFormulae: Bool = true
|
||||
@Published var isLoadingCasks: Bool = true
|
||||
|
||||
@Published var isLoadingTaps: Bool = true
|
||||
|
||||
@Published var isLoadingTopPackages: Bool = false
|
||||
|
||||
// MARK: - Loading errors
|
||||
@Published var failedWhileLoadingFormulae: Bool = false
|
||||
@Published var failedWhileLoadingCasks: Bool = false
|
||||
@Published var failedWhileLoadingTaps: Bool = false
|
||||
|
||||
@Published var failedWhileLoadingTopPackages: Bool = false
|
||||
|
||||
@Published var cachedDownloadsFolderSize: Int64 = AppConstants.shared.brewCachedDownloadsPath.directorySize
|
||||
@Published var cachedDownloads: [CachedDownload] = .init()
|
||||
|
||||
private var cachedDownloadsTemp: [CachedDownload] = .init()
|
||||
|
||||
// MARK: - Tagging
|
||||
@Published var taggedPackageNames: Set<String> = .init()
|
||||
|
||||
@Published var corruptedPackage: String = ""
|
||||
|
||||
// MARK: - Other
|
||||
var enableExtraAnimations: Bool
|
||||
{
|
||||
return UserDefaults.standard.bool(forKey: "enableExtraAnimations")
|
||||
}
|
||||
|
||||
// MARK: - Showing errors
|
||||
|
||||
|
|
@ -92,6 +88,18 @@ class AppState: ObservableObject
|
|||
|
||||
fatalAlertType = nil
|
||||
}
|
||||
|
||||
// MARK: - Showing sheets
|
||||
|
||||
func showSheet(ofType sheetType: DisplayableSheet)
|
||||
{
|
||||
self.sheetToShow = sheetType
|
||||
}
|
||||
|
||||
func dismissSheet()
|
||||
{
|
||||
self.sheetToShow = nil
|
||||
}
|
||||
|
||||
// MARK: - Notification setup
|
||||
|
||||
|
|
@ -149,74 +157,10 @@ class AppState: ObservableObject
|
|||
|
||||
@objc func startUpdateProcessForLegacySelectors(_: NSMenuItem!)
|
||||
{
|
||||
isShowingUpdateSheet = true
|
||||
self.showSheet(ofType: .fullUpdate)
|
||||
|
||||
sendNotification(title: String(localized: "notification.upgrade-process-started"))
|
||||
}
|
||||
|
||||
func loadCachedDownloadedPackages() async
|
||||
{
|
||||
let smallestDispalyableSize: Int = .init(cachedDownloadsFolderSize / 50)
|
||||
|
||||
var packagesThatAreTooSmallToDisplaySize: Int = 0
|
||||
|
||||
guard let cachedDownloadsFolderContents: [URL] = try? FileManager.default.contentsOfDirectory(at: AppConstants.shared.brewCachedDownloadsPath, includingPropertiesForKeys: [.isRegularFileKey], options: [.skipsHiddenFiles])
|
||||
else
|
||||
{
|
||||
return
|
||||
}
|
||||
|
||||
let usableCachedDownloads: [URL] = cachedDownloadsFolderContents.filter { $0.pathExtension != "json" }
|
||||
|
||||
for usableCachedDownload in usableCachedDownloads
|
||||
{
|
||||
guard var itemName: String = try? usableCachedDownload.lastPathComponent.regexMatch("(?<=--)(.*?)(?=\\.)")
|
||||
else
|
||||
{
|
||||
return
|
||||
}
|
||||
|
||||
AppConstants.shared.logger.debug("Temp item name: \(itemName, privacy: .public)")
|
||||
|
||||
if itemName.contains("--")
|
||||
{
|
||||
do
|
||||
{
|
||||
itemName = try itemName.regexMatch(".*?(?=--)")
|
||||
}
|
||||
catch {}
|
||||
}
|
||||
|
||||
guard let itemAttributes = try? FileManager.default.attributesOfItem(atPath: usableCachedDownload.path)
|
||||
else
|
||||
{
|
||||
return
|
||||
}
|
||||
|
||||
guard let itemSize = itemAttributes[.size] as? Int
|
||||
else
|
||||
{
|
||||
return
|
||||
}
|
||||
|
||||
if itemSize < smallestDispalyableSize
|
||||
{
|
||||
packagesThatAreTooSmallToDisplaySize = packagesThatAreTooSmallToDisplaySize + itemSize
|
||||
}
|
||||
else
|
||||
{
|
||||
cachedDownloads.append(CachedDownload(packageName: itemName, sizeInBytes: itemSize))
|
||||
}
|
||||
|
||||
AppConstants.shared.logger.debug("Others size: \(packagesThatAreTooSmallToDisplaySize, privacy: .public)")
|
||||
}
|
||||
|
||||
AppConstants.shared.logger.log("Cached downloads contents: \(self.cachedDownloads)")
|
||||
|
||||
cachedDownloads = cachedDownloads.sorted(by: { $0.sizeInBytes < $1.sizeInBytes })
|
||||
|
||||
cachedDownloads.append(.init(packageName: String(localized: "start-page.cached-downloads.graph.other-smaller-packages"), sizeInBytes: packagesThatAreTooSmallToDisplaySize, packageType: .other))
|
||||
}
|
||||
}
|
||||
|
||||
private extension UNUserNotificationCenter
|
||||
|
|
@ -226,36 +170,3 @@ private extension UNUserNotificationCenter
|
|||
await notificationSettings().authorizationStatus
|
||||
}
|
||||
}
|
||||
|
||||
extension AppState
|
||||
{
|
||||
func assignPackageTypeToCachedDownloads(brewData: BrewDataStorage)
|
||||
{
|
||||
var cachedDownloadsTracker: [CachedDownload] = .init()
|
||||
|
||||
AppConstants.shared.logger.debug("Package tracker in cached download assignment function has \(brewData.installedFormulae.count + brewData.installedCasks.count) packages")
|
||||
|
||||
for cachedDownload in cachedDownloads
|
||||
{
|
||||
let normalizedCachedPackageName: String = cachedDownload.packageName.onlyLetters
|
||||
|
||||
if brewData.installedFormulae.contains(where: { $0.name.localizedCaseInsensitiveContains(normalizedCachedPackageName) })
|
||||
{ /// The cached package is a formula
|
||||
AppConstants.shared.logger.debug("Cached package \(cachedDownload.packageName) (\(normalizedCachedPackageName)) is a formula")
|
||||
cachedDownloadsTracker.append(.init(packageName: cachedDownload.packageName, sizeInBytes: cachedDownload.sizeInBytes, packageType: .formula))
|
||||
}
|
||||
else if brewData.installedCasks.contains(where: { $0.name.localizedCaseInsensitiveContains(normalizedCachedPackageName) })
|
||||
{ /// The cached package is a cask
|
||||
AppConstants.shared.logger.debug("Cached package \(cachedDownload.packageName) (\(normalizedCachedPackageName)) is a cask")
|
||||
cachedDownloadsTracker.append(.init(packageName: cachedDownload.packageName, sizeInBytes: cachedDownload.sizeInBytes, packageType: .cask))
|
||||
}
|
||||
else
|
||||
{ /// The cached package cannot be found
|
||||
AppConstants.shared.logger.debug("Cached package \(cachedDownload.packageName) (\(normalizedCachedPackageName)) is unknown")
|
||||
cachedDownloadsTracker.append(.init(packageName: cachedDownload.packageName, sizeInBytes: cachedDownload.sizeInBytes, packageType: .unknown))
|
||||
}
|
||||
}
|
||||
|
||||
cachedDownloads = cachedDownloadsTracker
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,20 +77,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject
|
|||
}
|
||||
}
|
||||
|
||||
func applicationWillTerminate(_: Notification)
|
||||
{
|
||||
AppConstants.shared.logger.debug("Will die...")
|
||||
do
|
||||
{
|
||||
try saveTaggedIDsToDisk(appState: appState)
|
||||
}
|
||||
catch let dataSavingError as NSError
|
||||
{
|
||||
AppConstants.shared.logger.error("Failed while trying to save data to disk: \(dataSavingError, privacy: .public)")
|
||||
}
|
||||
AppConstants.shared.logger.debug("Died")
|
||||
}
|
||||
|
||||
func applicationDockMenu(_: NSApplication) -> NSMenu?
|
||||
{
|
||||
let menu: NSMenu = .init()
|
||||
|
|
@ -105,7 +91,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject
|
|||
updatePackagesMenuItem.title = String(localized: "start-page.updates.loading")
|
||||
updatePackagesMenuItem.isEnabled = false
|
||||
}
|
||||
else if appState.isShowingUpdateSheet
|
||||
else if appState.sheetToShow == .fullUpdate
|
||||
{
|
||||
updatePackagesMenuItem.title = String(localized: "update-packages.updating.updating")
|
||||
updatePackagesMenuItem.isEnabled = false
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"symbols" : [
|
||||
{
|
||||
"filename" : "custom.macwindow.badge.xmark.svg",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--Generator: Apple Native CoreSVG 341-->
|
||||
<!DOCTYPE svg
|
||||
PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 3300 2200">
|
||||
<!--glyph: "", point size: 100.0, font version: "20.0d10e1", template writer version: "138.0.0"-->
|
||||
<style>.monochrome-0 {-sfsymbols-motion-group:1;-sfsymbols-layer-tags:-2de6013cdb0de8d2 -77f23b8a0c0c9a0a macwindow}
|
||||
.monochrome-1 {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:1;-sfsymbols-layer-tags:-2de6013cdb0de8d2 -77f23b8a0c0c9a0a macwindow}
|
||||
.monochrome-2 {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:1;-sfsymbols-layer-tags:-2de6013cdb0de8d2 -77f23b8a0c0c9a0a macwindow}
|
||||
.monochrome-3 {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:1;-sfsymbols-layer-tags:-2de6013cdb0de8d2 -77f23b8a0c0c9a0a macwindow}
|
||||
.monochrome-4 {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:0;-sfsymbols-layer-tags:-2de6013cdb0de8d2 _badge xmark}
|
||||
.monochrome-5 {-sfsymbols-motion-group:0;-sfsymbols-always-pulses:true;-sfsymbols-layer-tags:-2de6013cdb0de8d2 _badge xmark}
|
||||
.monochrome-6 {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:0;-sfsymbols-always-pulses:true;-sfsymbols-layer-tags:-2de6013cdb0de8d2 _badge xmark}
|
||||
|
||||
.multicolor-0:tintColor {-sfsymbols-motion-group:1;-sfsymbols-layer-tags:-2de6013cdb0de8d2 -77f23b8a0c0c9a0a macwindow}
|
||||
.multicolor-1:tintColor {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:1;-sfsymbols-layer-tags:-2de6013cdb0de8d2 -77f23b8a0c0c9a0a macwindow}
|
||||
.multicolor-2:tintColor {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:1;-sfsymbols-layer-tags:-2de6013cdb0de8d2 -77f23b8a0c0c9a0a macwindow}
|
||||
.multicolor-3:tintColor {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:1;-sfsymbols-layer-tags:-2de6013cdb0de8d2 -77f23b8a0c0c9a0a macwindow}
|
||||
.multicolor-4:tintColor {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:0;-sfsymbols-layer-tags:-2de6013cdb0de8d2 _badge xmark}
|
||||
.multicolor-5:systemRedColor {-sfsymbols-motion-group:0;-sfsymbols-always-pulses:true;-sfsymbols-layer-tags:-2de6013cdb0de8d2 _badge xmark}
|
||||
.multicolor-6:white {-sfsymbols-motion-group:0;-sfsymbols-always-pulses:true;-sfsymbols-layer-tags:-2de6013cdb0de8d2 _badge xmark}
|
||||
|
||||
.hierarchical-0:secondary {-sfsymbols-motion-group:1;-sfsymbols-layer-tags:-2de6013cdb0de8d2 -77f23b8a0c0c9a0a macwindow}
|
||||
.hierarchical-1:primary {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:1;-sfsymbols-layer-tags:-2de6013cdb0de8d2 -77f23b8a0c0c9a0a macwindow}
|
||||
.hierarchical-2:primary {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:1;-sfsymbols-layer-tags:-2de6013cdb0de8d2 -77f23b8a0c0c9a0a macwindow}
|
||||
.hierarchical-3:primary {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:1;-sfsymbols-layer-tags:-2de6013cdb0de8d2 -77f23b8a0c0c9a0a macwindow}
|
||||
.hierarchical-4:primary {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:0;-sfsymbols-layer-tags:-2de6013cdb0de8d2 _badge xmark}
|
||||
.hierarchical-5:primary {-sfsymbols-motion-group:0;-sfsymbols-layer-tags:-2de6013cdb0de8d2 _badge xmark}
|
||||
.hierarchical-6:primary {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:0;-sfsymbols-layer-tags:-2de6013cdb0de8d2 _badge xmark}
|
||||
|
||||
.SFSymbolsPreviewWireframe {fill:none;opacity:1.0;stroke:black;stroke-width:0.5}
|
||||
</style>
|
||||
<g id="Notes">
|
||||
<rect height="2200" id="artboard" style="fill:white;opacity:1" width="3300" x="0" y="0"/>
|
||||
<line style="fill:none;stroke:black;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="292" y2="292"/>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;font-weight:bold;" transform="matrix(1 0 0 1 263 322)">Weight/Scale Variations</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 559.711 322)">Ultralight</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 856.422 322)">Thin</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 1153.13 322)">Light</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 1449.84 322)">Regular</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 1746.56 322)">Medium</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 2043.27 322)">Semibold</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 2339.98 322)">Bold</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 2636.69 322)">Heavy</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 2933.4 322)">Black</text>
|
||||
<line style="fill:none;stroke:black;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1903" y2="1903"/>
|
||||
<g transform="matrix(0.2 0 0 0.2 263 1933)">
|
||||
<path d="m46.2402 4.15039c21.7773 0 39.4531-17.627 39.4531-39.4043s-17.6758-39.4043-39.4531-39.4043c-21.7285 0-39.4043 17.627-39.4043 39.4043s17.6758 39.4043 39.4043 39.4043Zm0-7.42188c-17.6758 0-31.9336-14.3066-31.9336-31.9824s14.2578-31.9824 31.9336-31.9824 31.9824 14.3066 31.9824 31.9824-14.3066 31.9824-31.9824 31.9824Zm-17.9688-31.9824c0 2.14844 1.51367 3.61328 3.75977 3.61328h10.498v10.5957c0 2.19727 1.46484 3.71094 3.61328 3.71094 2.24609 0 3.71094-1.51367 3.71094-3.71094v-10.5957h10.5957c2.19727 0 3.71094-1.46484 3.71094-3.61328 0-2.19727-1.51367-3.71094-3.71094-3.71094h-10.5957v-10.5469c0-2.24609-1.46484-3.75977-3.71094-3.75977-2.14844 0-3.61328 1.51367-3.61328 3.75977v10.5469h-10.498c-2.24609 0-3.75977 1.51367-3.75977 3.71094Z"/>
|
||||
</g>
|
||||
<g transform="matrix(0.2 0 0 0.2 281.506 1933)">
|
||||
<path d="m58.5449 14.5508c27.4902 0 49.8047-22.3145 49.8047-49.8047s-22.3145-49.8047-49.8047-49.8047-49.8047 22.3145-49.8047 49.8047 22.3145 49.8047 49.8047 49.8047Zm0-8.30078c-22.9492 0-41.5039-18.5547-41.5039-41.5039s18.5547-41.5039 41.5039-41.5039 41.5039 18.5547 41.5039 41.5039-18.5547 41.5039-41.5039 41.5039Zm-22.6562-41.5039c0 2.39258 1.66016 4.00391 4.15039 4.00391h14.3555v14.4043c0 2.44141 1.66016 4.15039 4.05273 4.15039 2.44141 0 4.15039-1.66016 4.15039-4.15039v-14.4043h14.4043c2.44141 0 4.15039-1.61133 4.15039-4.00391 0-2.44141-1.70898-4.15039-4.15039-4.15039h-14.4043v-14.3555c0-2.49023-1.70898-4.19922-4.15039-4.19922-2.39258 0-4.05273 1.70898-4.05273 4.19922v14.3555h-14.3555c-2.49023 0-4.15039 1.70898-4.15039 4.15039Z"/>
|
||||
</g>
|
||||
<g transform="matrix(0.2 0 0 0.2 304.924 1933)">
|
||||
<path d="m74.8535 28.3203c35.1074 0 63.623-28.4668 63.623-63.5742s-28.5156-63.623-63.623-63.623-63.5742 28.5156-63.5742 63.623 28.4668 63.5742 63.5742 63.5742Zm0-9.08203c-30.127 0-54.4922-24.3652-54.4922-54.4922s24.3652-54.4922 54.4922-54.4922 54.4922 24.3652 54.4922 54.4922-24.3652 54.4922-54.4922 54.4922Zm-28.8574-54.4922c0 2.58789 1.85547 4.39453 4.58984 4.39453h19.7266v19.7754c0 2.68555 1.85547 4.58984 4.44336 4.58984 2.68555 0 4.54102-1.85547 4.54102-4.58984v-19.7754h19.7754c2.68555 0 4.58984-1.80664 4.58984-4.39453 0-2.73438-1.85547-4.58984-4.58984-4.58984h-19.7754v-19.7266c0-2.73438-1.85547-4.63867-4.54102-4.63867-2.58789 0-4.44336 1.9043-4.44336 4.63867v19.7266h-19.7266c-2.73438 0-4.58984 1.85547-4.58984 4.58984Z"/>
|
||||
</g>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;font-weight:bold;" transform="matrix(1 0 0 1 263 1953)">Design Variations</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 1971)">Symbols are supported in up to nine weights and three scales.</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 1989)">For optimal layout with text and other symbols, vertically align</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 2007)">symbols with the adjacent text.</text>
|
||||
<line style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="776" x2="776" y1="1919" y2="1933"/>
|
||||
<g transform="matrix(0.2 0 0 0.2 776 1933)">
|
||||
<path d="m16.5527 0.78125c2.58789 0 3.85742-0.976562 4.78516-3.71094l6.29883-17.2363h28.8086l6.29883 17.2363c0.927734 2.73438 2.19727 3.71094 4.73633 3.71094 2.58789 0 4.24805-1.5625 4.24805-4.00391 0-0.830078-0.146484-1.61133-0.537109-2.63672l-22.9004-60.9863c-1.12305-2.97852-3.125-4.49219-6.25-4.49219-3.02734 0-5.07812 1.46484-6.15234 4.44336l-22.9004 61.084c-0.390625 1.02539-0.537109 1.80664-0.537109 2.63672 0 2.44141 1.5625 3.95508 4.10156 3.95508Zm13.4766-28.3691 11.8652-32.8613h0.244141l11.8652 32.8613Z"/>
|
||||
</g>
|
||||
<line style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="792.836" x2="792.836" y1="1919" y2="1933"/>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;font-weight:bold;" transform="matrix(1 0 0 1 776 1953)">Margins</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 776 1971)">Leading and trailing margins on the left and right side of each symbol</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 776 1989)">can be adjusted by modifying the x-location of the margin guidelines.</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 776 2007)">Modifications are automatically applied proportionally to all</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 776 2025)">scales and weights.</text>
|
||||
<g transform="matrix(0.2 0 0 0.2 1289 1933)">
|
||||
<path d="m14.209 9.32617 8.49609 8.54492c4.29688 4.3457 9.22852 4.05273 13.8672-1.07422l53.4668-58.9355-4.83398-4.88281-53.0762 58.3984c-1.75781 2.00195-3.41797 2.49023-5.76172 0.146484l-5.85938-5.81055c-2.34375-2.29492-1.80664-4.00391 0.195312-5.81055l57.373-54.0039-4.88281-4.83398-57.959 54.4434c-4.93164 4.58984-5.32227 9.47266-1.02539 13.8184Zm32.0801-90.9668c-2.09961 2.05078-2.24609 4.93164-1.07422 6.88477 1.17188 1.80664 3.4668 2.97852 6.68945 2.14844 7.32422-1.70898 14.9414-2.00195 22.0703 2.68555l-2.92969 7.27539c-1.70898 4.15039-0.830078 7.08008 1.85547 9.81445l11.4746 11.5723c2.44141 2.44141 4.49219 2.53906 7.32422 2.05078l5.32227-0.976562 3.32031 3.36914-0.195312 2.7832c-0.195312 2.49023 0.439453 4.39453 2.88086 6.78711l3.80859 3.71094c2.39258 2.39258 5.46875 2.53906 7.8125 0.195312l14.5508-14.5996c2.34375-2.34375 2.24609-5.32227-0.146484-7.71484l-3.85742-3.80859c-2.39258-2.39258-4.24805-3.17383-6.64062-2.97852l-2.88086 0.244141-3.22266-3.17383 1.2207-5.61523c0.634766-2.83203-0.146484-5.0293-3.07617-7.95898l-10.9863-10.9375c-16.6992-16.6016-38.8672-16.2109-53.3203-1.75781Zm7.4707 1.85547c12.1582-8.88672 28.6133-7.37305 39.7461 3.75977l12.1582 12.0605c1.17188 1.17188 1.36719 2.09961 1.02539 3.80859l-1.61133 7.42188 7.51953 7.42188 4.93164-0.292969c1.26953-0.0488281 1.66016 0.0488281 2.63672 1.02539l2.88086 2.88086-12.207 12.207-2.88086-2.88086c-0.976562-0.976562-1.12305-1.36719-1.07422-2.68555l0.341797-4.88281-7.4707-7.42188-7.61719 1.26953c-1.61133 0.341797-2.34375 0.195312-3.56445-0.976562l-10.0098-10.0098c-1.26953-1.17188-1.41602-2.00195-0.634766-3.85742l4.39453-10.4492c-7.8125-7.27539-17.9688-10.4004-28.125-7.42188-0.78125 0.195312-1.07422-0.439453-0.439453-0.976562Z"/>
|
||||
</g>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;font-weight:bold;" transform="matrix(1 0 0 1 1289 1953)">Exporting</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 1289 1971)">Symbols should be outlined when exporting to ensure the</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 1289 1989)">design is preserved when submitting to Xcode.</text>
|
||||
<text id="template-version" style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:end;" transform="matrix(1 0 0 1 3036 1933)">Template v.6.0</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:end;" transform="matrix(1 0 0 1 3036 1951)">Requires Xcode 16 or greater</text>
|
||||
<text id="descriptive-name" style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:end;" transform="matrix(1 0 0 1 3036 1969)">Generated from </text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:end;" transform="matrix(1 0 0 1 3036 1987)">Typeset at 100.0 points</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 726)">Small</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 1156)">Medium</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 1586)">Large</text>
|
||||
</g>
|
||||
<g id="Guides">
|
||||
<g id="H-reference" style="fill:#27AAE1;stroke:none;" transform="matrix(1 0 0 1 339 696)">
|
||||
<path d="M0.993654 0L3.63775 0L29.3281-67.1323L30.0303-67.1323L30.0303-70.459L28.1226-70.459ZM11.6885-24.4799L46.9815-24.4799L46.2315-26.7285L12.4385-26.7285ZM55.1196 0L57.7637 0L30.6382-70.459L29.4326-70.459L29.4326-67.1323Z"/>
|
||||
</g>
|
||||
<line id="Baseline-S" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="696" y2="696"/>
|
||||
<line id="Capline-S" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="625.541" y2="625.541"/>
|
||||
<g id="H-reference" style="fill:#27AAE1;stroke:none;" transform="matrix(1 0 0 1 339 1126)">
|
||||
<path d="M0.993654 0L3.63775 0L29.3281-67.1323L30.0303-67.1323L30.0303-70.459L28.1226-70.459ZM11.6885-24.4799L46.9815-24.4799L46.2315-26.7285L12.4385-26.7285ZM55.1196 0L57.7637 0L30.6382-70.459L29.4326-70.459L29.4326-67.1323Z"/>
|
||||
</g>
|
||||
<line id="Baseline-M" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1126" y2="1126"/>
|
||||
<line id="Capline-M" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1055.54" y2="1055.54"/>
|
||||
<g id="H-reference" style="fill:#27AAE1;stroke:none;" transform="matrix(1 0 0 1 339 1556)">
|
||||
<path d="M0.993654 0L3.63775 0L29.3281-67.1323L30.0303-67.1323L30.0303-70.459L28.1226-70.459ZM11.6885-24.4799L46.9815-24.4799L46.2315-26.7285L12.4385-26.7285ZM55.1196 0L57.7637 0L30.6382-70.459L29.4326-70.459L29.4326-67.1323Z"/>
|
||||
</g>
|
||||
<line id="Baseline-L" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1556" y2="1556"/>
|
||||
<line id="Capline-L" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1485.54" y2="1485.54"/>
|
||||
<line id="right-margin-Black-S" style="fill:none;stroke:#FF3B30;stroke-width:0.5;opacity:1.0;" x1="2989.45" x2="2989.45" y1="600.785" y2="720.121"/>
|
||||
<line id="left-margin-Black-S" style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="2877.35" x2="2877.35" y1="600.785" y2="720.121"/>
|
||||
<line id="right-margin-Regular-S" style="fill:none;stroke:#FF3B30;stroke-width:0.5;opacity:1.0;" x1="1504.9" x2="1504.9" y1="600.785" y2="720.121"/>
|
||||
<line id="left-margin-Regular-S" style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="1394.79" x2="1394.79" y1="600.785" y2="720.121"/>
|
||||
<line id="right-margin-Ultralight-S" style="fill:none;stroke:#FF3B30;stroke-width:0.5;opacity:1.0;" x1="613.72" x2="613.72" y1="600.785" y2="720.121"/>
|
||||
<line id="left-margin-Ultralight-S" style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="505.702" x2="505.702" y1="600.785" y2="720.121"/>
|
||||
</g>
|
||||
<g id="Symbols">
|
||||
<g id="Black-S" transform="matrix(1 0 0 1 2877.35 696)">
|
||||
<path class="monochrome-0 multicolor-0:tintColor hierarchical-0:secondary SFSymbolsPreviewWireframe" d="M9.76562-12.3047C9.76562-2.73438 15.4297 2.92969 25 2.92969L87.1094 2.92969C96.6797 2.92969 102.344-2.73438 102.344-12.3047L102.344-58.1055C102.344-67.6758 96.6797-73.3398 87.1094-73.3398L25-73.3398C15.4297-73.3398 9.76562-67.6758 9.76562-58.1055ZM23.9258-14.5508L23.9258-50.8789L88.1836-50.8789L88.1836-14.5508C88.1836-12.1094 87.1094-11.2305 84.8633-11.2305L27.2461-11.2305C25-11.2305 23.9258-12.1094 23.9258-14.5508Z"/>
|
||||
<path class="monochrome-1 multicolor-1:tintColor hierarchical-1:primary SFSymbolsPreviewWireframe" d="M28.4668-57.8613C26.0742-57.8613 24.1699-59.7656 24.1699-62.1582C24.1699-64.502 26.0742-66.4062 28.4668-66.4062C30.8105-66.4062 32.7148-64.502 32.7148-62.1582C32.7148-59.7656 30.8105-57.8613 28.4668-57.8613Z"/>
|
||||
<path class="monochrome-2 multicolor-2:tintColor hierarchical-2:primary SFSymbolsPreviewWireframe" d="M41.1621-57.8613C38.7695-57.8613 36.8652-59.7656 36.8652-62.1582C36.8652-64.502 38.7695-66.4062 41.1621-66.4062C43.5059-66.4062 45.4102-64.502 45.4102-62.1582C45.4102-59.7656 43.5059-57.8613 41.1621-57.8613Z"/>
|
||||
<path class="monochrome-3 multicolor-3:tintColor hierarchical-3:primary SFSymbolsPreviewWireframe" d="M53.8574-57.8613C51.4648-57.8613 49.5605-59.7656 49.5605-62.1582C49.5605-64.502 51.4648-66.4062 53.8574-66.4062C56.2012-66.4062 58.1055-64.502 58.1055-62.1582C58.1055-59.7656 56.2012-57.8613 53.8574-57.8613Z"/>
|
||||
<path class="monochrome-4 multicolor-4:tintColor hierarchical-4:primary SFSymbolsPreviewWireframe" d="M100.89 20.249C115.831 20.249 128.185 7.8467 128.185-7.0947C128.185-22.0361 115.831-34.3408 100.89-34.3408C85.9486-34.3408 73.5951-22.0361 73.5951-7.0947C73.5951 7.8467 85.9486 20.249 100.89 20.249Z"/>
|
||||
<path class="monochrome-5 multicolor-5:systemRedColor hierarchical-5:primary SFSymbolsPreviewWireframe" d="M100.89 13.9014C112.365 13.9014 121.837 4.331 121.837-7.0947C121.837-18.5205 112.365-27.9932 100.89-27.9932C89.4154-27.9932 79.9427-18.5205 79.9427-7.0947C79.9427 4.331 89.4154 13.9014 100.89 13.9014Z"/>
|
||||
<path class="monochrome-6 multicolor-6:white hierarchical-6:primary SFSymbolsPreviewWireframe" d="M96.4955 2.915C95.0794 4.3799 92.4427 4.2822 90.9779 2.8662C89.4642 1.4502 89.4642-1.2354 90.929-2.6514L95.3724-7.0459L90.9779-11.4404C89.513-12.9053 89.513-15.4443 90.9779-16.958C92.4427-18.4717 95.0306-18.4229 96.4955-16.958L100.89-12.5635L105.333-17.0069C106.798-18.4717 109.288-18.4229 110.753-16.9092C112.218-15.3955 112.316-12.9541 110.851-11.4893L106.408-7.0459L110.753-2.7002C112.267-1.1865 112.267 1.2549 110.753 2.7685C109.24 4.2822 106.749 4.331 105.236 2.8174L100.89-1.5283L96.4955 2.915Z"/>
|
||||
</g>
|
||||
<g id="Regular-S" transform="matrix(1 0 0 1 1394.79 696)">
|
||||
<path class="monochrome-0 multicolor-0:tintColor hierarchical-0:secondary SFSymbolsPreviewWireframe" d="M9.76562-11.9141C9.76562-3.71094 13.916 0.390625 22.2168 0.390625L87.8906 0.390625C96.2402 0.390625 100.342-3.71094 100.342-11.9141L100.342-58.4961C100.342-66.6992 96.2402-70.8008 87.8906-70.8008L22.2168-70.8008C13.916-70.8008 9.76562-66.6992 9.76562-58.4961ZM16.7969-12.2559L16.7969-50.7812L93.3105-50.7812L93.3105-12.2559C93.3105-8.54492 91.3086-6.68945 87.793-6.68945L22.3145-6.68945C18.7988-6.68945 16.7969-8.54492 16.7969-12.2559Z"/>
|
||||
<path class="monochrome-1 multicolor-1:tintColor hierarchical-1:primary SFSymbolsPreviewWireframe" d="M22.4609-57.1289C20.459-57.1289 18.8477-58.8867 18.8477-60.791C18.8477-62.6953 20.459-64.4043 22.4609-64.4043C24.4629-64.4043 26.123-62.6953 26.123-60.791C26.123-58.8867 24.4629-57.1289 22.4609-57.1289Z"/>
|
||||
<path class="monochrome-2 multicolor-2:tintColor hierarchical-2:primary SFSymbolsPreviewWireframe" d="M34.0332-57.1289C31.9824-57.1289 30.3711-58.8867 30.3711-60.791C30.3711-62.6953 31.9824-64.4043 34.0332-64.4043C36.0352-64.4043 37.6465-62.6953 37.6465-60.791C37.6465-58.8867 36.0352-57.1289 34.0332-57.1289Z"/>
|
||||
<path class="monochrome-3 multicolor-3:tintColor hierarchical-3:primary SFSymbolsPreviewWireframe" d="M45.5566-57.1289C43.5547-57.1289 41.8945-58.8867 41.8945-60.791C41.8945-62.6953 43.5547-64.4043 45.5566-64.4043C47.5586-64.4043 49.1699-62.6953 49.1699-60.791C49.1699-58.8867 47.5586-57.1289 45.5566-57.1289Z"/>
|
||||
<path class="monochrome-4 multicolor-4:tintColor hierarchical-4:primary SFSymbolsPreviewWireframe" d="M99.099 18.3447C112.917 18.3447 124.49 6.82128 124.49-7.04592C124.49-20.9619 113.015-32.4365 99.099-32.4365C85.1342-32.4365 73.7084-20.9619 73.7084-7.04592C73.7084 6.96778 85.0854 18.3447 99.099 18.3447Z"/>
|
||||
<path class="monochrome-5 multicolor-5:systemRedColor hierarchical-5:primary SFSymbolsPreviewWireframe" d="M99.099 12.6807C109.744 12.6807 118.777 3.69628 118.777-7.04592C118.777-17.8369 109.89-26.7236 99.099-26.7236C88.308-26.7236 79.3725-17.8369 79.3725-7.04592C79.3725 3.79398 88.308 12.6807 99.099 12.6807Z"/>
|
||||
<path class="monochrome-6 multicolor-6:white hierarchical-6:primary SFSymbolsPreviewWireframe" d="M93.8256 2.08498C92.7514 3.15918 91.1401 2.96388 90.1147 1.93848C89.1381 0.913077 88.894-0.698223 89.9682-1.77242L95.3881-7.19242L90.4565-12.124C89.5288-13.1494 89.5288-14.7607 90.4565-15.6396C91.433-16.5674 92.9955-16.6162 94.0209-15.6396L98.9526-10.7568L104.324-16.1279C105.447-17.2509 107.058-17.0068 108.083-15.9814C109.06-15.0049 109.304-13.3935 108.181-12.2705L102.81-6.89942L107.742-1.91892C108.669-0.942423 108.669 0.668977 107.742 1.59668C106.765 2.52438 105.203 2.57328 104.177 1.59668L99.2456-3.33492L93.8256 2.08498Z"/>
|
||||
</g>
|
||||
<g id="Ultralight-S" transform="matrix(1 0 0 1 505.702 696)">
|
||||
<path class="monochrome-0 multicolor-0:tintColor hierarchical-0:secondary SFSymbolsPreviewWireframe" d="M9.76562-11.5508C9.76562-5.02782 13.2349-1.562 19.7647-1.562L88.2539-1.562C94.7871-1.562 98.2529-5.11864 98.2529-11.5508L98.2529-58.7686C98.2529-65.2007 94.7871-68.7573 88.2539-68.7573L19.7647-68.7573C13.2349-68.7573 9.76562-65.2915 9.76562-58.7686ZM11.9834-11.6655L11.9834-52.5522L96.0351-52.5522L96.0351-11.6655C96.0351-6.45607 93.1704-3.78323 88.1562-3.78323L19.8623-3.78323C14.6665-3.78323 11.9834-6.45607 11.9834-11.6655Z"/>
|
||||
<path class="monochrome-1 multicolor-1:tintColor hierarchical-1:primary SFSymbolsPreviewWireframe" d="M19.3731-58.2642C18.0523-58.2642 16.9404-59.3862 16.9404-60.6548C16.9404-61.9688 18.0523-63.042 19.3731-63.042C20.6485-63.042 21.7637-61.9688 21.7637-60.6548C21.7637-59.3862 20.6485-58.2642 19.3731-58.2642Z"/>
|
||||
<path class="monochrome-2 multicolor-2:tintColor hierarchical-2:primary SFSymbolsPreviewWireframe" d="M27.8121-58.2642C26.4878-58.2642 25.4214-59.3408 25.4214-60.6548C25.4214-61.9688 26.4878-63.042 27.8121-63.042C29.1329-63.042 30.1993-61.9688 30.1993-60.6548C30.1993-59.3408 29.1329-58.2642 27.8121-58.2642Z"/>
|
||||
<path class="monochrome-3 multicolor-3:tintColor hierarchical-3:primary SFSymbolsPreviewWireframe" d="M36.293-58.3096C34.9722-58.3096 33.857-59.3408 33.857-60.6548C33.857-61.9688 34.9722-63.042 36.293-63.042C37.5684-63.042 38.6803-61.9688 38.6803-60.6548C38.6803-59.3408 37.5684-58.3096 36.293-58.3096Z"/>
|
||||
<path class="monochrome-4 multicolor-4:tintColor hierarchical-4:primary SFSymbolsPreviewWireframe" d="M97.2163 14.3033C108.9 14.3033 118.565 4.68695 118.565-7.04595C118.565-18.7822 108.953-28.395 97.2163-28.395C85.5219-28.395 75.8671-18.7368 75.8671-7.04595C75.8671 4.69725 85.5185 14.3033 97.2163 14.3033Z"/>
|
||||
<path class="monochrome-5 multicolor-5:systemRedColor hierarchical-5:primary SFSymbolsPreviewWireframe" d="M97.2163 11.273C107.225 11.273 115.486 3.01515 115.486-7.04595C115.486-17.0649 107.281-25.3159 97.2163-25.3159C87.1972-25.3159 78.9428-17.0195 78.9428-7.04595C78.9428 3.02195 87.1972 11.273 97.2163 11.273Z"/>
|
||||
<path class="monochrome-6 multicolor-6:white hierarchical-6:primary SFSymbolsPreviewWireframe" d="M90.6713 1.72165C90.142 2.29635 89.1664 2.37355 88.5043 1.71135C87.8457 1.04925 87.9194 0.164552 88.5395-0.455648L95.1401-7.05615L88.8915-13.3047C88.3271-13.9214 88.1909-14.8061 88.8915-15.458C89.5957-16.0679 90.477-15.9805 91.0029-15.458L97.2968-9.21295L103.849-15.81C104.427-16.3882 105.357-16.3711 106.019-15.7998C106.678-15.1865 106.649-14.2109 106.026-13.5874L99.4287-7.03565L105.677-0.829148C106.287-0.215848 106.332 0.623552 105.677 1.32415C105.018 2.02485 104.092 1.93745 103.52 1.32415L97.3173-4.87895L90.6713 1.72165Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 24 KiB |
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"symbols" : [
|
||||
{
|
||||
"filename" : "custom.spigot.badge.xmark.svg",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--Generator: Apple Native CoreSVG 341-->
|
||||
<!DOCTYPE svg
|
||||
PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 3300 2200">
|
||||
<!--glyph: "", point size: 100.0, font version: "20.0d10e1", template writer version: "138.0.0"-->
|
||||
<style>.monochrome-0 {-sfsymbols-motion-group:1;-sfsymbols-layer-tags:-8a1b4e545d67453 39c0f5e3f7db3502 spigot}
|
||||
.monochrome-1 {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:0;-sfsymbols-layer-tags:39c0f5e3f7db3502 _badge xmark}
|
||||
.monochrome-2 {-sfsymbols-motion-group:0;-sfsymbols-always-pulses:true;-sfsymbols-layer-tags:39c0f5e3f7db3502 _badge xmark}
|
||||
.monochrome-3 {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:0;-sfsymbols-always-pulses:true;-sfsymbols-layer-tags:39c0f5e3f7db3502 _badge xmark}
|
||||
|
||||
.multicolor-0:tintColor {-sfsymbols-motion-group:1;-sfsymbols-layer-tags:-8a1b4e545d67453 39c0f5e3f7db3502 spigot}
|
||||
.multicolor-1:tintColor {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:0;-sfsymbols-layer-tags:39c0f5e3f7db3502 _badge xmark}
|
||||
.multicolor-2:systemRedColor {-sfsymbols-motion-group:0;-sfsymbols-always-pulses:true;-sfsymbols-layer-tags:39c0f5e3f7db3502 _badge xmark}
|
||||
.multicolor-3:white {-sfsymbols-motion-group:0;-sfsymbols-always-pulses:true;-sfsymbols-layer-tags:39c0f5e3f7db3502 _badge xmark}
|
||||
|
||||
.hierarchical-0:secondary {-sfsymbols-motion-group:1;-sfsymbols-layer-tags:-8a1b4e545d67453 39c0f5e3f7db3502 spigot}
|
||||
.hierarchical-1:primary {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:0;-sfsymbols-layer-tags:39c0f5e3f7db3502 _badge xmark}
|
||||
.hierarchical-2:primary {-sfsymbols-motion-group:0;-sfsymbols-layer-tags:39c0f5e3f7db3502 _badge xmark}
|
||||
.hierarchical-3:primary {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:0;-sfsymbols-layer-tags:39c0f5e3f7db3502 _badge xmark}
|
||||
|
||||
.SFSymbolsPreviewWireframe {fill:none;opacity:1.0;stroke:black;stroke-width:0.5}
|
||||
</style>
|
||||
<g id="Notes">
|
||||
<rect height="2200" id="artboard" style="fill:white;opacity:1" width="3300" x="0" y="0"/>
|
||||
<line style="fill:none;stroke:black;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="292" y2="292"/>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;font-weight:bold;" transform="matrix(1 0 0 1 263 322)">Weight/Scale Variations</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 559.711 322)">Ultralight</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 856.422 322)">Thin</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 1153.13 322)">Light</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 1449.84 322)">Regular</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 1746.56 322)">Medium</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 2043.27 322)">Semibold</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 2339.98 322)">Bold</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 2636.69 322)">Heavy</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 2933.4 322)">Black</text>
|
||||
<line style="fill:none;stroke:black;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1903" y2="1903"/>
|
||||
<g transform="matrix(0.2 0 0 0.2 263 1933)">
|
||||
<path d="m46.2402 4.15039c21.7773 0 39.4531-17.627 39.4531-39.4043s-17.6758-39.4043-39.4531-39.4043c-21.7285 0-39.4043 17.627-39.4043 39.4043s17.6758 39.4043 39.4043 39.4043Zm0-7.42188c-17.6758 0-31.9336-14.3066-31.9336-31.9824s14.2578-31.9824 31.9336-31.9824 31.9824 14.3066 31.9824 31.9824-14.3066 31.9824-31.9824 31.9824Zm-17.9688-31.9824c0 2.14844 1.51367 3.61328 3.75977 3.61328h10.498v10.5957c0 2.19727 1.46484 3.71094 3.61328 3.71094 2.24609 0 3.71094-1.51367 3.71094-3.71094v-10.5957h10.5957c2.19727 0 3.71094-1.46484 3.71094-3.61328 0-2.19727-1.51367-3.71094-3.71094-3.71094h-10.5957v-10.5469c0-2.24609-1.46484-3.75977-3.71094-3.75977-2.14844 0-3.61328 1.51367-3.61328 3.75977v10.5469h-10.498c-2.24609 0-3.75977 1.51367-3.75977 3.71094Z"/>
|
||||
</g>
|
||||
<g transform="matrix(0.2 0 0 0.2 281.506 1933)">
|
||||
<path d="m58.5449 14.5508c27.4902 0 49.8047-22.3145 49.8047-49.8047s-22.3145-49.8047-49.8047-49.8047-49.8047 22.3145-49.8047 49.8047 22.3145 49.8047 49.8047 49.8047Zm0-8.30078c-22.9492 0-41.5039-18.5547-41.5039-41.5039s18.5547-41.5039 41.5039-41.5039 41.5039 18.5547 41.5039 41.5039-18.5547 41.5039-41.5039 41.5039Zm-22.6562-41.5039c0 2.39258 1.66016 4.00391 4.15039 4.00391h14.3555v14.4043c0 2.44141 1.66016 4.15039 4.05273 4.15039 2.44141 0 4.15039-1.66016 4.15039-4.15039v-14.4043h14.4043c2.44141 0 4.15039-1.61133 4.15039-4.00391 0-2.44141-1.70898-4.15039-4.15039-4.15039h-14.4043v-14.3555c0-2.49023-1.70898-4.19922-4.15039-4.19922-2.39258 0-4.05273 1.70898-4.05273 4.19922v14.3555h-14.3555c-2.49023 0-4.15039 1.70898-4.15039 4.15039Z"/>
|
||||
</g>
|
||||
<g transform="matrix(0.2 0 0 0.2 304.924 1933)">
|
||||
<path d="m74.8535 28.3203c35.1074 0 63.623-28.4668 63.623-63.5742s-28.5156-63.623-63.623-63.623-63.5742 28.5156-63.5742 63.623 28.4668 63.5742 63.5742 63.5742Zm0-9.08203c-30.127 0-54.4922-24.3652-54.4922-54.4922s24.3652-54.4922 54.4922-54.4922 54.4922 24.3652 54.4922 54.4922-24.3652 54.4922-54.4922 54.4922Zm-28.8574-54.4922c0 2.58789 1.85547 4.39453 4.58984 4.39453h19.7266v19.7754c0 2.68555 1.85547 4.58984 4.44336 4.58984 2.68555 0 4.54102-1.85547 4.54102-4.58984v-19.7754h19.7754c2.68555 0 4.58984-1.80664 4.58984-4.39453 0-2.73438-1.85547-4.58984-4.58984-4.58984h-19.7754v-19.7266c0-2.73438-1.85547-4.63867-4.54102-4.63867-2.58789 0-4.44336 1.9043-4.44336 4.63867v19.7266h-19.7266c-2.73438 0-4.58984 1.85547-4.58984 4.58984Z"/>
|
||||
</g>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;font-weight:bold;" transform="matrix(1 0 0 1 263 1953)">Design Variations</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 1971)">Symbols are supported in up to nine weights and three scales.</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 1989)">For optimal layout with text and other symbols, vertically align</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 2007)">symbols with the adjacent text.</text>
|
||||
<line style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="776" x2="776" y1="1919" y2="1933"/>
|
||||
<g transform="matrix(0.2 0 0 0.2 776 1933)">
|
||||
<path d="m16.5527 0.78125c2.58789 0 3.85742-0.976562 4.78516-3.71094l6.29883-17.2363h28.8086l6.29883 17.2363c0.927734 2.73438 2.19727 3.71094 4.73633 3.71094 2.58789 0 4.24805-1.5625 4.24805-4.00391 0-0.830078-0.146484-1.61133-0.537109-2.63672l-22.9004-60.9863c-1.12305-2.97852-3.125-4.49219-6.25-4.49219-3.02734 0-5.07812 1.46484-6.15234 4.44336l-22.9004 61.084c-0.390625 1.02539-0.537109 1.80664-0.537109 2.63672 0 2.44141 1.5625 3.95508 4.10156 3.95508Zm13.4766-28.3691 11.8652-32.8613h0.244141l11.8652 32.8613Z"/>
|
||||
</g>
|
||||
<line style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="792.836" x2="792.836" y1="1919" y2="1933"/>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;font-weight:bold;" transform="matrix(1 0 0 1 776 1953)">Margins</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 776 1971)">Leading and trailing margins on the left and right side of each symbol</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 776 1989)">can be adjusted by modifying the x-location of the margin guidelines.</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 776 2007)">Modifications are automatically applied proportionally to all</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 776 2025)">scales and weights.</text>
|
||||
<g transform="matrix(0.2 0 0 0.2 1289 1933)">
|
||||
<path d="m14.209 9.32617 8.49609 8.54492c4.29688 4.3457 9.22852 4.05273 13.8672-1.07422l53.4668-58.9355-4.83398-4.88281-53.0762 58.3984c-1.75781 2.00195-3.41797 2.49023-5.76172 0.146484l-5.85938-5.81055c-2.34375-2.29492-1.80664-4.00391 0.195312-5.81055l57.373-54.0039-4.88281-4.83398-57.959 54.4434c-4.93164 4.58984-5.32227 9.47266-1.02539 13.8184Zm32.0801-90.9668c-2.09961 2.05078-2.24609 4.93164-1.07422 6.88477 1.17188 1.80664 3.4668 2.97852 6.68945 2.14844 7.32422-1.70898 14.9414-2.00195 22.0703 2.68555l-2.92969 7.27539c-1.70898 4.15039-0.830078 7.08008 1.85547 9.81445l11.4746 11.5723c2.44141 2.44141 4.49219 2.53906 7.32422 2.05078l5.32227-0.976562 3.32031 3.36914-0.195312 2.7832c-0.195312 2.49023 0.439453 4.39453 2.88086 6.78711l3.80859 3.71094c2.39258 2.39258 5.46875 2.53906 7.8125 0.195312l14.5508-14.5996c2.34375-2.34375 2.24609-5.32227-0.146484-7.71484l-3.85742-3.80859c-2.39258-2.39258-4.24805-3.17383-6.64062-2.97852l-2.88086 0.244141-3.22266-3.17383 1.2207-5.61523c0.634766-2.83203-0.146484-5.0293-3.07617-7.95898l-10.9863-10.9375c-16.6992-16.6016-38.8672-16.2109-53.3203-1.75781Zm7.4707 1.85547c12.1582-8.88672 28.6133-7.37305 39.7461 3.75977l12.1582 12.0605c1.17188 1.17188 1.36719 2.09961 1.02539 3.80859l-1.61133 7.42188 7.51953 7.42188 4.93164-0.292969c1.26953-0.0488281 1.66016 0.0488281 2.63672 1.02539l2.88086 2.88086-12.207 12.207-2.88086-2.88086c-0.976562-0.976562-1.12305-1.36719-1.07422-2.68555l0.341797-4.88281-7.4707-7.42188-7.61719 1.26953c-1.61133 0.341797-2.34375 0.195312-3.56445-0.976562l-10.0098-10.0098c-1.26953-1.17188-1.41602-2.00195-0.634766-3.85742l4.39453-10.4492c-7.8125-7.27539-17.9688-10.4004-28.125-7.42188-0.78125 0.195312-1.07422-0.439453-0.439453-0.976562Z"/>
|
||||
</g>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;font-weight:bold;" transform="matrix(1 0 0 1 1289 1953)">Exporting</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 1289 1971)">Symbols should be outlined when exporting to ensure the</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 1289 1989)">design is preserved when submitting to Xcode.</text>
|
||||
<text id="template-version" style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:end;" transform="matrix(1 0 0 1 3036 1933)">Template v.6.0</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:end;" transform="matrix(1 0 0 1 3036 1951)">Requires Xcode 16 or greater</text>
|
||||
<text id="descriptive-name" style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:end;" transform="matrix(1 0 0 1 3036 1969)">Generated from </text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:end;" transform="matrix(1 0 0 1 3036 1987)">Typeset at 100.0 points</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 726)">Small</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 1156)">Medium</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 1586)">Large</text>
|
||||
</g>
|
||||
<g id="Guides">
|
||||
<g id="H-reference" style="fill:#27AAE1;stroke:none;" transform="matrix(1 0 0 1 339 696)">
|
||||
<path d="M0.993654 0L3.63775 0L29.3281-67.1323L30.0303-67.1323L30.0303-70.459L28.1226-70.459ZM11.6885-24.4799L46.9815-24.4799L46.2315-26.7285L12.4385-26.7285ZM55.1196 0L57.7637 0L30.6382-70.459L29.4326-70.459L29.4326-67.1323Z"/>
|
||||
</g>
|
||||
<line id="Baseline-S" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="696" y2="696"/>
|
||||
<line id="Capline-S" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="625.541" y2="625.541"/>
|
||||
<g id="H-reference" style="fill:#27AAE1;stroke:none;" transform="matrix(1 0 0 1 339 1126)">
|
||||
<path d="M0.993654 0L3.63775 0L29.3281-67.1323L30.0303-67.1323L30.0303-70.459L28.1226-70.459ZM11.6885-24.4799L46.9815-24.4799L46.2315-26.7285L12.4385-26.7285ZM55.1196 0L57.7637 0L30.6382-70.459L29.4326-70.459L29.4326-67.1323Z"/>
|
||||
</g>
|
||||
<line id="Baseline-M" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1126" y2="1126"/>
|
||||
<line id="Capline-M" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1055.54" y2="1055.54"/>
|
||||
<g id="H-reference" style="fill:#27AAE1;stroke:none;" transform="matrix(1 0 0 1 339 1556)">
|
||||
<path d="M0.993654 0L3.63775 0L29.3281-67.1323L30.0303-67.1323L30.0303-70.459L28.1226-70.459ZM11.6885-24.4799L46.9815-24.4799L46.2315-26.7285L12.4385-26.7285ZM55.1196 0L57.7637 0L30.6382-70.459L29.4326-70.459L29.4326-67.1323Z"/>
|
||||
</g>
|
||||
<line id="Baseline-L" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1556" y2="1556"/>
|
||||
<line id="Capline-L" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1485.54" y2="1485.54"/>
|
||||
<line id="right-margin-Black-S" style="fill:none;stroke:#FF3B30;stroke-width:0.5;opacity:1.0;" x1="2993.82" x2="2993.82" y1="600.785" y2="720.121"/>
|
||||
<line id="left-margin-Black-S" style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="2872.98" x2="2872.98" y1="600.785" y2="720.121"/>
|
||||
<line id="right-margin-Regular-S" style="fill:none;stroke:#FF3B30;stroke-width:0.5;opacity:1.0;" x1="1507.14" x2="1507.14" y1="600.785" y2="720.121"/>
|
||||
<line id="left-margin-Regular-S" style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="1392.54" x2="1392.54" y1="600.785" y2="720.121"/>
|
||||
<line id="right-margin-Ultralight-S" style="fill:none;stroke:#FF3B30;stroke-width:0.5;opacity:1.0;" x1="609.518" x2="609.518" y1="600.785" y2="720.121"/>
|
||||
<line id="left-margin-Ultralight-S" style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="509.904" x2="509.904" y1="600.785" y2="720.121"/>
|
||||
</g>
|
||||
<g id="Symbols">
|
||||
<g id="Black-S" transform="matrix(1 0 0 1 2872.98 696)">
|
||||
<path class="monochrome-0 multicolor-0:tintColor hierarchical-0:secondary SFSymbolsPreviewWireframe" d="M55.0781-73.3398L33.0078-75.6348C28.3691-76.123 24.9512-73.0469 24.9512-68.6035C24.9512-64.1113 28.3691-61.0352 33.0078-61.5234L55.0781-63.8184ZM60.0098-59.2285C65.1855-59.2285 69.3848-63.4277 69.3848-68.6035C69.3848-73.7305 65.1855-77.9297 60.0098-77.9297C54.8828-77.9297 50.6836-73.7305 50.6836-68.6035C50.6836-63.4277 54.8828-59.2285 60.0098-59.2285ZM65.0391-73.3398L65.0391-63.8184L87.1094-61.5234C91.748-61.0352 95.166-64.1113 95.166-68.6035C95.166-73.0469 91.748-76.123 87.1094-75.6348ZM54.3945-62.5488L54.3945-50.6836L65.625-50.6836L65.625-62.5488ZM64.2578-41.1133L73.7793-41.1133L73.7793-48.7793C73.7793-53.4668 70.3613-56.8848 65.6738-56.8848L54.3457-56.8848C49.6582-56.8848 46.2402-53.4668 46.2402-48.7793L46.2402-41.1133L55.7617-41.1133L55.7617-47.8027C55.7617-48.1445 55.957-48.3398 56.2988-48.3398L63.7207-48.3398C64.0625-48.3398 64.2578-48.1445 64.2578-47.8027ZM60.0098 0.78125C70.6055 0.78125 74.3164-4.3457 77.3926-7.12891C78.5645-8.1543 79.6387-8.88672 80.957-8.88672C83.1543-8.88672 84.9609-6.88477 84.9609-3.66211L84.9609 1.70898C84.9609 4.93164 87.4512 7.42188 90.6738 7.42188L105.371 7.42188C108.594 7.42188 111.084 4.93164 111.084 1.70898L111.084-4.15039C111.084-27.5879 96.0449-35.2539 83.3496-35.2539L83.252-35.2539C81.6895-35.2539 80.3711-36.1328 79.834-37.6465L78.7598-40.6738C77.5879-43.9453 75.5371-45.5566 72.3633-45.5566L47.6562-45.5566C44.4824-45.5566 42.4316-43.9453 41.2598-40.6738L40.1855-37.6465C39.6484-36.1328 38.3301-35.2539 36.7676-35.2539L12.5977-35.2539L12.5977-25.2441L40.4785-25.2441C45.4102-25.2441 47.168-27.6367 48.3398-30.8105L50.0488-35.5469L69.9707-35.5469L71.6797-30.8105C72.8516-27.6367 75.0977-25.2441 80.0293-25.2441L82.1289-25.2441C92.1875-25.2441 101.123-20.3613 101.123-4.15039L101.123-2.58789L94.9707-2.58789L94.9707-3.66211C94.9707-16.2598 86.9629-18.8965 81.25-18.8965C76.6602-18.8965 73.8281-17.3828 71.582-15.5762C68.0664-12.7441 65.7715-9.22852 60.0098-9.22852C56.1523-9.22852 52.8809-10.5469 49.5605-14.502C47.0703-17.4316 45.6543-18.8965 39.7461-18.8965L12.5977-18.8965L12.5977-8.88672L39.3555-8.88672C40.625-8.88672 41.748-8.30078 42.5781-7.27539C46.9727-2.09961 53.125 0.78125 60.0098 0.78125ZM15.1367 0.634766C18.0664 0.634766 20.5078-1.80664 20.5078-4.73633L20.5078-39.4043C20.5078-42.334 18.0664-44.7754 15.1367-44.7754C12.207-44.7754 9.76562-42.334 9.76562-39.4043L9.76562-4.73633C9.76562-1.80664 12.207 0.634766 15.1367 0.634766Z"/>
|
||||
<path class="monochrome-1 multicolor-1:tintColor hierarchical-1:primary SFSymbolsPreviewWireframe" d="M108.756 20.249C123.697 20.249 136.051 7.8467 136.051-7.0947C136.051-22.0361 123.697-34.3408 108.756-34.3408C93.8146-34.3408 81.4611-22.0361 81.4611-7.0947C81.4611 7.8467 93.8146 20.249 108.756 20.249Z"/>
|
||||
<path class="monochrome-2 multicolor-2:systemRedColor hierarchical-2:primary SFSymbolsPreviewWireframe" d="M108.756 13.9014C120.231 13.9014 129.703 4.331 129.703-7.0947C129.703-18.5205 120.231-27.9932 108.756-27.9932C97.2814-27.9932 87.8087-18.5205 87.8087-7.0947C87.8087 4.331 97.2814 13.9014 108.756 13.9014Z"/>
|
||||
<path class="monochrome-3 multicolor-3:white hierarchical-3:primary SFSymbolsPreviewWireframe" d="M104.362 2.915C102.945 4.3799 100.309 4.2822 98.8439 2.8662C97.3302 1.4502 97.3302-1.2354 98.795-2.6514L103.238-7.0459L98.8439-11.4404C97.379-12.9053 97.379-15.4443 98.8439-16.958C100.309-18.4717 102.897-18.4229 104.362-16.958L108.756-12.5635L113.199-17.0069C114.664-18.4717 117.154-18.4229 118.619-16.9092C120.084-15.3955 120.182-12.9541 118.717-11.4893L114.274-7.0459L118.619-2.7002C120.133-1.1865 120.133 1.2549 118.619 2.7685C117.106 4.2822 114.615 4.331 113.102 2.8174L108.756-1.5283L104.362 2.915Z"/>
|
||||
</g>
|
||||
<g id="Regular-S" transform="matrix(1 0 0 1 1392.54 696)">
|
||||
<path class="monochrome-0 multicolor-0:tintColor hierarchical-0:secondary SFSymbolsPreviewWireframe" d="M52.7344-68.7988L32.373-70.9473C28.8086-71.3379 26.2695-69.043 26.2695-65.6738C26.2695-62.2559 28.8086-59.9609 32.373-60.3516L52.7344-62.5ZM56.4453-58.1543C60.5957-58.1543 63.9648-61.5234 63.9648-65.6738C63.9648-69.8242 60.5957-73.1445 56.4453-73.1445C52.2949-73.1445 48.9258-69.8242 48.9258-65.6738C48.9258-61.5234 52.2949-58.1543 56.4453-58.1543ZM60.1074-68.7988L60.1074-62.5L80.4688-60.3516C84.0332-59.9609 86.5723-62.2559 86.5723-65.6738C86.5723-69.043 84.0332-71.3379 80.4688-70.9473ZM52.6367-60.9863L52.6367-51.6113L60.2051-51.6113L60.2051-60.9863ZM62.4023-43.1641L68.3105-43.1641L68.3105-49.0723C68.3105-52.7344 65.7227-55.2734 62.0605-55.2734L50.6836-55.2734C47.0215-55.2734 44.4824-52.7344 44.4824-49.0723L44.4824-43.1641L50.3418-43.1641L50.3418-48.3887C50.3418-49.5117 51.1719-50.3418 52.2949-50.3418L60.4492-50.3418C61.6211-50.3418 62.4023-49.5117 62.4023-48.3887ZM56.3965-3.56445C62.9395-3.56445 67.334-6.15234 71.8262-10.498C73.4375-12.0605 75.293-12.8418 77.0508-12.8418C80.0781-12.8418 82.5684-10.5957 82.5684-6.20117L82.5684-1.2207C82.5684 1.02539 84.1797 2.63672 86.4258 2.63672L100.977 2.63672C103.174 2.63672 104.834 1.02539 104.834-1.2207L104.834-4.6875C104.834-28.4668 89.9414-35.7422 77.7832-35.7422L76.2695-35.7422C75.8789-35.7422 75.5859-36.0352 75.3906-36.377L73.1934-42.4316C72.2656-44.9707 70.8984-46.0449 68.5547-46.0449L44.1895-46.0449C41.8945-46.0449 40.5273-44.9707 39.5508-42.4316L37.3535-36.377C37.207-36.0352 36.8652-35.7422 36.4746-35.7422L12.5-35.7422L12.5-29.3945L37.0117-29.3945C40.918-29.3945 42.1875-31.0547 43.1641-33.7891L45.3613-39.6973L67.3828-39.6973L69.5801-33.7891C70.5566-31.0547 71.8262-29.3945 75.7324-29.3945L77.7344-29.3945C88.4277-29.3945 98.5352-23.6816 98.5352-4.6875L98.5352-3.71094L88.916-3.71094L88.916-6.20117C88.916-17.1387 82.2754-19.2871 77.4902-19.2871C73.3398-19.2871 70.3125-17.627 67.3828-14.7461C65.1367-12.6465 61.8652-9.91211 56.3965-9.91211C50.7812-9.91211 47.3633-12.3047 44.873-15.3809C42.334-18.5547 40.2344-19.4824 36.1328-19.4824L12.5-19.4824L12.5-13.1348L35.7422-13.1348C37.8418-13.1348 38.7207-12.8906 40.1855-11.1816C44.043-6.5918 49.8047-3.56445 56.3965-3.56445ZM13.2812-4.10156C15.2344-4.10156 16.7969-5.71289 16.7969-7.61719L16.7969-39.9902C16.7969-41.8945 15.2344-43.5059 13.2812-43.5059C11.377-43.5059 9.76562-41.8945 9.76562-39.9902L9.76562-7.61719C9.76562-5.71289 11.377-4.10156 13.2812-4.10156Z"/>
|
||||
<path class="monochrome-1 multicolor-1:tintColor hierarchical-1:primary SFSymbolsPreviewWireframe" d="M103.14 18.3447C116.958 18.3447 128.531 6.82128 128.531-7.04592C128.531-20.9619 117.056-32.4365 103.14-32.4365C89.1752-32.4365 77.7494-20.9619 77.7494-7.04592C77.7494 6.96778 89.1264 18.3447 103.14 18.3447Z"/>
|
||||
<path class="monochrome-2 multicolor-2:systemRedColor hierarchical-2:primary SFSymbolsPreviewWireframe" d="M103.14 12.6807C113.785 12.6807 122.818 3.69628 122.818-7.04592C122.818-17.8369 113.931-26.7236 103.14-26.7236C92.349-26.7236 83.4135-17.8369 83.4135-7.04592C83.4135 3.79398 92.349 12.6807 103.14 12.6807Z"/>
|
||||
<path class="monochrome-3 multicolor-3:white hierarchical-3:primary SFSymbolsPreviewWireframe" d="M97.8666 2.08498C96.7924 3.15918 95.1811 2.96388 94.1557 1.93848C93.1791 0.913077 92.935-0.698223 94.0092-1.77242L99.4291-7.19242L94.4975-12.124C93.5698-13.1494 93.5698-14.7607 94.4975-15.6396C95.474-16.5674 97.0365-16.6162 98.0619-15.6396L102.994-10.7568L108.365-16.1279C109.488-17.2509 111.099-17.0068 112.124-15.9814C113.101-15.0049 113.345-13.3935 112.222-12.2705L106.851-6.89942L111.783-1.91892C112.71-0.942423 112.71 0.668977 111.783 1.59668C110.806 2.52438 109.244 2.57328 108.218 1.59668L103.287-3.33492L97.8666 2.08498Z"/>
|
||||
</g>
|
||||
<g id="Ultralight-S" transform="matrix(1 0 0 1 509.904 696)">
|
||||
<path class="monochrome-0 multicolor-0:tintColor hierarchical-0:secondary SFSymbolsPreviewWireframe" d="M46.0591-63.8038L29.1035-65.9976C26.2202-66.3882 24.0899-64.4566 24.0899-61.6324C24.0899-58.7593 26.2202-56.8731 29.1035-57.2637L46.0591-59.4575ZM48.544-56.4287C51.4683-56.4287 53.7022-58.7534 53.7022-61.6324C53.7022-64.5113 51.4683-66.8326 48.544-66.8326C45.6197-66.8326 43.295-64.5113 43.295-61.6324C43.295-58.7534 45.6197-56.4287 48.544-56.4287ZM51.0709-63.8038L51.0709-59.4575L67.9811-57.2637C70.9098-56.8731 73.0401-58.7593 73.0401-61.6324C73.0401-64.4566 70.9098-66.3882 67.9811-65.9976ZM47.0059-58.8975L47.0059-51.1118L50.1695-51.1118L50.1695-58.8975ZM55.7725-44.6172L57.9117-44.6172L57.9117-49.708C57.9117-51.5537 56.6407-52.8667 54.8404-52.8667L42.192-52.8667C40.3917-52.8667 39.0787-51.5537 39.0787-49.708L39.0787-44.6172L41.2598-44.6172L41.2598-49.7056C41.2598-50.3291 41.6358-50.7505 42.2594-50.7505L54.7276-50.7505C55.3545-50.7505 55.7725-50.3291 55.7725-49.7056ZM48.4952-10.9208C55.7193-10.9208 59.3419-14.7348 63.3345-17.6274C64.7188-18.645 66.5289-19.4262 68.2413-19.4262C72.1768-19.4262 74.6671-16.726 74.6671-12.5585L74.6671-5.53463C74.6671-4.15133 75.0523-3.76606 76.3902-3.76606L88.0802-3.76606C89.3692-3.76606 89.8488-4.24215 89.8488-5.53463L89.8488-9.63717C89.8488-26.1963 81.1773-36.0146 66.4308-36.0146L64.0543-36.0146C63.3004-36.0146 62.6441-36.4438 62.4034-37.1489L59.7066-44.5205C59.3692-45.3794 58.8194-45.7725 57.8834-45.7725L39.1036-45.7725C38.1255-45.7725 37.6211-45.3794 37.2803-44.5205L34.5835-37.1489C34.3462-36.4438 33.6411-36.0146 32.8872-36.0146L10.8198-36.0146L10.8198-33.8447L32.3799-33.8447C35.0147-33.8447 36.0118-34.8691 36.7159-36.7861L39.231-43.6479L57.7105-43.6479L60.2711-36.7861C60.9298-34.8691 61.9268-33.8447 64.607-33.8447L66.4273-33.8447C79.9361-33.8447 87.7276-24.8169 87.7276-9.63717L87.7276-5.93602L76.7916-5.93602L76.7916-12.5585C76.7916-18.2285 73.4659-21.603 68.272-21.603C65.8018-21.603 63.6373-20.6694 61.979-19.3779C58.3707-16.5517 55.4625-13.0908 48.4952-13.0908C41.5631-13.0908 38.1905-16.8003 34.5196-19.6948C32.8887-21.0068 31.9698-21.3442 29.0943-21.3442L10.8198-21.3442L10.8198-19.1743L29.0669-19.1743C31.2574-19.1743 31.8184-18.9755 33.1924-17.9477C37.1407-15.038 41.3131-10.9208 48.4952-10.9208ZM10.8745-13.2289C11.4654-13.2289 11.9834-13.7504 11.9834-14.3378L11.9834-40.7168C11.9834-41.3496 11.4654-41.8257 10.8745-41.8257C10.2417-41.8257 9.76562-41.3496 9.76562-40.7168L9.76562-14.3378C9.76562-13.7504 10.2417-13.2289 10.8745-13.2289Z"/>
|
||||
<path class="monochrome-1 multicolor-1:tintColor hierarchical-1:primary SFSymbolsPreviewWireframe" d="M89.6527 14.3033C101.337 14.3033 111.002 4.68695 111.002-7.04595C111.002-18.7822 101.389-28.395 89.6527-28.395C77.9583-28.395 68.3035-18.7368 68.3035-7.04595C68.3035 4.69725 77.9549 14.3033 89.6527 14.3033Z"/>
|
||||
<path class="monochrome-2 multicolor-2:systemRedColor hierarchical-2:primary SFSymbolsPreviewWireframe" d="M89.6527 11.273C99.6615 11.273 107.923 3.01515 107.923-7.04595C107.923-17.0649 99.7171-25.3159 89.6527-25.3159C79.6336-25.3159 71.3792-17.0195 71.3792-7.04595C71.3792 3.02195 79.6336 11.273 89.6527 11.273Z"/>
|
||||
<path class="monochrome-3 multicolor-3:white hierarchical-3:primary SFSymbolsPreviewWireframe" d="M83.1077 1.72165C82.5784 2.29635 81.6028 2.37355 80.9407 1.71135C80.2821 1.04925 80.3558 0.164552 80.9759-0.455648L87.5765-7.05615L81.3279-13.3047C80.7635-13.9214 80.6273-14.8061 81.3279-15.458C82.0321-16.0679 82.9134-15.9805 83.4393-15.458L89.7332-9.21295L96.285-15.81C96.8631-16.3882 97.7933-16.3711 98.4554-15.7998C99.1141-15.1865 99.0858-14.2109 98.4622-13.5874L91.8651-7.03565L98.1136-0.829148C98.7235-0.215848 98.7689 0.623552 98.1136 1.32415C97.4549 2.02485 96.5281 1.93745 95.9568 1.32415L89.7537-4.87895L83.1077 1.72165Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 26 KiB |
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"symbols" : [
|
||||
{
|
||||
"filename" : "custom.terminal.badge.xmark.svg",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--Generator: Apple Native CoreSVG 341-->
|
||||
<!DOCTYPE svg
|
||||
PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 3300 2200">
|
||||
<!--glyph: "", point size: 100.0, font version: "20.0d10e1", template writer version: "138.0.0"-->
|
||||
<style>.monochrome-0 {-sfsymbols-motion-group:1;-sfsymbols-layer-tags:-116e57ac81a25b71 6eb21f60b3d84617 apple.terminal}
|
||||
.monochrome-1 {-sfsymbols-motion-group:1;-sfsymbols-layer-tags:-116e57ac81a25b71 6eb21f60b3d84617 apple.terminal}
|
||||
.monochrome-2 {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:0;-sfsymbols-layer-tags:-116e57ac81a25b71 _badge xmark}
|
||||
.monochrome-3 {-sfsymbols-motion-group:0;-sfsymbols-always-pulses:true;-sfsymbols-layer-tags:-116e57ac81a25b71 _badge xmark}
|
||||
.monochrome-4 {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:0;-sfsymbols-always-pulses:true;-sfsymbols-layer-tags:-116e57ac81a25b71 _badge xmark}
|
||||
|
||||
.multicolor-0:tintColor {-sfsymbols-motion-group:1;-sfsymbols-layer-tags:-116e57ac81a25b71 6eb21f60b3d84617 apple.terminal}
|
||||
.multicolor-1:tintColor {-sfsymbols-motion-group:1;-sfsymbols-layer-tags:-116e57ac81a25b71 6eb21f60b3d84617 apple.terminal}
|
||||
.multicolor-2:tintColor {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:0;-sfsymbols-layer-tags:-116e57ac81a25b71 _badge xmark}
|
||||
.multicolor-3:systemRedColor {-sfsymbols-motion-group:0;-sfsymbols-always-pulses:true;-sfsymbols-layer-tags:-116e57ac81a25b71 _badge xmark}
|
||||
.multicolor-4:white {-sfsymbols-motion-group:0;-sfsymbols-always-pulses:true;-sfsymbols-layer-tags:-116e57ac81a25b71 _badge xmark}
|
||||
|
||||
.hierarchical-0:secondary {-sfsymbols-motion-group:1;-sfsymbols-layer-tags:-116e57ac81a25b71 6eb21f60b3d84617 apple.terminal}
|
||||
.hierarchical-1:secondary {-sfsymbols-motion-group:1;-sfsymbols-layer-tags:-116e57ac81a25b71 6eb21f60b3d84617 apple.terminal}
|
||||
.hierarchical-2:primary {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:0;-sfsymbols-layer-tags:-116e57ac81a25b71 _badge xmark}
|
||||
.hierarchical-3:primary {-sfsymbols-motion-group:0;-sfsymbols-layer-tags:-116e57ac81a25b71 _badge xmark}
|
||||
.hierarchical-4:primary {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:0;-sfsymbols-layer-tags:-116e57ac81a25b71 _badge xmark}
|
||||
|
||||
.SFSymbolsPreviewWireframe {fill:none;opacity:1.0;stroke:black;stroke-width:0.5}
|
||||
</style>
|
||||
<g id="Notes">
|
||||
<rect height="2200" id="artboard" style="fill:white;opacity:1" width="3300" x="0" y="0"/>
|
||||
<line style="fill:none;stroke:black;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="292" y2="292"/>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;font-weight:bold;" transform="matrix(1 0 0 1 263 322)">Weight/Scale Variations</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 559.711 322)">Ultralight</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 856.422 322)">Thin</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 1153.13 322)">Light</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 1449.84 322)">Regular</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 1746.56 322)">Medium</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 2043.27 322)">Semibold</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 2339.98 322)">Bold</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 2636.69 322)">Heavy</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 2933.4 322)">Black</text>
|
||||
<line style="fill:none;stroke:black;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1903" y2="1903"/>
|
||||
<g transform="matrix(0.2 0 0 0.2 263 1933)">
|
||||
<path d="m46.2402 4.15039c21.7773 0 39.4531-17.627 39.4531-39.4043s-17.6758-39.4043-39.4531-39.4043c-21.7285 0-39.4043 17.627-39.4043 39.4043s17.6758 39.4043 39.4043 39.4043Zm0-7.42188c-17.6758 0-31.9336-14.3066-31.9336-31.9824s14.2578-31.9824 31.9336-31.9824 31.9824 14.3066 31.9824 31.9824-14.3066 31.9824-31.9824 31.9824Zm-17.9688-31.9824c0 2.14844 1.51367 3.61328 3.75977 3.61328h10.498v10.5957c0 2.19727 1.46484 3.71094 3.61328 3.71094 2.24609 0 3.71094-1.51367 3.71094-3.71094v-10.5957h10.5957c2.19727 0 3.71094-1.46484 3.71094-3.61328 0-2.19727-1.51367-3.71094-3.71094-3.71094h-10.5957v-10.5469c0-2.24609-1.46484-3.75977-3.71094-3.75977-2.14844 0-3.61328 1.51367-3.61328 3.75977v10.5469h-10.498c-2.24609 0-3.75977 1.51367-3.75977 3.71094Z"/>
|
||||
</g>
|
||||
<g transform="matrix(0.2 0 0 0.2 281.506 1933)">
|
||||
<path d="m58.5449 14.5508c27.4902 0 49.8047-22.3145 49.8047-49.8047s-22.3145-49.8047-49.8047-49.8047-49.8047 22.3145-49.8047 49.8047 22.3145 49.8047 49.8047 49.8047Zm0-8.30078c-22.9492 0-41.5039-18.5547-41.5039-41.5039s18.5547-41.5039 41.5039-41.5039 41.5039 18.5547 41.5039 41.5039-18.5547 41.5039-41.5039 41.5039Zm-22.6562-41.5039c0 2.39258 1.66016 4.00391 4.15039 4.00391h14.3555v14.4043c0 2.44141 1.66016 4.15039 4.05273 4.15039 2.44141 0 4.15039-1.66016 4.15039-4.15039v-14.4043h14.4043c2.44141 0 4.15039-1.61133 4.15039-4.00391 0-2.44141-1.70898-4.15039-4.15039-4.15039h-14.4043v-14.3555c0-2.49023-1.70898-4.19922-4.15039-4.19922-2.39258 0-4.05273 1.70898-4.05273 4.19922v14.3555h-14.3555c-2.49023 0-4.15039 1.70898-4.15039 4.15039Z"/>
|
||||
</g>
|
||||
<g transform="matrix(0.2 0 0 0.2 304.924 1933)">
|
||||
<path d="m74.8535 28.3203c35.1074 0 63.623-28.4668 63.623-63.5742s-28.5156-63.623-63.623-63.623-63.5742 28.5156-63.5742 63.623 28.4668 63.5742 63.5742 63.5742Zm0-9.08203c-30.127 0-54.4922-24.3652-54.4922-54.4922s24.3652-54.4922 54.4922-54.4922 54.4922 24.3652 54.4922 54.4922-24.3652 54.4922-54.4922 54.4922Zm-28.8574-54.4922c0 2.58789 1.85547 4.39453 4.58984 4.39453h19.7266v19.7754c0 2.68555 1.85547 4.58984 4.44336 4.58984 2.68555 0 4.54102-1.85547 4.54102-4.58984v-19.7754h19.7754c2.68555 0 4.58984-1.80664 4.58984-4.39453 0-2.73438-1.85547-4.58984-4.58984-4.58984h-19.7754v-19.7266c0-2.73438-1.85547-4.63867-4.54102-4.63867-2.58789 0-4.44336 1.9043-4.44336 4.63867v19.7266h-19.7266c-2.73438 0-4.58984 1.85547-4.58984 4.58984Z"/>
|
||||
</g>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;font-weight:bold;" transform="matrix(1 0 0 1 263 1953)">Design Variations</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 1971)">Symbols are supported in up to nine weights and three scales.</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 1989)">For optimal layout with text and other symbols, vertically align</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 2007)">symbols with the adjacent text.</text>
|
||||
<line style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="776" x2="776" y1="1919" y2="1933"/>
|
||||
<g transform="matrix(0.2 0 0 0.2 776 1933)">
|
||||
<path d="m16.5527 0.78125c2.58789 0 3.85742-0.976562 4.78516-3.71094l6.29883-17.2363h28.8086l6.29883 17.2363c0.927734 2.73438 2.19727 3.71094 4.73633 3.71094 2.58789 0 4.24805-1.5625 4.24805-4.00391 0-0.830078-0.146484-1.61133-0.537109-2.63672l-22.9004-60.9863c-1.12305-2.97852-3.125-4.49219-6.25-4.49219-3.02734 0-5.07812 1.46484-6.15234 4.44336l-22.9004 61.084c-0.390625 1.02539-0.537109 1.80664-0.537109 2.63672 0 2.44141 1.5625 3.95508 4.10156 3.95508Zm13.4766-28.3691 11.8652-32.8613h0.244141l11.8652 32.8613Z"/>
|
||||
</g>
|
||||
<line style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="792.836" x2="792.836" y1="1919" y2="1933"/>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;font-weight:bold;" transform="matrix(1 0 0 1 776 1953)">Margins</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 776 1971)">Leading and trailing margins on the left and right side of each symbol</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 776 1989)">can be adjusted by modifying the x-location of the margin guidelines.</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 776 2007)">Modifications are automatically applied proportionally to all</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 776 2025)">scales and weights.</text>
|
||||
<g transform="matrix(0.2 0 0 0.2 1289 1933)">
|
||||
<path d="m14.209 9.32617 8.49609 8.54492c4.29688 4.3457 9.22852 4.05273 13.8672-1.07422l53.4668-58.9355-4.83398-4.88281-53.0762 58.3984c-1.75781 2.00195-3.41797 2.49023-5.76172 0.146484l-5.85938-5.81055c-2.34375-2.29492-1.80664-4.00391 0.195312-5.81055l57.373-54.0039-4.88281-4.83398-57.959 54.4434c-4.93164 4.58984-5.32227 9.47266-1.02539 13.8184Zm32.0801-90.9668c-2.09961 2.05078-2.24609 4.93164-1.07422 6.88477 1.17188 1.80664 3.4668 2.97852 6.68945 2.14844 7.32422-1.70898 14.9414-2.00195 22.0703 2.68555l-2.92969 7.27539c-1.70898 4.15039-0.830078 7.08008 1.85547 9.81445l11.4746 11.5723c2.44141 2.44141 4.49219 2.53906 7.32422 2.05078l5.32227-0.976562 3.32031 3.36914-0.195312 2.7832c-0.195312 2.49023 0.439453 4.39453 2.88086 6.78711l3.80859 3.71094c2.39258 2.39258 5.46875 2.53906 7.8125 0.195312l14.5508-14.5996c2.34375-2.34375 2.24609-5.32227-0.146484-7.71484l-3.85742-3.80859c-2.39258-2.39258-4.24805-3.17383-6.64062-2.97852l-2.88086 0.244141-3.22266-3.17383 1.2207-5.61523c0.634766-2.83203-0.146484-5.0293-3.07617-7.95898l-10.9863-10.9375c-16.6992-16.6016-38.8672-16.2109-53.3203-1.75781Zm7.4707 1.85547c12.1582-8.88672 28.6133-7.37305 39.7461 3.75977l12.1582 12.0605c1.17188 1.17188 1.36719 2.09961 1.02539 3.80859l-1.61133 7.42188 7.51953 7.42188 4.93164-0.292969c1.26953-0.0488281 1.66016 0.0488281 2.63672 1.02539l2.88086 2.88086-12.207 12.207-2.88086-2.88086c-0.976562-0.976562-1.12305-1.36719-1.07422-2.68555l0.341797-4.88281-7.4707-7.42188-7.61719 1.26953c-1.61133 0.341797-2.34375 0.195312-3.56445-0.976562l-10.0098-10.0098c-1.26953-1.17188-1.41602-2.00195-0.634766-3.85742l4.39453-10.4492c-7.8125-7.27539-17.9688-10.4004-28.125-7.42188-0.78125 0.195312-1.07422-0.439453-0.439453-0.976562Z"/>
|
||||
</g>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;font-weight:bold;" transform="matrix(1 0 0 1 1289 1953)">Exporting</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 1289 1971)">Symbols should be outlined when exporting to ensure the</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 1289 1989)">design is preserved when submitting to Xcode.</text>
|
||||
<text id="template-version" style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:end;" transform="matrix(1 0 0 1 3036 1933)">Template v.6.0</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:end;" transform="matrix(1 0 0 1 3036 1951)">Requires Xcode 16 or greater</text>
|
||||
<text id="descriptive-name" style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:end;" transform="matrix(1 0 0 1 3036 1969)">Generated from </text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:end;" transform="matrix(1 0 0 1 3036 1987)">Typeset at 100.0 points</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 726)">Small</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 1156)">Medium</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 1586)">Large</text>
|
||||
</g>
|
||||
<g id="Guides">
|
||||
<g id="H-reference" style="fill:#27AAE1;stroke:none;" transform="matrix(1 0 0 1 339 696)">
|
||||
<path d="M0.993654 0L3.63775 0L29.3281-67.1323L30.0303-67.1323L30.0303-70.459L28.1226-70.459ZM11.6885-24.4799L46.9815-24.4799L46.2315-26.7285L12.4385-26.7285ZM55.1196 0L57.7637 0L30.6382-70.459L29.4326-70.459L29.4326-67.1323Z"/>
|
||||
</g>
|
||||
<line id="Baseline-S" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="696" y2="696"/>
|
||||
<line id="Capline-S" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="625.541" y2="625.541"/>
|
||||
<g id="H-reference" style="fill:#27AAE1;stroke:none;" transform="matrix(1 0 0 1 339 1126)">
|
||||
<path d="M0.993654 0L3.63775 0L29.3281-67.1323L30.0303-67.1323L30.0303-70.459L28.1226-70.459ZM11.6885-24.4799L46.9815-24.4799L46.2315-26.7285L12.4385-26.7285ZM55.1196 0L57.7637 0L30.6382-70.459L29.4326-70.459L29.4326-67.1323Z"/>
|
||||
</g>
|
||||
<line id="Baseline-M" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1126" y2="1126"/>
|
||||
<line id="Capline-M" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1055.54" y2="1055.54"/>
|
||||
<g id="H-reference" style="fill:#27AAE1;stroke:none;" transform="matrix(1 0 0 1 339 1556)">
|
||||
<path d="M0.993654 0L3.63775 0L29.3281-67.1323L30.0303-67.1323L30.0303-70.459L28.1226-70.459ZM11.6885-24.4799L46.9815-24.4799L46.2315-26.7285L12.4385-26.7285ZM55.1196 0L57.7637 0L30.6382-70.459L29.4326-70.459L29.4326-67.1323Z"/>
|
||||
</g>
|
||||
<line id="Baseline-L" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1556" y2="1556"/>
|
||||
<line id="Capline-L" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1485.54" y2="1485.54"/>
|
||||
<line id="right-margin-Black-S" style="fill:none;stroke:#FF3B30;stroke-width:0.5;opacity:1.0;" x1="2989.45" x2="2989.45" y1="600.785" y2="720.121"/>
|
||||
<line id="left-margin-Black-S" style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="2877.35" x2="2877.35" y1="600.785" y2="720.121"/>
|
||||
<line id="right-margin-Regular-S" style="fill:none;stroke:#FF3B30;stroke-width:0.5;opacity:1.0;" x1="1504.9" x2="1504.9" y1="600.785" y2="720.121"/>
|
||||
<line id="left-margin-Regular-S" style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="1394.79" x2="1394.79" y1="600.785" y2="720.121"/>
|
||||
<line id="right-margin-Ultralight-S" style="fill:none;stroke:#FF3B30;stroke-width:0.5;opacity:1.0;" x1="613.72" x2="613.72" y1="600.785" y2="720.121"/>
|
||||
<line id="left-margin-Ultralight-S" style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="505.702" x2="505.702" y1="600.785" y2="720.121"/>
|
||||
</g>
|
||||
<g id="Symbols">
|
||||
<g id="Black-S" transform="matrix(1 0 0 1 2877.35 696)">
|
||||
<path class="monochrome-0 multicolor-0:tintColor hierarchical-0:secondary SFSymbolsPreviewWireframe" d="M25 2.92969L87.1094 2.92969C96.6797 2.92969 102.344-2.73438 102.344-12.3047L102.344-58.1055C102.344-67.6758 96.6797-73.3398 87.1094-73.3398L25-73.3398C15.4297-73.3398 9.76562-67.6758 9.76562-58.1055L9.76562-12.3047C9.76562-2.73438 15.4297 2.92969 25 2.92969ZM27.2461-11.2305C25-11.2305 23.9258-12.1094 23.9258-14.5508L23.9258-55.8594C23.9258-58.3008 25-59.1797 27.2461-59.1797L84.8633-59.1797C87.1094-59.1797 88.1836-58.3008 88.1836-55.8594L88.1836-14.5508C88.1836-12.1094 87.1094-11.2305 84.8633-11.2305Z"/>
|
||||
<path class="monochrome-1 multicolor-1:tintColor hierarchical-1:secondary SFSymbolsPreviewWireframe" d="M31.4941-39.9902C26.6602-37.0605 30.8105-29.6875 36.0352-32.959L45.8496-39.1602C49.0234-41.1621 48.9746-45.8496 45.8496-47.8027L36.0352-53.9551C30.8594-57.2266 26.6602-49.8535 31.4453-46.9727L37.2559-43.457ZM47.2168-34.1797C47.2168-31.9336 49.0234-30.0781 51.3184-30.0781L64.5508-30.0781C66.8457-30.0781 68.6035-31.9336 68.6035-34.1797C68.6035-36.4746 66.8457-38.2324 64.5508-38.2324L51.3184-38.2324C49.0723-38.2324 47.2168-36.4746 47.2168-34.1797Z"/>
|
||||
<path class="monochrome-2 multicolor-2:tintColor hierarchical-2:primary SFSymbolsPreviewWireframe" d="M100.89 20.249C115.831 20.249 128.185 7.8467 128.185-7.0947C128.185-22.0361 115.831-34.3408 100.89-34.3408C85.9486-34.3408 73.5951-22.0361 73.5951-7.0947C73.5951 7.8467 85.9486 20.249 100.89 20.249Z"/>
|
||||
<path class="monochrome-3 multicolor-3:systemRedColor hierarchical-3:primary SFSymbolsPreviewWireframe" d="M100.89 13.9014C112.365 13.9014 121.837 4.331 121.837-7.0947C121.837-18.5205 112.365-27.9932 100.89-27.9932C89.4154-27.9932 79.9427-18.5205 79.9427-7.0947C79.9427 4.331 89.4154 13.9014 100.89 13.9014Z"/>
|
||||
<path class="monochrome-4 multicolor-4:white hierarchical-4:primary SFSymbolsPreviewWireframe" d="M96.4955 2.915C95.0794 4.3799 92.4427 4.2822 90.9779 2.8662C89.4642 1.4502 89.4642-1.2354 90.929-2.6514L95.3724-7.0459L90.9779-11.4404C89.513-12.9053 89.513-15.4443 90.9779-16.958C92.4427-18.4717 95.0306-18.4229 96.4955-16.958L100.89-12.5635L105.333-17.0069C106.798-18.4717 109.288-18.4229 110.753-16.9092C112.218-15.3955 112.316-12.9541 110.851-11.4893L106.408-7.0459L110.753-2.7002C112.267-1.1865 112.267 1.2549 110.753 2.7685C109.24 4.2822 106.749 4.331 105.236 2.8174L100.89-1.5283L96.4955 2.915Z"/>
|
||||
</g>
|
||||
<g id="Regular-S" transform="matrix(1 0 0 1 1394.79 696)">
|
||||
<path class="monochrome-0 multicolor-0:tintColor hierarchical-0:secondary SFSymbolsPreviewWireframe" d="M22.2168 0.390625L87.8906 0.390625C96.2402 0.390625 100.342-3.71094 100.342-11.9141L100.342-58.4961C100.342-66.6992 96.2402-70.8008 87.8906-70.8008L22.2168-70.8008C13.916-70.8008 9.76562-66.6992 9.76562-58.4961L9.76562-11.9141C9.76562-3.71094 13.916 0.390625 22.2168 0.390625ZM22.3145-6.68945C18.7988-6.68945 16.7969-8.59375 16.7969-12.2559L16.7969-58.1543C16.7969-61.8652 18.7988-63.7207 22.3145-63.7207L87.793-63.7207C91.3086-63.7207 93.3105-61.8652 93.3105-58.1543L93.3105-12.2559C93.3105-8.59375 91.3086-6.68945 87.793-6.68945Z"/>
|
||||
<path class="monochrome-1 multicolor-1:tintColor hierarchical-1:secondary SFSymbolsPreviewWireframe" d="M26.6113-40.8203C23.2422-38.7695 26.0742-33.6426 29.7852-35.9863L40.6738-42.8223C43.0664-44.2871 42.9688-47.9492 40.6738-49.4141L29.7852-56.2012C26.0742-58.5938 23.2422-53.4668 26.6113-51.416L35.3516-46.0938ZM43.0176-36.2793C43.0176-34.7656 44.2383-33.4961 45.8008-33.4961L60.3516-33.4961C61.9141-33.4961 63.1348-34.7656 63.1348-36.2793C63.1348-37.8418 61.9141-39.0625 60.3516-39.0625L45.8008-39.0625C44.2383-39.0625 43.0176-37.8418 43.0176-36.2793Z"/>
|
||||
<path class="monochrome-2 multicolor-2:tintColor hierarchical-2:primary SFSymbolsPreviewWireframe" d="M99.099 18.3447C112.917 18.3447 124.49 6.82128 124.49-7.04592C124.49-20.9619 113.015-32.4365 99.099-32.4365C85.1342-32.4365 73.7084-20.9619 73.7084-7.04592C73.7084 6.96778 85.0854 18.3447 99.099 18.3447Z"/>
|
||||
<path class="monochrome-3 multicolor-3:systemRedColor hierarchical-3:primary SFSymbolsPreviewWireframe" d="M99.099 12.6807C109.744 12.6807 118.777 3.69628 118.777-7.04592C118.777-17.8369 109.89-26.7236 99.099-26.7236C88.308-26.7236 79.3725-17.8369 79.3725-7.04592C79.3725 3.79398 88.308 12.6807 99.099 12.6807Z"/>
|
||||
<path class="monochrome-4 multicolor-4:white hierarchical-4:primary SFSymbolsPreviewWireframe" d="M93.8256 2.08498C92.7514 3.15918 91.1401 2.96388 90.1147 1.93848C89.1381 0.913077 88.894-0.698223 89.9682-1.77242L95.3881-7.19242L90.4565-12.124C89.5288-13.1494 89.5288-14.7607 90.4565-15.6396C91.433-16.5674 92.9955-16.6162 94.0209-15.6396L98.9526-10.7568L104.324-16.1279C105.447-17.2509 107.058-17.0068 108.083-15.9814C109.06-15.0049 109.304-13.3935 108.181-12.2705L102.81-6.89942L107.742-1.91892C108.669-0.942423 108.669 0.668977 107.742 1.59668C106.765 2.52438 105.203 2.57328 104.177 1.59668L99.2456-3.33492L93.8256 2.08498Z"/>
|
||||
</g>
|
||||
<g id="Ultralight-S" transform="matrix(1 0 0 1 505.702 696)">
|
||||
<path class="monochrome-0 multicolor-0:tintColor hierarchical-0:secondary SFSymbolsPreviewWireframe" d="M19.7647-1.65282L88.2539-1.65282C94.7871-1.65282 98.2529-5.20946 98.2529-11.6416L98.2529-58.8594C98.2529-65.2915 94.7871-68.8482 88.2539-68.8482L19.7647-68.8482C13.2349-68.8482 9.76562-65.3823 9.76562-58.8594L9.76562-11.6416C9.76562-5.11864 13.2349-1.65282 19.7647-1.65282ZM19.8623-3.87405C14.6665-3.87405 11.9834-6.55031 11.9834-11.7564L11.9834-58.7446C11.9834-63.9541 14.6665-66.6269 19.8623-66.6269L88.1562-66.6269C93.1704-66.6269 96.0351-63.9541 96.0351-58.7446L96.0351-11.7564C96.0351-6.55031 93.1704-3.87405 88.1562-3.87405Z"/>
|
||||
<path class="monochrome-1 multicolor-1:tintColor hierarchical-1:secondary SFSymbolsPreviewWireframe" d="M24.6133-41.2744C23.1968-40.4497 24.3941-38.456 25.7891-39.3467L37.3589-46.5913C38.4346-47.1933 38.337-48.7212 37.3589-49.3232L25.7891-56.5645C24.3941-57.4131 23.106-55.5556 24.6133-54.6401L35.6694-47.9556ZM39.4302-38.1411C39.4302-37.4902 39.8789-37.0835 40.4878-37.0835L55.0386-37.0835C55.6929-37.0835 56.1417-37.4902 56.1417-38.1411C56.1417-38.75 55.6929-39.1987 55.0386-39.1987L40.4878-39.1987C39.8789-39.1987 39.4302-38.75 39.4302-38.1411Z"/>
|
||||
<path class="monochrome-2 multicolor-2:tintColor hierarchical-2:primary SFSymbolsPreviewWireframe" d="M97.2163 14.3033C108.9 14.3033 118.565 4.68695 118.565-7.04595C118.565-18.7822 108.953-28.395 97.2163-28.395C85.5219-28.395 75.8671-18.7368 75.8671-7.04595C75.8671 4.69725 85.5185 14.3033 97.2163 14.3033Z"/>
|
||||
<path class="monochrome-3 multicolor-3:systemRedColor hierarchical-3:primary SFSymbolsPreviewWireframe" d="M97.2163 11.273C107.225 11.273 115.486 3.01515 115.486-7.04595C115.486-17.0649 107.281-25.3159 97.2163-25.3159C87.1972-25.3159 78.9428-17.0195 78.9428-7.04595C78.9428 3.02195 87.1972 11.273 97.2163 11.273Z"/>
|
||||
<path class="monochrome-4 multicolor-4:white hierarchical-4:primary SFSymbolsPreviewWireframe" d="M90.6713 1.72165C90.142 2.29635 89.1664 2.37355 88.5043 1.71135C87.8457 1.04925 87.9194 0.164552 88.5395-0.455648L95.1401-7.05615L88.8915-13.3047C88.3271-13.9214 88.1909-14.8061 88.8915-15.458C89.5957-16.0679 90.477-15.9805 91.0029-15.458L97.2968-9.21295L103.849-15.81C104.427-16.3882 105.357-16.3711 106.019-15.7998C106.678-15.1865 106.649-14.2109 106.026-13.5874L99.4287-7.03565L105.677-0.829148C106.287-0.215848 106.332 0.623552 105.677 1.32415C105.018 2.02485 104.092 1.93745 103.52 1.32415L97.3173-4.87895L90.6713 1.72165Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 22 KiB |
File diff suppressed because it is too large
Load Diff
|
|
@ -12,6 +12,7 @@ import CorkShared
|
|||
import DavidFoundation
|
||||
import SwiftUI
|
||||
import UserNotifications
|
||||
import ButtonKit
|
||||
|
||||
@main
|
||||
struct CorkApp: App
|
||||
|
|
@ -19,7 +20,9 @@ struct CorkApp: App
|
|||
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate: AppDelegate
|
||||
|
||||
@StateObject var brewData: BrewDataStorage = .init()
|
||||
@StateObject var availableTaps: AvailableTaps = .init()
|
||||
@StateObject var availableTaps: TapTracker = .init()
|
||||
|
||||
@StateObject var cachedDownloadsTracker: CachedPackagesTracker = .init()
|
||||
|
||||
@StateObject var topPackagesTracker: TopPackagesTracker = .init()
|
||||
|
||||
|
|
@ -36,6 +39,7 @@ struct CorkApp: App
|
|||
@AppStorage("hasFinishedLicensingWorkflow") var hasFinishedLicensingWorkflow: Bool = false
|
||||
|
||||
@Environment(\.openWindow) private var openWindow: OpenWindowAction
|
||||
|
||||
@AppStorage("showInMenuBar") var showInMenuBar: Bool = false
|
||||
|
||||
@AppStorage("areNotificationsEnabled") var areNotificationsEnabled: Bool = false
|
||||
|
|
@ -78,6 +82,7 @@ struct CorkApp: App
|
|||
})
|
||||
.environmentObject(appDelegate.appState)
|
||||
.environmentObject(brewData)
|
||||
.environmentObject(cachedDownloadsTracker)
|
||||
.environmentObject(availableTaps)
|
||||
.environmentObject(updateProgressTracker)
|
||||
.environmentObject(outdatedPackageTracker)
|
||||
|
|
@ -160,7 +165,7 @@ struct CorkApp: App
|
|||
{ (completion: NSBackgroundActivityScheduler.CompletionHandler) in
|
||||
AppConstants.shared.logger.log("Scheduled event fired at \(Date(), privacy: .auto)")
|
||||
|
||||
Task(priority: .background)
|
||||
Task
|
||||
{ @MainActor in
|
||||
var updateResult: TerminalOutput = await shell(AppConstants.shared.brewExecutablePath, ["update"])
|
||||
|
||||
|
|
@ -269,7 +274,7 @@ struct CorkApp: App
|
|||
}
|
||||
.onChange(of: areNotificationsEnabled)
|
||||
{ newValue in // Remove the badge from the app icon if the user turns off notifications, put it back when they turn them back on
|
||||
Task(priority: .background)
|
||||
Task
|
||||
{
|
||||
await appDelegate.appState.requestNotificationAuthorization()
|
||||
|
||||
|
|
@ -410,6 +415,15 @@ struct CorkApp: App
|
|||
}
|
||||
.windowResizability(.contentSize)
|
||||
.windowToolbarStyle(.unifiedCompact)
|
||||
|
||||
WindowGroup(id: .errorInspectorWindowID, for: String.self)
|
||||
{ $errorToInspect in
|
||||
if let errorToInspect
|
||||
{
|
||||
ErrorInspector(errorText: errorToInspect)
|
||||
}
|
||||
}
|
||||
.windowToolbarStyle(.unifiedCompact)
|
||||
|
||||
Settings
|
||||
{
|
||||
|
|
@ -425,6 +439,7 @@ struct CorkApp: App
|
|||
.environmentObject(appDelegate.appState)
|
||||
.environmentObject(brewData)
|
||||
.environmentObject(availableTaps)
|
||||
.environmentObject(cachedDownloadsTracker)
|
||||
.environmentObject(outdatedPackageTracker)
|
||||
}
|
||||
}
|
||||
|
|
@ -520,87 +535,83 @@ struct CorkApp: App
|
|||
@ViewBuilder
|
||||
var backupAndRestoreMenuBarSection: some View
|
||||
{
|
||||
Button
|
||||
AsyncButton
|
||||
{
|
||||
Task(priority: .userInitiated)
|
||||
do
|
||||
{
|
||||
do
|
||||
brewfileContents = try await exportBrewfile(appState: appDelegate.appState)
|
||||
|
||||
isShowingBrewfileExporter = true
|
||||
}
|
||||
catch let brewfileExportError as BrewfileDumpingError
|
||||
{
|
||||
AppConstants.shared.logger.error("\(brewfileExportError)")
|
||||
|
||||
switch brewfileExportError
|
||||
{
|
||||
brewfileContents = try await exportBrewfile(appState: appDelegate.appState)
|
||||
case .couldNotDetermineWorkingDirectory:
|
||||
appDelegate.appState.showAlert(errorToShow: .couldNotGetWorkingDirectory)
|
||||
|
||||
isShowingBrewfileExporter = true
|
||||
}
|
||||
catch let brewfileExportError as BrewfileDumpingError
|
||||
{
|
||||
AppConstants.shared.logger.error("\(brewfileExportError)")
|
||||
case .errorWhileDumpingBrewfile(let error):
|
||||
appDelegate.appState.showAlert(errorToShow: .couldNotDumpBrewfile(error: error))
|
||||
|
||||
switch brewfileExportError
|
||||
{
|
||||
case .couldNotDetermineWorkingDirectory:
|
||||
appDelegate.appState.showAlert(errorToShow: .couldNotGetWorkingDirectory)
|
||||
|
||||
case .errorWhileDumpingBrewfile(let error):
|
||||
appDelegate.appState.showAlert(errorToShow: .couldNotDumpBrewfile(error: error))
|
||||
|
||||
case .couldNotReadBrewfile:
|
||||
appDelegate.appState.showAlert(errorToShow: .couldNotReadBrewfile)
|
||||
}
|
||||
case .couldNotReadBrewfile:
|
||||
appDelegate.appState.showAlert(errorToShow: .couldNotReadBrewfile(error: brewfileExportError.localizedDescription))
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Text("navigation.menu.import-export.export-brewfile")
|
||||
}
|
||||
.asyncButtonStyle(.plainStyle)
|
||||
|
||||
Button
|
||||
AsyncButton
|
||||
{
|
||||
Task(priority: .userInitiated)
|
||||
do
|
||||
{
|
||||
do
|
||||
let picker: NSOpenPanel = .init()
|
||||
picker.allowsMultipleSelection = false
|
||||
picker.canChooseDirectories = false
|
||||
picker.allowedFileTypes = ["brewbak", ""]
|
||||
|
||||
if picker.runModal() == .OK
|
||||
{
|
||||
let picker: NSOpenPanel = .init()
|
||||
picker.allowsMultipleSelection = false
|
||||
picker.canChooseDirectories = false
|
||||
picker.allowedFileTypes = ["brewbak", ""]
|
||||
|
||||
if picker.runModal() == .OK
|
||||
guard let brewfileURL = picker.url
|
||||
else
|
||||
{
|
||||
guard let brewfileURL = picker.url
|
||||
else
|
||||
{
|
||||
throw BrewfileReadingError.couldNotGetBrewfileLocation
|
||||
}
|
||||
throw BrewfileReadingError.couldNotGetBrewfileLocation
|
||||
}
|
||||
|
||||
AppConstants.shared.logger.debug("\(brewfileURL.path)")
|
||||
AppConstants.shared.logger.debug("\(brewfileURL.path)")
|
||||
|
||||
do
|
||||
{
|
||||
try await importBrewfile(from: brewfileURL, appState: appDelegate.appState, brewData: brewData)
|
||||
}
|
||||
catch let brewfileImportingError
|
||||
{
|
||||
AppConstants.shared.logger.error("\(brewfileImportingError.localizedDescription, privacy: .public)")
|
||||
do
|
||||
{
|
||||
try await importBrewfile(from: brewfileURL, appState: appDelegate.appState, brewData: brewData, cachedPackagesTracker: cachedDownloadsTracker)
|
||||
}
|
||||
catch let brewfileImportingError
|
||||
{
|
||||
AppConstants.shared.logger.error("\(brewfileImportingError.localizedDescription, privacy: .public)")
|
||||
|
||||
appDelegate.appState.showAlert(errorToShow: .malformedBrewfile)
|
||||
appDelegate.appState.showAlert(errorToShow: .malformedBrewfile)
|
||||
|
||||
appDelegate.appState.isShowingBrewfileImportProgress = false
|
||||
}
|
||||
appDelegate.appState.showSheet(ofType: .brewfileImport)
|
||||
}
|
||||
}
|
||||
catch let error as BrewfileReadingError
|
||||
}
|
||||
catch let error as BrewfileReadingError
|
||||
{
|
||||
switch error
|
||||
{
|
||||
switch error
|
||||
{
|
||||
case .couldNotGetBrewfileLocation:
|
||||
appDelegate.appState.showAlert(errorToShow: .couldNotGetBrewfileLocation)
|
||||
case .couldNotGetBrewfileLocation:
|
||||
appDelegate.appState.showAlert(errorToShow: .couldNotGetBrewfileLocation)
|
||||
|
||||
case .couldNotImportFile:
|
||||
appDelegate.appState.showAlert(errorToShow: .couldNotImportBrewfile)
|
||||
}
|
||||
case .couldNotImportFile:
|
||||
appDelegate.appState.showAlert(errorToShow: .couldNotImportBrewfile)
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Text("navigation.menu.import-export.import-brewfile")
|
||||
}
|
||||
.asyncButtonStyle(.plainStyle)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
|
|
@ -622,7 +633,7 @@ struct CorkApp: App
|
|||
{
|
||||
Button
|
||||
{
|
||||
appDelegate.appState.isShowingInstallationSheet.toggle()
|
||||
appDelegate.appState.showSheet(ofType: .packageInstallation)
|
||||
} label: {
|
||||
Text("navigation.menu.packages.install")
|
||||
}
|
||||
|
|
@ -630,7 +641,7 @@ struct CorkApp: App
|
|||
|
||||
Button
|
||||
{
|
||||
appDelegate.appState.isShowingAddTapSheet.toggle()
|
||||
appDelegate.appState.showSheet(ofType: .tapAddition)
|
||||
} label: {
|
||||
Text("navigation.menu.packages.add-tap")
|
||||
}
|
||||
|
|
@ -640,7 +651,7 @@ struct CorkApp: App
|
|||
|
||||
Button
|
||||
{
|
||||
appDelegate.appState.isShowingUpdateSheet = true
|
||||
appDelegate.appState.showSheet(ofType: .fullUpdate)
|
||||
} label: {
|
||||
Text("navigation.menu.packages.update")
|
||||
}
|
||||
|
|
@ -664,7 +675,7 @@ struct CorkApp: App
|
|||
{
|
||||
Button
|
||||
{
|
||||
appDelegate.appState.isShowingMaintenanceSheet.toggle()
|
||||
appDelegate.appState.showSheet(ofType: .maintenance(fastCacheDeletion: false))
|
||||
} label: {
|
||||
Text("navigation.menu.maintenance.perform")
|
||||
}
|
||||
|
|
@ -672,12 +683,12 @@ struct CorkApp: App
|
|||
|
||||
Button
|
||||
{
|
||||
appDelegate.appState.isShowingFastCacheDeletionMaintenanceView.toggle()
|
||||
appDelegate.appState.showSheet(ofType: .maintenance(fastCacheDeletion: true))
|
||||
} label: {
|
||||
Text("navigation.menu.maintenance.delete-cached-downloads")
|
||||
}
|
||||
.keyboardShortcut("m", modifiers: [.command, .option])
|
||||
.disabled(appDelegate.appState.cachedDownloadsFolderSize == 0)
|
||||
.disabled(cachedDownloadsTracker.cachedDownloadsSize == 0)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
|
|
@ -707,6 +718,18 @@ struct CorkApp: App
|
|||
} label: {
|
||||
Text("debug.action.licensing")
|
||||
}
|
||||
|
||||
Menu
|
||||
{
|
||||
Button
|
||||
{
|
||||
openWindow(id: .errorInspectorWindowID, value: PackageLoadingError.packageIsNotAFolder("Hello I am an error", packageURL: .applicationDirectory).localizedDescription)
|
||||
} label: {
|
||||
Text("debug.action.show-error-inspector")
|
||||
}
|
||||
} label: {
|
||||
Text("debug.action.ui")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Functions
|
||||
|
|
|
|||
|
|
@ -10,11 +10,12 @@ import SwiftUI
|
|||
|
||||
enum DisplayableAlert: LocalizedError
|
||||
{
|
||||
case couldNotLoadAnyPackages(LocalizedError), couldNotLoadCertainPackage(String, URL, failureReason: String)
|
||||
case couldNotGetContentsOfPackageFolder(String), couldNotLoadAnyPackages(LocalizedError), couldNotLoadCertainPackage(String, URL, failureReason: String)
|
||||
case licenseCheckingFailedDueToAuthorizationComplexNotBeingEncodedProperly, licenseCheckingFailedDueToNoInternet, licenseCheckingFailedDueToTimeout, licenseCheckingFailedForOtherReason(localizedDescription: String)
|
||||
case tapLoadingFailedDueToTapParentLocation(localizedDescription: String), tapLoadingFailedDueToTapItself(localizedDescription: String)
|
||||
case customBrewExcutableGotDeleted
|
||||
case couldNotFindPackageUUIDInList
|
||||
case uninstallationNotPossibleDueToDependency(packageThatTheUserIsTryingToUninstall: BrewPackage, offendingDependencyProhibitingUninstallation: String), couldNotApplyTaggedStateToPackages, couldNotClearMetadata, metadataFolderDoesNotExist, couldNotCreateCorkMetadataDirectory, couldNotCreateCorkMetadataFile, installedPackageHasNoVersions(corruptedPackageName: String), installedPackageIsNotAFolder(itemName: String, itemURL: URL), homePathNotSet
|
||||
case uninstallationNotPossibleDueToDependency(packageThatTheUserIsTryingToUninstall: BrewPackage, offendingDependencyProhibitingUninstallation: String), couldNotApplyTaggedStateToPackages, couldNotClearMetadata, metadataFolderDoesNotExist, couldNotCreateCorkMetadataDirectory, couldNotCreateCorkMetadataFile, installedPackageHasNoVersions(corruptedPackageName: String), installedPackageIsNotAFolder(itemName: String, itemURL: URL), homePathNotSet, numberOfLoadedPackagesDoesNotMatchNumberOfPackageFolders, triedToThreatFolderContainingPackagesAsPackage(packageType: PackageType)
|
||||
case couldNotObtainNotificationPermissions
|
||||
case couldNotRemoveTapDueToPackagesFromItStillBeingInstalled(offendingTapProhibitingRemovalOfTap: String)
|
||||
case couldNotParseTopPackages(error: String)
|
||||
|
|
@ -23,10 +24,13 @@ enum DisplayableAlert: LocalizedError
|
|||
case couldNotAssociateAnyPackageWithProvidedPackageUUID
|
||||
case couldNotFindPackageInParentDirectory
|
||||
case fatalPackageInstallationError(String)
|
||||
case fatalPackageUninstallationError(packageName: String, errorDetails: String)
|
||||
case couldNotSynchronizePackages(error: String)
|
||||
|
||||
case couldNotDeleteCachedDownloads(error: String)
|
||||
|
||||
// MARK: - Brewfile exporting/importing
|
||||
|
||||
case couldNotGetWorkingDirectory, couldNotDumpBrewfile(error: String), couldNotReadBrewfile
|
||||
case couldNotGetWorkingDirectory, couldNotDumpBrewfile(error: String), couldNotReadBrewfile(error: String)
|
||||
case couldNotGetBrewfileLocation, couldNotImportBrewfile, malformedBrewfile
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ extension DisplayableAlert
|
|||
{
|
||||
switch self
|
||||
{
|
||||
case .couldNotGetContentsOfPackageFolder:
|
||||
return String(localized: "alert.could-not-get-contents-of-package-folder.title")
|
||||
case .couldNotLoadAnyPackages(let error):
|
||||
return String(localized: "alert.fatal.could-not-load-any-packages-\(error.localizedDescription).title")
|
||||
case .couldNotLoadCertainPackage(let offendingPackage, _, _):
|
||||
|
|
@ -44,10 +46,12 @@ extension DisplayableAlert
|
|||
return String(localized: "alert.could-not-create-metadata-file.title")
|
||||
case .installedPackageHasNoVersions(let corruptedPackageName):
|
||||
return String(localized: "alert.package-corrupted.title-\(corruptedPackageName)")
|
||||
case .installedPackageIsNotAFolder(itemName: let itemName, itemURL: _):
|
||||
case .installedPackageIsNotAFolder(let itemName, _):
|
||||
return String(localized: "alert.tried-to-load-package-that-is-not-a-folder.title-\(itemName)")
|
||||
case .homePathNotSet:
|
||||
return String(localized: "alert.home-not-set.title")
|
||||
case .numberOfLoadedPackagesDoesNotMatchNumberOfPackageFolders:
|
||||
return PackageLoadingError.numberOLoadedPackagesDosNotMatchNumberOfPackageFolders.localizedDescription
|
||||
case .couldNotObtainNotificationPermissions:
|
||||
return String(localized: "alert.notifications-error-while-obtaining-permissions.title")
|
||||
case .couldNotRemoveTapDueToPackagesFromItStillBeingInstalled(let offendingTapProhibitingRemovalOfTap):
|
||||
|
|
@ -64,6 +68,8 @@ extension DisplayableAlert
|
|||
return String(localized: "alert.could-not-find-package-in-parent-directory.title")
|
||||
case .fatalPackageInstallationError:
|
||||
return String(localized: "alert.fatal-installation.error")
|
||||
case .fatalPackageUninstallationError(let packageName, _):
|
||||
return String(localized: "alert.unable-to-uninstall-\(packageName).title")
|
||||
case .couldNotSynchronizePackages:
|
||||
return String(localized: "alert.fatal.could-not-synchronize-packages.title")
|
||||
case .couldNotGetWorkingDirectory:
|
||||
|
|
@ -78,6 +84,14 @@ extension DisplayableAlert
|
|||
return String(localized: "alert.could-not-import-brewfile.title")
|
||||
case .malformedBrewfile:
|
||||
return String(localized: "alert.malformed-brewfile.title")
|
||||
case .tapLoadingFailedDueToTapParentLocation:
|
||||
return String(localized: "alert.tap-loading-failed.tap-parent.title")
|
||||
case .tapLoadingFailedDueToTapItself:
|
||||
return String(localized: "alert.tap-loading-failed.tap-itself.title")
|
||||
case .triedToThreatFolderContainingPackagesAsPackage:
|
||||
return String(localized: "alert.homebrew-broken.title")
|
||||
case .couldNotDeleteCachedDownloads:
|
||||
return String(localized: "alert.cache-deletion-failed.title")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,10 +9,13 @@ import Foundation
|
|||
|
||||
extension DisplayableAlert
|
||||
{
|
||||
/// Message in the alert
|
||||
var recoverySuggestion: String?
|
||||
{
|
||||
switch self
|
||||
{
|
||||
case .couldNotGetContentsOfPackageFolder(let localizedError):
|
||||
return String(localized: "alert.could-not-get-contents-of-package-folder.message-\(localizedError)")
|
||||
case .couldNotLoadAnyPackages:
|
||||
return String(localized: "alert.restart-or-reinstall")
|
||||
case .couldNotLoadCertainPackage(_, _, let failureReason):
|
||||
|
|
@ -43,10 +46,12 @@ extension DisplayableAlert
|
|||
return String(localized: "alert.could-not-create-metadata-directory-or-folder.message")
|
||||
case .installedPackageHasNoVersions:
|
||||
return String(localized: "alert.package-corrupted.message")
|
||||
case .installedPackageIsNotAFolder(itemName: let itemName, _):
|
||||
case .installedPackageIsNotAFolder(let itemName, _):
|
||||
return String(localized: "alert.tried-to-load-package-that-is-not-a-folder.message-\(itemName)")
|
||||
case .homePathNotSet:
|
||||
return String(localized: "alert.home-not-set.message")
|
||||
case .numberOfLoadedPackagesDoesNotMatchNumberOfPackageFolders:
|
||||
return nil
|
||||
case .couldNotObtainNotificationPermissions:
|
||||
return String(localized: "alert.notifications-error-while-obtaining-permissions.message")
|
||||
case .couldNotRemoveTapDueToPackagesFromItStillBeingInstalled(let offendingTapProhibitingRemovalOfTap):
|
||||
|
|
@ -63,20 +68,30 @@ extension DisplayableAlert
|
|||
return String(localized: "message.try-again-or-restart")
|
||||
case .fatalPackageInstallationError(let errorDetails):
|
||||
return errorDetails
|
||||
case .fatalPackageUninstallationError(_, let errorDetails):
|
||||
return errorDetails
|
||||
case .couldNotSynchronizePackages(let error):
|
||||
return error
|
||||
case .couldNotGetWorkingDirectory:
|
||||
return String(localized: "message.try-again-or-restart")
|
||||
case .couldNotDumpBrewfile(let error):
|
||||
return String(localized: "message.try-again-or-restart-\(error)")
|
||||
case .couldNotReadBrewfile:
|
||||
return String(localized: "message.try-again-or-restart")
|
||||
case .couldNotReadBrewfile(let error):
|
||||
return error
|
||||
case .couldNotGetBrewfileLocation:
|
||||
return String(localized: "alert.could-not-get-brewfile-location.message")
|
||||
case .couldNotImportBrewfile:
|
||||
return String(localized: "alert.could-not-import-brewfile.message")
|
||||
case .malformedBrewfile:
|
||||
return String(localized: "alert.malformed-brewfile.message")
|
||||
case .tapLoadingFailedDueToTapParentLocation(let localizedDescription):
|
||||
return localizedDescription
|
||||
case .tapLoadingFailedDueToTapItself(let localizedDescription):
|
||||
return localizedDescription
|
||||
case .triedToThreatFolderContainingPackagesAsPackage(let packageType):
|
||||
return PackageLoadingError.triedToThreatFolderContainingPackagesAsPackage(packageType: packageType).localizedDescription
|
||||
case .couldNotDeleteCachedDownloads(let associatedError):
|
||||
return associatedError
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,17 +7,16 @@
|
|||
|
||||
import AppIntents
|
||||
import Charts
|
||||
import CorkShared
|
||||
import Foundation
|
||||
|
||||
enum PackageType: String, CustomStringConvertible, Plottable, AppEntity, Codable
|
||||
{
|
||||
enum PackageType: String, CustomStringConvertible, Plottable, AppEntity, Codable {
|
||||
case formula
|
||||
case cask
|
||||
|
||||
var description: String
|
||||
{
|
||||
switch self
|
||||
{
|
||||
/// User-readable description of the package type
|
||||
var description: String {
|
||||
switch self {
|
||||
case .formula:
|
||||
return String(localized: "package-details.type.formula")
|
||||
case .cask:
|
||||
|
|
@ -25,12 +24,20 @@ enum PackageType: String, CustomStringConvertible, Plottable, AppEntity, Codable
|
|||
}
|
||||
}
|
||||
|
||||
/// Parent folder for this package type
|
||||
var parentFolder: URL {
|
||||
switch self {
|
||||
case .formula:
|
||||
return AppConstants.shared.brewCellarPath
|
||||
case .cask:
|
||||
return AppConstants.shared.brewCaskPath
|
||||
}
|
||||
}
|
||||
|
||||
static let typeDisplayRepresentation: TypeDisplayRepresentation = .init(name: "package-details.type")
|
||||
|
||||
var displayRepresentation: DisplayRepresentation
|
||||
{
|
||||
switch self
|
||||
{
|
||||
var displayRepresentation: DisplayRepresentation {
|
||||
switch self {
|
||||
case .formula:
|
||||
DisplayRepresentation(title: "package-details.type.formula")
|
||||
case .cask:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
//
|
||||
// Sheets - Main Window.swift
|
||||
// Cork
|
||||
//
|
||||
// Created by David Bureš - P on 19.01.2025.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum DisplayableSheet: Identifiable, Equatable
|
||||
{
|
||||
|
||||
case packageInstallation
|
||||
|
||||
case tapAddition
|
||||
|
||||
case fullUpdate, partialUpdate
|
||||
|
||||
case corruptedPackageFix(corruptedPackage: CorruptedPackage)
|
||||
|
||||
case sudoRequiredForPackageRemoval
|
||||
|
||||
case maintenance(fastCacheDeletion: Bool)
|
||||
|
||||
case brewfileExport, brewfileImport
|
||||
|
||||
var id: UUID
|
||||
{
|
||||
return .init()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
//
|
||||
// Cached Download Deletion Error.swift
|
||||
// Cork
|
||||
//
|
||||
// Created by David Bureš - P on 20.01.2025.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum CachedDownloadDeletionError: LocalizedError
|
||||
{
|
||||
case couldNotReadContentsOfCachedFormulaeDownloadsFolder(associatedError: String)
|
||||
|
||||
case couldNotReadContentsOfCachedCasksDownloadsFolder(associatedError: String)
|
||||
|
||||
case couldNotReadContentsOfCachedDownloadsFolder(associatedError: String)
|
||||
|
||||
var errorDescription: String?
|
||||
{
|
||||
switch self
|
||||
{
|
||||
case .couldNotReadContentsOfCachedFormulaeDownloadsFolder:
|
||||
return String(localized: "error.cache-deletion.could-not-read-contents-of-cached-formulae-downloads-folder")
|
||||
case .couldNotReadContentsOfCachedCasksDownloadsFolder:
|
||||
return String(localized: "error.cache-deletion.could-not-read-contents-of-cached-casks-downloads-folder")
|
||||
case .couldNotReadContentsOfCachedDownloadsFolder:
|
||||
return String(localized: "error.cache-deletion.could-not-read-contents-of-cached-downloads-folder")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
//
|
||||
// Intent Error.swift
|
||||
// Cork
|
||||
//
|
||||
// Created by David Bureš on 13.11.2024.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum IntentError: LocalizedError
|
||||
{
|
||||
case failedWhilePerformingIntent
|
||||
|
||||
var errorDescription: String?
|
||||
{
|
||||
switch self
|
||||
{
|
||||
case .failedWhilePerformingIntent:
|
||||
return String(localized: "error.intents.general-failure")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
//
|
||||
// Package Loading Error.swift
|
||||
// Cork
|
||||
//
|
||||
// Created by David Bureš on 10.11.2024.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Error representing failures while loading
|
||||
enum PackageLoadingError: LocalizedError, Hashable, Identifiable
|
||||
{
|
||||
/// Tried to treat the folder `Cellar` or `Caskroom` itself as a package - means Homebrew itself is broken
|
||||
case triedToThreatFolderContainingPackagesAsPackage(packageType: PackageType)
|
||||
|
||||
/// The `Cellar` and `Caskroom` folder itself couldn't be loaded
|
||||
case couldNotReadContentsOfParentFolder(failureReason: String, folderURL: URL)
|
||||
|
||||
/// Failed while trying to read contents of package folder
|
||||
case failedWhileReadingContentsOfPackageFolder(folderURL: URL, reportedError: String)
|
||||
|
||||
case failedWhileTryingToDetermineIntentionalInstallation(folderURL: URL, associatedIntentionalDiscoveryError: IntentionalInstallationDiscoveryError)
|
||||
|
||||
/// The package root folder exists, but the package itself doesn't have any versions
|
||||
case packageDoesNotHaveAnyVersionsInstalled(packageURL: URL)
|
||||
|
||||
/// A folder that should have contained the package is not actually a folder
|
||||
case packageIsNotAFolder(String, packageURL: URL)
|
||||
|
||||
/// The number of loaded packages does not match the number of package parent folders
|
||||
case numberOLoadedPackagesDosNotMatchNumberOfPackageFolders
|
||||
|
||||
var errorDescription: String?
|
||||
{
|
||||
switch self
|
||||
{
|
||||
case .couldNotReadContentsOfParentFolder(let failureReason, _):
|
||||
return String(localized: "error.package-loading.could-not-read-contents-of-parent-folder.\(failureReason)")
|
||||
|
||||
case .triedToThreatFolderContainingPackagesAsPackage(let packageType):
|
||||
switch packageType
|
||||
{
|
||||
case .formula:
|
||||
return "error.package-loading.last-path-component-of-checked-package-url-is-folder-containing-packages-itself.formulae"
|
||||
case .cask:
|
||||
return "error.package-loading.last-path-component-of-checked-package-url-is-folder-containing-packages-itself.casks"
|
||||
}
|
||||
|
||||
case .failedWhileReadingContentsOfPackageFolder(let folderURL, let reportedError):
|
||||
return String(localized: "error.package-loading.could-not-load-\(folderURL.packageNameFromURL())-at-\(folderURL.absoluteString)-because-\(reportedError)", comment: "Couldn't load package (package name) at (package URL) because (failure reason)")
|
||||
|
||||
case .failedWhileTryingToDetermineIntentionalInstallation(_, let associatedIntentionalDiscoveryError):
|
||||
return associatedIntentionalDiscoveryError.localizedDescription
|
||||
|
||||
case .packageDoesNotHaveAnyVersionsInstalled(let packageURL):
|
||||
return String(localized: "error.package-loading.\(packageURL.packageNameFromURL())-does-not-have-any-versions-installed")
|
||||
|
||||
case .packageIsNotAFolder(let string, _):
|
||||
return String(localized: "error.package-loading.\(string)-not-a-folder", comment: "Package folder in this context means a folder that encloses package versions. Every package has its own folder, and this error occurs when the provided URL does not point to a folder that encloses package versions")
|
||||
|
||||
case .numberOLoadedPackagesDosNotMatchNumberOfPackageFolders:
|
||||
return String(localized: "error.package-loading.number-of-loaded-poackages-does-not-match-number-of-package-folders", comment: "This error occurs when there's a mismatch between the number of loaded packages, and the number of package folders in the package folders")
|
||||
}
|
||||
}
|
||||
|
||||
var id: UUID
|
||||
{
|
||||
return UUID()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
//
|
||||
// Package Synchronization Error.swift
|
||||
// Cork
|
||||
//
|
||||
// Created by David Bureš - P on 15.01.2025.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum PackageSynchronizationError: LocalizedError
|
||||
{
|
||||
case synchronizationReturnedNil
|
||||
|
||||
var errorDescription: String?
|
||||
{
|
||||
switch self
|
||||
{
|
||||
case .synchronizationReturnedNil:
|
||||
return String(localized: "error.package-synchronization.returned-nil")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
//
|
||||
// Tap Loading Error.swift
|
||||
// Cork
|
||||
//
|
||||
// Created by David Bureš - P on 04.01.2025.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum TapLoadingError: LocalizedError, Hashable
|
||||
{
|
||||
/// Could not read the folder that includes the taps
|
||||
case couldNotAccessParentTapFolder(errorDetails: String)
|
||||
|
||||
/// Could not read a tap itself
|
||||
case couldNotReadTapFolderContents(errorDetails: String)
|
||||
|
||||
var errorDescription: String?
|
||||
{
|
||||
switch self {
|
||||
case .couldNotAccessParentTapFolder(let errorDetails):
|
||||
return errorDetails
|
||||
case .couldNotReadTapFolderContents(let errorDetails):
|
||||
return errorDetails
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
extension String
|
||||
{
|
||||
|
|
@ -13,4 +14,5 @@ extension String
|
|||
static let previewWindowID: String = "window.package-preview"
|
||||
static let servicesWindowID: String = "services"
|
||||
static let aboutWindowID: String = "about"
|
||||
static let errorInspectorWindowID: String = "error-inspector"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
//
|
||||
// URL - Package Name from URL.swift
|
||||
// Cork
|
||||
//
|
||||
// Created by David Bureš - P on 18.01.2025.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension URL
|
||||
{
|
||||
func packageNameFromURL() -> String
|
||||
{
|
||||
return self.lastPathComponent
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -23,11 +23,27 @@ struct GetInstalledCasksIntent: AppIntent
|
|||
|
||||
if allowAccessToFile
|
||||
{
|
||||
let installedFormulae: Set<BrewPackage> = await loadUpPackages(whatToLoad: .cask, appState: AppState())
|
||||
let dummyBrewData: BrewDataStorage = await .init()
|
||||
|
||||
guard let installedCasks: BrewPackages = await dummyBrewData.loadInstalledPackages(packageTypeToLoad: .cask, appState: AppState()) else
|
||||
{
|
||||
throw IntentError.failedWhilePerformingIntent
|
||||
}
|
||||
|
||||
/// Filter out all packages that gave an error
|
||||
let validInstalledCasks: Set<BrewPackage> = Set(installedCasks.compactMap({ rawResult in
|
||||
if case let .success(success) = rawResult {
|
||||
return success
|
||||
}
|
||||
else
|
||||
{
|
||||
return nil
|
||||
}
|
||||
}))
|
||||
|
||||
AppConstants.shared.brewCaskPath.stopAccessingSecurityScopedResource()
|
||||
|
||||
let minimalPackages: [MinimalHomebrewPackage] = installedFormulae.map
|
||||
let minimalPackages: [MinimalHomebrewPackage] = validInstalledCasks.map
|
||||
{ package in
|
||||
.init(name: package.name, type: .cask, installDate: package.installedOn, installedIntentionally: true)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,11 +40,27 @@ struct GetInstalledFormulaeIntent: AppIntent
|
|||
|
||||
if allowAccessToFile
|
||||
{
|
||||
let installedFormulae: Set<BrewPackage> = await loadUpPackages(whatToLoad: .formula, appState: AppState())
|
||||
let dummyBrewData: BrewDataStorage = await .init()
|
||||
|
||||
guard let installedFormulae: BrewPackages = await dummyBrewData.loadInstalledPackages(packageTypeToLoad: .formula, appState: AppState()) else
|
||||
{
|
||||
throw IntentError.failedWhilePerformingIntent
|
||||
}
|
||||
|
||||
/// Filter out all packages that gave an error
|
||||
let validInstalledFormulae: Set<BrewPackage> = Set(installedFormulae.compactMap { rawResult in
|
||||
if case let .success(success) = rawResult {
|
||||
return success
|
||||
}
|
||||
else
|
||||
{
|
||||
return nil
|
||||
}
|
||||
})
|
||||
|
||||
AppConstants.shared.brewCellarPath.stopAccessingSecurityScopedResource()
|
||||
|
||||
var minimalPackages: [MinimalHomebrewPackage] = installedFormulae.map
|
||||
var minimalPackages: [MinimalHomebrewPackage] = validInstalledFormulae.map
|
||||
{ package in
|
||||
.init(name: package.name, type: .formula, installedIntentionally: package.installedIntentionally)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import CorkShared
|
|||
|
||||
enum BrewfileDumpingError: LocalizedError
|
||||
{
|
||||
case couldNotDetermineWorkingDirectory, errorWhileDumpingBrewfile(error: String), couldNotReadBrewfile
|
||||
case couldNotDetermineWorkingDirectory, errorWhileDumpingBrewfile(error: String), couldNotReadBrewfile(error: String)
|
||||
|
||||
var errorDescription: String?
|
||||
{
|
||||
|
|
@ -20,8 +20,8 @@ enum BrewfileDumpingError: LocalizedError
|
|||
return String(localized: "error.brewfile.export.could-not-determine-working-directory")
|
||||
case .errorWhileDumpingBrewfile(let error):
|
||||
return String(localized: "error.brewfile.export.could-not-dump-with-error.\(error)")
|
||||
case .couldNotReadBrewfile:
|
||||
return String(localized: "error.brewfile.export.could-not-read-temporary-brewfile")
|
||||
case .couldNotReadBrewfile(let error):
|
||||
return error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -30,11 +30,11 @@ enum BrewfileDumpingError: LocalizedError
|
|||
@MainActor
|
||||
func exportBrewfile(appState: AppState) async throws -> String
|
||||
{
|
||||
appState.isShowingBrewfileExportProgress = true
|
||||
appState.showSheet(ofType: .brewfileExport)
|
||||
|
||||
defer
|
||||
{
|
||||
appState.isShowingBrewfileExportProgress = false
|
||||
appState.dismissSheet()
|
||||
}
|
||||
|
||||
let brewfileParentLocation: URL = URL.temporaryDirectory
|
||||
|
|
@ -56,7 +56,7 @@ func exportBrewfile(appState: AppState) async throws -> String
|
|||
throw BrewfileDumpingError.couldNotDetermineWorkingDirectory
|
||||
}
|
||||
|
||||
if !brewfileDumpingResult.standardError.isEmpty
|
||||
if brewfileDumpingResult.standardError.contains("(E|e)rror")
|
||||
{
|
||||
throw BrewfileDumpingError.errorWhileDumpingBrewfile(error: brewfileDumpingResult.standardError)
|
||||
}
|
||||
|
|
@ -79,6 +79,6 @@ func exportBrewfile(appState: AppState) async throws -> String
|
|||
catch let brewfileReadingError
|
||||
{
|
||||
AppConstants.shared.logger.error("Error while reading contents of Brewfile: \(brewfileReadingError, privacy: .public)")
|
||||
throw BrewfileDumpingError.couldNotReadBrewfile
|
||||
throw BrewfileDumpingError.couldNotReadBrewfile(error: brewfileReadingError.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,9 +25,9 @@ enum BrewfileReadingError: LocalizedError
|
|||
}
|
||||
|
||||
@MainActor
|
||||
func importBrewfile(from url: URL, appState: AppState, brewData: BrewDataStorage) async throws
|
||||
func importBrewfile(from url: URL, appState: AppState, brewData: BrewDataStorage, cachedPackagesTracker: CachedPackagesTracker) async throws
|
||||
{
|
||||
appState.isShowingBrewfileImportProgress = true
|
||||
appState.showSheet(ofType: .brewfileImport)
|
||||
|
||||
appState.brewfileImportingStage = .importing
|
||||
|
||||
|
|
@ -44,5 +44,12 @@ func importBrewfile(from url: URL, appState: AppState, brewData: BrewDataStorage
|
|||
|
||||
appState.brewfileImportingStage = .finished
|
||||
|
||||
await synchronizeInstalledPackages(brewData: brewData)
|
||||
do
|
||||
{
|
||||
try await brewData.synchronizeInstalledPackages(cachedPackagesTracker: cachedPackagesTracker)
|
||||
}
|
||||
catch let synchronizationError
|
||||
{
|
||||
appState.showAlert(errorToShow: .couldNotSynchronizePackages(error: synchronizationError.localizedDescription))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,82 @@
|
|||
//
|
||||
// Load Cached Package Downloads.swift
|
||||
// Cork
|
||||
//
|
||||
// Created by David Bureš - P on 16.01.2025.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CorkShared
|
||||
|
||||
extension CachedPackagesTracker
|
||||
{
|
||||
/// Load cached downloads and assign their types
|
||||
@MainActor
|
||||
func loadCachedDownloadedPackages(brewData: BrewDataStorage) async
|
||||
{
|
||||
AppConstants.shared.logger.info("Will load cached downloaded packages")
|
||||
|
||||
self.cachedDownloads = .init()
|
||||
|
||||
let smallestDispalyableSize: Int = .init(AppConstants.shared.brewCachedDownloadsPath.directorySize / 50)
|
||||
|
||||
var packagesThatAreTooSmallToDisplaySize: Int = 0
|
||||
|
||||
guard let cachedDownloadsFolderContents: [URL] = try? FileManager.default.contentsOfDirectory(at: AppConstants.shared.brewCachedDownloadsPath, includingPropertiesForKeys: [.isRegularFileKey], options: [.skipsHiddenFiles])
|
||||
else
|
||||
{
|
||||
return
|
||||
}
|
||||
|
||||
let usableCachedDownloads: [URL] = cachedDownloadsFolderContents.filter { $0.pathExtension != "json" }
|
||||
|
||||
for usableCachedDownload in usableCachedDownloads
|
||||
{
|
||||
guard var itemName: String = try? usableCachedDownload.lastPathComponent.regexMatch("(?<=--)(.*?)(?=\\.)")
|
||||
else
|
||||
{
|
||||
return
|
||||
}
|
||||
|
||||
AppConstants.shared.logger.debug("Temp item name: \(itemName, privacy: .public)")
|
||||
|
||||
if itemName.contains("--")
|
||||
{
|
||||
do
|
||||
{
|
||||
itemName = try itemName.regexMatch(".*?(?=--)")
|
||||
}
|
||||
catch {}
|
||||
}
|
||||
|
||||
guard let itemAttributes = try? FileManager.default.attributesOfItem(atPath: usableCachedDownload.path)
|
||||
else
|
||||
{
|
||||
return
|
||||
}
|
||||
|
||||
guard let itemSize = itemAttributes[.size] as? Int
|
||||
else
|
||||
{
|
||||
return
|
||||
}
|
||||
|
||||
if itemSize < smallestDispalyableSize
|
||||
{
|
||||
packagesThatAreTooSmallToDisplaySize = packagesThatAreTooSmallToDisplaySize + itemSize
|
||||
}
|
||||
else
|
||||
{
|
||||
cachedDownloads.append(CachedDownload(packageName: itemName, sizeInBytes: itemSize))
|
||||
}
|
||||
|
||||
AppConstants.shared.logger.debug("Others size: \(packagesThatAreTooSmallToDisplaySize, privacy: .public)")
|
||||
}
|
||||
|
||||
cachedDownloads = cachedDownloads.sorted(by: { $0.sizeInBytes < $1.sizeInBytes })
|
||||
|
||||
cachedDownloads.append(.init(packageName: String(localized: "start-page.cached-downloads.graph.other-smaller-packages"), sizeInBytes: packagesThatAreTooSmallToDisplaySize, packageType: .other))
|
||||
|
||||
self.assignPackageTypeToCachedDownloads(brewData: brewData)
|
||||
}
|
||||
}
|
||||
|
|
@ -9,33 +9,7 @@ import Foundation
|
|||
import SwiftUI
|
||||
import CorkShared
|
||||
|
||||
enum PackageLoadingError: LocalizedError
|
||||
{
|
||||
case failedWhileLoadingPackages(failureReason: String?), failedWhileLoadingCertainPackage(String, URL, failureReason: String), packageDoesNotHaveAnyVersionsInstalled(String), packageIsNotAFolder(String, URL)
|
||||
|
||||
var errorDescription: String?
|
||||
{
|
||||
switch self
|
||||
{
|
||||
case .failedWhileLoadingPackages(let failureReason):
|
||||
if let failureReason
|
||||
{
|
||||
return String(localized: "error.package-loading.could-not-load-packages.\(failureReason)")
|
||||
}
|
||||
else
|
||||
{
|
||||
return String(localized: "error.package-loading.could-not-load-packages")
|
||||
}
|
||||
case .failedWhileLoadingCertainPackage(let string, let uRL, let failureReason):
|
||||
return String(localized: "error.package-loading.could-not-load-\(string)-at-\(uRL.absoluteString)-because-\(failureReason)", comment: "Couldn't load package (package name) at (package URL) because (failure reason)")
|
||||
case .packageDoesNotHaveAnyVersionsInstalled(let string):
|
||||
return String(localized: "error.package-loading.\(string)-does-not-have-any-versions-installed")
|
||||
case .packageIsNotAFolder(let string, _):
|
||||
return String(localized: "error.package-loading.\(string)-not-a-folder", comment: "Package folder in this context means a folder that encloses package versions. Every package has its own folder, and this error occurs when the provided URL does not point to a folder that encloses package versions")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
func getContentsOfFolder(targetFolder: URL) async throws -> Set<BrewPackage>
|
||||
{
|
||||
do
|
||||
|
|
@ -112,7 +86,8 @@ func getContentsOfFolder(targetFolder: URL) async throws -> Set<BrewPackage>
|
|||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
// MARK: - Sub-functions
|
||||
|
||||
private extension URL
|
||||
|
|
@ -138,114 +113,6 @@ private extension URL
|
|||
return items
|
||||
}
|
||||
|
||||
/// This function checks whether the package was installed intentionally.
|
||||
/// - For Formulae, this info gets read from the install receipt
|
||||
/// - Casks are always instaled intentionally
|
||||
/// - Parameter versionURLs: All available versions for this package. Some packages have multiple versions installed at a time (for example, the package `xz` might have versions 1.2 and 1.3 installed at once)
|
||||
/// - Returns: Indication whether this package was installed intentionally or not
|
||||
func checkIfPackageWasInstalledIntentionally(_ versionURLs: [URL]) async throws -> Bool
|
||||
{
|
||||
guard let localPackagePath = versionURLs.first
|
||||
else
|
||||
{
|
||||
throw PackageLoadingError.failedWhileLoadingCertainPackage(lastPathComponent, self, failureReason: String(localized: "error.package-loading.could-not-load-version-to-check-from-available-versions"))
|
||||
}
|
||||
|
||||
guard localPackagePath.lastPathComponent != "Cellar"
|
||||
else
|
||||
{
|
||||
AppConstants.shared.logger.error("The last path component of the requested URL is the package container folder itself - perhaps a misconfigured package folder? Tried to load URL \(localPackagePath)")
|
||||
|
||||
throw PackageLoadingError.failedWhileLoadingPackages(failureReason: String(localized: "error.package-loading.last-path-component-of-checked-package-url-is-folder-containing-packages-itself.formulae"))
|
||||
}
|
||||
|
||||
guard localPackagePath.lastPathComponent != "Caskroom"
|
||||
else
|
||||
{
|
||||
AppConstants.shared.logger.error("The last path component of the requested URL is the package container folder itself - perhaps a misconfigured package folder? Tried to load URL \(localPackagePath)")
|
||||
|
||||
throw PackageLoadingError.failedWhileLoadingPackages(failureReason: String(localized: "error.package-loading.last-path-component-of-checked-package-url-is-folder-containing-packages-itself.casks"))
|
||||
}
|
||||
|
||||
if path.contains("Cellar")
|
||||
{
|
||||
let localPackageInfoJSONPath: URL = localPackagePath.appendingPathComponent("INSTALL_RECEIPT.json", conformingTo: .json)
|
||||
if FileManager.default.fileExists(atPath: localPackageInfoJSONPath.path)
|
||||
{
|
||||
struct InstallRecepitParser: Codable
|
||||
{
|
||||
let installedOnRequest: Bool
|
||||
}
|
||||
|
||||
let decoder: JSONDecoder = {
|
||||
let decoder: JSONDecoder = .init()
|
||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||
|
||||
return decoder
|
||||
}()
|
||||
|
||||
do
|
||||
{
|
||||
let installReceiptContents: Data = try .init(contentsOf: localPackageInfoJSONPath)
|
||||
|
||||
do
|
||||
{
|
||||
return try decoder.decode(InstallRecepitParser.self, from: installReceiptContents).installedOnRequest
|
||||
}
|
||||
catch let installReceiptParsingError
|
||||
{
|
||||
AppConstants.shared.logger.error("Failed to decode install receipt for package \(self.lastPathComponent, privacy: .public) with error \(installReceiptParsingError.localizedDescription, privacy: .public)")
|
||||
|
||||
throw PackageLoadingError.failedWhileLoadingCertainPackage(self.lastPathComponent, self, failureReason: String(localized: "error.package-loading.could-not-decode-installa-receipt-\(installReceiptParsingError.localizedDescription)"))
|
||||
}
|
||||
}
|
||||
catch let installReceiptLoadingError
|
||||
{
|
||||
AppConstants.shared.logger.error("Failed to load contents of install receipt for package \(self.lastPathComponent, privacy: .public) with error \(installReceiptLoadingError.localizedDescription, privacy: .public)")
|
||||
throw PackageLoadingError.failedWhileLoadingCertainPackage(self.lastPathComponent, self, failureReason: String(localized: "error.package-loading.could-not-convert-contents-of-install-receipt-to-data-\(installReceiptLoadingError.localizedDescription)"))
|
||||
}
|
||||
}
|
||||
else
|
||||
{ /// There's no install receipt for this package - silently fail and return that the packagw was not installed intentionally
|
||||
// TODO: Add a setting like "Strictly check for errors" that would instead throw an error here
|
||||
|
||||
AppConstants.shared.logger.error("There appears to be no install receipt for package \(localPackageInfoJSONPath.lastPathComponent, privacy: .public)")
|
||||
|
||||
let shouldStrictlyCheckForHomebrewErrors: Bool = UserDefaults.standard.bool(forKey: "strictlyCheckForHomebrewErrors")
|
||||
|
||||
if shouldStrictlyCheckForHomebrewErrors
|
||||
{
|
||||
throw PackageLoadingError.failedWhileLoadingCertainPackage(lastPathComponent, self, failureReason: String(localized: "error.package-loading.missing-install-receipt"))
|
||||
}
|
||||
else
|
||||
{
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
else if path.contains("Caskroom")
|
||||
{
|
||||
return true
|
||||
}
|
||||
else
|
||||
{
|
||||
throw PackageLoadingError.failedWhileLoadingCertainPackage(lastPathComponent, self, failureReason: String(localized: "error.package-loading.unexpected-folder-name"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Determine a package's type type from its URL
|
||||
var packageType: PackageType
|
||||
{
|
||||
if path.contains("Cellar")
|
||||
{
|
||||
return .formula
|
||||
}
|
||||
else
|
||||
{
|
||||
return .cask
|
||||
}
|
||||
}
|
||||
|
||||
/// Get URLs to a package's versions
|
||||
var packageVersionURLs: [URL]?
|
||||
{
|
||||
|
|
@ -288,25 +155,23 @@ extension [URL]
|
|||
|
||||
// MARK: - Getting list of URLs in folder
|
||||
|
||||
func getContentsOfFolder(targetFolder: URL, options: FileManager.DirectoryEnumerationOptions? = nil) -> [URL]
|
||||
func getContentsOfFolder(targetFolder: URL, options: FileManager.DirectoryEnumerationOptions? = nil) throws -> [URL]
|
||||
{
|
||||
var contentsOfFolder: [URL] = .init()
|
||||
|
||||
do
|
||||
{
|
||||
if let options
|
||||
{
|
||||
contentsOfFolder = try FileManager.default.contentsOfDirectory(at: targetFolder, includingPropertiesForKeys: nil, options: options)
|
||||
return try FileManager.default.contentsOfDirectory(at: targetFolder, includingPropertiesForKeys: nil, options: options)
|
||||
}
|
||||
else
|
||||
{
|
||||
contentsOfFolder = try FileManager.default.contentsOfDirectory(at: targetFolder, includingPropertiesForKeys: nil)
|
||||
return try FileManager.default.contentsOfDirectory(at: targetFolder, includingPropertiesForKeys: nil)
|
||||
}
|
||||
}
|
||||
catch let folderReadingError as NSError
|
||||
catch let folderReadingError
|
||||
{
|
||||
AppConstants.shared.logger.error("\(folderReadingError.localizedDescription)")
|
||||
|
||||
throw folderReadingError
|
||||
}
|
||||
|
||||
return contentsOfFolder
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,39 +7,3 @@
|
|||
|
||||
import Foundation
|
||||
import CorkShared
|
||||
|
||||
@MainActor
|
||||
func applyUninstallationSpinner(to package: BrewPackage, brewData: BrewDataStorage)
|
||||
{
|
||||
AppConstants.shared.logger.debug("""
|
||||
Brew data:
|
||||
Installed Formulae: \(brewData.installedFormulae)
|
||||
Installed Casks: \(brewData.installedCasks)
|
||||
""")
|
||||
AppConstants.shared.logger.debug("Will try to apply uninstallation spinner to package \(package.name)")
|
||||
|
||||
if package.type == .cask
|
||||
{
|
||||
brewData.installedFormulae = Set(brewData.installedFormulae.map
|
||||
{ formula in
|
||||
var copyFormula: BrewPackage = formula
|
||||
if copyFormula.name == package.name
|
||||
{
|
||||
copyFormula.changeBeingModifiedStatus()
|
||||
}
|
||||
return copyFormula
|
||||
})
|
||||
}
|
||||
else
|
||||
{
|
||||
brewData.installedFormulae = Set(brewData.installedCasks.map
|
||||
{ cask in
|
||||
var copyCask: BrewPackage = cask
|
||||
if copyCask.name == package.name
|
||||
{
|
||||
copyCask.changeBeingModifiedStatus()
|
||||
}
|
||||
return copyCask
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,54 +5,37 @@
|
|||
// Created by David Bureš on 05.02.2023.
|
||||
//
|
||||
|
||||
import CorkShared
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import CorkShared
|
||||
|
||||
extension BrewDataStorage
|
||||
{
|
||||
@MainActor
|
||||
func uninstallSelectedPackage(
|
||||
package: BrewPackage,
|
||||
cachedPackagesTracker: CachedPackagesTracker,
|
||||
appState: AppState,
|
||||
outdatedPackageTracker: OutdatedPackageTracker,
|
||||
shouldRemoveAllAssociatedFiles: Bool,
|
||||
shouldApplyUninstallSpinnerToRelevantItemInSidebar: Bool = false
|
||||
shouldApplyUninstallSpinnerToRelevantItemInSidebar _: Bool = false
|
||||
) async throws
|
||||
{
|
||||
/// Store the old navigation selection to see if it got updated in the middle of switching
|
||||
let oldNavigationSelectionId: UUID? = appState.navigationTargetId
|
||||
|
||||
if shouldApplyUninstallSpinnerToRelevantItemInSidebar
|
||||
{
|
||||
if package.type == .formula
|
||||
{
|
||||
installedFormulae = Set(installedFormulae.map
|
||||
{ formula in
|
||||
var copyFormula: BrewPackage = formula
|
||||
if copyFormula.name == package.name
|
||||
{
|
||||
copyFormula.changeBeingModifiedStatus()
|
||||
}
|
||||
return copyFormula
|
||||
})
|
||||
}
|
||||
else
|
||||
{
|
||||
installedCasks = Set(installedCasks.map
|
||||
{ cask in
|
||||
var copyCask: BrewPackage = cask
|
||||
if copyCask.name == package.name
|
||||
{
|
||||
copyCask.changeBeingModifiedStatus()
|
||||
}
|
||||
return copyCask
|
||||
})
|
||||
}
|
||||
self.updatePackageInPlace(package)
|
||||
{ package in
|
||||
package.changeBeingModifiedStatus()
|
||||
}
|
||||
else
|
||||
|
||||
defer
|
||||
{
|
||||
appState.isShowingUninstallationProgressView = true
|
||||
AppConstants.shared.logger.debug("Would reset state now")
|
||||
self.updatePackageInPlace(package)
|
||||
{ package in
|
||||
package.isBeingModified = false
|
||||
}
|
||||
}
|
||||
|
||||
AppConstants.shared.logger.info("Will try to remove package \(package.name, privacy: .auto)")
|
||||
|
|
@ -73,9 +56,6 @@ extension BrewDataStorage
|
|||
{
|
||||
AppConstants.shared.logger.warning("Could not uninstall this package because it's a dependency")
|
||||
|
||||
/// If the uninstallation failed, change the status back to "not being modified"
|
||||
resetPackageState(package: package)
|
||||
|
||||
do
|
||||
{
|
||||
let dependencyName: String = try uninstallCommandOutput.standardError.regexMatch("(?<=required by ).*?(?=, which)")
|
||||
|
|
@ -97,36 +77,38 @@ extension BrewDataStorage
|
|||
AppConstants.shared.logger.error("Could not uninstall this package because sudo is required")
|
||||
|
||||
appState.packageTryingToBeUninstalledWithSudo = package
|
||||
appState.isShowingSudoRequiredForUninstallSheet = true
|
||||
|
||||
resetPackageState(package: package)
|
||||
appState.showSheet(ofType: .sudoRequiredForPackageRemoval)
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
AppConstants.shared.logger.info("Uninstalling can proceed")
|
||||
|
||||
switch package.type
|
||||
do
|
||||
{
|
||||
case .formula:
|
||||
withAnimation
|
||||
try await self.synchronizeInstalledPackages(cachedPackagesTracker: cachedPackagesTracker)
|
||||
|
||||
if !uninstallCommandOutput.standardError.isEmpty && uninstallCommandOutput.standardError.contains("Error:")
|
||||
{
|
||||
self.removeFormulaFromTracker(withName: package.name)
|
||||
AppConstants.shared.logger.error("There was a serious uninstall error: \(uninstallCommandOutput.standardError)")
|
||||
|
||||
appState.showAlert(errorToShow: .fatalPackageUninstallationError(packageName: package.name, errorDetails: uninstallCommandOutput.standardError))
|
||||
}
|
||||
|
||||
case .cask:
|
||||
withAnimation
|
||||
else
|
||||
{
|
||||
self.removeCaskFromTracker(withName: package.name)
|
||||
AppConstants.shared.logger.info("Uninstalling can proceed")
|
||||
|
||||
if appState.navigationTargetId != nil
|
||||
{
|
||||
/// Switch to the status page only if the user didn't open another details window in the middle of the uninstall process
|
||||
if oldNavigationSelectionId == appState.navigationTargetId
|
||||
{
|
||||
appState.navigationTargetId = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if appState.navigationTargetId != nil
|
||||
catch let synchronizationError
|
||||
{
|
||||
/// Switch to the status page only if the user didn't open another details window in the middle of the uninstall process
|
||||
if oldNavigationSelectionId == appState.navigationTargetId
|
||||
{
|
||||
appState.navigationTargetId = nil
|
||||
}
|
||||
appState.showAlert(errorToShow: .couldNotSynchronizePackages(error: synchronizationError.localizedDescription))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -135,41 +117,9 @@ extension BrewDataStorage
|
|||
AppConstants.shared.logger.info("Package uninstallation process output:\nStandard output: \(uninstallCommandOutput.standardOutput, privacy: .public)\nStandard error: \(uninstallCommandOutput.standardError, privacy: .public)")
|
||||
|
||||
/// If the user removed a package that was outdated, remove it from the outdated package tracker
|
||||
Task
|
||||
if let index = outdatedPackageTracker.displayableOutdatedPackages.firstIndex(where: { $0.package.name == package.name })
|
||||
{
|
||||
if let index = outdatedPackageTracker.displayableOutdatedPackages.firstIndex(where: { $0.package.name == package.name })
|
||||
{
|
||||
outdatedPackageTracker.outdatedPackages.remove(at: index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func resetPackageState(package: BrewPackage)
|
||||
{
|
||||
if package.type == .formula
|
||||
{
|
||||
installedFormulae = Set(installedFormulae.map
|
||||
{ formula in
|
||||
var copyFormula: BrewPackage = formula
|
||||
if copyFormula.name == package.name, copyFormula.isBeingModified == true
|
||||
{
|
||||
copyFormula.changeBeingModifiedStatus()
|
||||
}
|
||||
return copyFormula
|
||||
})
|
||||
}
|
||||
else
|
||||
{
|
||||
installedCasks = Set(installedCasks.map
|
||||
{ cask in
|
||||
var copyCask: BrewPackage = cask
|
||||
if copyCask.name == package.name, copyCask.isBeingModified == true
|
||||
{
|
||||
copyCask.changeBeingModifiedStatus()
|
||||
}
|
||||
return copyCask
|
||||
})
|
||||
outdatedPackageTracker.outdatedPackages.remove(at: index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,55 +5,34 @@
|
|||
// Created by David Bureš on 23.02.2023.
|
||||
//
|
||||
|
||||
import CorkShared
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
/// What this function does:
|
||||
/// **Background**
|
||||
/// When a package is installed or uninstalled, its dependencies don't show up in the package list, because there's no system for getting them. I needed a way for all new packages to show up/disappear even when they were not installed manually
|
||||
/// **Motivation**
|
||||
/// Because I can't fucking figure out why I can't subtract one array from another, I had to come up with this contrived mess
|
||||
/// **Process**
|
||||
/// The function works like this
|
||||
/// 1. Load up installed *formulae* and *casks* after an install/uninstall process has finished
|
||||
/// 2. Sort the array of both the old and new packages by install date
|
||||
/// 3. Compare them to see which one is newer:
|
||||
/// - If the new array is **BIGGER**, it means that packages have been *uninstalled* and we have to be **REMOVING** packages from the old array
|
||||
/// - If the new array is **SMALLER**, it means that packages have been *installed* and we have to be **ADDING** packages to the old array
|
||||
/// 4. Depending on what turns out to be the case:
|
||||
/// - In case we need to be adding packages:
|
||||
/// 1. Take the length of the old array. This is where all the old packages are. Save this length into a variable *x*
|
||||
/// 2. From the new array, remove the first *x* elements, so we are only left with the new packages.
|
||||
/// - In case we need to be removing packages:
|
||||
/// 1. Copy the old array into another array, called *mutable*, where we will do all the operations
|
||||
/// 2. In the first for loop, check each package in the old array against each package in the new array. If their names match, it means that this package was not removed, so remove it from the *mutable* array
|
||||
/// 3. When this process finished, we are left with only packages that are in the old array, but not in the new array: In other words, all the packages that have been removed
|
||||
/// 4. In the second for loop, check each package in the old array against each package in the final *mutable* array. If the names of packages match, remove them; if they match, it means that this package has been removed
|
||||
|
||||
@MainActor
|
||||
func synchronizeInstalledPackages(brewData: BrewDataStorage) async
|
||||
extension BrewDataStorage
|
||||
{
|
||||
let dummyAppState: AppState = .init()
|
||||
dummyAppState.isLoadingFormulae = false
|
||||
dummyAppState.isLoadingCasks = false
|
||||
|
||||
/// These have to use this dummy AppState, which forces them to not activate the "loading" animation. We don't want the entire thing to re-draw
|
||||
let newFormulae: Set<BrewPackage> = await loadUpPackages(whatToLoad: .formula, appState: dummyAppState)
|
||||
let newCasks: Set<BrewPackage> = await loadUpPackages(whatToLoad: .cask, appState: dummyAppState)
|
||||
|
||||
if newFormulae.count != brewData.installedFormulae.count
|
||||
/// Synchronizes installed packages and cached downloads
|
||||
func synchronizeInstalledPackages(cachedPackagesTracker: CachedPackagesTracker) async throws(PackageSynchronizationError)
|
||||
{
|
||||
AppConstants.shared.logger.debug("Will start synchronization process")
|
||||
|
||||
async let updatedFormulaeTracker: BrewPackages? = await self.loadInstalledPackages(packageTypeToLoad: .formula, appState: AppState())
|
||||
async let updatedCasksTracker: BrewPackages? = await self.loadInstalledPackages(packageTypeToLoad: .cask, appState: AppState())
|
||||
|
||||
print("Updated formulae: \(String(describing: await updatedFormulaeTracker))")
|
||||
print("Updated casks: \(String(describing: await updatedCasksTracker))")
|
||||
|
||||
guard let safeUpdatedFormulaeTracker = await updatedFormulaeTracker, let safeUpdatedCasksTracker = await updatedCasksTracker else
|
||||
{
|
||||
throw .synchronizationReturnedNil
|
||||
}
|
||||
|
||||
withAnimation
|
||||
{
|
||||
brewData.installedFormulae = newFormulae
|
||||
}
|
||||
}
|
||||
|
||||
if newCasks.count != brewData.installedCasks.count
|
||||
{
|
||||
withAnimation
|
||||
{
|
||||
brewData.installedCasks = newCasks
|
||||
self.installedFormulae = safeUpdatedFormulaeTracker
|
||||
self.installedCasks = safeUpdatedCasksTracker
|
||||
}
|
||||
|
||||
await cachedPackagesTracker.loadCachedDownloadedPackages(brewData: self)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,104 +8,131 @@
|
|||
import Foundation
|
||||
import CorkShared
|
||||
|
||||
@MainActor
|
||||
func loadUpTappedTaps() async -> [BrewTap]
|
||||
extension TapTracker
|
||||
{
|
||||
var finalAvailableTaps: [BrewTap] = .init()
|
||||
|
||||
let contentsOfTapFolder: [URL] = getContentsOfFolder(targetFolder: AppConstants.shared.tapPath, options: .skipsHiddenFiles)
|
||||
|
||||
AppConstants.shared.logger.debug("Contents of tap folder: \(contentsOfTapFolder)")
|
||||
|
||||
for tapRepoParentURL in contentsOfTapFolder
|
||||
@MainActor
|
||||
func loadUpTappedTaps() async throws(TapLoadingError) -> [BrewTap]
|
||||
{
|
||||
AppConstants.shared.logger.debug("Tap repo: \(tapRepoParentURL)")
|
||||
|
||||
var finalAvailableTaps: [BrewTap] = .init()
|
||||
|
||||
let contentsOfTapRepoParent: [URL] = getContentsOfFolder(targetFolder: tapRepoParentURL, options: .skipsHiddenFiles)
|
||||
|
||||
for repoURL in contentsOfTapRepoParent
|
||||
do
|
||||
{
|
||||
let repoParentComponents: [String] = repoURL.pathComponents
|
||||
let contentsOfTapFolder: [URL] = try getContentsOfFolder(targetFolder: AppConstants.shared.tapPath, options: .skipsHiddenFiles)
|
||||
|
||||
let repoParentName: String = repoParentComponents.penultimate()!
|
||||
AppConstants.shared.logger.debug("Contents of tap folder: \(contentsOfTapFolder)")
|
||||
|
||||
let repoNameRaw: String = repoParentComponents.last!
|
||||
let repoName: String = .init(repoNameRaw.dropFirst(9))
|
||||
|
||||
let fullTapName: String = "\(repoParentName)/\(repoName)"
|
||||
|
||||
AppConstants.shared.logger.info("Full tap name: \(fullTapName)")
|
||||
|
||||
finalAvailableTaps.append(BrewTap(name: fullTapName))
|
||||
}
|
||||
}
|
||||
|
||||
let nonLocalBasicTaps: [BrewTap] = await withTaskGroup(of: BrewTap?.self)
|
||||
{ taskGroup in
|
||||
if finalAvailableTaps.filter({ $0.name == "homebrew/core" }).isEmpty
|
||||
{
|
||||
AppConstants.shared.logger.warning("Couldn't find homebrew/core in local taps")
|
||||
taskGroup.addTask
|
||||
for tapRepoParentURL in contentsOfTapFolder
|
||||
{
|
||||
let isCoreAdded: Bool = await checkIfTapIsAdded(tapToCheck: "homebrew/core")
|
||||
if isCoreAdded
|
||||
AppConstants.shared.logger.debug("Tap repo: \(tapRepoParentURL)")
|
||||
|
||||
do
|
||||
{
|
||||
AppConstants.shared.logger.info("homebrew/core is added, but not in local taps")
|
||||
return BrewTap(name: "homebrew/core")
|
||||
let contentsOfTapRepoParent: [URL] = try getContentsOfFolder(targetFolder: tapRepoParentURL, options: .skipsHiddenFiles)
|
||||
|
||||
for repoURL in contentsOfTapRepoParent
|
||||
{
|
||||
let repoParentComponents: [String] = repoURL.pathComponents
|
||||
|
||||
let repoParentName: String = repoParentComponents.penultimate()!
|
||||
|
||||
let repoNameRaw: String = repoParentComponents.last!
|
||||
let repoName: String = .init(repoNameRaw.dropFirst(9))
|
||||
|
||||
let fullTapName: String = "\(repoParentName)/\(repoName)"
|
||||
|
||||
AppConstants.shared.logger.info("Full tap name: \(fullTapName)")
|
||||
|
||||
finalAvailableTaps.append(BrewTap(name: fullTapName))
|
||||
}
|
||||
}
|
||||
catch let tapFolderReadingError
|
||||
{
|
||||
throw TapLoadingError.couldNotReadTapFolderContents(errorDetails: tapFolderReadingError.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
let nonLocalBasicTaps: [BrewTap] = await withTaskGroup(of: BrewTap?.self)
|
||||
{ taskGroup in
|
||||
if finalAvailableTaps.filter({ $0.name == "homebrew/core" }).isEmpty
|
||||
{
|
||||
AppConstants.shared.logger.warning("Couldn't find homebrew/core in local taps")
|
||||
taskGroup.addTask
|
||||
{
|
||||
let isCoreAdded: Bool = await self.checkIfTapIsAdded(tapToCheck: "homebrew/core")
|
||||
if isCoreAdded
|
||||
{
|
||||
AppConstants.shared.logger.info("homebrew/core is added, but not in local taps")
|
||||
return BrewTap(name: "homebrew/core")
|
||||
}
|
||||
else
|
||||
{
|
||||
AppConstants.shared.logger.warning("homebrew/core is not added and not in local taps")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
AppConstants.shared.logger.warning("homebrew/core is not added and not in local taps")
|
||||
return nil
|
||||
AppConstants.shared.logger.info("Found homebrew/core in local taps")
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
AppConstants.shared.logger.info("Found homebrew/core in local taps")
|
||||
}
|
||||
|
||||
if finalAvailableTaps.filter({ $0.name == "homebrew/cask" }).isEmpty
|
||||
{
|
||||
AppConstants.shared.logger.warning("Couldn't find homebrew/cask in local taps")
|
||||
taskGroup.addTask
|
||||
{
|
||||
let isCaskAdded: Bool = await checkIfTapIsAdded(tapToCheck: "homebrew/cask")
|
||||
if isCaskAdded
|
||||
if finalAvailableTaps.filter({ $0.name == "homebrew/cask" }).isEmpty
|
||||
{
|
||||
return BrewTap(name: "homebrew/cask")
|
||||
AppConstants.shared.logger.warning("Couldn't find homebrew/cask in local taps")
|
||||
taskGroup.addTask
|
||||
{
|
||||
let isCaskAdded: Bool = await self.checkIfTapIsAdded(tapToCheck: "homebrew/cask")
|
||||
if isCaskAdded
|
||||
{
|
||||
return BrewTap(name: "homebrew/cask")
|
||||
}
|
||||
else
|
||||
{
|
||||
AppConstants.shared.logger.warning("homebrew/cask is not added and not in local taps")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
AppConstants.shared.logger.warning("homebrew/cask is not added and not in local taps")
|
||||
return nil
|
||||
AppConstants.shared.logger.info("Found homebrew/cask in local taps")
|
||||
}
|
||||
|
||||
var nonLocalBasicTapsInternal: [BrewTap] = .init()
|
||||
|
||||
for await tap in taskGroup
|
||||
{
|
||||
if let tap = tap
|
||||
{
|
||||
nonLocalBasicTapsInternal.append(tap)
|
||||
}
|
||||
}
|
||||
|
||||
return nonLocalBasicTapsInternal
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
AppConstants.shared.logger.info("Found homebrew/cask in local taps")
|
||||
}
|
||||
|
||||
var nonLocalBasicTapsInternal: [BrewTap] = .init()
|
||||
finalAvailableTaps.append(contentsOf: nonLocalBasicTaps)
|
||||
|
||||
for await tap in taskGroup
|
||||
return finalAvailableTaps
|
||||
}
|
||||
catch let tapFolderReadingError
|
||||
{
|
||||
if let tap = tap
|
||||
let shouldStrictlyCheckForHomebrewErrors: Bool = UserDefaults.standard.bool(forKey: "strictlyCheckForHomebrewErrors")
|
||||
|
||||
if shouldStrictlyCheckForHomebrewErrors
|
||||
{
|
||||
nonLocalBasicTapsInternal.append(tap)
|
||||
throw TapLoadingError.couldNotAccessParentTapFolder(errorDetails: tapFolderReadingError.localizedDescription)
|
||||
}
|
||||
else
|
||||
{
|
||||
return [.init(name: "homebrew/core"), .init(name: "homebrew/cask")]
|
||||
}
|
||||
}
|
||||
|
||||
return nonLocalBasicTapsInternal
|
||||
}
|
||||
|
||||
finalAvailableTaps.append(contentsOf: nonLocalBasicTaps)
|
||||
|
||||
return finalAvailableTaps
|
||||
}
|
||||
|
||||
private func checkIfTapIsAdded(tapToCheck _: String) async -> Bool
|
||||
{
|
||||
return true
|
||||
private func checkIfTapIsAdded(tapToCheck _: String) async -> Bool
|
||||
{
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,65 +8,103 @@
|
|||
import Foundation
|
||||
import CorkShared
|
||||
|
||||
func deleteCachedDownloads()
|
||||
func deleteCachedDownloads() throws(CachedDownloadDeletionError)
|
||||
{
|
||||
let shouldStrictlyCheckForHomebrewErrors: Bool = UserDefaults.standard.bool(forKey: "strictlyCheckForHomebrewErrors")
|
||||
|
||||
/// This folder has the symlinks, so we have do **delete ONLY THE SYMLINKS**
|
||||
for url in getContentsOfFolder(targetFolder: AppConstants.shared.brewCachedFormulaeDownloadsPath)
|
||||
do
|
||||
{
|
||||
if let isSymlink = url.isSymlink()
|
||||
for url in try getContentsOfFolder(targetFolder: AppConstants.shared.brewCachedFormulaeDownloadsPath)
|
||||
{
|
||||
if isSymlink
|
||||
if let isSymlink = url.isSymlink()
|
||||
{
|
||||
try? FileManager.default.removeItem(at: url)
|
||||
if isSymlink
|
||||
{
|
||||
try? FileManager.default.removeItem(at: url)
|
||||
}
|
||||
else
|
||||
{
|
||||
AppConstants.shared.logger.info("Ignoring cached download at location \(url, privacy: .auto)")
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
AppConstants.shared.logger.info("Ignoring cached download at location \(url, privacy: .auto)")
|
||||
AppConstants.shared.logger.warning("Could not check symlink status of \(url)")
|
||||
}
|
||||
}
|
||||
else
|
||||
}
|
||||
catch let brewCachedFormulaeDownloadsFolderReadingError
|
||||
{
|
||||
AppConstants.shared.logger.error("Failed while deleting cached downloads (brewCachedFormulaeDownloadsPath): \(brewCachedFormulaeDownloadsFolderReadingError)")
|
||||
|
||||
if shouldStrictlyCheckForHomebrewErrors
|
||||
{
|
||||
AppConstants.shared.logger.warning("Could not check symlink status of \(url)")
|
||||
throw .couldNotReadContentsOfCachedFormulaeDownloadsFolder(associatedError: brewCachedFormulaeDownloadsFolderReadingError.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
/// This folder has the symlinks, so we have to **delete ONLY THE SYMLINKS**
|
||||
for url in getContentsOfFolder(targetFolder: AppConstants.shared.brewCachedCasksDownloadsPath)
|
||||
do
|
||||
{
|
||||
if let isSymlink = url.isSymlink()
|
||||
for url in try getContentsOfFolder(targetFolder: AppConstants.shared.brewCachedCasksDownloadsPath)
|
||||
{
|
||||
if isSymlink
|
||||
if let isSymlink = url.isSymlink()
|
||||
{
|
||||
try? FileManager.default.removeItem(at: url)
|
||||
if isSymlink
|
||||
{
|
||||
try? FileManager.default.removeItem(at: url)
|
||||
}
|
||||
else
|
||||
{
|
||||
AppConstants.shared.logger.info("Ignoring cached download at location \(url, privacy: .auto)")
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
AppConstants.shared.logger.info("Ignoring cached download at location \(url, privacy: .auto)")
|
||||
AppConstants.shared.logger.warning("Could not check symlink status of \(url)")
|
||||
}
|
||||
}
|
||||
else
|
||||
}
|
||||
catch let brewCachedCasksDownloadsFolderReadingError
|
||||
{
|
||||
AppConstants.shared.logger.error("Failed while deleting cached downloads (brewCachedCasksDownloadsPath): \(brewCachedCasksDownloadsFolderReadingError)")
|
||||
|
||||
if shouldStrictlyCheckForHomebrewErrors
|
||||
{
|
||||
AppConstants.shared.logger.warning("Could not check symlink status of \(url)")
|
||||
throw .couldNotReadContentsOfCachedDownloadsFolder(associatedError: brewCachedCasksDownloadsFolderReadingError.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
/// This folder has the downloads themselves, so we have do **DELETE EVERYTHING THAT IS NOT A SYMLINK**
|
||||
for url in getContentsOfFolder(targetFolder: AppConstants.shared.brewCachedDownloadsPath)
|
||||
do
|
||||
{
|
||||
if let isSymlink = url.isSymlink()
|
||||
for url in try getContentsOfFolder(targetFolder: AppConstants.shared.brewCachedDownloadsPath)
|
||||
{
|
||||
if isSymlink
|
||||
if let isSymlink = url.isSymlink()
|
||||
{
|
||||
AppConstants.shared.logger.info("Ignoring cached download at location \(url, privacy: .auto)")
|
||||
if isSymlink
|
||||
{
|
||||
AppConstants.shared.logger.info("Ignoring cached download at location \(url, privacy: .auto)")
|
||||
}
|
||||
else
|
||||
{
|
||||
try? FileManager.default.removeItem(at: url)
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
try? FileManager.default.removeItem(at: url)
|
||||
AppConstants.shared.logger.warning("Could not check symlink status of \(url)")
|
||||
}
|
||||
}
|
||||
else
|
||||
}
|
||||
catch let brewCachedDownloadsFolderReadingError
|
||||
{
|
||||
AppConstants.shared.logger.error("Failed while deleting cached downloads (brewCachedDownloadsPath): \(brewCachedDownloadsFolderReadingError)")
|
||||
|
||||
if shouldStrictlyCheckForHomebrewErrors
|
||||
{
|
||||
AppConstants.shared.logger.warning("Could not check symlink status of \(url)")
|
||||
throw .couldNotReadContentsOfCachedDownloadsFolder(associatedError: brewCachedDownloadsFolderReadingError.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@
|
|||
// Created by David Bureš on 21.06.2024.
|
||||
//
|
||||
|
||||
import CorkShared
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import CorkShared
|
||||
|
||||
enum OutdatedPackageRetrievalError: LocalizedError
|
||||
{
|
||||
|
|
@ -57,17 +57,33 @@ extension OutdatedPackageTracker
|
|||
let formulae: [Formulae]
|
||||
let casks: [Casks]
|
||||
}
|
||||
|
||||
func getOutdatedPackages(brewData: BrewDataStorage) async throws
|
||||
{
|
||||
/// ``Set<OutdatedPackage>`` that holds packages whose updates are managed by Homebrew
|
||||
async let outdatedPackagesNonGreedy: Set<OutdatedPackage> = try await getOutdatedPackagesInternal(brewData: brewData, forUpdatingType: .homebrew)
|
||||
|
||||
/// ``Set<OutdatedPackage>`` that holds packages whose updates are managed by Homebrew, plus those that are not
|
||||
async let outdatedPackagesGreedy: Set<OutdatedPackage> = try await getOutdatedPackagesInternal(brewData: brewData, forUpdatingType: .selfUpdating)
|
||||
|
||||
print("Contents of non-greedy update checker: \(try await outdatedPackagesNonGreedy.map(\.package.name)), \(try await outdatedPackagesNonGreedy.count)")
|
||||
print("Contents of greedy update checker: \(try await outdatedPackagesGreedy.map(\.package.name)), \(try await outdatedPackagesGreedy.count)")
|
||||
|
||||
/// This includes only those packages that are greedy
|
||||
let difference: Set<OutdatedPackage> = try await outdatedPackagesGreedy.subtracting(outdatedPackagesNonGreedy)
|
||||
|
||||
self.outdatedPackages = try await outdatedPackagesNonGreedy.union(difference)
|
||||
}
|
||||
|
||||
/// Load outdated packages into the outdated package tracker
|
||||
func getOutdatedPackages(brewData: BrewDataStorage) async throws
|
||||
private func getOutdatedPackagesInternal(brewData: BrewDataStorage, forUpdatingType updatingType: OutdatedPackage.PackageUpdatingType) async throws -> Set<OutdatedPackage>
|
||||
{
|
||||
// First, we have to pull the newest updates
|
||||
await shell(AppConstants.shared.brewExecutablePath, ["update"])
|
||||
|
||||
|
||||
// Then we can get the updating under way
|
||||
let rawOutput: TerminalOutput = await shell(AppConstants.shared.brewExecutablePath, ["outdated", "--json=v2"])
|
||||
|
||||
print("Outdated package function oputput: \(rawOutput)")
|
||||
/// Introduces an empty argument in case the updating is non-greedy
|
||||
let rawOutput: TerminalOutput = await shell(AppConstants.shared.brewExecutablePath, ["outdated", updatingType.argument, "--json=v2"])
|
||||
|
||||
// MARK: - Error checking
|
||||
|
||||
|
|
@ -105,10 +121,10 @@ extension OutdatedPackageTracker
|
|||
|
||||
// MARK: - Outdated package matching
|
||||
|
||||
async let finalOutdatedFormulae: Set<OutdatedPackage> = await getOutdatedFormulae(from: rawDecodedOutdatedPackages.formulae, brewData: brewData)
|
||||
async let finalOutdatedCasks: Set<OutdatedPackage> = await getOutdatedCasks(from: rawDecodedOutdatedPackages.casks, brewData: brewData)
|
||||
async let finalOutdatedFormulae: Set<OutdatedPackage> = await getOutdatedFormulae(from: rawDecodedOutdatedPackages.formulae, brewData: brewData, forUpdatingType: updatingType)
|
||||
async let finalOutdatedCasks: Set<OutdatedPackage> = await getOutdatedCasks(from: rawDecodedOutdatedPackages.casks, brewData: brewData, forUpdatingType: updatingType)
|
||||
|
||||
outdatedPackages = await finalOutdatedFormulae.union(finalOutdatedCasks)
|
||||
return await finalOutdatedFormulae.union(finalOutdatedCasks)
|
||||
}
|
||||
catch let decodingError
|
||||
{
|
||||
|
|
@ -119,18 +135,19 @@ extension OutdatedPackageTracker
|
|||
|
||||
// MARK: - Helper functions
|
||||
|
||||
private func getOutdatedFormulae(from intermediaryArray: [OutdatedPackageCommandOutput.Formulae], brewData: BrewDataStorage) async -> Set<OutdatedPackage>
|
||||
private func getOutdatedFormulae(from intermediaryArray: [OutdatedPackageCommandOutput.Formulae], brewData: BrewDataStorage, forUpdatingType updatingType: OutdatedPackage.PackageUpdatingType) async -> Set<OutdatedPackage>
|
||||
{
|
||||
var finalOutdatedFormulaTracker: Set<OutdatedPackage> = .init()
|
||||
|
||||
for outdatedFormula in intermediaryArray
|
||||
{
|
||||
if let foundOutdatedFormula = brewData.installedFormulae.first(where: { $0.name == outdatedFormula.name })
|
||||
if let foundOutdatedFormula = brewData.successfullyLoadedFormulae.first(where: { $0.name == outdatedFormula.name })
|
||||
{
|
||||
finalOutdatedFormulaTracker.insert(.init(
|
||||
package: foundOutdatedFormula,
|
||||
installedVersions: outdatedFormula.installedVersions,
|
||||
newerVersion: outdatedFormula.currentVersion
|
||||
newerVersion: outdatedFormula.currentVersion,
|
||||
updatingManagedBy: updatingType
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -139,18 +156,19 @@ extension OutdatedPackageTracker
|
|||
return finalOutdatedFormulaTracker
|
||||
}
|
||||
|
||||
private func getOutdatedCasks(from intermediaryArray: [OutdatedPackageCommandOutput.Casks], brewData: BrewDataStorage) async -> Set<OutdatedPackage>
|
||||
private func getOutdatedCasks(from intermediaryArray: [OutdatedPackageCommandOutput.Casks], brewData: BrewDataStorage, forUpdatingType updatingType: OutdatedPackage.PackageUpdatingType) async -> Set<OutdatedPackage>
|
||||
{
|
||||
var finalOutdatedCaskTracker: Set<OutdatedPackage> = .init()
|
||||
|
||||
for outdatedCask in intermediaryArray
|
||||
{
|
||||
if let foundOutdatedCask = brewData.installedCasks.first(where: { $0.name == outdatedCask.name })
|
||||
if let foundOutdatedCask = brewData.successfullyLoadedCasks.first(where: { $0.name == outdatedCask.name })
|
||||
{
|
||||
finalOutdatedCaskTracker.insert(.init(
|
||||
package: foundOutdatedCask,
|
||||
installedVersions: outdatedCask.installedVersions,
|
||||
newerVersion: outdatedCask.currentVersion
|
||||
newerVersion: outdatedCask.currentVersion,
|
||||
updatingManagedBy: updatingType
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,120 @@
|
|||
//
|
||||
// Check if Package Was Installed Intentionally.swift
|
||||
// Cork
|
||||
//
|
||||
// Created by David Bureš on 13.11.2024.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CorkShared
|
||||
|
||||
enum IntentionalInstallationDiscoveryError: Error, Hashable
|
||||
{
|
||||
/// The function could not determine the most relevant version of the package to read the recepit from
|
||||
case failedToDetermineMostRelevantVersion(packageURL: URL)
|
||||
|
||||
/// The installation receipt is there, but cannot be read due to permission issues
|
||||
case failedToReadInstallationRecepit(packageURL: URL)
|
||||
|
||||
/// The installation receipt could be read, but not parsed
|
||||
case failedToParseInstallationReceipt(packageURL: URL)
|
||||
|
||||
/// The installation receipt is missing completely
|
||||
case installationReceiptMissingCompletely(packageURL: URL)
|
||||
|
||||
/// The provided `URL` has an unexpected form
|
||||
case unexpectedFolderName(packageURL: URL)
|
||||
}
|
||||
|
||||
extension URL
|
||||
{
|
||||
/// This function checks whether the package was installed intentionally.
|
||||
/// - For Formulae, this info gets read from the install receipt
|
||||
/// - Casks are always instaled intentionally
|
||||
/// - Parameter versionURLs: All available versions for this package. Some packages have multiple versions installed at a time (for example, the package `xz` might have versions 1.2 and 1.3 installed at once)
|
||||
/// - Returns: Indication whether this package was installed intentionally or not
|
||||
func checkIfPackageWasInstalledIntentionally(versionURLs: [URL]) async throws(IntentionalInstallationDiscoveryError) -> Bool
|
||||
{
|
||||
|
||||
// TODO: Convert this so it uses the most recent version instead of a random one
|
||||
guard let localPackagePath = versionURLs.first
|
||||
else
|
||||
{
|
||||
throw .failedToDetermineMostRelevantVersion(packageURL: self)
|
||||
|
||||
// throw .failedWhileLoadingCertainPackage(lastPathComponent, self, failureReason: String(localized: "error.package-loading.could-not-load-version-to-check-from-available-versions"))
|
||||
}
|
||||
|
||||
if path.contains("Cellar")
|
||||
{
|
||||
let localPackageInfoJSONPath: URL = localPackagePath.appendingPathComponent("INSTALL_RECEIPT.json", conformingTo: .json)
|
||||
if FileManager.default.fileExists(atPath: localPackageInfoJSONPath.path)
|
||||
{
|
||||
struct InstallRecepitParser: Codable
|
||||
{
|
||||
let installedOnRequest: Bool
|
||||
}
|
||||
|
||||
let decoder: JSONDecoder = {
|
||||
let decoder: JSONDecoder = .init()
|
||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||
|
||||
return decoder
|
||||
}()
|
||||
|
||||
do
|
||||
{
|
||||
let installReceiptContents: Data = try .init(contentsOf: localPackageInfoJSONPath)
|
||||
|
||||
do
|
||||
{
|
||||
return try decoder.decode(InstallRecepitParser.self, from: installReceiptContents).installedOnRequest
|
||||
}
|
||||
catch let installReceiptParsingError
|
||||
{
|
||||
AppConstants.shared.logger.error("Failed to decode install receipt for package \(self.lastPathComponent, privacy: .public) with error \(installReceiptParsingError.localizedDescription, privacy: .public)")
|
||||
|
||||
throw IntentionalInstallationDiscoveryError.failedToParseInstallationReceipt(packageURL: self)
|
||||
|
||||
// throw PackageLoadingError.failedWhileLoadingCertainPackage(self.lastPathComponent, self, failureReason: String(localized: "error.package-loading.could-not-decode-installa-receipt-\(installReceiptParsingError.localizedDescription)"))
|
||||
}
|
||||
}
|
||||
catch let installReceiptLoadingError
|
||||
{
|
||||
AppConstants.shared.logger.error("Failed to load contents of install receipt for package \(self.lastPathComponent, privacy: .public) with error \(installReceiptLoadingError.localizedDescription, privacy: .public)")
|
||||
|
||||
throw .failedToReadInstallationRecepit(packageURL: self)
|
||||
|
||||
// throw .failedWhileLoadingCertainPackage(self.lastPathComponent, self, failureReason: String(localized: "error.package-loading.could-not-convert-contents-of-install-receipt-to-data-\(installReceiptLoadingError.localizedDescription)"))
|
||||
}
|
||||
}
|
||||
else
|
||||
{ /// There's no install receipt for this package - silently fail and return that the packagw was not installed intentionally
|
||||
|
||||
AppConstants.shared.logger.error("There appears to be no install receipt for package \(localPackageInfoJSONPath.lastPathComponent, privacy: .public)")
|
||||
|
||||
let shouldStrictlyCheckForHomebrewErrors: Bool = UserDefaults.standard.bool(forKey: "strictlyCheckForHomebrewErrors")
|
||||
|
||||
if shouldStrictlyCheckForHomebrewErrors
|
||||
{
|
||||
throw .installationReceiptMissingCompletely(packageURL: self)
|
||||
|
||||
// throw .failedWhileLoadingCertainPackage(lastPathComponent, self, failureReason: String(localized: "error.package-loading.missing-install-receipt"))
|
||||
}
|
||||
else
|
||||
{
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
else if path.contains("Caskroom")
|
||||
{
|
||||
return true
|
||||
}
|
||||
else
|
||||
{
|
||||
throw .unexpectedFolderName(packageURL: self)
|
||||
// throw .failedWhileLoadingCertainPackage(lastPathComponent, self, failureReason: String(localized: "error.package-loading.unexpected-folder-name"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
//
|
||||
// Filter Symlinks.swift
|
||||
// Cork
|
||||
//
|
||||
// Created by David Bureš on 13.11.2024.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension [URL]
|
||||
{
|
||||
/// Filter out all symlinks from an array of URLs
|
||||
var withoutSymlinks: [URL]
|
||||
{
|
||||
return self.filter
|
||||
{ url in
|
||||
/// If the existence of a symlink cannot be verified, be safe and return `false`
|
||||
guard let isSymlink = url.isSymlink()
|
||||
else
|
||||
{
|
||||
return false
|
||||
}
|
||||
|
||||
/// `isSymlink` is `true` for a symlink. Therefore, if we want to filter out symlinks, we have to return the opposite of `true`, which is `false`
|
||||
return !isSymlink
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
//
|
||||
// Get Package Type from URL.swift
|
||||
// Cork
|
||||
//
|
||||
// Created by David Bureš on 13.11.2024.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension URL
|
||||
{
|
||||
/// Determine a package's type type from its URL
|
||||
var packageType: PackageType
|
||||
{
|
||||
if self.pathComponents.contains("Cellar")
|
||||
{
|
||||
return .formula
|
||||
}
|
||||
else
|
||||
{
|
||||
return .cask
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,244 @@
|
|||
//
|
||||
// Load Up Installed Packages.swift
|
||||
// Cork
|
||||
//
|
||||
// Created by David Bureš on 10.11.2024.
|
||||
//
|
||||
|
||||
import CorkShared
|
||||
import Foundation
|
||||
|
||||
/// A representation of the loaded ``BrewPackage``s
|
||||
/// Includes packages that were loaded properly, along those whose loading failed
|
||||
typealias BrewPackages = Set<Result<BrewPackage, PackageLoadingError>>
|
||||
|
||||
extension BrewDataStorage
|
||||
{
|
||||
/// Parent function for loading installed packages from disk
|
||||
/// Abstracts away the function ``loadInstalledPackagesFromFolder(packageTypeToLoad:)``, transforming errors thrown by ``loadInstalledPackagesFromFolder(packageTypeToLoad:)`` into displayable errors
|
||||
/// - Parameters:
|
||||
/// - packageTypeToLoad: Which ``PackageType`` to load
|
||||
/// - appState: ``AppState`` used to display loading errors
|
||||
/// - Returns: A set of loaded ``BrewPackage``s for the specified ``PackageType``
|
||||
func loadInstalledPackages(
|
||||
packageTypeToLoad: PackageType, appState: AppState
|
||||
) async -> BrewPackages?
|
||||
{
|
||||
/// Start tracking when loading started
|
||||
let timeLoadingStarted: Date = .now
|
||||
AppConstants.shared.logger.debug(
|
||||
"Started \(packageTypeToLoad.rawValue, privacy: .public) loading task at \(timeLoadingStarted, privacy: .public)"
|
||||
)
|
||||
|
||||
/// Calculate how long loading took
|
||||
defer
|
||||
{
|
||||
AppConstants.shared.logger.debug("Finished \(packageTypeToLoad.rawValue, privacy: .public) loading task. Took \(timeLoadingStarted.timeIntervalSince(.now), privacy: .public)")
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
return try await self.loadInstalledPackagesFromFolder(
|
||||
packageTypeToLoad: packageTypeToLoad)
|
||||
}
|
||||
catch let packageLoadingError
|
||||
{
|
||||
switch packageLoadingError
|
||||
{
|
||||
case .couldNotReadContentsOfParentFolder(let loadingError, let folderURL):
|
||||
AppConstants.shared.logger.error("Failed while loading packages: Could not read contents of parent folder (\(folderURL.path()): \(loadingError)")
|
||||
appState.showAlert(errorToShow: .couldNotGetContentsOfPackageFolder(loadingError))
|
||||
case .packageDoesNotHaveAnyVersionsInstalled(let packageURL):
|
||||
AppConstants.shared.logger.error("Failed while loading packages: Package \(packageURL.packageNameFromURL()) does not have any versions installed")
|
||||
appState.showAlert(
|
||||
errorToShow: .installedPackageHasNoVersions(
|
||||
corruptedPackageName: packageURL.packageNameFromURL()))
|
||||
case .packageIsNotAFolder(let offendingFile, let offendingFileURL):
|
||||
AppConstants.shared.logger.error("Failed while loading packages: Package \(offendingFileURL.path()) is not a folder")
|
||||
appState.showAlert(
|
||||
errorToShow: .installedPackageIsNotAFolder(
|
||||
itemName: offendingFile, itemURL: offendingFileURL
|
||||
))
|
||||
case .numberOLoadedPackagesDosNotMatchNumberOfPackageFolders:
|
||||
AppConstants.shared.logger.error("Failed while loading packages: Number of loaded packages does not match the number of URLs in package folder")
|
||||
appState.showAlert(errorToShow: .numberOfLoadedPackagesDoesNotMatchNumberOfPackageFolders)
|
||||
case .triedToThreatFolderContainingPackagesAsPackage(let packageType):
|
||||
appState.showAlert(errorToShow: .triedToThreatFolderContainingPackagesAsPackage(packageType: packageType))
|
||||
case .failedWhileReadingContentsOfPackageFolder(let folderURL, let reportedError):
|
||||
AppConstants.shared.logger.error("Failed while loading packages: Couldn't read contents of package folder \(folderURL) with this error: \(reportedError)")
|
||||
case .failedWhileTryingToDetermineIntentionalInstallation(let folderURL, let associatedIntentionalDiscoveryError):
|
||||
AppConstants.shared.logger.error("Failed while loading packages: Couldn't determine intentional installation status for package \(folderURL) with this error: \(associatedIntentionalDiscoveryError.localizedDescription)")
|
||||
}
|
||||
|
||||
switch packageTypeToLoad
|
||||
{
|
||||
case .formula:
|
||||
appState.failedWhileLoadingFormulae = true
|
||||
case .cask:
|
||||
appState.failedWhileLoadingCasks = true
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension BrewDataStorage
|
||||
{
|
||||
/// Load packages from disk, and convert them into ``BrewPackage``s
|
||||
func loadInstalledPackagesFromFolder(
|
||||
packageTypeToLoad: PackageType
|
||||
) async throws(PackageLoadingError) -> BrewPackages
|
||||
{
|
||||
do
|
||||
{
|
||||
/// This gets URLs to all package folders in a folder.
|
||||
/// `/opt/homebrew/Caskroom/microsoft-edge/`
|
||||
let urlsInParentFolder: [URL] = try getContentsOfFolder(targetFolder: packageTypeToLoad.parentFolder, options: [.skipsHiddenFiles])
|
||||
|
||||
AppConstants.shared.logger.debug("Loaded contents of folder: \(urlsInParentFolder)")
|
||||
|
||||
let packageLoader: BrewPackages = await withTaskGroup(of: Result<BrewPackage, PackageLoadingError>.self)
|
||||
{ taskGroup in
|
||||
for packageURL in urlsInParentFolder
|
||||
{
|
||||
AppConstants.shared.logger.debug("Will add package at URL \(packageURL) to the package loading task group")
|
||||
|
||||
taskGroup.addTask
|
||||
{
|
||||
await self.loadInstalledPackage(packageURL: packageURL)
|
||||
}
|
||||
|
||||
/*
|
||||
guard taskGroup.addTaskUnlessCancelled(priority: .high, operation: {
|
||||
await self.loadInstalledPackage(packageURL: packageURL)
|
||||
})
|
||||
else
|
||||
{
|
||||
AppConstants.shared.logger.warning("Package loading task group got cancelled")
|
||||
break
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
var loadedPackages: BrewPackages = .init(minimumCapacity: urlsInParentFolder.count)
|
||||
for await loadedPackage in taskGroup
|
||||
{
|
||||
AppConstants.shared.logger.debug("Will insert package \(loadedPackages) to the package result array")
|
||||
|
||||
loadedPackages.insert(loadedPackage)
|
||||
}
|
||||
|
||||
let loggableLoadedPackages: String = loadedPackages.compactMap { rawResult in
|
||||
switch rawResult {
|
||||
case .success(let success):
|
||||
return success.name
|
||||
case .failure(let failure):
|
||||
return failure.errorDescription
|
||||
}
|
||||
}.formatted(.list(type: .and))
|
||||
|
||||
AppConstants.shared.logger.debug("Loaded \(packageTypeToLoad.description): \(loggableLoadedPackages)")
|
||||
|
||||
return loadedPackages
|
||||
}
|
||||
|
||||
let shouldStrictlyCheckForHomebrewErrors: Bool = UserDefaults.standard.bool(forKey: "strictlyCheckForHomebrewErrors")
|
||||
|
||||
if shouldStrictlyCheckForHomebrewErrors
|
||||
{
|
||||
/// Check if the number of loaded packages, both successful and failed, matches the number of package URLs in the package container folder
|
||||
guard packageLoader.count == urlsInParentFolder.count
|
||||
else
|
||||
{
|
||||
throw PackageLoadingError.numberOLoadedPackagesDosNotMatchNumberOfPackageFolders
|
||||
}
|
||||
}
|
||||
|
||||
return packageLoader
|
||||
}
|
||||
catch let parentFolderReadingError
|
||||
{
|
||||
AppConstants.shared.logger.error("Couldn't get contents of folder \(packageTypeToLoad.parentFolder, privacy: .public)")
|
||||
|
||||
throw .couldNotReadContentsOfParentFolder(failureReason: parentFolderReadingError.localizedDescription, folderURL: packageTypeToLoad.parentFolder)
|
||||
}
|
||||
}
|
||||
|
||||
/// For a given `URL` to a package folder containing the various versions of the package, parse the package contained within
|
||||
/// - Parameter packageURL: `URL` to the package parent folder
|
||||
/// - Returns: A parsed package of the ``BrewPackage`` type
|
||||
func loadInstalledPackage(packageURL: URL) async -> Result<BrewPackage, PackageLoadingError>
|
||||
{
|
||||
/// Get the name of the package - at this stage, it is the last path component
|
||||
let packageName: String = packageURL.packageNameFromURL()
|
||||
|
||||
AppConstants.shared.logger.debug("Package name to load: \(packageName). Will check if this is a legit package")
|
||||
|
||||
/// Check if we're not trying to read versions in the Cellar or Caskroom folder itself - this usually means Homebrew is broken
|
||||
guard packageName != "Cellar", packageName != "Caskroom"
|
||||
else
|
||||
{
|
||||
AppConstants.shared.logger.error("The last path component of the requested URL is the package container folder itself - perhaps a misconfigured package folder? Tried to load URL \(packageURL)")
|
||||
|
||||
return .failure(.triedToThreatFolderContainingPackagesAsPackage(packageType: packageURL.packageType))
|
||||
}
|
||||
|
||||
AppConstants.shared.logger.debug("Package \(packageName) is legit. Will try to process it")
|
||||
|
||||
/// Let's try to parse the package now
|
||||
do
|
||||
{
|
||||
/// Gets URL to installed versions of a package provided as ``packageURL``
|
||||
/// `/opt/homebrew/Cellar/cmake/3.30.5`, `/opt/homebrew/Cellar/cmake/3.30.4`
|
||||
let versionURLs: [URL] = try getContentsOfFolder(targetFolder: packageURL, options: [.skipsHiddenFiles])
|
||||
|
||||
guard !versionURLs.isEmpty else
|
||||
{
|
||||
AppConstants.shared.logger.error("Failed while loading package \(packageURL.packageNameFromURL()) because it has no versions installed")
|
||||
|
||||
return .failure(.packageDoesNotHaveAnyVersionsInstalled(packageURL: packageURL))
|
||||
}
|
||||
|
||||
/// Gets the name of the version, which at this stage is the last path component of the `versionURLs` URL
|
||||
let versionNamesForPackage: [String] = versionURLs.map
|
||||
{ versionURL in
|
||||
versionURL.lastPathComponent
|
||||
}
|
||||
|
||||
AppConstants.shared.logger.debug("Package \(packageURL.lastPathComponent) has these versions available: \(versionURLs.map { $0.absoluteString }.joined(separator: ", "))")
|
||||
|
||||
do
|
||||
{
|
||||
AppConstants.shared.logger.debug("Will check if package \(packageName) was installed intentionally")
|
||||
|
||||
let wasPackageInstalledIntentionally: Bool = try await packageURL.checkIfPackageWasInstalledIntentionally(versionURLs: versionURLs)
|
||||
|
||||
AppConstants.shared.logger.debug("Package \(packageName) \(wasPackageInstalledIntentionally ? "was" : "was not") installed intentionally")
|
||||
|
||||
let loadedPackage: Result<BrewPackage, PackageLoadingError> = .success(
|
||||
.init(
|
||||
name: packageName,
|
||||
type: packageURL.packageType,
|
||||
installedOn: packageURL.creationDate,
|
||||
versions: versionNamesForPackage,
|
||||
installedIntentionally: wasPackageInstalledIntentionally,
|
||||
sizeInBytes: packageURL.directorySize
|
||||
)
|
||||
)
|
||||
|
||||
return loadedPackage
|
||||
}
|
||||
catch let intentionalInstallationDiscoveryError
|
||||
{
|
||||
throw PackageLoadingError.failedWhileTryingToDetermineIntentionalInstallation(folderURL: packageURL, associatedIntentionalDiscoveryError: intentionalInstallationDiscoveryError)
|
||||
}
|
||||
}
|
||||
catch let loadingError
|
||||
{
|
||||
AppConstants.shared.logger.error("Failed while loading package \(packageURL.lastPathComponent, privacy: .public): \(loadingError.localizedDescription)")
|
||||
|
||||
return .failure(.failedWhileReadingContentsOfPackageFolder(folderURL: packageURL, reportedError: loadingError.localizedDescription))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,9 +5,10 @@
|
|||
// Created by David Bureš on 11.02.2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CorkShared
|
||||
import Foundation
|
||||
|
||||
/*
|
||||
@MainActor
|
||||
func loadUpPackages(whatToLoad: PackageType, appState: AppState) async -> Set<BrewPackage>
|
||||
{
|
||||
|
|
@ -29,6 +30,8 @@ func loadUpPackages(whatToLoad: PackageType, appState: AppState) async -> Set<Br
|
|||
{
|
||||
switch packageLoadingError
|
||||
{
|
||||
case .couldNotReadContentsOfParentFolder(let failureReason):
|
||||
appState.showAlert(errorToShow: .couldNotGetContentsOfPackageFolder(failureReason))
|
||||
case .failedWhileLoadingPackages:
|
||||
appState.showAlert(errorToShow: .couldNotLoadAnyPackages(packageLoadingError))
|
||||
case .failedWhileLoadingCertainPackage(let offendingPackage, let offendingPackageURL, let failureReason):
|
||||
|
|
@ -48,3 +51,4 @@ func loadUpPackages(whatToLoad: PackageType, appState: AppState) async -> Set<Br
|
|||
|
||||
return contentsOfFolder
|
||||
}
|
||||
*/
|
||||
|
|
@ -98,7 +98,7 @@ extension BrewPackage
|
|||
}
|
||||
|
||||
/// The stable files
|
||||
let stable: Stable
|
||||
let stable: Stable?
|
||||
}
|
||||
|
||||
/// Info about the relevant files
|
||||
|
|
@ -130,9 +130,14 @@ extension BrewPackage
|
|||
}
|
||||
}
|
||||
|
||||
func getCompatibility() -> Bool
|
||||
func getCompatibility() -> Bool?
|
||||
{
|
||||
for compatibleSystem in bottle.stable.files.keys
|
||||
guard let stable = bottle.stable else
|
||||
{
|
||||
AppConstants.shared.logger.debug("Package \(name) has unknown compatibility")
|
||||
return nil
|
||||
}
|
||||
for compatibleSystem in stable.files.keys
|
||||
{
|
||||
if compatibleSystem.contains(AppConstants.shared.osVersionString.lookupName)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
//
|
||||
// Update Package Tracker In place.swift
|
||||
// Cork
|
||||
//
|
||||
// Created by David Bureš on 03.01.2025.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension BrewDataStorage
|
||||
{
|
||||
/// Update a ``BrewPackage``'s property in-place
|
||||
/// Used to update the UI when a property on ``BrewPackage`` changes
|
||||
@MainActor
|
||||
func updatePackageInPlace(_ package: BrewPackage, modification: (inout BrewPackage) -> Void)
|
||||
{
|
||||
if package.type == .formula
|
||||
{
|
||||
var updatedFormulae: BrewPackages = installedFormulae
|
||||
if let index = updatedFormulae.firstIndex(where: {
|
||||
if case .success(let packageCopy) = $0
|
||||
{
|
||||
return packageCopy.id == package.id
|
||||
}
|
||||
return false
|
||||
})
|
||||
{
|
||||
var updatedPackage: BrewPackage = package
|
||||
modification(&updatedPackage)
|
||||
updatedFormulae.remove(at: index)
|
||||
updatedFormulae.insert(.success(updatedPackage))
|
||||
installedFormulae = updatedFormulae
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var updatedCasks: BrewPackages = installedCasks
|
||||
if let index = updatedCasks.firstIndex(where: {
|
||||
if case .success(let packageCopy) = $0
|
||||
{
|
||||
return packageCopy.id == package.id
|
||||
}
|
||||
return false
|
||||
})
|
||||
{
|
||||
var updatedPackage: BrewPackage = package
|
||||
modification(&updatedPackage)
|
||||
updatedCasks.remove(at: index)
|
||||
updatedCasks.insert(.success(updatedPackage))
|
||||
installedCasks = updatedCasks
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@
|
|||
import Foundation
|
||||
import CorkShared
|
||||
|
||||
func searchForPackage(packageName: String, packageType: PackageType) async throws -> [String]
|
||||
func searchForPackage(packageName: String, packageType: PackageType) async -> [String]
|
||||
{
|
||||
var finalPackageArray: [String]
|
||||
|
||||
|
|
|
|||
|
|
@ -14,11 +14,3 @@ struct SearchResults
|
|||
let foundFormulae: [String]
|
||||
let foundCasks: [String]
|
||||
}
|
||||
|
||||
func getListOfFoundPackages(searchWord: String) async -> String
|
||||
{
|
||||
var parsedResponse: String?
|
||||
parsedResponse = await shell(AppConstants.shared.brewExecutablePath, ["search", searchWord]).standardOutput
|
||||
|
||||
return parsedResponse!
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,6 +52,126 @@ func shell(
|
|||
environment: [String: String]? = nil,
|
||||
workingDirectory: URL? = nil
|
||||
) -> AsyncStream<StreamedTerminalOutput>
|
||||
{
|
||||
let task: Process = .init()
|
||||
|
||||
var finalEnvironment: [String: String] = .init()
|
||||
|
||||
// MARK: - Set up the $HOME environment variable so brew commands work on versions 4.1 and up
|
||||
|
||||
if var environment
|
||||
{
|
||||
environment["HOME"] = FileManager.default.homeDirectoryForCurrentUser.path
|
||||
finalEnvironment = environment
|
||||
}
|
||||
else
|
||||
{
|
||||
finalEnvironment = ["HOME": FileManager.default.homeDirectoryForCurrentUser.path]
|
||||
}
|
||||
|
||||
// MARK: - Set up mirrors if the environment variables exist
|
||||
|
||||
if let brewApiDomain = ProcessInfo.processInfo.environment["HOMEBREW_API_DOMAIN"]
|
||||
{
|
||||
finalEnvironment["HOMEBREW_API_DOMAIN"] = brewApiDomain
|
||||
}
|
||||
if let brewBottleDomain = ProcessInfo.processInfo.environment["HOMEBREW_BOTTLE_DOMAIN"]
|
||||
{
|
||||
finalEnvironment["HOMEBREW_BOTTLE_DOMAIN"] = brewBottleDomain
|
||||
}
|
||||
|
||||
// MARK: - Set up proxy if it's enabled
|
||||
|
||||
if let proxySettings = AppConstants.shared.proxySettings
|
||||
{
|
||||
AppConstants.shared.logger.info("Proxy is enabled")
|
||||
finalEnvironment["ALL_PROXY"] = "\(proxySettings.host):\(proxySettings.port)"
|
||||
}
|
||||
|
||||
// MARK: - Block automatic cleanup is configured
|
||||
|
||||
if !UserDefaults.standard.bool(forKey: "isAutomaticCleanupEnabled")
|
||||
{
|
||||
finalEnvironment["HOMEBREW_NO_INSTALL_CLEANUP"] = "TRUE"
|
||||
}
|
||||
|
||||
AppConstants.shared.logger.debug("Final environment: \(finalEnvironment)")
|
||||
|
||||
// MARK: - Set working directory if provided
|
||||
|
||||
if let workingDirectory
|
||||
{
|
||||
AppConstants.shared.logger.info("Working directory configured: \(workingDirectory)")
|
||||
task.currentDirectoryURL = workingDirectory
|
||||
}
|
||||
|
||||
let sudoHelperURL: URL = Bundle.main.resourceURL!.appendingPathComponent("Sudo Helper", conformingTo: .executable)
|
||||
|
||||
finalEnvironment["SUDO_ASKPASS"] = sudoHelperURL.path
|
||||
|
||||
task.environment = finalEnvironment
|
||||
task.launchPath = launchPath.absoluteString
|
||||
|
||||
/// Filter out empty things from the arguments so they don't fuck it up
|
||||
task.arguments = arguments.filter({ $0 != "" })
|
||||
|
||||
let pipe: Pipe = .init()
|
||||
task.standardOutput = pipe
|
||||
|
||||
let errorPipe: Pipe = .init()
|
||||
task.standardError = errorPipe
|
||||
|
||||
do
|
||||
{
|
||||
try task.run()
|
||||
}
|
||||
catch
|
||||
{
|
||||
AppConstants.shared.logger.error("\(String(describing: error))")
|
||||
}
|
||||
|
||||
return AsyncStream
|
||||
{ continuation in
|
||||
pipe.fileHandleForReading.readabilityHandler = { handler in
|
||||
guard let standardOutput = String(data: handler.availableData, encoding: .utf8)
|
||||
else
|
||||
{
|
||||
return
|
||||
}
|
||||
|
||||
guard !standardOutput.isEmpty
|
||||
else
|
||||
{
|
||||
return
|
||||
}
|
||||
|
||||
continuation.yield(.standardOutput(standardOutput))
|
||||
}
|
||||
|
||||
errorPipe.fileHandleForReading.readabilityHandler = { handler in
|
||||
guard let errorOutput = String(data: handler.availableData, encoding: .utf8)
|
||||
else
|
||||
{
|
||||
return
|
||||
}
|
||||
|
||||
guard !errorOutput.isEmpty else { return }
|
||||
|
||||
continuation.yield(.standardError(errorOutput))
|
||||
}
|
||||
|
||||
task.terminationHandler = { _ in
|
||||
continuation.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func shell(
|
||||
_ launchPath: URL,
|
||||
_ arguments: [String],
|
||||
environment: [String: String]? = nil,
|
||||
workingDirectory: URL? = nil
|
||||
) -> (stream: AsyncStream<StreamedTerminalOutput>, process: Process)
|
||||
{
|
||||
let task: Process = .init()
|
||||
|
||||
|
|
@ -94,6 +214,11 @@ func shell(
|
|||
{
|
||||
finalEnvironment["HOMEBREW_NO_INSTALL_CLEANUP"] = "TRUE"
|
||||
}
|
||||
|
||||
// MARK: - Automatically accept EULA if enabled
|
||||
if UserDefaults.standard.bool(forKey: "automaticallyAcceptEULA") {
|
||||
finalEnvironment["HOMEBREW_ACCEPT_EULA"] = "Y"
|
||||
}
|
||||
|
||||
AppConstants.shared.logger.debug("Final environment: \(finalEnvironment)")
|
||||
|
||||
|
|
@ -111,7 +236,9 @@ func shell(
|
|||
|
||||
task.environment = finalEnvironment
|
||||
task.launchPath = launchPath.absoluteString
|
||||
task.arguments = arguments
|
||||
|
||||
/// Filter out empty things from the arguments so they don't fuck it up
|
||||
task.arguments = arguments.filter({ $0 != "" })
|
||||
|
||||
let pipe: Pipe = .init()
|
||||
task.standardOutput = pipe
|
||||
|
|
@ -128,7 +255,7 @@ func shell(
|
|||
AppConstants.shared.logger.error("\(String(describing: error))")
|
||||
}
|
||||
|
||||
return AsyncStream
|
||||
let stream: AsyncStream<StreamedTerminalOutput> = AsyncStream
|
||||
{ continuation in
|
||||
pipe.fileHandleForReading.readabilityHandler = { handler in
|
||||
guard let standardOutput = String(data: handler.availableData, encoding: .utf8)
|
||||
|
|
@ -162,4 +289,5 @@ func shell(
|
|||
continuation.finish()
|
||||
}
|
||||
}
|
||||
return (stream, task)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,30 +8,43 @@
|
|||
import Foundation
|
||||
import CorkShared
|
||||
|
||||
@MainActor
|
||||
func applyTagsToPackageTrackingArray(appState: AppState, brewData: BrewDataStorage) async throws
|
||||
extension BrewDataStorage
|
||||
{
|
||||
for taggedName in appState.taggedPackageNames
|
||||
@MainActor
|
||||
func applyTags(appState: AppState) async throws
|
||||
{
|
||||
AppConstants.shared.logger.log("Will attempt to place package name \(taggedName, privacy: .public)")
|
||||
brewData.installedFormulae = Set(brewData.installedFormulae.map
|
||||
{ formula in
|
||||
var copyFormula: BrewPackage = formula
|
||||
if copyFormula.name == taggedName
|
||||
{
|
||||
copyFormula.changeTaggedStatus()
|
||||
}
|
||||
return copyFormula
|
||||
})
|
||||
for taggedName in appState.taggedPackageNames
|
||||
{
|
||||
AppConstants.shared.logger.log("Will attempt to place package name \(taggedName, privacy: .public)")
|
||||
self.installedFormulae = Set(self.installedFormulae.map
|
||||
{ formula in
|
||||
switch formula
|
||||
{
|
||||
case .success(var brewPackage):
|
||||
if brewPackage.name == taggedName
|
||||
{
|
||||
brewPackage.changeTaggedStatus()
|
||||
}
|
||||
return .success(brewPackage)
|
||||
case .failure(let error):
|
||||
return .failure(error)
|
||||
}
|
||||
})
|
||||
|
||||
brewData.installedCasks = Set(brewData.installedCasks.map
|
||||
{ cask in
|
||||
var copyCask: BrewPackage = cask
|
||||
if copyCask.name == taggedName
|
||||
{
|
||||
copyCask.changeTaggedStatus()
|
||||
}
|
||||
return copyCask
|
||||
})
|
||||
self.installedCasks = Set(self.installedCasks.map
|
||||
{ cask in
|
||||
switch cask
|
||||
{
|
||||
case .success(var brewPackage):
|
||||
if brewPackage.name == taggedName
|
||||
{
|
||||
brewPackage.changeTaggedStatus()
|
||||
}
|
||||
return .success(brewPackage)
|
||||
case .failure(let error):
|
||||
return .failure(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,51 +0,0 @@
|
|||
//
|
||||
// Tag Package.swift
|
||||
// Cork
|
||||
//
|
||||
// Created by David Bureš on 21.03.2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CorkShared
|
||||
|
||||
@MainActor
|
||||
func changePackageTagStatus(package: BrewPackage, brewData: BrewDataStorage, appState: AppState) async
|
||||
{
|
||||
if package.type == .formula
|
||||
{
|
||||
brewData.installedFormulae = Set(brewData.installedFormulae.map
|
||||
{ formula in
|
||||
var copyFormula: BrewPackage = formula
|
||||
if copyFormula.name == package.name
|
||||
{
|
||||
copyFormula.changeTaggedStatus()
|
||||
}
|
||||
return copyFormula
|
||||
})
|
||||
}
|
||||
else
|
||||
{
|
||||
brewData.installedFormulae = Set(brewData.installedCasks.map
|
||||
{ cask in
|
||||
var copyCask: BrewPackage = cask
|
||||
if copyCask.name == package.name
|
||||
{
|
||||
copyCask.changeTaggedStatus()
|
||||
}
|
||||
return copyCask
|
||||
})
|
||||
}
|
||||
|
||||
if appState.taggedPackageNames.contains(package.name)
|
||||
{
|
||||
appState.taggedPackageNames.remove(package.name)
|
||||
}
|
||||
else
|
||||
{
|
||||
appState.taggedPackageNames.insert(package.name)
|
||||
}
|
||||
|
||||
AppConstants.shared.logger.debug("Tagged package with ID \(package.id, privacy: .public): \(package.name, privacy: .public)")
|
||||
|
||||
AppConstants.shared.logger.debug("Tagged packages: \(appState.taggedPackageNames, privacy: .public)")
|
||||
}
|
||||
|
|
@ -24,7 +24,7 @@ enum UntapError: LocalizedError
|
|||
}
|
||||
|
||||
@MainActor
|
||||
func removeTap(name: String, availableTaps: AvailableTaps, appState: AppState, shouldApplyUninstallSpinnerToRelevantItemInSidebar: Bool = false) async throws
|
||||
func removeTap(name: String, availableTaps: TapTracker, appState: AppState, shouldApplyUninstallSpinnerToRelevantItemInSidebar: Bool = false) async throws
|
||||
{
|
||||
var indexToReplaceGlobal: Int?
|
||||
|
||||
|
|
|
|||
|
|
@ -13,8 +13,9 @@ import CorkShared
|
|||
func updatePackages(updateProgressTracker: UpdateProgressTracker, detailStage: UpdatingProcessDetails) async
|
||||
{
|
||||
let showRealTimeTerminalOutputs: Bool = UserDefaults.standard.bool(forKey: "showRealTimeTerminalOutputOfOperations")
|
||||
let includeGreedyPackages: Bool = UserDefaults.standard.bool(forKey: "includeGreedyOutdatedPackages")
|
||||
|
||||
for await output in shell(AppConstants.shared.brewExecutablePath, ["upgrade"])
|
||||
for await output in shell(AppConstants.shared.brewExecutablePath, ["upgrade", includeGreedyPackages ? "--greedy" : ""])
|
||||
{
|
||||
switch output
|
||||
{
|
||||
|
|
|
|||
|
|
@ -11,21 +11,85 @@ import SwiftUI
|
|||
@MainActor
|
||||
class BrewDataStorage: ObservableObject
|
||||
{
|
||||
@Published var installedFormulae: Set<BrewPackage> = .init()
|
||||
@Published var installedCasks: Set<BrewPackage> = .init()
|
||||
@Published var installedFormulae: BrewPackages = .init()
|
||||
@Published var installedCasks: BrewPackages = .init()
|
||||
|
||||
// MARK: - Successfully loaded packages
|
||||
/// Formulae that were successfuly loaded from disk
|
||||
var successfullyLoadedFormulae: Set<BrewPackage>
|
||||
{
|
||||
return Set(installedFormulae.compactMap
|
||||
{ rawResult in
|
||||
if case .success(let success) = rawResult
|
||||
{
|
||||
return success
|
||||
}
|
||||
else
|
||||
{
|
||||
return nil
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Collected errors from failed Formulae loading
|
||||
var unsuccessfullyLoadedFormulaeErrors: [PackageLoadingError]
|
||||
{
|
||||
return installedFormulae.compactMap
|
||||
{ rawResult in
|
||||
if case .failure(let failure) = rawResult {
|
||||
return failure
|
||||
}
|
||||
else
|
||||
{
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Casks that were successfuly loaded from disk
|
||||
var successfullyLoadedCasks: Set<BrewPackage>
|
||||
{
|
||||
return Set(installedCasks.compactMap
|
||||
{ rawResult in
|
||||
if case .success(let success) = rawResult
|
||||
{
|
||||
return success
|
||||
}
|
||||
else
|
||||
{
|
||||
return nil
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Collected errors from failed Casks loading
|
||||
var unsuccessfullyLoadedCasksErrors: [PackageLoadingError]
|
||||
{
|
||||
return installedCasks.compactMap
|
||||
{ rawResult in
|
||||
if case .failure(let failure) = rawResult {
|
||||
return failure
|
||||
}
|
||||
else
|
||||
{
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func insertPackageIntoTracker(_ package: BrewPackage)
|
||||
{
|
||||
if package.type == .formula
|
||||
{
|
||||
installedFormulae.insert(package)
|
||||
installedFormulae.insert(.success(package))
|
||||
}
|
||||
else
|
||||
{
|
||||
installedCasks.insert(package)
|
||||
installedCasks.insert(.success(package))
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
func removeFormulaFromTracker(withName name: String)
|
||||
{
|
||||
removePackageFromTracker(withName: name, tracker: .formula)
|
||||
|
|
@ -52,10 +116,38 @@ class BrewDataStorage: ObservableObject
|
|||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
extension BrewDataStorage
|
||||
{
|
||||
var numberOfInstalledFormulae: Int
|
||||
{
|
||||
let displayOnlyIntentionallyInstalledPackagesByDefault: Bool = UserDefaults.standard.bool(forKey: "displayOnlyIntentionallyInstalledPackagesByDefault")
|
||||
|
||||
if displayOnlyIntentionallyInstalledPackagesByDefault
|
||||
{
|
||||
return self.successfullyLoadedFormulae.filter(\.installedIntentionally).count
|
||||
}
|
||||
else
|
||||
{
|
||||
return self.successfullyLoadedFormulae.count
|
||||
}
|
||||
}
|
||||
|
||||
var numberOfInstalledCasks: Int
|
||||
{
|
||||
return self.successfullyLoadedCasks.count
|
||||
}
|
||||
|
||||
var numberOfInstalledPackages: Int
|
||||
{
|
||||
return self.numberOfInstalledFormulae + self.numberOfInstalledCasks
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
class AvailableTaps: ObservableObject
|
||||
class TapTracker: ObservableObject
|
||||
{
|
||||
@Published var addedTaps: [BrewTap] = .init()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
//
|
||||
// Assign Types to Cached Packages.swift
|
||||
// Cork
|
||||
//
|
||||
// Created by David Bureš - P on 16.01.2025.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CorkShared
|
||||
|
||||
extension CachedPackagesTracker
|
||||
{
|
||||
@MainActor
|
||||
func assignPackageTypeToCachedDownloads(brewData: BrewDataStorage)
|
||||
{
|
||||
var cachedDownloadsTracker: [CachedDownload] = .init()
|
||||
|
||||
AppConstants.shared.logger.debug("Package tracker in cached download assignment function has \(brewData.installedFormulae.count + brewData.installedCasks.count) packages")
|
||||
|
||||
for cachedDownload in cachedDownloads
|
||||
{
|
||||
let normalizedCachedPackageName: String = cachedDownload.packageName.onlyLetters
|
||||
|
||||
if brewData.successfullyLoadedFormulae.contains(where: { $0.name.localizedCaseInsensitiveContains(normalizedCachedPackageName) })
|
||||
{ /// The cached package is a formula
|
||||
AppConstants.shared.logger.debug("Cached package \(cachedDownload.packageName) (\(normalizedCachedPackageName)) is a formula")
|
||||
cachedDownloadsTracker.append(.init(packageName: cachedDownload.packageName, sizeInBytes: cachedDownload.sizeInBytes, packageType: .formula))
|
||||
}
|
||||
else if brewData.successfullyLoadedCasks.contains(where: { $0.name.localizedCaseInsensitiveContains(normalizedCachedPackageName) })
|
||||
{ /// The cached package is a cask
|
||||
AppConstants.shared.logger.debug("Cached package \(cachedDownload.packageName) (\(normalizedCachedPackageName)) is a cask")
|
||||
cachedDownloadsTracker.append(.init(packageName: cachedDownload.packageName, sizeInBytes: cachedDownload.sizeInBytes, packageType: .cask))
|
||||
}
|
||||
else
|
||||
{ /// The cached package cannot be found
|
||||
AppConstants.shared.logger.debug("Cached package \(cachedDownload.packageName) (\(normalizedCachedPackageName)) is unknown")
|
||||
cachedDownloadsTracker.append(.init(packageName: cachedDownload.packageName, sizeInBytes: cachedDownload.sizeInBytes, packageType: .unknown))
|
||||
}
|
||||
}
|
||||
|
||||
cachedDownloads = cachedDownloadsTracker
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
//
|
||||
// Cached Packages Tracker.swift
|
||||
// Cork
|
||||
//
|
||||
// Created by David Bureš - P on 16.01.2025.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import CorkShared
|
||||
|
||||
class CachedPackagesTracker: ObservableObject
|
||||
{
|
||||
@Published var cachedDownloads: [CachedDownload] = .init()
|
||||
|
||||
private var cachedDownloadsTemp: [CachedDownload] = .init()
|
||||
|
||||
/// Calculate the size of the cached downloads dynamically without accessing the file system for the operation
|
||||
var cachedDownloadsSize: Int
|
||||
{
|
||||
return cachedDownloads.reduce(0) { $0 + $1.sizeInBytes }
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
struct CorruptedPackage: Identifiable
|
||||
struct CorruptedPackage: Identifiable, Equatable
|
||||
{
|
||||
let id: UUID = .init()
|
||||
let name: String
|
||||
|
|
|
|||
|
|
@ -12,18 +12,34 @@ import SwiftUI
|
|||
class OutdatedPackageTracker: ObservableObject, Sendable
|
||||
{
|
||||
@AppStorage("displayOnlyIntentionallyInstalledPackagesByDefault") var displayOnlyIntentionallyInstalledPackagesByDefault: Bool = true
|
||||
|
||||
@AppStorage("includeGreedyOutdatedPackages") var includeGreedyOutdatedPackages: Bool = false
|
||||
|
||||
@Published var outdatedPackages: Set<OutdatedPackage> = .init()
|
||||
|
||||
var displayableOutdatedPackages: Set<OutdatedPackage>
|
||||
{
|
||||
if displayOnlyIntentionallyInstalledPackagesByDefault
|
||||
/// Depending on whether greedy updating is enabled:
|
||||
/// - If enabled, include packages that are also self-updating
|
||||
/// - If disabled, include only packages whose updates are managed by Homebrew
|
||||
var relevantOutdatedPackages: Set<OutdatedPackage>
|
||||
|
||||
if includeGreedyOutdatedPackages
|
||||
{
|
||||
return outdatedPackages.filter(\.package.installedIntentionally)
|
||||
relevantOutdatedPackages = outdatedPackages
|
||||
}
|
||||
else
|
||||
{
|
||||
return outdatedPackages
|
||||
relevantOutdatedPackages = outdatedPackages.filter{ $0.updatingManagedBy == .homebrew }
|
||||
}
|
||||
|
||||
if displayOnlyIntentionallyInstalledPackagesByDefault
|
||||
{
|
||||
return relevantOutdatedPackages.filter(\.package.installedIntentionally)
|
||||
}
|
||||
else
|
||||
{
|
||||
return relevantOutdatedPackages
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,26 @@ import Foundation
|
|||
|
||||
struct OutdatedPackage: Identifiable, Equatable, Hashable
|
||||
{
|
||||
enum PackageUpdatingType
|
||||
{
|
||||
/// The package is updating through Homebrew
|
||||
case homebrew
|
||||
|
||||
/// The package updates itself
|
||||
case selfUpdating
|
||||
|
||||
var argument: String
|
||||
{
|
||||
switch self
|
||||
{
|
||||
case .homebrew:
|
||||
return .init()
|
||||
case .selfUpdating:
|
||||
return "--greedy"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let id: UUID = .init()
|
||||
|
||||
let package: BrewPackage
|
||||
|
|
@ -17,4 +37,15 @@ struct OutdatedPackage: Identifiable, Equatable, Hashable
|
|||
let newerVersion: String
|
||||
|
||||
var isMarkedForUpdating: Bool = true
|
||||
|
||||
var updatingManagedBy: PackageUpdatingType
|
||||
|
||||
static func ==(lhs: OutdatedPackage, rhs: OutdatedPackage) -> Bool
|
||||
{
|
||||
return lhs.package.name == rhs.package.name
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(package.name)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,14 +15,30 @@ class InstallationProgressTracker: ObservableObject
|
|||
@Published var numberOfPackageDependencies: Int = 0
|
||||
@Published var numberInLineOfPackageCurrentlyBeingFetched: Int = 0
|
||||
@Published var numberInLineOfPackageCurrentlyBeingInstalled: Int = 0
|
||||
|
||||
private var installationProcess: Process?
|
||||
|
||||
private var showRealTimeTerminalOutputs: Bool
|
||||
{
|
||||
UserDefaults.standard.bool(forKey: "showRealTimeTerminalOutputOfOperations")
|
||||
}
|
||||
|
||||
deinit
|
||||
{
|
||||
cancel()
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func cancel() -> Bool
|
||||
{
|
||||
guard let installationProcess else {return false}
|
||||
installationProcess.terminate()
|
||||
self.installationProcess = nil
|
||||
return true
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func installPackage(using brewData: BrewDataStorage) async throws -> TerminalOutput
|
||||
func installPackage(using brewData: BrewDataStorage, cachedPackagesTracker: CachedPackagesTracker) async throws -> TerminalOutput
|
||||
{
|
||||
let package: BrewPackage = packageBeingInstalled.package
|
||||
|
||||
|
|
@ -48,7 +64,14 @@ class InstallationProgressTracker: ObservableObject
|
|||
try await installCask(using: brewData)
|
||||
}
|
||||
|
||||
await synchronizeInstalledPackages(brewData: brewData)
|
||||
do
|
||||
{
|
||||
try await brewData.synchronizeInstalledPackages(cachedPackagesTracker: cachedPackagesTracker)
|
||||
}
|
||||
catch let synchronizationError
|
||||
{
|
||||
AppConstants.shared.logger.error("Package isntallation function failed to synchronize packages: \(synchronizationError.localizedDescription)")
|
||||
}
|
||||
|
||||
return installationResult
|
||||
}
|
||||
|
|
@ -64,7 +87,9 @@ class InstallationProgressTracker: ObservableObject
|
|||
|
||||
AppConstants.shared.logger.info("Package \(package.name, privacy: .public) is Formula")
|
||||
|
||||
for await output in shell(AppConstants.shared.brewExecutablePath, ["install", package.name])
|
||||
let (stream, process): (AsyncStream<StreamedTerminalOutput>, Process) = shell(AppConstants.shared.brewExecutablePath, ["install", package.name])
|
||||
installationProcess = process
|
||||
for await output in stream
|
||||
{
|
||||
switch output
|
||||
{
|
||||
|
|
@ -179,7 +204,9 @@ class InstallationProgressTracker: ObservableObject
|
|||
AppConstants.shared.logger.info("Package is Cask")
|
||||
AppConstants.shared.logger.debug("Installing package \(package.name, privacy: .public)")
|
||||
|
||||
for await output in shell(AppConstants.shared.brewExecutablePath, ["install", "--no-quarantine", package.name])
|
||||
let (stream, process): (AsyncStream<StreamedTerminalOutput>, Process) = shell(AppConstants.shared.brewExecutablePath, ["install", "--no-quarantine", package.name])
|
||||
installationProcess = process
|
||||
for await output in stream
|
||||
{
|
||||
switch output
|
||||
{
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ class BrewPackageDetails: ObservableObject
|
|||
let outdated: Bool
|
||||
let caveats: String?
|
||||
|
||||
let isCompatible: Bool
|
||||
let isCompatible: Bool?
|
||||
|
||||
// MARK: - Mutable properties
|
||||
|
||||
|
|
@ -48,7 +48,7 @@ class BrewPackageDetails: ObservableObject
|
|||
|
||||
// MARK: - Init
|
||||
|
||||
init(name: String, description: String?, homepage: URL, tap: BrewTap, installedAsDependency: Bool, dependents: [String]? = nil, dependencies: [BrewPackageDependency]? = nil, outdated: Bool, caveats: String? = nil, pinned: Bool, isCompatible: Bool)
|
||||
init(name: String, description: String?, homepage: URL, tap: BrewTap, installedAsDependency: Bool, dependents: [String]? = nil, dependencies: [BrewPackageDependency]? = nil, outdated: Bool, caveats: String? = nil, pinned: Bool, isCompatible: Bool?)
|
||||
{
|
||||
self.name = name
|
||||
self.description = description
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
//
|
||||
// AsyncButton - Plain Style.swift
|
||||
// Cork
|
||||
//
|
||||
// Created by David Bureš - P on 25.01.2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import ButtonKit
|
||||
|
||||
/// Style that doesn' change the text of the button, and disables it when the async operation is in progress
|
||||
struct PlainAsyncButtonStyle: AsyncButtonStyle
|
||||
{
|
||||
init() {}
|
||||
|
||||
func makeLabel(configuration: LabelConfiguration) -> some View {
|
||||
configuration.label
|
||||
.disabledWhenLoading()
|
||||
.asyncButtonStyle(.none)
|
||||
}
|
||||
}
|
||||
|
||||
extension AsyncButtonStyle where Self == PlainAsyncButtonStyle
|
||||
{
|
||||
static var plainStyle: PlainAsyncButtonStyle
|
||||
{
|
||||
PlainAsyncButtonStyle()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
//
|
||||
// Error Inspector.swift
|
||||
// Cork
|
||||
//
|
||||
// Created by David Bureš - P on 20.01.2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ErrorInspector: View
|
||||
{
|
||||
let errorText: String
|
||||
|
||||
var body: some View
|
||||
{
|
||||
NavigationStack
|
||||
{
|
||||
TextEditor(text: .constant(errorText))
|
||||
.navigationTitle("error-inspector.title")
|
||||
.frame(minWidth: 300, minHeight: 200)
|
||||
}
|
||||
.modify
|
||||
{ viewProxy in
|
||||
if #available(macOS 15, *)
|
||||
{
|
||||
viewProxy
|
||||
.windowMinimizeBehavior(.disabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,9 +5,10 @@
|
|||
// Created by David Bureš on 03.07.2022.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import CorkShared
|
||||
import CorkNotifications
|
||||
import CorkShared
|
||||
import SwiftUI
|
||||
import ButtonKit
|
||||
|
||||
struct AddFormulaView: View
|
||||
{
|
||||
|
|
@ -18,6 +19,8 @@ struct AddFormulaView: View
|
|||
@EnvironmentObject var brewData: BrewDataStorage
|
||||
@EnvironmentObject var appState: AppState
|
||||
|
||||
@EnvironmentObject var cachedDownloadsTracker: CachedPackagesTracker
|
||||
|
||||
@State private var foundPackageSelection: UUID? = nil
|
||||
|
||||
@ObservedObject var searchResultTracker: SearchResultTracker = .init()
|
||||
|
|
@ -32,168 +35,141 @@ struct AddFormulaView: View
|
|||
@AppStorage("showPackagesStillLeftToInstall") var showPackagesStillLeftToInstall: Bool = false
|
||||
@AppStorage("notifyAboutPackageInstallationResults") var notifyAboutPackageInstallationResults: Bool = false
|
||||
|
||||
var shouldShowSheetTitle: Bool
|
||||
{
|
||||
[.ready, .presentingSearchResults].contains(packageInstallationProcessStep)
|
||||
}
|
||||
|
||||
var isDismissable: Bool
|
||||
{
|
||||
[.ready, .presentingSearchResults, .fatalError, .anotherProcessAlreadyRunning, .binaryAlreadyExists, .requiresSudoPassword, .wrongArchitecture, .anotherProcessAlreadyRunning, .installationTerminatedUnexpectedly, .installing].contains(packageInstallationProcessStep)
|
||||
}
|
||||
|
||||
var sheetTitle: LocalizedStringKey
|
||||
{
|
||||
switch packageInstallationProcessStep
|
||||
{
|
||||
case .ready:
|
||||
return "add-package.title"
|
||||
case .searching:
|
||||
return ""
|
||||
case .presentingSearchResults:
|
||||
return "add-package.title"
|
||||
case .installing:
|
||||
return ""
|
||||
case .finished:
|
||||
return ""
|
||||
case .fatalError:
|
||||
return ""
|
||||
case .requiresSudoPassword:
|
||||
return ""
|
||||
case .wrongArchitecture:
|
||||
return ""
|
||||
case .binaryAlreadyExists:
|
||||
return ""
|
||||
case .anotherProcessAlreadyRunning:
|
||||
return ""
|
||||
case .installationTerminatedUnexpectedly:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View
|
||||
{
|
||||
VStack(alignment: .leading, spacing: 10)
|
||||
NavigationStack
|
||||
{
|
||||
switch packageInstallationProcessStep
|
||||
SheetTemplate(isShowingTitle: shouldShowSheetTitle)
|
||||
{
|
||||
case .ready:
|
||||
SheetWithTitle(title: "add-package.title")
|
||||
Group
|
||||
{
|
||||
InstallationInitialView(
|
||||
searchResultTracker: searchResultTracker,
|
||||
packageRequested: $packageRequested,
|
||||
foundPackageSelection: $foundPackageSelection,
|
||||
installationProgressTracker: installationProgressTracker,
|
||||
packageInstallationProcessStep: $packageInstallationProcessStep
|
||||
)
|
||||
}
|
||||
|
||||
case .searching:
|
||||
InstallationSearchingView(
|
||||
packageRequested: $packageRequested,
|
||||
searchResultTracker: searchResultTracker,
|
||||
packageInstallationProcessStep: $packageInstallationProcessStep
|
||||
)
|
||||
|
||||
case .presentingSearchResults:
|
||||
PresentingSearchResultsView(
|
||||
searchResultTracker: searchResultTracker,
|
||||
packageRequested: $packageRequested,
|
||||
foundPackageSelection: $foundPackageSelection,
|
||||
packageInstallationProcessStep: $packageInstallationProcessStep,
|
||||
installationProgressTracker: installationProgressTracker
|
||||
)
|
||||
|
||||
case .installing:
|
||||
InstallingPackageView(
|
||||
installationProgressTracker: installationProgressTracker,
|
||||
packageInstallationProcessStep: $packageInstallationProcessStep
|
||||
)
|
||||
|
||||
case .finished:
|
||||
DisappearableSheet
|
||||
{
|
||||
ComplexWithIcon(systemName: "checkmark.seal")
|
||||
switch packageInstallationProcessStep
|
||||
{
|
||||
HeadlineWithSubheadline(
|
||||
headline: "add-package.finished",
|
||||
subheadline: "add-package.finished.description",
|
||||
alignment: .leading
|
||||
case .ready:
|
||||
InstallationInitialView(
|
||||
searchResultTracker: searchResultTracker,
|
||||
packageRequested: $packageRequested,
|
||||
foundPackageSelection: $foundPackageSelection,
|
||||
installationProgressTracker: installationProgressTracker,
|
||||
packageInstallationProcessStep: $packageInstallationProcessStep
|
||||
)
|
||||
}
|
||||
}
|
||||
.onAppear
|
||||
{
|
||||
appState.cachedDownloadsFolderSize = AppConstants.shared.brewCachedDownloadsPath.directorySize
|
||||
|
||||
if notifyAboutPackageInstallationResults
|
||||
{
|
||||
sendNotification(title: String(localized: "notification.install-finished"))
|
||||
}
|
||||
}
|
||||
|
||||
case .fatalError: /// This shows up when the function for executing the install action throws an error
|
||||
VStack(alignment: .leading)
|
||||
{
|
||||
ComplexWithIcon(systemName: "exclamationmark.triangle")
|
||||
{
|
||||
HeadlineWithSubheadline(
|
||||
headline: "add-package.fatal-error-\(installationProgressTracker.packageBeingInstalled.package.name)",
|
||||
subheadline: "add-package.fatal-error.description",
|
||||
alignment: .leading
|
||||
case .searching:
|
||||
InstallationSearchingView(
|
||||
packageRequested: $packageRequested,
|
||||
searchResultTracker: searchResultTracker,
|
||||
packageInstallationProcessStep: $packageInstallationProcessStep
|
||||
)
|
||||
}
|
||||
|
||||
HStack
|
||||
{
|
||||
Button
|
||||
{
|
||||
restartApp()
|
||||
} label: {
|
||||
Text("action.restart")
|
||||
}
|
||||
case .presentingSearchResults:
|
||||
PresentingSearchResultsView(
|
||||
searchResultTracker: searchResultTracker,
|
||||
packageRequested: $packageRequested,
|
||||
foundPackageSelection: $foundPackageSelection,
|
||||
packageInstallationProcessStep: $packageInstallationProcessStep,
|
||||
installationProgressTracker: installationProgressTracker
|
||||
)
|
||||
|
||||
Spacer()
|
||||
case .installing:
|
||||
InstallingPackageView(
|
||||
installationProgressTracker: installationProgressTracker,
|
||||
packageInstallationProcessStep: $packageInstallationProcessStep
|
||||
)
|
||||
|
||||
DismissSheetButton()
|
||||
case .finished:
|
||||
InstallationFinishedSuccessfullyView()
|
||||
|
||||
case .fatalError: /// This shows up when the function for executing the install action throws an error
|
||||
InstallationFatalErrorView(installationProgressTracker: installationProgressTracker)
|
||||
|
||||
case .requiresSudoPassword:
|
||||
SudoRequiredView(installationProgressTracker: installationProgressTracker)
|
||||
|
||||
case .wrongArchitecture:
|
||||
WrongArchitectureView(installationProgressTracker: installationProgressTracker)
|
||||
|
||||
case .binaryAlreadyExists:
|
||||
BinaryAlreadyExistsView(installationProgressTracker: installationProgressTracker)
|
||||
|
||||
case .anotherProcessAlreadyRunning:
|
||||
AnotherProcessAlreadyRunningView()
|
||||
|
||||
case .installationTerminatedUnexpectedly:
|
||||
InstallationTerminatedUnexpectedlyView(terminalOutputOfTheInstallation: installationProgressTracker.packageBeingInstalled.realTimeTerminalOutput)
|
||||
}
|
||||
}
|
||||
|
||||
case .requiresSudoPassword:
|
||||
SudoRequiredView(installationProgressTracker: installationProgressTracker)
|
||||
|
||||
case .wrongArchitecture:
|
||||
WrongArchitectureView(installationProgressTracker: installationProgressTracker)
|
||||
|
||||
case .binaryAlreadyExists:
|
||||
BinaryAlreadyExistsView(installationProgressTracker: installationProgressTracker)
|
||||
|
||||
case .anotherProcessAlreadyRunning:
|
||||
VStack(alignment: .leading)
|
||||
.navigationTitle(sheetTitle)
|
||||
.toolbar
|
||||
{
|
||||
ComplexWithImage(image: Image(localURL: URL(string: "/System/Library/CoreServices/KeyboardSetupAssistant.app/Contents/Resources/AppIcon.icns")!)!)
|
||||
if isDismissable
|
||||
{
|
||||
VStack(alignment: .leading, spacing: 10)
|
||||
ToolbarItem(placement: .cancellationAction)
|
||||
{
|
||||
Text("add-package.install.another-homebrew-process-blocking-install.title")
|
||||
.font(.headline)
|
||||
|
||||
Text("add-package.install.another-homebrew-process-blocking-install.description")
|
||||
|
||||
HStack
|
||||
AsyncButton
|
||||
{
|
||||
Button("add-package.clear-brew-locks", role: .destructive)
|
||||
dismiss()
|
||||
installationProgressTracker.cancel()
|
||||
|
||||
do
|
||||
{
|
||||
if let contentsOfLockFolder = try? FileManager.default.contentsOfDirectory(at: URL(fileURLWithPath: "/usr/local/var/homebrew/locks"), includingPropertiesForKeys: [.isRegularFileKey])
|
||||
{
|
||||
for lockURL in contentsOfLockFolder
|
||||
{
|
||||
try? FileManager.default.removeItem(at: lockURL)
|
||||
}
|
||||
}
|
||||
|
||||
dismiss()
|
||||
try await brewData.synchronizeInstalledPackages(cachedPackagesTracker: cachedDownloadsTracker)
|
||||
}
|
||||
Spacer()
|
||||
DismissSheetButton()
|
||||
catch let synchronizationError
|
||||
{
|
||||
appState.showAlert(errorToShow: .couldNotSynchronizePackages(error: synchronizationError.localizedDescription))
|
||||
}
|
||||
} label: {
|
||||
Text("action.cancel")
|
||||
}
|
||||
.keyboardShortcut(.cancelAction)
|
||||
.disabledWhenLoading()
|
||||
}
|
||||
}
|
||||
}
|
||||
.fixedSize()
|
||||
|
||||
/*
|
||||
default:
|
||||
VStack(alignment: .leading)
|
||||
{
|
||||
ComplexWithIcon(systemName: "wifi.exclamationmark")
|
||||
{
|
||||
HeadlineWithSubheadline(
|
||||
headline: "add-package.network-error",
|
||||
subheadline: "add-package.network-error.description",
|
||||
alignment: .leading
|
||||
)
|
||||
}
|
||||
|
||||
HStack
|
||||
{
|
||||
Spacer()
|
||||
|
||||
DismissSheetButton()
|
||||
}
|
||||
}
|
||||
*/
|
||||
case .installationTerminatedUnexpectedly:
|
||||
InstallationTerminatedUnexpectedlyView(terminalOutputOfTheInstallation: installationProgressTracker.packageBeingInstalled.realTimeTerminalOutput)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.fixedSize() // TODO: Remove this fixedSize later
|
||||
.onDisappear
|
||||
{
|
||||
appState.assignPackageTypeToCachedDownloads(brewData: brewData)
|
||||
cachedDownloadsTracker.assignPackageTypeToCachedDownloads(brewData: brewData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,9 @@ struct SearchResultRow: View, Sendable
|
|||
@EnvironmentObject var brewData: BrewDataStorage
|
||||
|
||||
let searchedForPackage: BrewPackage
|
||||
|
||||
let context: Self.Context
|
||||
let downloadCount: Int?
|
||||
|
||||
@State private var description: String?
|
||||
@State private var isCompatible: Bool?
|
||||
|
||||
|
|
@ -30,35 +32,46 @@ struct SearchResultRow: View, Sendable
|
|||
HStack(alignment: .center)
|
||||
{
|
||||
SanitizedPackageName(packageName: searchedForPackage.name, shouldShowVersion: true)
|
||||
|
||||
if searchedForPackage.type == .formula
|
||||
|
||||
switch context
|
||||
{
|
||||
if brewData.installedFormulae.contains(where: { $0.name == searchedForPackage.name })
|
||||
case .topPackages:
|
||||
Spacer()
|
||||
|
||||
if let downloadCount
|
||||
{
|
||||
PillTextWithLocalizableText(localizedText: "add-package.result.already-installed")
|
||||
Text("add-package.top-packages.list-item-\(downloadCount)")
|
||||
.foregroundStyle(.secondary)
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if brewData.installedCasks.contains(where: { $0.name == searchedForPackage.name })
|
||||
|
||||
case .searchResults:
|
||||
if searchedForPackage.type == .formula
|
||||
{
|
||||
PillTextWithLocalizableText(localizedText: "add-package.result.already-installed")
|
||||
}
|
||||
}
|
||||
|
||||
if let isCompatible
|
||||
{
|
||||
if !isCompatible
|
||||
{
|
||||
if showCompatibilityWarning
|
||||
if brewData.successfullyLoadedFormulae.contains(where: { $0.name == searchedForPackage.name })
|
||||
{
|
||||
HStack(alignment: .center, spacing: 4)
|
||||
PillTextWithLocalizableText(localizedText: "add-package.result.already-installed")
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if brewData.successfullyLoadedCasks.contains(where: { $0.name == searchedForPackage.name })
|
||||
{
|
||||
PillTextWithLocalizableText(localizedText: "add-package.result.already-installed")
|
||||
}
|
||||
}
|
||||
|
||||
if let isCompatible
|
||||
{
|
||||
if !isCompatible
|
||||
{
|
||||
if showCompatibilityWarning
|
||||
{
|
||||
Image(systemName: "exclamationmark.circle")
|
||||
Text("add-package.result.not-optimized-for-\(AppConstants.shared.osVersionString.fullName)")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -137,4 +150,9 @@ struct SearchResultRow: View, Sendable
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum Context {
|
||||
case searchResults
|
||||
case topPackages
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,9 +47,9 @@ struct InstallationInitialView: View
|
|||
{
|
||||
List(selection: $foundPackageSelection)
|
||||
{
|
||||
TopPackagesSection(packageTracker: topPackagesTracker.sortedTopFormulae, trackerType: .formula)
|
||||
TopPackagesSection(packageTracker: topPackagesTracker, trackerType: .formula)
|
||||
|
||||
TopPackagesSection(packageTracker: topPackagesTracker.sortedTopCasks, trackerType: .cask)
|
||||
TopPackagesSection(packageTracker: topPackagesTracker, trackerType: .cask)
|
||||
}
|
||||
.listStyle(.bordered(alternatesRowBackgrounds: true))
|
||||
.frame(minHeight: 200)
|
||||
|
|
@ -72,58 +72,22 @@ struct InstallationInitialView: View
|
|||
{
|
||||
foundPackageSelection = nil // Clear all selected items when the user looks for a different package
|
||||
}
|
||||
|
||||
HStack
|
||||
}
|
||||
.toolbar
|
||||
{
|
||||
if enableDiscoverability
|
||||
{
|
||||
DismissSheetButton()
|
||||
|
||||
Spacer()
|
||||
|
||||
if enableDiscoverability
|
||||
ToolbarItemGroup(placement: .automatic)
|
||||
{
|
||||
PreviewPackageButtonWithCustomAction
|
||||
{
|
||||
guard let packageToPreview: BrewPackage = getTopPackageFromTracker() else
|
||||
{
|
||||
AppConstants.shared.logger.error("Could not retrieve top package to preview")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
openWindow(value: packageToPreview)
|
||||
}
|
||||
.disabled(foundPackageSelection == nil)
|
||||
previewPackageButton
|
||||
|
||||
Button
|
||||
{
|
||||
guard let packageToInstall: BrewPackage = getTopPackageFromTracker() else
|
||||
{
|
||||
AppConstants.shared.logger.error("Could not retrieve top package to install")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
installationProgressTracker.packageBeingInstalled = PackageInProgressOfBeingInstalled(package: packageToInstall, installationStage: .ready, packageInstallationProgress: 0)
|
||||
|
||||
AppConstants.shared.logger.debug("Packages to install: \(installationProgressTracker.packageBeingInstalled.package.name, privacy: .public)")
|
||||
|
||||
packageInstallationProcessStep = .installing
|
||||
|
||||
} label: {
|
||||
Text("add-package.install.action")
|
||||
}
|
||||
.keyboardShortcut(foundPackageSelection != nil ? .defaultAction : .init(.end))
|
||||
.disabled(foundPackageSelection == nil)
|
||||
startInstallProcessForTopPackageButton
|
||||
}
|
||||
|
||||
Button
|
||||
{
|
||||
packageInstallationProcessStep = .searching
|
||||
} label: {
|
||||
Text("add-package.search.action")
|
||||
}
|
||||
.keyboardShortcut(foundPackageSelection == nil ? .defaultAction : .init(.end))
|
||||
.disabled(packageRequested.isEmpty)
|
||||
}
|
||||
|
||||
ToolbarItem(placement: .primaryAction)
|
||||
{
|
||||
searchForPackageButton
|
||||
}
|
||||
}
|
||||
.onAppear
|
||||
|
|
@ -132,6 +96,61 @@ struct InstallationInitialView: View
|
|||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var previewPackageButton: some View
|
||||
{
|
||||
PreviewPackageButtonWithCustomAction
|
||||
{
|
||||
guard let packageToPreview: BrewPackage = getTopPackageFromTracker() else
|
||||
{
|
||||
AppConstants.shared.logger.error("Could not retrieve top package to preview")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
openWindow(value: packageToPreview)
|
||||
}
|
||||
.disabled(foundPackageSelection == nil)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var startInstallProcessForTopPackageButton: some View
|
||||
{
|
||||
Button
|
||||
{
|
||||
guard let packageToInstall: BrewPackage = getTopPackageFromTracker() else
|
||||
{
|
||||
AppConstants.shared.logger.error("Could not retrieve top package to install")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
installationProgressTracker.packageBeingInstalled = PackageInProgressOfBeingInstalled(package: packageToInstall, installationStage: .ready, packageInstallationProgress: 0)
|
||||
|
||||
AppConstants.shared.logger.debug("Packages to install: \(installationProgressTracker.packageBeingInstalled.package.name, privacy: .public)")
|
||||
|
||||
packageInstallationProcessStep = .installing
|
||||
|
||||
} label: {
|
||||
Text("add-package.install.action")
|
||||
}
|
||||
.keyboardShortcut(foundPackageSelection != nil ? .defaultAction : .init(.end))
|
||||
.disabled(foundPackageSelection == nil)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var searchForPackageButton: some View
|
||||
{
|
||||
Button
|
||||
{
|
||||
packageInstallationProcessStep = .searching
|
||||
} label: {
|
||||
Text("add-package.search.action")
|
||||
}
|
||||
.keyboardShortcut(foundPackageSelection == nil ? .defaultAction : .init(.end))
|
||||
.disabled(packageRequested.isEmpty)
|
||||
}
|
||||
|
||||
func getTopPackageFromTracker() -> BrewPackage?
|
||||
{
|
||||
if let foundPackageSelection
|
||||
|
|
|
|||
|
|
@ -1,27 +0,0 @@
|
|||
//
|
||||
// Top Package List Item.swift
|
||||
// Cork
|
||||
//
|
||||
// Created by David Bureš on 30.08.2023.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct TopPackageListItem: View
|
||||
{
|
||||
let topPackage: TopPackage
|
||||
|
||||
var body: some View
|
||||
{
|
||||
HStack(alignment: .center)
|
||||
{
|
||||
SanitizedPackageName(packageName: topPackage.packageName, shouldShowVersion: true)
|
||||
|
||||
Spacer()
|
||||
|
||||
Text("add-package.top-packages.list-item-\(topPackage.packageDownloads)")
|
||||
.foregroundStyle(.secondary)
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -11,9 +11,26 @@ struct TopPackagesSection: View
|
|||
{
|
||||
@EnvironmentObject var brewData: BrewDataStorage
|
||||
|
||||
let packageTracker: [TopPackage]
|
||||
let packageTracker: TopPackagesTracker
|
||||
|
||||
let trackerType: PackageType
|
||||
|
||||
private var packages: [TopPackage]
|
||||
{
|
||||
switch trackerType
|
||||
{
|
||||
case .formula:
|
||||
packageTracker.sortedTopFormulae.filter
|
||||
{
|
||||
!brewData.successfullyLoadedFormulae.map(\.name).contains($0.packageName)
|
||||
}
|
||||
case .cask:
|
||||
packageTracker.sortedTopCasks.filter
|
||||
{
|
||||
!brewData.successfullyLoadedCasks.map(\.name).contains($0.packageName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@State private var isCollapsed: Bool = false
|
||||
|
||||
|
|
@ -23,19 +40,9 @@ struct TopPackagesSection: View
|
|||
{
|
||||
if !isCollapsed
|
||||
{
|
||||
ForEach(packageTracker.filter
|
||||
{
|
||||
switch trackerType
|
||||
{
|
||||
case .formula:
|
||||
!brewData.installedFormulae.map(\.name).contains($0.packageName)
|
||||
case .cask:
|
||||
!brewData.installedCasks.map(\.name).contains($0.packageName)
|
||||
}
|
||||
|
||||
}.prefix(15))
|
||||
{ topFormula in
|
||||
TopPackageListItem(topPackage: topFormula)
|
||||
ForEach(packages.prefix(15))
|
||||
{ topPackage in
|
||||
SearchResultRow(searchedForPackage: BrewPackage(name: topPackage.packageName, type: trackerType, installedOn: nil, versions: [], sizeInBytes: nil), context: .topPackages, downloadCount: topPackage.packageDownloads)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ struct InstallingPackageView: View
|
|||
|
||||
@EnvironmentObject var appState: AppState
|
||||
@EnvironmentObject var brewData: BrewDataStorage
|
||||
|
||||
@EnvironmentObject var cachedPackagesTracker: CachedPackagesTracker
|
||||
|
||||
@ObservedObject var installationProgressTracker: InstallationProgressTracker
|
||||
|
||||
|
|
@ -111,11 +113,15 @@ struct InstallingPackageView: View
|
|||
}
|
||||
}
|
||||
}
|
||||
.task(priority: .userInitiated)
|
||||
.task
|
||||
{
|
||||
do
|
||||
{
|
||||
let installationResult: TerminalOutput = try await installationProgressTracker.installPackage(using: brewData)
|
||||
let installationResult: TerminalOutput = try await installationProgressTracker.installPackage(
|
||||
using: brewData,
|
||||
cachedPackagesTracker: cachedPackagesTracker
|
||||
)
|
||||
|
||||
AppConstants.shared.logger.debug("Installation result:\nStandard output: \(installationResult.standardOutput, privacy: .public)\nStandard error: \(installationResult.standardError, privacy: .public)")
|
||||
|
||||
/// Check if the package installation stag at the end of the install process was something unexpected. Normal package installations go through multiple steps, and the three listed below are not supposed to be the end state. This means that something went wrong during the installation
|
||||
|
|
|
|||
|
|
@ -33,11 +33,6 @@ struct PresentingSearchResultsView: View
|
|||
{
|
||||
VStack
|
||||
{
|
||||
InstallProcessCustomSearchField(search: $packageRequested, isFocused: $isSearchFieldFocused, customPromptText: String(localized: "add-package.search.prompt"))
|
||||
{
|
||||
foundPackageSelection = nil // Clear all selected items when the user looks for a different package
|
||||
}
|
||||
|
||||
List(selection: $foundPackageSelection)
|
||||
{
|
||||
SearchResultsSection(
|
||||
|
|
@ -51,56 +46,79 @@ struct PresentingSearchResultsView: View
|
|||
)
|
||||
}
|
||||
.listStyle(.bordered(alternatesRowBackgrounds: true))
|
||||
.frame(width: 300, height: 300)
|
||||
.frame(minHeight: 200)
|
||||
|
||||
HStack
|
||||
InstallProcessCustomSearchField(search: $packageRequested, isFocused: $isSearchFieldFocused, customPromptText: String(localized: "add-package.search.prompt"))
|
||||
{
|
||||
DismissSheetButton()
|
||||
|
||||
Spacer()
|
||||
|
||||
PreviewPackageButtonWithCustomAction
|
||||
{
|
||||
do
|
||||
{
|
||||
let requestedPackageToPreview: BrewPackage = try foundPackageSelection!.getPackage(tracker: searchResultTracker)
|
||||
|
||||
openWindow(value: requestedPackageToPreview)
|
||||
|
||||
AppConstants.shared.logger.debug("Would preview package \(requestedPackageToPreview.name)")
|
||||
}
|
||||
catch {}
|
||||
}
|
||||
.disabled(foundPackageSelection == nil)
|
||||
|
||||
if isSearchFieldFocused
|
||||
{
|
||||
Button
|
||||
{
|
||||
packageInstallationProcessStep = .searching
|
||||
} label: {
|
||||
Text("add-package.search.action")
|
||||
}
|
||||
.keyboardShortcut(.defaultAction)
|
||||
.disabled(packageRequested.isEmpty)
|
||||
}
|
||||
else
|
||||
{
|
||||
Button
|
||||
{
|
||||
getRequestedPackages()
|
||||
|
||||
packageInstallationProcessStep = .installing
|
||||
} label: {
|
||||
Text("add-package.install.action")
|
||||
}
|
||||
.keyboardShortcut(.defaultAction)
|
||||
.disabled(foundPackageSelection == nil)
|
||||
}
|
||||
foundPackageSelection = nil // Clear all selected items when the user looks for a different package
|
||||
}
|
||||
}
|
||||
.toolbar
|
||||
{
|
||||
ToolbarItem(placement: .primaryAction)
|
||||
{
|
||||
searchForPackageButton
|
||||
}
|
||||
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
startInstallProcessButton
|
||||
}
|
||||
|
||||
ToolbarItemGroup(placement: .automatic)
|
||||
{
|
||||
previewPackageButton
|
||||
|
||||
startInstallProcessButton
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var previewPackageButton: some View
|
||||
{
|
||||
PreviewPackageButtonWithCustomAction
|
||||
{
|
||||
do
|
||||
{
|
||||
let requestedPackageToPreview: BrewPackage = try foundPackageSelection!.getPackage(tracker: searchResultTracker)
|
||||
|
||||
openWindow(value: requestedPackageToPreview)
|
||||
|
||||
AppConstants.shared.logger.debug("Would preview package \(requestedPackageToPreview.name)")
|
||||
}
|
||||
catch {}
|
||||
}
|
||||
.disabled(foundPackageSelection == nil)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var searchForPackageButton: some View
|
||||
{
|
||||
Button
|
||||
{
|
||||
packageInstallationProcessStep = .searching
|
||||
} label: {
|
||||
Text("add-package.search.action")
|
||||
}
|
||||
.keyboardShortcut(.defaultAction)
|
||||
.disabled(packageRequested.isEmpty || !isSearchFieldFocused)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var startInstallProcessButton: some View
|
||||
{
|
||||
Button
|
||||
{
|
||||
getRequestedPackages()
|
||||
|
||||
packageInstallationProcessStep = .installing
|
||||
} label: {
|
||||
Text("add-package.install.action")
|
||||
}
|
||||
.keyboardShortcut(.defaultAction)
|
||||
.disabled(foundPackageSelection == nil)
|
||||
}
|
||||
|
||||
private func getRequestedPackages()
|
||||
{
|
||||
if let foundPackageSelection
|
||||
|
|
@ -150,7 +168,7 @@ private struct SearchResultsSection: View
|
|||
{
|
||||
ForEach(packageList)
|
||||
{ package in
|
||||
SearchResultRow(searchedForPackage: package)
|
||||
SearchResultRow(searchedForPackage: package, context: .searchResults, downloadCount: nil)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
|
|
|
|||
|
|
@ -18,27 +18,24 @@ struct InstallationSearchingView: View, Sendable
|
|||
var body: some View
|
||||
{
|
||||
ProgressView("add-package.searching-\(packageRequested)")
|
||||
.onAppear
|
||||
.task
|
||||
{
|
||||
Task
|
||||
searchResultTracker.foundFormulae = []
|
||||
searchResultTracker.foundCasks = []
|
||||
|
||||
async let foundFormulae: [String] = searchForPackage(packageName: packageRequested, packageType: .formula)
|
||||
async let foundCasks: [String] = searchForPackage(packageName: packageRequested, packageType: .cask)
|
||||
|
||||
for formula in await foundFormulae
|
||||
{
|
||||
searchResultTracker.foundFormulae = []
|
||||
searchResultTracker.foundCasks = []
|
||||
|
||||
async let foundFormulae: [String] = try searchForPackage(packageName: packageRequested, packageType: .formula)
|
||||
async let foundCasks: [String] = try searchForPackage(packageName: packageRequested, packageType: .cask)
|
||||
|
||||
for formula in try await foundFormulae
|
||||
{
|
||||
searchResultTracker.foundFormulae.append(BrewPackage(name: formula, type: .formula, installedOn: nil, versions: [], sizeInBytes: nil))
|
||||
}
|
||||
for cask in try await foundCasks
|
||||
{
|
||||
searchResultTracker.foundCasks.append(BrewPackage(name: cask, type: .cask, installedOn: nil, versions: [], sizeInBytes: nil))
|
||||
}
|
||||
|
||||
packageInstallationProcessStep = .presentingSearchResults
|
||||
searchResultTracker.foundFormulae.append(BrewPackage(name: formula, type: .formula, installedOn: nil, versions: [], sizeInBytes: nil))
|
||||
}
|
||||
for cask in await foundCasks
|
||||
{
|
||||
searchResultTracker.foundCasks.append(BrewPackage(name: cask, type: .cask, installedOn: nil, versions: [], sizeInBytes: nil))
|
||||
}
|
||||
|
||||
packageInstallationProcessStep = .presentingSearchResults
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
//
|
||||
// Another Process Already Running.swift
|
||||
// Cork
|
||||
//
|
||||
// Created by David Bureš - P on 24.01.2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct AnotherProcessAlreadyRunningView: View
|
||||
{
|
||||
@EnvironmentObject var appState: AppState
|
||||
|
||||
var body: some View
|
||||
{
|
||||
ComplexWithImage(image: Image(localURL: URL(string: "/System/Library/CoreServices/KeyboardSetupAssistant.app/Contents/Resources/AppIcon.icns")!)!)
|
||||
{
|
||||
VStack(alignment: .leading, spacing: 10)
|
||||
{
|
||||
Text("add-package.install.another-homebrew-process-blocking-install.title")
|
||||
.font(.headline)
|
||||
|
||||
Text("add-package.install.another-homebrew-process-blocking-install.description")
|
||||
}
|
||||
.toolbar
|
||||
{
|
||||
ToolbarItem(placement: .destructiveAction)
|
||||
{
|
||||
Button("add-package.clear-brew-locks", role: .destructive)
|
||||
{
|
||||
if let contentsOfLockFolder = try? FileManager.default.contentsOfDirectory(at: URL(fileURLWithPath: "/usr/local/var/homebrew/locks"), includingPropertiesForKeys: [.isRegularFileKey])
|
||||
{
|
||||
for lockURL in contentsOfLockFolder
|
||||
{
|
||||
try? FileManager.default.removeItem(at: lockURL)
|
||||
}
|
||||
}
|
||||
|
||||
appState.dismissSheet()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ struct BinaryAlreadyExistsView: View, Sendable
|
|||
{
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
|
||||
@EnvironmentObject var appState: AppState
|
||||
@EnvironmentObject var brewData: BrewDataStorage
|
||||
|
||||
@ObservedObject var installationProgressTracker: InstallationProgressTracker
|
||||
|
|
@ -27,10 +28,10 @@ struct BinaryAlreadyExistsView: View, Sendable
|
|||
subheadline: "add-package.install.binary-already-exists.subheadline",
|
||||
alignment: .leading
|
||||
)
|
||||
|
||||
Spacer()
|
||||
|
||||
HStack
|
||||
}
|
||||
.toolbar
|
||||
{
|
||||
ToolbarItem(placement: .primaryAction)
|
||||
{
|
||||
Button
|
||||
{
|
||||
|
|
@ -38,21 +39,6 @@ struct BinaryAlreadyExistsView: View, Sendable
|
|||
} label: {
|
||||
Text("action.reveal-applications-folder-in-finder")
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Button
|
||||
{
|
||||
dismiss()
|
||||
|
||||
Task.detached
|
||||
{
|
||||
await synchronizeInstalledPackages(brewData: brewData)
|
||||
}
|
||||
} label: {
|
||||
Text("action.close")
|
||||
}
|
||||
.keyboardShortcut(.cancelAction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
//
|
||||
// Installation Fatal Error.swift
|
||||
// Cork
|
||||
//
|
||||
// Created by David Bureš - P on 24.01.2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct InstallationFatalErrorView: View
|
||||
{
|
||||
@ObservedObject var installationProgressTracker: InstallationProgressTracker
|
||||
|
||||
var body: some View
|
||||
{
|
||||
ComplexWithIcon(systemName: "exclamationmark.triangle")
|
||||
{
|
||||
HeadlineWithSubheadline(
|
||||
headline: "add-package.fatal-error-\(installationProgressTracker.packageBeingInstalled.package.name)",
|
||||
subheadline: "add-package.fatal-error.description",
|
||||
alignment: .leading
|
||||
)
|
||||
}
|
||||
.toolbar
|
||||
{
|
||||
ToolbarItem(placement: .destructiveAction)
|
||||
{
|
||||
Button
|
||||
{
|
||||
restartApp()
|
||||
} label: {
|
||||
Text("action.restart")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
//
|
||||
// Installation Finished Successfully.swift
|
||||
// Cork
|
||||
//
|
||||
// Created by David Bureš - P on 25.01.2025.
|
||||
//
|
||||
|
||||
import CorkNotifications
|
||||
import CorkShared
|
||||
import SwiftUI
|
||||
|
||||
struct InstallationFinishedSuccessfullyView: View
|
||||
{
|
||||
@EnvironmentObject var cachedDownloadsTracker: CachedPackagesTracker
|
||||
|
||||
@AppStorage("notifyAboutPackageInstallationResults") var notifyAboutPackageInstallationResults: Bool = false
|
||||
|
||||
var body: some View
|
||||
{
|
||||
DisappearableSheet
|
||||
{
|
||||
ComplexWithIcon(systemName: "checkmark.seal")
|
||||
{
|
||||
HeadlineWithSubheadline(
|
||||
headline: "add-package.finished",
|
||||
subheadline: "add-package.finished.description",
|
||||
alignment: .leading
|
||||
)
|
||||
}
|
||||
}
|
||||
.onAppear
|
||||
{
|
||||
if notifyAboutPackageInstallationResults
|
||||
{
|
||||
sendNotification(
|
||||
title: String(localized: "notification.install-finished"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -24,26 +24,26 @@ struct InstallationTerminatedUnexpectedlyView: View
|
|||
subheadline: "add-package.install.installation-terminated.subheadline",
|
||||
alignment: .leading
|
||||
)
|
||||
|
||||
DisclosureGroup
|
||||
|
||||
if usableLiveTerminalOutput.isEmpty
|
||||
{
|
||||
List
|
||||
{
|
||||
ForEach(usableLiveTerminalOutput)
|
||||
{ outputLine in
|
||||
Text(outputLine.line)
|
||||
}
|
||||
}
|
||||
.frame(height: 100, alignment: .leading)
|
||||
} label: {
|
||||
Text("action.show-terminal-output")
|
||||
OutlinedPillText(text: "add-package.install.installation-terminated.no-terminal-output-provided", color: .secondary)
|
||||
}
|
||||
|
||||
HStack
|
||||
else
|
||||
{
|
||||
Spacer()
|
||||
|
||||
DismissSheetButton()
|
||||
DisclosureGroup
|
||||
{
|
||||
List
|
||||
{
|
||||
ForEach(usableLiveTerminalOutput)
|
||||
{ outputLine in
|
||||
Text(outputLine.line)
|
||||
}
|
||||
}
|
||||
.frame(height: 100, alignment: .leading)
|
||||
} label: {
|
||||
Text("action.show-terminal-output")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ struct SudoRequiredView: View, Sendable
|
|||
{
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
|
||||
@EnvironmentObject var appState: AppState
|
||||
@EnvironmentObject var brewData: BrewDataStorage
|
||||
|
||||
@ObservedObject var installationProgressTracker: InstallationProgressTracker
|
||||
|
|
@ -34,24 +35,11 @@ struct SudoRequiredView: View, Sendable
|
|||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
HStack
|
||||
}
|
||||
.toolbar
|
||||
{
|
||||
ToolbarItem(placement: .primaryAction)
|
||||
{
|
||||
Button
|
||||
{
|
||||
dismiss()
|
||||
|
||||
Task.detached
|
||||
{
|
||||
await synchronizeInstalledPackages(brewData: brewData)
|
||||
}
|
||||
} label: {
|
||||
Text("action.close")
|
||||
}
|
||||
.keyboardShortcut(.cancelAction)
|
||||
|
||||
Spacer()
|
||||
|
||||
Button
|
||||
{
|
||||
openTerminal()
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ struct WrongArchitectureView: View, Sendable
|
|||
{
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
|
||||
@EnvironmentObject var appState: AppState
|
||||
@EnvironmentObject var brewData: BrewDataStorage
|
||||
|
||||
@ObservedObject var installationProgressTracker: InstallationProgressTracker
|
||||
|
|
@ -26,24 +27,6 @@ struct WrongArchitectureView: View, Sendable
|
|||
subheadline: "add-package.install.wrong-architecture-\(installationProgressTracker.packageBeingInstalled.package.name).user-architecture-is-\(ProcessInfo().CPUArchitecture == .arm ? "Apple Silicon" : "Intel")",
|
||||
alignment: .leading
|
||||
)
|
||||
|
||||
HStack
|
||||
{
|
||||
Spacer()
|
||||
|
||||
Button
|
||||
{
|
||||
dismiss()
|
||||
|
||||
Task.detached
|
||||
{
|
||||
await synchronizeInstalledPackages(brewData: brewData)
|
||||
}
|
||||
} label: {
|
||||
Text("action.close")
|
||||
}
|
||||
.keyboardShortcut(.cancelAction)
|
||||
}
|
||||
}
|
||||
}
|
||||
.fixedSize()
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import SwiftUI
|
||||
import CorkShared
|
||||
import ButtonKit
|
||||
|
||||
struct Licensing_NotBoughtOrActivatedView: View
|
||||
{
|
||||
|
|
@ -139,60 +140,57 @@ struct Licensing_NotBoughtOrActivatedView: View
|
|||
.disabled(isDemoButtonDisabled)
|
||||
}
|
||||
|
||||
Button
|
||||
AsyncButton
|
||||
{
|
||||
Task(priority: .userInitiated)
|
||||
withAnimation
|
||||
{
|
||||
withAnimation
|
||||
{
|
||||
isCheckingLicense = true
|
||||
}
|
||||
isCheckingLicense = true
|
||||
}
|
||||
|
||||
defer
|
||||
defer
|
||||
{
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 3)
|
||||
{
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 3)
|
||||
withAnimation
|
||||
{
|
||||
withAnimation
|
||||
{
|
||||
isCheckingLicense = false
|
||||
hasCheckingFailed = false
|
||||
}
|
||||
isCheckingLicense = false
|
||||
hasCheckingFailed = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
do
|
||||
do
|
||||
{
|
||||
let hasSpecifiedUserBoughtCork: Bool = try await checkIfUserBoughtCork(for: emailFieldContents)
|
||||
|
||||
AppConstants.shared.logger.debug("Has \(emailFieldContents) bought Cork? \(hasSpecifiedUserBoughtCork ? "YES" : "NO")")
|
||||
|
||||
if hasSpecifiedUserBoughtCork
|
||||
{
|
||||
let hasSpecifiedUserBoughtCork: Bool = try await checkIfUserBoughtCork(for: emailFieldContents)
|
||||
|
||||
AppConstants.shared.logger.debug("Has \(emailFieldContents) bought Cork? \(hasSpecifiedUserBoughtCork ? "YES" : "NO")")
|
||||
|
||||
if hasSpecifiedUserBoughtCork
|
||||
appState.licensingState = .bought
|
||||
}
|
||||
else
|
||||
{
|
||||
withAnimation
|
||||
{
|
||||
appState.licensingState = .bought
|
||||
}
|
||||
else
|
||||
{
|
||||
withAnimation
|
||||
{
|
||||
hasCheckingFailed = true
|
||||
}
|
||||
hasCheckingFailed = true
|
||||
}
|
||||
}
|
||||
catch let licenseCheckingError as CorkLicenseRetrievalError
|
||||
{
|
||||
AppConstants.shared.logger.error("\(licenseCheckingError.localizedDescription, privacy: .public)")
|
||||
}
|
||||
catch let licenseCheckingError as CorkLicenseRetrievalError
|
||||
{
|
||||
AppConstants.shared.logger.error("\(licenseCheckingError.localizedDescription, privacy: .public)")
|
||||
|
||||
switch licenseCheckingError
|
||||
{
|
||||
case .authorizationComplexNotEncodedProperly:
|
||||
appState.showAlert(errorToShow: .licenseCheckingFailedDueToAuthorizationComplexNotBeingEncodedProperly)
|
||||
case .notConnectedToTheInternet:
|
||||
appState.showAlert(errorToShow: .licenseCheckingFailedDueToNoInternet)
|
||||
case .operationTimedOut:
|
||||
appState.showAlert(errorToShow: .licenseCheckingFailedDueToTimeout)
|
||||
case .otherError(let localizedDescription):
|
||||
appState.showAlert(errorToShow: .licenseCheckingFailedForOtherReason(localizedDescription: localizedDescription))
|
||||
}
|
||||
switch licenseCheckingError
|
||||
{
|
||||
case .authorizationComplexNotEncodedProperly:
|
||||
appState.showAlert(errorToShow: .licenseCheckingFailedDueToAuthorizationComplexNotBeingEncodedProperly)
|
||||
case .notConnectedToTheInternet:
|
||||
appState.showAlert(errorToShow: .licenseCheckingFailedDueToNoInternet)
|
||||
case .operationTimedOut:
|
||||
appState.showAlert(errorToShow: .licenseCheckingFailedDueToTimeout)
|
||||
case .otherError(let localizedDescription):
|
||||
appState.showAlert(errorToShow: .licenseCheckingFailedForOtherReason(localizedDescription: localizedDescription))
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
|
|
@ -200,6 +198,8 @@ struct Licensing_NotBoughtOrActivatedView: View
|
|||
}
|
||||
.keyboardShortcut(.defaultAction)
|
||||
.disabled(emailFieldContents.isEmpty || !emailFieldContents.contains("@") || !emailFieldContents.contains("."))
|
||||
.disabledWhenLoading()
|
||||
.asyncButtonStyle(.none)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ enum MaintenanceSteps
|
|||
|
||||
struct MaintenanceView: View
|
||||
{
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
|
||||
@EnvironmentObject var brewData: BrewDataStorage
|
||||
@EnvironmentObject var appState: AppState
|
||||
|
||||
|
|
@ -35,49 +37,107 @@ struct MaintenanceView: View
|
|||
@State var reclaimedSpaceAfterCachePurge: Int = 0
|
||||
|
||||
@State var forcedOptions: Bool? = false
|
||||
|
||||
var isDismissable: Bool
|
||||
{
|
||||
[.ready, .finished].contains(maintenanceSteps)
|
||||
}
|
||||
|
||||
var shouldShowTitle: Bool
|
||||
{
|
||||
[.ready].contains(maintenanceSteps)
|
||||
}
|
||||
|
||||
var sheetTitle: LocalizedStringKey
|
||||
{
|
||||
switch maintenanceSteps
|
||||
{
|
||||
case .ready:
|
||||
return "maintenance.title"
|
||||
case .maintenanceRunning:
|
||||
return ""
|
||||
case .finished:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
var dismissButtonTitle: LocalizedStringKey
|
||||
{
|
||||
switch maintenanceSteps {
|
||||
case .ready:
|
||||
return "action.cancel"
|
||||
case .maintenanceRunning:
|
||||
return ""
|
||||
case .finished:
|
||||
return "action.close"
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View
|
||||
{
|
||||
VStack(alignment: .leading, spacing: 10)
|
||||
NavigationStack
|
||||
{
|
||||
switch maintenanceSteps
|
||||
SheetTemplate(isShowingTitle: shouldShowTitle)
|
||||
{
|
||||
case .ready:
|
||||
MaintenanceReadyView(
|
||||
shouldUninstallOrphans: $shouldUninstallOrphans,
|
||||
shouldPurgeCache: $shouldPurgeCache,
|
||||
shouldDeleteDownloads: $shouldDeleteDownloads,
|
||||
shouldPerformHealthCheck: $shouldPerformHealthCheck,
|
||||
maintenanceSteps: $maintenanceSteps,
|
||||
isShowingControlButtons: true,
|
||||
forcedOptions: forcedOptions!
|
||||
)
|
||||
Group
|
||||
{
|
||||
switch maintenanceSteps
|
||||
{
|
||||
case .ready:
|
||||
MaintenanceReadyView(
|
||||
shouldUninstallOrphans: $shouldUninstallOrphans,
|
||||
shouldPurgeCache: $shouldPurgeCache,
|
||||
shouldDeleteDownloads: $shouldDeleteDownloads,
|
||||
shouldPerformHealthCheck: $shouldPerformHealthCheck,
|
||||
maintenanceSteps: $maintenanceSteps,
|
||||
isShowingControlButtons: true,
|
||||
forcedOptions: forcedOptions!
|
||||
)
|
||||
|
||||
case .maintenanceRunning:
|
||||
MaintenanceRunningView(
|
||||
shouldUninstallOrphans: shouldUninstallOrphans,
|
||||
shouldPurgeCache: shouldPurgeCache,
|
||||
shouldDeleteDownloads: shouldDeleteDownloads,
|
||||
shouldPerformHealthCheck: shouldPerformHealthCheck,
|
||||
numberOfOrphansRemoved: $numberOfOrphansRemoved,
|
||||
packagesHoldingBackCachePurge: $packagesHoldingBackCachePurge,
|
||||
reclaimedSpaceAfterCachePurge: $reclaimedSpaceAfterCachePurge,
|
||||
brewHealthCheckFoundNoProblems: $brewHealthCheckFoundNoProblems,
|
||||
maintenanceSteps: $maintenanceSteps
|
||||
)
|
||||
case .maintenanceRunning:
|
||||
MaintenanceRunningView(
|
||||
shouldUninstallOrphans: shouldUninstallOrphans,
|
||||
shouldPurgeCache: shouldPurgeCache,
|
||||
shouldDeleteDownloads: shouldDeleteDownloads,
|
||||
shouldPerformHealthCheck: shouldPerformHealthCheck,
|
||||
numberOfOrphansRemoved: $numberOfOrphansRemoved,
|
||||
packagesHoldingBackCachePurge: $packagesHoldingBackCachePurge,
|
||||
reclaimedSpaceAfterCachePurge: $reclaimedSpaceAfterCachePurge,
|
||||
brewHealthCheckFoundNoProblems: $brewHealthCheckFoundNoProblems,
|
||||
maintenanceSteps: $maintenanceSteps
|
||||
)
|
||||
|
||||
case .finished:
|
||||
MaintenanceFinishedView(
|
||||
shouldUninstallOrphans: shouldUninstallOrphans,
|
||||
shouldPurgeCache: shouldPurgeCache,
|
||||
shouldDeleteDownloads: shouldDeleteDownloads,
|
||||
shouldPerformHealthCheck: shouldPerformHealthCheck,
|
||||
packagesHoldingBackCachePurge: packagesHoldingBackCachePurge,
|
||||
numberOfOrphansRemoved: numberOfOrphansRemoved,
|
||||
reclaimedSpaceAfterCachePurge: reclaimedSpaceAfterCachePurge,
|
||||
brewHealthCheckFoundNoProblems: brewHealthCheckFoundNoProblems,
|
||||
maintenanceFoundNoProblems: $maintenanceFoundNoProblems
|
||||
)
|
||||
case .finished:
|
||||
MaintenanceFinishedView(
|
||||
shouldUninstallOrphans: shouldUninstallOrphans,
|
||||
shouldPurgeCache: shouldPurgeCache,
|
||||
shouldDeleteDownloads: shouldDeleteDownloads,
|
||||
shouldPerformHealthCheck: shouldPerformHealthCheck,
|
||||
packagesHoldingBackCachePurge: packagesHoldingBackCachePurge,
|
||||
numberOfOrphansRemoved: numberOfOrphansRemoved,
|
||||
reclaimedSpaceAfterCachePurge: reclaimedSpaceAfterCachePurge,
|
||||
brewHealthCheckFoundNoProblems: brewHealthCheckFoundNoProblems,
|
||||
maintenanceFoundNoProblems: $maintenanceFoundNoProblems
|
||||
)
|
||||
}
|
||||
}
|
||||
.navigationTitle(sheetTitle)
|
||||
.toolbar
|
||||
{
|
||||
if isDismissable
|
||||
{
|
||||
ToolbarItem(placement: .cancellationAction)
|
||||
{
|
||||
Button
|
||||
{
|
||||
dismiss()
|
||||
} label: {
|
||||
Text(dismissButtonTitle)
|
||||
}
|
||||
.keyboardShortcut(.cancelAction)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
// Created by David Bureš on 04.10.2023.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import CorkShared
|
||||
import SwiftUI
|
||||
|
||||
struct MaintenanceFinishedView: View
|
||||
{
|
||||
|
|
@ -16,6 +16,9 @@ struct MaintenanceFinishedView: View
|
|||
|
||||
@EnvironmentObject var appState: AppState
|
||||
@EnvironmentObject var brewData: BrewDataStorage
|
||||
|
||||
@EnvironmentObject var cachedDownloadsTracker: CachedPackagesTracker
|
||||
|
||||
@EnvironmentObject var outdatedPackageTacker: OutdatedPackageTracker
|
||||
|
||||
let shouldUninstallOrphans: Bool
|
||||
|
|
@ -67,120 +70,102 @@ struct MaintenanceFinishedView: View
|
|||
{
|
||||
ComplexWithIcon(systemName: "checkmark.seal")
|
||||
{
|
||||
VStack(alignment: .center)
|
||||
VStack(alignment: .leading, spacing: 5)
|
||||
{
|
||||
VStack(alignment: .leading, spacing: 5)
|
||||
Text("maintenance.finished")
|
||||
.font(.headline)
|
||||
|
||||
if shouldUninstallOrphans
|
||||
{
|
||||
Text("maintenance.finished")
|
||||
.font(.headline)
|
||||
Text("maintenance.results.orphans-count-\(numberOfOrphansRemoved)")
|
||||
}
|
||||
|
||||
if shouldUninstallOrphans
|
||||
if shouldPurgeCache
|
||||
{
|
||||
VStack(alignment: .leading)
|
||||
{
|
||||
Text("maintenance.results.orphans-count-\(numberOfOrphansRemoved)")
|
||||
}
|
||||
Text("maintenance.results.package-cache")
|
||||
|
||||
if shouldPurgeCache
|
||||
{
|
||||
VStack(alignment: .leading)
|
||||
if !displayablePackagesHoldingBackCachePurge.isEmpty
|
||||
{
|
||||
Text("maintenance.results.package-cache")
|
||||
|
||||
if !displayablePackagesHoldingBackCachePurge.isEmpty
|
||||
if displayablePackagesHoldingBackCachePurge.count >= 3
|
||||
{
|
||||
if displayablePackagesHoldingBackCachePurge.count >= 3
|
||||
{
|
||||
let packageNamesNotTruncated: [String] = Array(displayablePackagesHoldingBackCachePurge.prefix(3))
|
||||
let packageNamesNotTruncated: [String] = Array(displayablePackagesHoldingBackCachePurge.prefix(3))
|
||||
|
||||
let numberOfTruncatedPackages: Int = displayablePackagesHoldingBackCachePurge.count - packageNamesNotTruncated.count
|
||||
let numberOfTruncatedPackages: Int = displayablePackagesHoldingBackCachePurge.count - packageNamesNotTruncated.count
|
||||
|
||||
Text("maintenance.results.package-cache.skipped-\(packageNamesNotTruncated.formatted(.list(type: .and)))-and-\(numberOfTruncatedPackages)-others")
|
||||
.font(.caption)
|
||||
.foregroundColor(Color(nsColor: NSColor.systemGray))
|
||||
}
|
||||
else
|
||||
{
|
||||
Text("maintenance.results.package-cache.skipped-\(displayablePackagesHoldingBackCachePurge.formatted(.list(type: .and)))")
|
||||
.font(.caption)
|
||||
.foregroundColor(Color(nsColor: NSColor.systemGray))
|
||||
}
|
||||
Text("maintenance.results.package-cache.skipped-\(packageNamesNotTruncated.formatted(.list(type: .and)))-and-\(numberOfTruncatedPackages)-others")
|
||||
.font(.caption)
|
||||
.foregroundColor(Color(nsColor: NSColor.systemGray))
|
||||
}
|
||||
else
|
||||
{
|
||||
Text("maintenance.results.package-cache.skipped-\(displayablePackagesHoldingBackCachePurge.formatted(.list(type: .and)))")
|
||||
.font(.caption)
|
||||
.foregroundColor(Color(nsColor: NSColor.systemGray))
|
||||
}
|
||||
|
||||
/*
|
||||
if cachePurgingSkippedPackagesDueToMostRecentVersionsNotBeingInstalled
|
||||
{
|
||||
if packagesHoldingBackCachePurgeTracker.count > 2
|
||||
{
|
||||
|
||||
Text("maintenance.results.package-cache.skipped-\(packagesHoldingBackCachePurgeTracker[0...1].joined(separator: ", "))-and-\(packagesHoldingBackCachePurgeTracker.count - 2)-others")
|
||||
.font(.caption)
|
||||
.foregroundColor(Color(nsColor: NSColor.systemGray))
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
Text("maintenance.results.package-cache.skipped-\(packagesHoldingBackCachePurgeTracker.joined(separator: ", "))")
|
||||
.font(.caption)
|
||||
.foregroundColor(Color(nsColor: NSColor.systemGray))
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
if shouldDeleteDownloads
|
||||
{
|
||||
VStack(alignment: .leading)
|
||||
{
|
||||
Text("maintenance.results.cached-downloads")
|
||||
Text("maintenance.results.cached-downloads.summary-\(reclaimedSpaceAfterCachePurge.formatted(.byteCount(style: .file)))")
|
||||
.font(.caption)
|
||||
.foregroundColor(Color(nsColor: NSColor.systemGray))
|
||||
}
|
||||
}
|
||||
/*
|
||||
if cachePurgingSkippedPackagesDueToMostRecentVersionsNotBeingInstalled
|
||||
{
|
||||
if packagesHoldingBackCachePurgeTracker.count > 2
|
||||
{
|
||||
|
||||
if shouldPerformHealthCheck
|
||||
{
|
||||
if brewHealthCheckFoundNoProblems
|
||||
{
|
||||
Text("maintenance.results.health-check.problems-none")
|
||||
}
|
||||
else
|
||||
{
|
||||
Text("maintenance.results.health-check.problems")
|
||||
.onAppear
|
||||
{
|
||||
maintenanceFoundNoProblems = false
|
||||
}
|
||||
}
|
||||
Text("maintenance.results.package-cache.skipped-\(packagesHoldingBackCachePurgeTracker[0...1].joined(separator: ", "))-and-\(packagesHoldingBackCachePurgeTracker.count - 2)-others")
|
||||
.font(.caption)
|
||||
.foregroundColor(Color(nsColor: NSColor.systemGray))
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
Text("maintenance.results.package-cache.skipped-\(packagesHoldingBackCachePurgeTracker.joined(separator: ", "))")
|
||||
.font(.caption)
|
||||
.foregroundColor(Color(nsColor: NSColor.systemGray))
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
HStack
|
||||
if shouldDeleteDownloads
|
||||
{
|
||||
Spacer()
|
||||
|
||||
Button
|
||||
VStack(alignment: .leading)
|
||||
{
|
||||
dismiss()
|
||||
|
||||
appState.cachedDownloadsFolderSize = AppConstants.shared.brewCachedDownloadsPath.directorySize
|
||||
} label: {
|
||||
Text("action.close")
|
||||
Text("maintenance.results.cached-downloads")
|
||||
Text("maintenance.results.cached-downloads.summary-\(reclaimedSpaceAfterCachePurge.formatted(.byteCount(style: .file)))")
|
||||
.font(.caption)
|
||||
.foregroundColor(Color(nsColor: NSColor.systemGray))
|
||||
}
|
||||
}
|
||||
|
||||
if shouldPerformHealthCheck
|
||||
{
|
||||
if brewHealthCheckFoundNoProblems
|
||||
{
|
||||
Text("maintenance.results.health-check.problems-none")
|
||||
}
|
||||
else
|
||||
{
|
||||
Text("maintenance.results.health-check.problems")
|
||||
.onAppear
|
||||
{
|
||||
maintenanceFoundNoProblems = false
|
||||
}
|
||||
}
|
||||
.keyboardShortcut(.defaultAction)
|
||||
}
|
||||
}
|
||||
.fixedSize()
|
||||
}
|
||||
.padding()
|
||||
// .frame(minWidth: 300, minHeight: 150)
|
||||
.onAppear // This should stay this way, I don' want the task to be cancelled when the view disappears
|
||||
.task
|
||||
{
|
||||
Task
|
||||
do
|
||||
{
|
||||
await synchronizeInstalledPackages(brewData: brewData)
|
||||
try await brewData.synchronizeInstalledPackages(cachedPackagesTracker: cachedDownloadsTracker)
|
||||
}
|
||||
catch let synchronizationError
|
||||
{
|
||||
appState.showAlert(errorToShow: .couldNotSynchronizePackages(error: synchronizationError.localizedDescription))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ struct MaintenanceRunningView: View
|
|||
@EnvironmentObject var appState: AppState
|
||||
@EnvironmentObject var brewData: BrewDataStorage
|
||||
|
||||
@EnvironmentObject var cachedDownloadsTracker: CachedPackagesTracker
|
||||
|
||||
@State var currentMaintenanceStepText: LocalizedStringKey = "maintenance.step.initial"
|
||||
|
||||
let shouldUninstallOrphans: Bool
|
||||
|
|
@ -31,7 +33,7 @@ struct MaintenanceRunningView: View
|
|||
ProgressView
|
||||
{
|
||||
Text(currentMaintenanceStepText)
|
||||
.task(priority: .userInitiated)
|
||||
.task
|
||||
{
|
||||
if shouldUninstallOrphans
|
||||
{
|
||||
|
|
@ -77,13 +79,28 @@ struct MaintenanceRunningView: View
|
|||
|
||||
currentMaintenanceStepText = "maintenance.step.deleting-cached-downloads"
|
||||
|
||||
deleteCachedDownloads()
|
||||
do throws(CachedDownloadDeletionError)
|
||||
{
|
||||
try deleteCachedDownloads()
|
||||
}
|
||||
catch let cacheDeletionError
|
||||
{
|
||||
switch cacheDeletionError
|
||||
{
|
||||
case .couldNotReadContentsOfCachedFormulaeDownloadsFolder(let associatedError):
|
||||
appState.showAlert(errorToShow: .couldNotDeleteCachedDownloads(error: associatedError))
|
||||
|
||||
case .couldNotReadContentsOfCachedCasksDownloadsFolder(let associatedError):
|
||||
appState.showAlert(errorToShow: .couldNotDeleteCachedDownloads(error: associatedError))
|
||||
|
||||
case .couldNotReadContentsOfCachedDownloadsFolder(let associatedError):
|
||||
appState.showAlert(errorToShow: .couldNotDeleteCachedDownloads(error: associatedError))
|
||||
}
|
||||
}
|
||||
|
||||
/// I have to assign the original value of the appState variable to a different variable, because when it updates at the end of the process, I don't want it to update in the result overview
|
||||
reclaimedSpaceAfterCachePurge = Int(appState.cachedDownloadsFolderSize)
|
||||
reclaimedSpaceAfterCachePurge = Int(cachedDownloadsTracker.cachedDownloadsSize)
|
||||
|
||||
await appState.loadCachedDownloadedPackages()
|
||||
appState.assignPackageTypeToCachedDownloads(brewData: brewData)
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -114,7 +131,5 @@ struct MaintenanceRunningView: View
|
|||
maintenanceSteps = .finished
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.frame(width: 200)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
// Created by David Bureš on 25.02.2023.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import CorkShared
|
||||
import SwiftUI
|
||||
|
||||
struct MaintenanceReadyView: View
|
||||
{
|
||||
|
|
@ -30,91 +30,81 @@ struct MaintenanceReadyView: View
|
|||
|
||||
var body: some View
|
||||
{
|
||||
SheetWithTitle(title: "maintenance.title")
|
||||
VStack(alignment: .leading, spacing: 10)
|
||||
{
|
||||
VStack(alignment: .leading, spacing: 10)
|
||||
Form
|
||||
{
|
||||
Form
|
||||
LabeledContent("maintenance.steps.packages")
|
||||
{
|
||||
LabeledContent("maintenance.steps.packages")
|
||||
VStack(alignment: .leading)
|
||||
{
|
||||
VStack(alignment: .leading)
|
||||
Toggle(isOn: $shouldUninstallOrphans)
|
||||
{
|
||||
Toggle(isOn: $shouldUninstallOrphans)
|
||||
{
|
||||
Text("maintenance.steps.packages.uninstall-orphans")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LabeledContent("maintenance.steps.downloads")
|
||||
{
|
||||
VStack(alignment: .leading)
|
||||
{
|
||||
Toggle(isOn: $shouldPurgeCache)
|
||||
{
|
||||
Text("maintenance.steps.downloads.purge-cache")
|
||||
}
|
||||
Toggle(isOn: $shouldDeleteDownloads)
|
||||
{
|
||||
Text("maintenance.steps.downloads.delete-cached-downloads")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LabeledContent("maintenance.steps.other")
|
||||
{
|
||||
Toggle(isOn: $shouldPerformHealthCheck)
|
||||
{
|
||||
Text("maintenance.steps.other.health-check")
|
||||
Text("maintenance.steps.packages.uninstall-orphans")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if isShowingControlButtons
|
||||
LabeledContent("maintenance.steps.downloads")
|
||||
{
|
||||
HStack
|
||||
VStack(alignment: .leading)
|
||||
{
|
||||
DismissSheetButton()
|
||||
|
||||
Spacer()
|
||||
|
||||
Button
|
||||
Toggle(isOn: $shouldPurgeCache)
|
||||
{
|
||||
AppConstants.shared.logger.debug("Start")
|
||||
|
||||
maintenanceSteps = .maintenanceRunning
|
||||
} label: {
|
||||
Text("maintenance.steps.start")
|
||||
Text("maintenance.steps.downloads.purge-cache")
|
||||
}
|
||||
Toggle(isOn: $shouldDeleteDownloads)
|
||||
{
|
||||
Text("maintenance.steps.downloads.delete-cached-downloads")
|
||||
}
|
||||
.keyboardShortcut(.defaultAction)
|
||||
.disabled(isStartDisabled)
|
||||
}
|
||||
// .padding(.top)
|
||||
}
|
||||
}
|
||||
.onAppear
|
||||
{
|
||||
if !forcedOptions
|
||||
|
||||
LabeledContent("maintenance.steps.other")
|
||||
{
|
||||
/// Replace the provided values with those from AppStorage
|
||||
/// I have to do this because I don't want the settings in the sheet itself to affect those in the defaults
|
||||
shouldUninstallOrphans = default_shouldUninstallOrphans
|
||||
shouldPurgeCache = default_shouldPurgeCache
|
||||
shouldDeleteDownloads = default_shouldDeleteDownloads
|
||||
shouldPerformHealthCheck = default_shouldPerformHealthCheck
|
||||
Toggle(isOn: $shouldPerformHealthCheck)
|
||||
{
|
||||
Text("maintenance.steps.other.health-check")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.if(enablePadding, transform: { viewProxy in
|
||||
viewProxy
|
||||
.padding()
|
||||
})
|
||||
.toolbar
|
||||
{
|
||||
if isShowingControlButtons
|
||||
{
|
||||
ToolbarItem(placement: .primaryAction)
|
||||
{
|
||||
Button
|
||||
{
|
||||
AppConstants.shared.logger.debug("Start")
|
||||
|
||||
maintenanceSteps = .maintenanceRunning
|
||||
} label: {
|
||||
Text("maintenance.steps.start")
|
||||
}
|
||||
.keyboardShortcut(.defaultAction)
|
||||
.disabled(isStartDisabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear
|
||||
{
|
||||
if !forcedOptions
|
||||
{
|
||||
/// Replace the provided values with those from AppStorage
|
||||
/// I have to do this because I don't want the settings in the sheet itself to affect those in the defaults
|
||||
shouldUninstallOrphans = default_shouldUninstallOrphans
|
||||
shouldPurgeCache = default_shouldPurgeCache
|
||||
shouldDeleteDownloads = default_shouldDeleteDownloads
|
||||
shouldPerformHealthCheck = default_shouldPerformHealthCheck
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var isStartDisabled: Bool
|
||||
{
|
||||
[shouldUninstallOrphans, shouldPurgeCache, shouldDeleteDownloads, shouldPerformHealthCheck].allSatisfy
|
||||
[shouldUninstallOrphans, shouldPurgeCache, shouldDeleteDownloads, shouldPerformHealthCheck].allSatisfy
|
||||
{
|
||||
!$0
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ struct MenuBarItem: View
|
|||
@EnvironmentObject var appState: AppState
|
||||
|
||||
@EnvironmentObject var brewData: BrewDataStorage
|
||||
@EnvironmentObject var availableTaps: AvailableTaps
|
||||
@EnvironmentObject var availableTaps: TapTracker
|
||||
|
||||
@EnvironmentObject var outdatedPackageTracker: OutdatedPackageTracker
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
import SwiftUI
|
||||
import CorkShared
|
||||
import CorkNotifications
|
||||
import ButtonKit
|
||||
|
||||
struct MenuBar_CacheCleanup: View
|
||||
{
|
||||
|
|
@ -17,50 +18,49 @@ struct MenuBar_CacheCleanup: View
|
|||
{
|
||||
if !isPurgingHomebrewCache
|
||||
{
|
||||
Button("maintenance.steps.downloads.purge-cache")
|
||||
AsyncButton
|
||||
{
|
||||
Task(priority: .userInitiated)
|
||||
AppConstants.shared.logger.log("Will purge cache")
|
||||
|
||||
isPurgingHomebrewCache = true
|
||||
|
||||
defer
|
||||
{
|
||||
AppConstants.shared.logger.log("Will purge cache")
|
||||
isPurgingHomebrewCache = false
|
||||
}
|
||||
|
||||
isPurgingHomebrewCache = true
|
||||
do
|
||||
{
|
||||
let packagesHoldingBackCachePurge: [String] = try await purgeHomebrewCacheUtility()
|
||||
|
||||
defer
|
||||
if packagesHoldingBackCachePurge.isEmpty
|
||||
{
|
||||
isPurgingHomebrewCache = false
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
let packagesHoldingBackCachePurge: [String] = try await purgeHomebrewCacheUtility()
|
||||
|
||||
if packagesHoldingBackCachePurge.isEmpty
|
||||
{
|
||||
sendNotification(
|
||||
title: String(localized: "maintenance.results.package-cache"),
|
||||
sensitivity: .active
|
||||
)
|
||||
}
|
||||
else
|
||||
{
|
||||
sendNotification(
|
||||
title: String(localized: "maintenance.results.package-cache"),
|
||||
body: String(localized: "maintenance.results.package-cache.skipped-\(packagesHoldingBackCachePurge.formatted(.list(type: .and)))"),
|
||||
sensitivity: .active
|
||||
)
|
||||
}
|
||||
}
|
||||
catch let cachePurgingError
|
||||
{
|
||||
AppConstants.shared.logger.warning("There were errors while purging Homebrew cache: \(cachePurgingError.localizedDescription, privacy: .public)")
|
||||
|
||||
sendNotification(
|
||||
title: String(localized: "maintenance.results.package-cache.failure"),
|
||||
body: String(localized: "maintenance.results.package-cache.failure.details-\(cachePurgingError.localizedDescription)"),
|
||||
title: String(localized: "maintenance.results.package-cache"),
|
||||
sensitivity: .active
|
||||
)
|
||||
}
|
||||
else
|
||||
{
|
||||
sendNotification(
|
||||
title: String(localized: "maintenance.results.package-cache"),
|
||||
body: String(localized: "maintenance.results.package-cache.skipped-\(packagesHoldingBackCachePurge.formatted(.list(type: .and)))"),
|
||||
sensitivity: .active
|
||||
)
|
||||
}
|
||||
}
|
||||
catch let cachePurgingError
|
||||
{
|
||||
AppConstants.shared.logger.warning("There were errors while purging Homebrew cache: \(cachePurgingError.localizedDescription, privacy: .public)")
|
||||
|
||||
sendNotification(
|
||||
title: String(localized: "maintenance.results.package-cache.failure"),
|
||||
body: String(localized: "maintenance.results.package-cache.failure.details-\(cachePurgingError.localizedDescription)"),
|
||||
sensitivity: .active
|
||||
)
|
||||
}
|
||||
} label: {
|
||||
Text("maintenance.steps.downloads.purge-cache")
|
||||
}
|
||||
}
|
||||
else
|
||||
|
|
|
|||
|
|
@ -13,21 +13,40 @@ struct MenuBar_CachedDownloadsCleanup: View
|
|||
{
|
||||
@EnvironmentObject var appState: AppState
|
||||
|
||||
@EnvironmentObject var cachedDownloadsTracker: CachedPackagesTracker
|
||||
|
||||
@State private var isDeletingCachedDownloads: Bool = false
|
||||
|
||||
var body: some View
|
||||
{
|
||||
if !isDeletingCachedDownloads
|
||||
{
|
||||
Button(appState.cachedDownloadsFolderSize != 0 ? "maintenance.steps.downloads.delete-cached-downloads" : "navigation.menu.maintenance.no-cached-downloads")
|
||||
Button(cachedDownloadsTracker.cachedDownloadsSize != 0 ? "maintenance.steps.downloads.delete-cached-downloads" : "navigation.menu.maintenance.no-cached-downloads")
|
||||
{
|
||||
AppConstants.shared.logger.log("Will delete cached downloads")
|
||||
|
||||
isDeletingCachedDownloads = true
|
||||
|
||||
let reclaimedSpaceAfterCachePurge: Int = .init(appState.cachedDownloadsFolderSize)
|
||||
let reclaimedSpaceAfterCachePurge: Int = .init(cachedDownloadsTracker.cachedDownloadsSize)
|
||||
|
||||
deleteCachedDownloads()
|
||||
do throws(CachedDownloadDeletionError)
|
||||
{
|
||||
try deleteCachedDownloads()
|
||||
}
|
||||
catch let cacheDeletionError
|
||||
{
|
||||
switch cacheDeletionError
|
||||
{
|
||||
case .couldNotReadContentsOfCachedFormulaeDownloadsFolder(let associatedError):
|
||||
appState.showAlert(errorToShow: .couldNotDeleteCachedDownloads(error: associatedError))
|
||||
|
||||
case .couldNotReadContentsOfCachedCasksDownloadsFolder(let associatedError):
|
||||
appState.showAlert(errorToShow: .couldNotDeleteCachedDownloads(error: associatedError))
|
||||
|
||||
case .couldNotReadContentsOfCachedDownloadsFolder(let associatedError):
|
||||
appState.showAlert(errorToShow: .couldNotDeleteCachedDownloads(error: associatedError))
|
||||
}
|
||||
}
|
||||
|
||||
sendNotification(
|
||||
title: String(localized: "maintenance.results.cached-downloads"),
|
||||
|
|
@ -36,10 +55,8 @@ struct MenuBar_CachedDownloadsCleanup: View
|
|||
)
|
||||
|
||||
isDeletingCachedDownloads = false
|
||||
|
||||
appState.cachedDownloadsFolderSize = AppConstants.shared.brewCachedDownloadsPath.directorySize
|
||||
}
|
||||
.disabled(appState.cachedDownloadsFolderSize == 0)
|
||||
.disabled(cachedDownloadsTracker.cachedDownloadsSize == 0)
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
|||
|
|
@ -5,13 +5,16 @@
|
|||
// Created by David Bureš on 30.03.2024.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import CorkShared
|
||||
import ButtonKit
|
||||
import CorkNotifications
|
||||
import CorkShared
|
||||
import SwiftUI
|
||||
|
||||
struct MenuBar_OrphanCleanup: View
|
||||
{
|
||||
@EnvironmentObject var appState: AppState
|
||||
@EnvironmentObject var brewData: BrewDataStorage
|
||||
@EnvironmentObject var cachedPackagesTracker: CachedPackagesTracker
|
||||
|
||||
@State private var isUninstallingOrphanedPackages: Bool = false
|
||||
|
||||
|
|
@ -19,35 +22,41 @@ struct MenuBar_OrphanCleanup: View
|
|||
{
|
||||
if !isUninstallingOrphanedPackages
|
||||
{
|
||||
Button("maintenance.steps.packages.uninstall-orphans")
|
||||
AsyncButton
|
||||
{
|
||||
Task(priority: .userInitiated)
|
||||
AppConstants.shared.logger.log("Will delete orphans")
|
||||
|
||||
do
|
||||
{
|
||||
AppConstants.shared.logger.log("Will delete orphans")
|
||||
let orphanUninstallResult: Int = try await uninstallOrphansUtility()
|
||||
|
||||
do
|
||||
{
|
||||
let orphanUninstallResult: Int = try await uninstallOrphansUtility()
|
||||
|
||||
sendNotification(
|
||||
title: String(localized: "maintenance.results.orphans-removed"),
|
||||
body: String(localized: "maintenance.results.orphans-count-\(orphanUninstallResult)"),
|
||||
sensitivity: .active
|
||||
)
|
||||
}
|
||||
catch let orphanUninstallationError
|
||||
{
|
||||
AppConstants.shared.logger.error("Failed while uninstalling orphans: \(orphanUninstallationError, privacy: .public)")
|
||||
|
||||
sendNotification(
|
||||
title: String(localized: "maintenance.results.orphans.failure"),
|
||||
body: String(localized: "maintenance.results.orphans.failure.details-\(orphanUninstallationError.localizedDescription)"),
|
||||
sensitivity: .active
|
||||
)
|
||||
}
|
||||
|
||||
await synchronizeInstalledPackages(brewData: brewData)
|
||||
sendNotification(
|
||||
title: String(localized: "maintenance.results.orphans-removed"),
|
||||
body: String(localized: "maintenance.results.orphans-count-\(orphanUninstallResult)"),
|
||||
sensitivity: .active
|
||||
)
|
||||
}
|
||||
catch let orphanUninstallationError
|
||||
{
|
||||
AppConstants.shared.logger.error("Failed while uninstalling orphans: \(orphanUninstallationError, privacy: .public)")
|
||||
|
||||
sendNotification(
|
||||
title: String(localized: "maintenance.results.orphans.failure"),
|
||||
body: String(localized: "maintenance.results.orphans.failure.details-\(orphanUninstallationError.localizedDescription)"),
|
||||
sensitivity: .active
|
||||
)
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
try await brewData.synchronizeInstalledPackages(cachedPackagesTracker: cachedPackagesTracker)
|
||||
}
|
||||
catch let synchronizationError
|
||||
{
|
||||
appState.showAlert(errorToShow: .couldNotSynchronizePackages(error: synchronizationError.localizedDescription))
|
||||
}
|
||||
} label: {
|
||||
Text("maintenance.steps.packages.uninstall-orphans")
|
||||
}
|
||||
}
|
||||
else
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ struct MenuBar_PackageInstallation: View
|
|||
{
|
||||
openWindow(id: "main")
|
||||
switchCorkToForeground()
|
||||
appState.isShowingInstallationSheet.toggle()
|
||||
appState.showSheet(ofType: .packageInstallation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import SwiftUI
|
|||
struct MenuBar_PackageOverview: View
|
||||
{
|
||||
@EnvironmentObject var brewData: BrewDataStorage
|
||||
@EnvironmentObject var availableTaps: AvailableTaps
|
||||
@EnvironmentObject var availableTaps: TapTracker
|
||||
|
||||
var body: some View
|
||||
{
|
||||
|
|
|
|||
|
|
@ -23,22 +23,29 @@ struct MenuBar_PackageUpdating: View
|
|||
{
|
||||
if !outdatedPackageTracker.displayableOutdatedPackages.isEmpty
|
||||
{
|
||||
if !appState.isShowingUpdateSheet
|
||||
if let sanitizedSheetState = appState.sheetToShow
|
||||
{
|
||||
Menu
|
||||
if sanitizedSheetState != .fullUpdate || sanitizedSheetState != .partialUpdate
|
||||
{
|
||||
ForEach(outdatedPackageTracker.displayableOutdatedPackages.sorted(by: { $0.package.installedOn! < $1.package.installedOn! }))
|
||||
{ outdatedPackage in
|
||||
SanitizedPackageName(packageName: outdatedPackage.package.name, shouldShowVersion: false)
|
||||
Menu
|
||||
{
|
||||
ForEach(outdatedPackageTracker.displayableOutdatedPackages.sorted(by: { $0.package.installedOn! < $1.package.installedOn! }))
|
||||
{ outdatedPackage in
|
||||
SanitizedPackageName(packageName: outdatedPackage.package.name, shouldShowVersion: false)
|
||||
}
|
||||
} label: {
|
||||
Text("notification.outdated-packages-found.body-\(outdatedPackageTracker.displayableOutdatedPackages.count)")
|
||||
}
|
||||
|
||||
Button("navigation.upgrade-packages")
|
||||
{
|
||||
switchCorkToForeground()
|
||||
appState.showSheet(ofType: .fullUpdate)
|
||||
}
|
||||
} label: {
|
||||
Text("notification.outdated-packages-found.body-\(outdatedPackageTracker.displayableOutdatedPackages.count)")
|
||||
}
|
||||
|
||||
Button("navigation.upgrade-packages")
|
||||
else
|
||||
{
|
||||
switchCorkToForeground()
|
||||
appState.isShowingUpdateSheet = true
|
||||
Text("update-packages.detail-stage.pouring")
|
||||
}
|
||||
}
|
||||
else
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ struct PackageDetailView: View, Sendable
|
|||
}
|
||||
}
|
||||
.frame(minWidth: 450, minHeight: 400, alignment: .topLeading)
|
||||
.task(id: package.id, priority: .userInitiated)
|
||||
.task(id: package.id)
|
||||
{
|
||||
isLoadingDetails = true
|
||||
defer
|
||||
|
|
|
|||
|
|
@ -6,9 +6,17 @@
|
|||
//
|
||||
|
||||
import SwiftUI
|
||||
import CorkShared
|
||||
|
||||
struct PackageDetailHeaderComplex: View
|
||||
{
|
||||
enum PackageDependantsDisplayStage: Equatable
|
||||
{
|
||||
case loadingDependants, showingDependants(dependantsToShow: [String]), noDependantsToShow
|
||||
}
|
||||
|
||||
@EnvironmentObject var appState: AppState
|
||||
|
||||
let package: BrewPackage
|
||||
|
||||
var isInPreviewWindow: Bool
|
||||
|
|
@ -16,6 +24,47 @@ struct PackageDetailHeaderComplex: View
|
|||
@ObservedObject var packageDetails: BrewPackageDetails
|
||||
|
||||
let isLoadingDetails: Bool
|
||||
|
||||
@Namespace var packageDependantsAnimationNamespace
|
||||
|
||||
/// Controls whether the pill for showing dependants is shown
|
||||
var packageDependantsDisplayStage: PackageDependantsDisplayStage
|
||||
{
|
||||
if packageDetails.installedAsDependency
|
||||
{
|
||||
if let dependants = packageDetails.dependents
|
||||
{
|
||||
if dependants.isEmpty // This happens when the package was originally installed as a dependency, but the parent is no longer installed
|
||||
{
|
||||
return .noDependantsToShow
|
||||
}
|
||||
else
|
||||
{
|
||||
return .showingDependants(dependantsToShow: dependants)
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return .loadingDependants
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return .noDependantsToShow
|
||||
}
|
||||
}
|
||||
|
||||
var packageDependantsPillColor: Color
|
||||
{
|
||||
switch self.packageDependantsDisplayStage {
|
||||
case .loadingDependants:
|
||||
return .init(nsColor: NSColor.tertiaryLabelColor)
|
||||
case .showingDependants:
|
||||
return .secondary
|
||||
case .noDependantsToShow:
|
||||
return .clear
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View
|
||||
{
|
||||
|
|
@ -46,36 +95,15 @@ struct PackageDetailHeaderComplex: View
|
|||
{
|
||||
if !isInPreviewWindow
|
||||
{
|
||||
if packageDetails.installedAsDependency
|
||||
{
|
||||
if let packageDependents = packageDetails.dependents
|
||||
{
|
||||
if !packageDependents.isEmpty // This happens when the package was originally installed as a dependency, but the parent is no longer installed
|
||||
{
|
||||
OutlinedPillText(text: "package-details.dependants.dependency-of-\(packageDependents.formatted(.list(type: .and)))", color: .secondary)
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
OutlinedPill(content: {
|
||||
HStack(alignment: .center, spacing: 5)
|
||||
{
|
||||
ProgressView()
|
||||
.controlSize(.mini)
|
||||
|
||||
Text("package-details.dependants.loading")
|
||||
}
|
||||
}, color: Color(nsColor: NSColor.tertiaryLabelColor))
|
||||
}
|
||||
}
|
||||
if packageDetails.outdated
|
||||
{
|
||||
OutlinedPillText(text: "package-details.outdated", color: .orange)
|
||||
}
|
||||
|
||||
dependantsPill
|
||||
|
||||
packageDetailsPill
|
||||
}
|
||||
|
||||
PackageCaveatMinifiedDisplayView(caveats: packageDetails.caveats)
|
||||
}
|
||||
.animation(appState.enableExtraAnimations ? .interpolatingSpring : .none, value: packageDependantsDisplayStage)
|
||||
|
||||
if !isLoadingDetails
|
||||
{
|
||||
|
|
@ -92,4 +120,41 @@ struct PackageDetailHeaderComplex: View
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var dependantsPill: some View
|
||||
{
|
||||
if packageDetails.installedAsDependency
|
||||
{
|
||||
|
||||
OutlinedPill(content: {
|
||||
switch packageDependantsDisplayStage
|
||||
{
|
||||
case .loadingDependants:
|
||||
HStack(alignment: .center, spacing: 5)
|
||||
{
|
||||
ProgressView()
|
||||
.controlSize(.mini)
|
||||
|
||||
Text("package-details.dependants.loading")
|
||||
.matchedGeometryEffect(id: "dependantsPillContents", in: packageDependantsAnimationNamespace)
|
||||
}
|
||||
case .showingDependants(let dependantsToShow):
|
||||
Text("package-details.dependants.dependency-of-\(dependantsToShow.formatted(.list(type: .and)))")
|
||||
.matchedGeometryEffect(id: "dependantsPillContents", in: packageDependantsAnimationNamespace)
|
||||
case .noDependantsToShow:
|
||||
EmptyView()
|
||||
}
|
||||
}, color: packageDependantsPillColor)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var packageDetailsPill: some View
|
||||
{
|
||||
if packageDetails.outdated
|
||||
{
|
||||
OutlinedPillText(text: "package-details.outdated", color: .orange)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import SwiftUI
|
||||
import CorkShared
|
||||
import ButtonKit
|
||||
|
||||
struct PackageModificationButtons: View
|
||||
{
|
||||
|
|
@ -15,14 +16,13 @@ struct PackageModificationButtons: View
|
|||
|
||||
@EnvironmentObject var brewData: BrewDataStorage
|
||||
@EnvironmentObject var appState: AppState
|
||||
@EnvironmentObject var cachedPackagesTracker: CachedPackagesTracker
|
||||
@EnvironmentObject var outdatedPackageTracker: OutdatedPackageTracker
|
||||
@EnvironmentObject var uninstallationConfirmationTracker: UninstallationConfirmationTracker
|
||||
|
||||
let package: BrewPackage
|
||||
@ObservedObject var packageDetails: BrewPackageDetails
|
||||
|
||||
@State private var isPinning: Bool = false
|
||||
|
||||
let isLoadingDetails: Bool
|
||||
|
||||
var body: some View
|
||||
|
|
@ -35,45 +35,21 @@ struct PackageModificationButtons: View
|
|||
{
|
||||
if package.type == .formula
|
||||
{
|
||||
Button
|
||||
AsyncButton
|
||||
{
|
||||
Task
|
||||
do
|
||||
{
|
||||
withAnimation
|
||||
{
|
||||
isPinning = true
|
||||
}
|
||||
|
||||
defer
|
||||
{
|
||||
withAnimation
|
||||
{
|
||||
isPinning = false
|
||||
}
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
try await packageDetails.changePinnedStatus()
|
||||
}
|
||||
catch let pinningUnpinningError
|
||||
{
|
||||
AppConstants.shared.logger.error("Failed while pinning/unpinning package \(package.name): \(pinningUnpinningError)")
|
||||
}
|
||||
try await packageDetails.changePinnedStatus()
|
||||
}
|
||||
catch let pinningUnpinningError
|
||||
{
|
||||
AppConstants.shared.logger.error("Failed while pinning/unpinning package \(package.name): \(pinningUnpinningError)")
|
||||
}
|
||||
} label: {
|
||||
HStack(alignment: .center, spacing: 5)
|
||||
{
|
||||
if isPinning
|
||||
{
|
||||
ProgressView()
|
||||
.controlSize(.mini)
|
||||
.transition(.move(edge: .leading).combined(with: .opacity))
|
||||
}
|
||||
Text(packageDetails.pinned ? "package-details.action.unpin-version-\(package.versions.formatted(.list(type: .and)))" : "package-details.action.pin-version-\(package.versions.formatted(.list(type: .and)))")
|
||||
}
|
||||
Text(packageDetails.pinned ? "package-details.action.unpin-version-\(package.versions.formatted(.list(type: .and)))" : "package-details.action.pin-version-\(package.versions.formatted(.list(type: .and)))")
|
||||
}
|
||||
.disabled(isPinning)
|
||||
.asyncButtonStyle(.leading)
|
||||
.disabledWhenLoading()
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
|
@ -108,6 +84,7 @@ struct PackageModificationButtons: View
|
|||
|
||||
try await brewData.uninstallSelectedPackage(
|
||||
package: package,
|
||||
cachedPackagesTracker: cachedPackagesTracker,
|
||||
appState: appState,
|
||||
outdatedPackageTracker: outdatedPackageTracker,
|
||||
shouldRemoveAllAssociatedFiles: false,
|
||||
|
|
@ -122,7 +99,6 @@ struct PackageModificationButtons: View
|
|||
}
|
||||
}
|
||||
.fixedSize()
|
||||
.disabled(isPinning)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,8 +11,11 @@ import CorkShared
|
|||
|
||||
struct ReinstallCorruptedPackageView: View
|
||||
{
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
|
||||
@EnvironmentObject var appState: AppState
|
||||
@EnvironmentObject var brewData: BrewDataStorage
|
||||
@EnvironmentObject var cachedPackagesTracker: CachedPackagesTracker
|
||||
|
||||
let corruptedPackageToReinstall: CorruptedPackage
|
||||
|
||||
|
|
@ -20,41 +23,69 @@ struct ReinstallCorruptedPackageView: View
|
|||
|
||||
var body: some View
|
||||
{
|
||||
switch corruptedPackageReinstallationStage
|
||||
NavigationStack
|
||||
{
|
||||
case .installing:
|
||||
ProgressView
|
||||
switch corruptedPackageReinstallationStage
|
||||
{
|
||||
Text("repair-package.repair-process-\(corruptedPackageToReinstall.name)")
|
||||
}
|
||||
.progressViewStyle(.linear)
|
||||
.padding()
|
||||
.task(priority: .userInitiated)
|
||||
{
|
||||
let reinstallationResult: TerminalOutput = await shell(AppConstants.shared.brewExecutablePath, ["reinstall", corruptedPackageToReinstall.name])
|
||||
AppConstants.shared.logger.debug("Reinstallation result:\nStandard output: \(reinstallationResult.standardOutput, privacy: .public)\nStandard error:\(reinstallationResult.standardError, privacy: .public)")
|
||||
|
||||
corruptedPackageReinstallationStage = .finished
|
||||
}
|
||||
|
||||
case .finished:
|
||||
DisappearableSheet
|
||||
{
|
||||
ComplexWithIcon(systemName: "checkmark.seal")
|
||||
case .installing:
|
||||
ProgressView
|
||||
{
|
||||
HeadlineWithSubheadline(
|
||||
headline: "repair-package.repairing-finished.headline-\(corruptedPackageToReinstall.name)",
|
||||
subheadline: "repair-package.repairing-finished.subheadline",
|
||||
alignment: .leading
|
||||
)
|
||||
VStack(alignment: .leading)
|
||||
{
|
||||
Text("repair-package.repair-process-\(corruptedPackageToReinstall.name)")
|
||||
|
||||
SubtitleText(text: "repair-package.repair-length.explanation")
|
||||
}
|
||||
}
|
||||
.task(priority: .background)
|
||||
.progressViewStyle(.linear)
|
||||
.padding()
|
||||
.toolbar
|
||||
{
|
||||
await synchronizeInstalledPackages(brewData: brewData)
|
||||
ToolbarItem(placement: .cancellationAction)
|
||||
{
|
||||
Button
|
||||
{
|
||||
dismiss()
|
||||
} label: {
|
||||
Text("action.cancel")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
.task
|
||||
{
|
||||
let reinstallationResult: TerminalOutput = await shell(AppConstants.shared.brewExecutablePath, ["reinstall", corruptedPackageToReinstall.name])
|
||||
AppConstants.shared.logger.debug("Reinstallation result:\nStandard output: \(reinstallationResult.standardOutput, privacy: .public)\nStandard error:\(reinstallationResult.standardError, privacy: .public)")
|
||||
|
||||
corruptedPackageReinstallationStage = .finished
|
||||
}
|
||||
|
||||
case .finished:
|
||||
DisappearableSheet
|
||||
{
|
||||
ComplexWithIcon(systemName: "checkmark.seal")
|
||||
{
|
||||
HeadlineWithSubheadline(
|
||||
headline: "repair-package.repairing-finished.headline-\(corruptedPackageToReinstall.name)",
|
||||
subheadline: "repair-package.repairing-finished.subheadline",
|
||||
alignment: .leading
|
||||
)
|
||||
}
|
||||
.task
|
||||
{
|
||||
do
|
||||
{
|
||||
try await brewData.synchronizeInstalledPackages(cachedPackagesTracker: cachedPackagesTracker)
|
||||
}
|
||||
catch let synchronizationError
|
||||
{
|
||||
appState.showAlert(errorToShow: .couldNotSynchronizePackages(error: synchronizationError.localizedDescription))
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.fixedSize()
|
||||
}
|
||||
.padding()
|
||||
.fixedSize()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
//
|
||||
|
||||
import SwiftUI
|
||||
import ButtonKit
|
||||
|
||||
struct SudoRequiredForRemovalSheet: View, Sendable
|
||||
{
|
||||
|
|
@ -13,6 +14,7 @@ struct SudoRequiredForRemovalSheet: View, Sendable
|
|||
|
||||
@EnvironmentObject var brewData: BrewDataStorage
|
||||
@EnvironmentObject var appState: AppState
|
||||
@EnvironmentObject var cachedPackagesTracker: CachedPackagesTracker
|
||||
|
||||
var body: some View
|
||||
{
|
||||
|
|
@ -38,18 +40,23 @@ struct SudoRequiredForRemovalSheet: View, Sendable
|
|||
|
||||
HStack
|
||||
{
|
||||
Button
|
||||
AsyncButton
|
||||
{
|
||||
dismiss()
|
||||
|
||||
Task.detached
|
||||
do
|
||||
{
|
||||
await synchronizeInstalledPackages(brewData: brewData)
|
||||
try await brewData.synchronizeInstalledPackages(cachedPackagesTracker: cachedPackagesTracker)
|
||||
}
|
||||
catch let synchronizationError
|
||||
{
|
||||
appState.showAlert(errorToShow: .couldNotSynchronizePackages(error: synchronizationError.localizedDescription))
|
||||
}
|
||||
} label: {
|
||||
Text("action.close")
|
||||
}
|
||||
.keyboardShortcut(.cancelAction)
|
||||
.asyncButtonStyle(.plainStyle)
|
||||
|
||||
Spacer()
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
//
|
||||
// Reinstall Homebrew.swift
|
||||
// Cork
|
||||
//
|
||||
// Created by David Bureš - P on 18.01.2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ReinstallHomebrewButton: View
|
||||
{
|
||||
var body: some View
|
||||
{
|
||||
ButtonThatOpensWebsites(websiteURL: URL(string: "https://github.com/homebrew/install?tab=readme-ov-file#uninstall-homebrew")!, buttonText: "action.reinstall-homebrew")
|
||||
}
|
||||
}
|
||||
|
|
@ -110,3 +110,25 @@ struct GroupBoxHeadlineGroupWithArbitraryContent<Content: View>: View
|
|||
.padding(2)
|
||||
}
|
||||
}
|
||||
|
||||
struct GroupBoxHeadlineGroupWithArbitraryImageAndContent<Content: View>: View
|
||||
{
|
||||
var imageName: String?
|
||||
|
||||
@ViewBuilder var content: Content
|
||||
var body: some View {
|
||||
HStack(alignment: .top, spacing: 15)
|
||||
{
|
||||
if let imageName
|
||||
{
|
||||
Image(imageName)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 26, height: 26)
|
||||
}
|
||||
|
||||
content
|
||||
}
|
||||
.padding(2)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,3 +34,29 @@ struct NoContentAvailableView: View
|
|||
.fillAvailableSpace()
|
||||
}
|
||||
}
|
||||
|
||||
struct NoContentAvailableViewWithArbitraryImage: View
|
||||
{
|
||||
let title: LocalizedStringKey
|
||||
let image: String
|
||||
let description: Text? = nil
|
||||
|
||||
var body: some View
|
||||
{
|
||||
VStack(alignment: .center, spacing: 10)
|
||||
{
|
||||
Image(image)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 50, height: 50)
|
||||
|
||||
Text(title)
|
||||
.font(.title)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
description
|
||||
}
|
||||
.foregroundColor(.gray)
|
||||
.fillAvailableSpace()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import SwiftUI
|
|||
|
||||
struct DisappearableSheet<Content: View>: View
|
||||
{
|
||||
@EnvironmentObject var appState: AppState
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
|
||||
@ViewBuilder var sheetContent: Content
|
||||
|
|
@ -20,7 +21,7 @@ struct DisappearableSheet<Content: View>: View
|
|||
{
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 3)
|
||||
{
|
||||
dismiss()
|
||||
appState.dismissSheet()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue