This commit is contained in:
Alin
2024-04-17 17:36:04 -06:00
parent b9a60f5977
commit 390e23e31d
24 changed files with 1097 additions and 529 deletions
+16
View File
@@ -16,11 +16,13 @@
C72893052AFD42E600C8C1CD /* DeepLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = C72893042AFD42E600C8C1CD /* DeepLink.swift */; }; C72893052AFD42E600C8C1CD /* DeepLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = C72893042AFD42E600C8C1CD /* DeepLink.swift */; };
C72893122AFD51EA00C8C1CD /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = C72893112AFD51EA00C8C1CD /* main.swift */; }; C72893122AFD51EA00C8C1CD /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = C72893112AFD51EA00C8C1CD /* main.swift */; };
C74412C72BBF249000DDFCA8 /* AppPathsFetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C74412C62BBF249000DDFCA8 /* AppPathsFetch.swift */; }; C74412C72BBF249000DDFCA8 /* AppPathsFetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C74412C62BBF249000DDFCA8 /* AppPathsFetch.swift */; };
C74FDC632BCD833D00B8960F /* Conditions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C74FDC622BCD833D00B8960F /* Conditions.swift */; };
C7575D392B0182DE006A600A /* PearcleanerSentinel in CopyFiles */ = {isa = PBXBuildFile; fileRef = C728930F2AFD51EA00C8C1CD /* PearcleanerSentinel */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; C7575D392B0182DE006A600A /* PearcleanerSentinel in CopyFiles */ = {isa = PBXBuildFile; fileRef = C728930F2AFD51EA00C8C1CD /* PearcleanerSentinel */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
C76D08482AF83C3F00D07867 /* Update.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76D08472AF83C3F00D07867 /* Update.swift */; }; C76D08482AF83C3F00D07867 /* Update.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76D08472AF83C3F00D07867 /* Update.swift */; };
C76D084A2AF83C6E00D07867 /* General.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76D08492AF83C6E00D07867 /* General.swift */; }; C76D084A2AF83C6E00D07867 /* General.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76D08492AF83C6E00D07867 /* General.swift */; };
C76D08512AF850F300D07867 /* About.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76D08502AF850F300D07867 /* About.swift */; }; C76D08512AF850F300D07867 /* About.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76D08502AF850F300D07867 /* About.swift */; };
C76D08552AF89CDE00D07867 /* RegularMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76D08542AF89CDE00D07867 /* RegularMode.swift */; }; C76D08552AF89CDE00D07867 /* RegularMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76D08542AF89CDE00D07867 /* RegularMode.swift */; };
C771CB762BD028AF00C33A18 /* SegmentedPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = C771CB752BD028AF00C33A18 /* SegmentedPicker.swift */; };
C77B90042AF18E2E009CC655 /* PearcleanerApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C77B90032AF18E2E009CC655 /* PearcleanerApp.swift */; }; C77B90042AF18E2E009CC655 /* PearcleanerApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C77B90032AF18E2E009CC655 /* PearcleanerApp.swift */; };
C77B90082AF18E2F009CC655 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C77B90072AF18E2F009CC655 /* Assets.xcassets */; }; C77B90082AF18E2F009CC655 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C77B90072AF18E2F009CC655 /* Assets.xcassets */; };
C77B90162AF19377009CC655 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = C77B90152AF19377009CC655 /* AppState.swift */; }; C77B90162AF19377009CC655 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = C77B90152AF19377009CC655 /* AppState.swift */; };
@@ -106,10 +108,12 @@
C728930F2AFD51EA00C8C1CD /* PearcleanerSentinel */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = PearcleanerSentinel; sourceTree = BUILT_PRODUCTS_DIR; }; C728930F2AFD51EA00C8C1CD /* PearcleanerSentinel */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = PearcleanerSentinel; sourceTree = BUILT_PRODUCTS_DIR; };
C72893112AFD51EA00C8C1CD /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; }; C72893112AFD51EA00C8C1CD /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
C74412C62BBF249000DDFCA8 /* AppPathsFetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPathsFetch.swift; sourceTree = "<group>"; }; C74412C62BBF249000DDFCA8 /* AppPathsFetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPathsFetch.swift; sourceTree = "<group>"; };
C74FDC622BCD833D00B8960F /* Conditions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Conditions.swift; sourceTree = "<group>"; };
C76D08472AF83C3F00D07867 /* Update.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Update.swift; sourceTree = "<group>"; }; C76D08472AF83C3F00D07867 /* Update.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Update.swift; sourceTree = "<group>"; };
C76D08492AF83C6E00D07867 /* General.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = General.swift; sourceTree = "<group>"; }; C76D08492AF83C6E00D07867 /* General.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = General.swift; sourceTree = "<group>"; };
C76D08502AF850F300D07867 /* About.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = About.swift; sourceTree = "<group>"; }; C76D08502AF850F300D07867 /* About.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = About.swift; sourceTree = "<group>"; };
C76D08542AF89CDE00D07867 /* RegularMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegularMode.swift; sourceTree = "<group>"; }; C76D08542AF89CDE00D07867 /* RegularMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegularMode.swift; sourceTree = "<group>"; };
C771CB752BD028AF00C33A18 /* SegmentedPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentedPicker.swift; sourceTree = "<group>"; };
C77B90002AF18E2E009CC655 /* Pearcleaner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Pearcleaner.app; sourceTree = BUILT_PRODUCTS_DIR; }; C77B90002AF18E2E009CC655 /* Pearcleaner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Pearcleaner.app; sourceTree = BUILT_PRODUCTS_DIR; };
C77B90032AF18E2E009CC655 /* PearcleanerApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PearcleanerApp.swift; sourceTree = "<group>"; }; C77B90032AF18E2E009CC655 /* PearcleanerApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PearcleanerApp.swift; sourceTree = "<group>"; };
C77B90072AF18E2F009CC655 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; C77B90072AF18E2F009CC655 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
@@ -197,6 +201,7 @@
C72893042AFD42E600C8C1CD /* DeepLink.swift */, C72893042AFD42E600C8C1CD /* DeepLink.swift */,
C77B901B2AF193A3009CC655 /* Styles.swift */, C77B901B2AF193A3009CC655 /* Styles.swift */,
C7D31D502AFF00F300C7ED9E /* Locations.swift */, C7D31D502AFF00F300C7ED9E /* Locations.swift */,
C74FDC622BCD833D00B8960F /* Conditions.swift */,
C7A9CE462B89164700EB6E78 /* Authorization.swift */, C7A9CE462B89164700EB6E78 /* Authorization.swift */,
C7ACADE52B92A737000B5845 /* Features.swift */, C7ACADE52B92A737000B5845 /* Features.swift */,
C7ED86B22BA8ED8800F13DE4 /* Trash.swift */, C7ED86B22BA8ED8800F13DE4 /* Trash.swift */,
@@ -246,6 +251,14 @@
path = Resources; path = Resources;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
C771CB742BD028A000C33A18 /* Picker */ = {
isa = PBXGroup;
children = (
C771CB752BD028AF00C33A18 /* SegmentedPicker.swift */,
);
path = Picker;
sourceTree = "<group>";
};
C77B8FF72AF18E2E009CC655 = { C77B8FF72AF18E2E009CC655 = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -283,6 +296,7 @@
C77B901F2AF1B390009CC655 /* Views */ = { C77B901F2AF1B390009CC655 /* Views */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
C771CB742BD028A000C33A18 /* Picker */,
C77B90222AF2D616009CC655 /* FilesView.swift */, C77B90222AF2D616009CC655 /* FilesView.swift */,
C71848432B8D2D600046CB13 /* ZombieView.swift */, C71848432B8D2D600046CB13 /* ZombieView.swift */,
C76D08542AF89CDE00D07867 /* RegularMode.swift */, C76D08542AF89CDE00D07867 /* RegularMode.swift */,
@@ -471,9 +485,11 @@
C77B90182AF19382009CC655 /* AppCommands.swift in Sources */, C77B90182AF19382009CC655 /* AppCommands.swift in Sources */,
C7D31D4A2AFEB26700C7ED9E /* AppListItems.swift in Sources */, C7D31D4A2AFEB26700C7ED9E /* AppListItems.swift in Sources */,
C77B901E2AF1A9DB009CC655 /* SettingsWindow.swift in Sources */, C77B901E2AF1A9DB009CC655 /* SettingsWindow.swift in Sources */,
C771CB762BD028AF00C33A18 /* SegmentedPicker.swift in Sources */,
C74412C72BBF249000DDFCA8 /* AppPathsFetch.swift in Sources */, C74412C72BBF249000DDFCA8 /* AppPathsFetch.swift in Sources */,
C79D05202BC99EB40083F976 /* ThemeManager.swift in Sources */, C79D05202BC99EB40083F976 /* ThemeManager.swift in Sources */,
C7D31D482AFEB23700C7ED9E /* TopBar.swift in Sources */, C7D31D482AFEB23700C7ED9E /* TopBar.swift in Sources */,
C74FDC632BCD833D00B8960F /* Conditions.swift in Sources */,
C7ACADE62B92A737000B5845 /* Features.swift in Sources */, C7ACADE62B92A737000B5845 /* Features.swift in Sources */,
C7F539382AF60865007DF1B2 /* Utilities.swift in Sources */, C7F539382AF60865007DF1B2 /* Utilities.swift in Sources */,
C76D08552AF89CDE00D07867 /* RegularMode.swift in Sources */, C76D08552AF89CDE00D07867 /* RegularMode.swift in Sources */,
+1 -11
View File
@@ -8,7 +8,6 @@
import SwiftUI import SwiftUI
struct AppCommands: Commands { struct AppCommands: Commands {
@AppStorage("settings.general.instant") private var instantSearch: Bool = true
let appState: AppState let appState: AppState
let locations: Locations let locations: Locations
@@ -42,17 +41,9 @@ struct AppCommands: Commands {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
appState.sortedApps = [] appState.sortedApps = []
appState.sortedApps = sortedApps appState.sortedApps = sortedApps
if instantSearch {
Task(priority: .high){
loadAllPaths(allApps: sortedApps, appState: appState, locations: locations) {
appState.reload.toggle() appState.reload.toggle()
} }
} }
} else {
appState.reload.toggle()
}
}
}
} label: { } label: {
Text("Refresh Apps") Text("Refresh Apps")
} }
@@ -78,8 +69,7 @@ struct AppCommands: Commands {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
appState.sortedApps = sortedApps appState.sortedApps = sortedApps
for app in appState.trashedFiles { for app in appState.trashedFiles {
let pathFinder = AppPathFinder(appInfo: app, appState: appState, locations: locations) AppPathFinder(appInfo: app, appState: appState, locations: locations).findPaths()
pathFinder.findPaths()
} }
} }
} }
+1 -1
View File
@@ -62,7 +62,7 @@ class AppInfoFetcher {
let system = !path.path.contains(NSHomeDirectory()) let system = !path.path.contains(NSHomeDirectory())
return AppInfo(id: UUID(), path: path, bundleIdentifier: bundleIdentifier, appName: appName, appVersion: appVersion, appIcon: appIcon, return AppInfo(id: UUID(), path: path, bundleIdentifier: bundleIdentifier, appName: appName, appVersion: appVersion, appIcon: appIcon,
webApp: webApp, wrapped: wrapped, system: system, files: [], fileSize: [:], fileIcon: [:]) webApp: webApp, wrapped: wrapped, system: system, files: [], fileSize: [:], fileSizeLogical: [:], fileIcon: [:])
} }
private static func fetchAppIcon(for path: URL, wrapped: Bool) -> NSImage? { private static func fetchAppIcon(for path: URL, wrapped: Bool) -> NSImage? {
+360 -98
View File
@@ -9,6 +9,7 @@ import Foundation
import AppKit import AppKit
import SwiftUI import SwiftUI
class AppPathFinder { class AppPathFinder {
private var appInfo: AppInfo private var appInfo: AppInfo
private var appState: AppState private var appState: AppState
@@ -18,7 +19,6 @@ class AppPathFinder {
private var completion: () -> Void = {} private var completion: () -> Void = {}
private var collection: [URL] = [] private var collection: [URL] = []
private let collectionAccessQueue = DispatchQueue(label: "com.alienator88.Pearcleaner.appPathFinder.collectionAccess") private let collectionAccessQueue = DispatchQueue(label: "com.alienator88.Pearcleaner.appPathFinder.collectionAccess")
@AppStorage("settings.general.instant") var instantSearch: Bool = true
init(appInfo: AppInfo = .empty, appState: AppState, locations: Locations, backgroundRun: Bool = false, reverseAddon: Bool = false, completion: @escaping () -> Void = {}) { init(appInfo: AppInfo = .empty, appState: AppState, locations: Locations, backgroundRun: Bool = false, reverseAddon: Bool = false, completion: @escaping () -> Void = {}) {
self.appInfo = appInfo self.appInfo = appInfo
@@ -124,10 +124,21 @@ class AppPathFinder {
collectionAccessQueue.sync { collectionAccessQueue.sync {
containsItem = self.collection.contains(itemURL) containsItem = self.collection.contains(itemURL)
} }
return itemL.hasPrefix("comapple") && !["comappleconfigurator", "comappledt", "comappleiwork", "comapplesfsymbols", "comappletestflight"].contains(where: itemL.hasPrefix) || containsItem || !isSupportedFileType(at: itemURL.path) if containsItem || !isSupportedFileType(at: itemURL.path) {
return true
} }
for skipCondition in skipConditions {
if itemL.hasPrefix(skipCondition.skipPrefix) {
let isAllowed = skipCondition.allowPrefixes.contains(where: itemL.hasPrefix)
if !isAllowed {
return true // Skip because it starts with a base prefix but is not in the allowed list
}
}
}
return false
}
@@ -138,85 +149,20 @@ class AppPathFinder {
let nameL = self.appInfo.appName.pearFormat() let nameL = self.appInfo.appName.pearFormat()
let nameP = self.appInfo.path.lastPathComponent.replacingOccurrences(of: ".app", with: "") let nameP = self.appInfo.path.lastPathComponent.replacingOccurrences(of: ".app", with: "")
let exclusions = [ for condition in conditions {
"comappledtxcode": ["comrobotsandpencilsxcodesapp", "comoneminutegamesxcodecleaner", "iohyperappxcodecleaner", "xcodesjson"], if bundleIdentifierL.contains(condition.bundle_id) {
"comrobotsandpencilsxcodesapp": ["comappledtxcode", "comoneminutegamesxcodecleaner", "iohyperappxcodecleaner"], // Exclude and include keywords
"iohyperappxcodecleaner": ["comrobotsandpencilsxcodesapp", "comoneminutegamesxcodecleaner", "comappledtxcode", "xcodesjson"] let hasIncludeKeyword = condition.include.contains(where: itemL.contains)
] let hasExcludeKeyword = condition.exclude.contains(where: itemL.contains)
if let excludedApps = exclusions[bundleIdentifierL], excludedApps.contains(where: { itemL.contains($0) }) { if hasExcludeKeyword {
return false return false
} }
if hasIncludeKeyword {
if bundleIdentifierL.contains("comappledtxcode") { if !condition.exclude.contains(where: itemL.contains) {
// Include items that are part of the Xcode ecosystem
if itemL.contains("comappledt") || itemL.contains("xcode") || itemL.contains("simulator") {
return true return true
} }
} }
if bundleIdentifierL.contains("uszoomxos") {
// Include items for Zoom that are not similar to the app name and/or bundle id
if itemL.contains("zoom") {
return true
}
}
if bundleIdentifierL.contains("combravebrowser") {
// Include items for Brave that are not similar to the app name and/or bundle id
if itemL.contains("brave") {
return true
}
}
if bundleIdentifierL.contains("comgooglechrome") {
// Include items for Chrome that are not similar to the app name and/or bundle id
if (itemL.contains("google") && !itemL.contains("iterm")) || (itemL.contains("chrome") && !itemL.contains("chromefeaturestate")) {
return true
}
}
if bundleIdentifierL.contains("commicrosoftedgemac") {
// Include items for Edge that are not similar to the app name and/or bundle id
let exclusions = ["vscode","rdc","appcenter","office","oneauth"]
if itemL.contains("microsoft") && !exclusions.contains(where: itemL.contains) {
return true
}
}
if bundleIdentifierL.contains("orgmozillafirefox") {
// Include items for Firefox that are not similar to the app name and/or bundle id
if itemL.contains("mozilla") {
return true
}
}
if bundleIdentifierL.contains("comlogioptionsplus") {
// Include items for Zoom that are not similar to the app name and/or bundle id
if itemL.contains("logi") && !itemL.contains("login") && !itemL.contains("logic") {
return true
}
}
if bundleIdentifierL.contains("commicrosoftvscode") {
// Include items for vscode
if itemL.contains("vscode") {
return true
}
}
if bundleIdentifierL.contains("comfacebookarchondeveloperid") {
// Include items for FB Messenger
if itemL.contains("archonloginhelper") {
return true
}
}
if bundleIdentifierL.contains("euexelbanstats") {
// Include items for Stats that are not similar to the app name and/or bundle id
if itemL.contains("video") {
return false
} }
} }
@@ -263,37 +209,35 @@ class AppPathFinder {
private func handleOutliers() -> [URL] { private func handleOutliers() -> [URL] {
var outliers: [URL] = [] var outliers: [URL] = []
let bundleIdentifier = self.appInfo.bundleIdentifier.pearFormat()
// Handle VSCode folder // Find conditions that match the current app's bundle identifier
if self.appInfo.bundleIdentifier.pearFormat().contains("commicrosoftvscode") { let matchingConditions = conditions.filter { condition in
let appSupportCodePath = "\(home)/Library/Application Support/Code" bundleIdentifier.contains(condition.bundle_id)
if let codeDirectoryURL = URL(string: appSupportCodePath), FileManager.default.fileExists(atPath: codeDirectoryURL.path) {
outliers.append(codeDirectoryURL)
}
} }
// Handle Xcode folder for condition in matchingConditions {
if self.appInfo.bundleIdentifier.pearFormat().contains("comappledtxcode") { if let forceIncludes = condition.includeForce {
let simulators = "\(home)/Library/Containers/com.apple.iphonesimulator.ShareExtension" for path in forceIncludes {
if let simulatorsURL = URL(string: simulators), FileManager.default.fileExists(atPath: simulatorsURL.path) { if let url = URL(string: path), FileManager.default.fileExists(atPath: url.path) {
outliers.append(simulatorsURL) outliers.append(url)
}
}
} }
} }
// Add other outlier cases here as needed
return outliers return outliers
} }
private func finalizeCollection() { private func finalizeCollection() {
DispatchQueue.global(qos: .userInitiated).async { DispatchQueue.global(qos: .userInitiated).async {
let groupContainers = self.getGroupContainers(bundleURL: self.appInfo.path) // let groupContainers = self.getGroupContainers(bundleURL: self.appInfo.path)
let outliers = self.handleOutliers() let outliers = self.handleOutliers()
var tempCollection: [URL] = [] var tempCollection: [URL] = []
self.collectionAccessQueue.sync { self.collectionAccessQueue.sync {
tempCollection = self.collection tempCollection = self.collection
} }
tempCollection.append(contentsOf: groupContainers) // tempCollection.append(contentsOf: groupContainers)
tempCollection.append(contentsOf: outliers) tempCollection.append(contentsOf: outliers)
// Sort and standardize URLs to ensure consistent comparisons // Sort and standardize URLs to ensure consistent comparisons
@@ -318,10 +262,13 @@ class AppPathFinder {
private func handlePostProcessing(sortedCollection: [URL]) { private func handlePostProcessing(sortedCollection: [URL]) {
// Calculate file details (sizes and icons), update app state, and call completion // Calculate file details (sizes and icons), update app state, and call completion
var fileSize: [URL: Int64] = [:] var fileSize: [URL: Int64] = [:]
var fileSizeLogical: [URL: Int64] = [:]
var fileIcon: [URL: NSImage?] = [:] var fileIcon: [URL: NSImage?] = [:]
for path in sortedCollection { for path in sortedCollection {
fileSize[path] = totalSizeOnDisk(for: path) let size = totalSizeOnDisk(for: path)
fileSize[path] = size.real
fileSizeLogical[path] = size.logical
fileIcon[path] = getIconForFileOrFolderNS(atPath: path) fileIcon[path] = getIconForFileOrFolderNS(atPath: path)
} }
@@ -332,8 +279,8 @@ class AppPathFinder {
} }
// Update appInfo and appState with the new values // Update appInfo and appState with the new values
self.appInfo.files = updatedCollection
self.appInfo.fileSize = fileSize self.appInfo.fileSize = fileSize
self.appInfo.fileSizeLogical = fileSizeLogical
self.appInfo.fileIcon = fileIcon self.appInfo.fileIcon = fileIcon
if !self.backgroundRun { if !self.backgroundRun {
@@ -341,12 +288,6 @@ class AppPathFinder {
self.appState.selectedItems = Set(updatedCollection) self.appState.selectedItems = Set(updatedCollection)
} }
// Only append object to store if instant search. Same for calculating progress.
if self.instantSearch {
self.appState.appInfoStore.append(self.appInfo)
self.appState.instantProgress += 1
}
// Append object to store if running reverse search with empty store // Append object to store if running reverse search with empty store
if self.reverseAddon { if self.reverseAddon {
self.appState.appInfoStore.append(self.appInfo) self.appState.appInfoStore.append(self.appInfo)
@@ -356,3 +297,324 @@ class AppPathFinder {
} }
} }
} }
// Async Test
//class AppPathFinder {
// private var appInfo: AppInfo
// private var appState: AppState
// private var locations: Locations
// private var backgroundRun: Bool
// private var reverseAddon: Bool
// private var completion: () -> Void = {}
// private var collection: [URL] = []
// private let collectionAccessQueue = DispatchQueue(label: "com.alienator88.Pearcleaner.appPathFinder.collectionAccess")
// private var state = PathFinderState() // Actor instance
//
// init(appInfo: AppInfo = .empty, appState: AppState, locations: Locations, backgroundRun: Bool = false, reverseAddon: Bool = false, completion: @escaping () -> Void = {}) {
// self.appInfo = appInfo
// self.appState = appState
// self.locations = locations
// self.backgroundRun = backgroundRun
// self.reverseAddon = reverseAddon
// self.completion = completion
// }
//
// func findPaths() async {
// await initialURLProcessing()
// await collectDirectories()
// await collectFiles()
// await finalizeCollection()
// }
//
// private func initialURLProcessing() async {
// if let url = URL(string: self.appInfo.path.absoluteString), !url.path.contains(".Trash") {
// let modifiedUrl = url.path.contains("Wrapper") ? url.deletingLastPathComponent().deletingLastPathComponent() : url
// collectionAccessQueue.sync {
// self.collection.append(modifiedUrl)
// }
// }
// }
//
// private func collectDirectories() async {
// for location in self.locations.apps.paths {
// await processDirectoryLocation(location)
// }
// }
//
// private func processDirectoryLocation(_ location: String) async {
// do {
// let contents = try FileManager.default.contentsOfDirectory(atPath: location)
// for item in contents {
// let itemURL = URL(fileURLWithPath: location).appendingPathComponent(item)
// let itemL = item.replacingOccurrences(of: ".", with: "").replacingOccurrences(of: " ", with: "").lowercased()
//
// var isDirectory: ObjCBool = false
// if FileManager.default.fileExists(atPath: itemURL.path, isDirectory: &isDirectory), isDirectory.boolValue {
// // Perform the check to skip the item if needed
// if shouldSkipItem(itemL, at: itemURL) {
// continue
// }
//
// collectionAccessQueue.sync {
// let alreadyIncluded = self.collection.contains { existingURL in
// itemURL.path.hasPrefix(existingURL.path)
// }
//
// if !alreadyIncluded && specificCondition(itemL: itemL, itemURL: itemURL) {
// self.collection.append(itemURL)
// }
// }
// }
// }
// } catch {
// print("Error processing directory location: \(location), error: \(error)")
// }
// }
//
// private func collectFiles() async {
// for location in self.locations.apps.paths {
// await processFileLocation(location)
// }
// }
//
// private func processFileLocation(_ location: String) async {
// do {
// let contents = try FileManager.default.contentsOfDirectory(atPath: location)
// for item in contents {
// let itemURL = URL(fileURLWithPath: location).appendingPathComponent(item)
// let itemL = item.replacingOccurrences(of: ".", with: "").replacingOccurrences(of: " ", with: "").lowercased()
//
// if FileManager.default.fileExists(atPath: itemURL.path),
// !shouldSkipItem(itemL, at: itemURL),
// specificCondition(itemL: itemL, itemURL: itemURL) {
// collectionAccessQueue.sync {
// self.collection.append(itemURL)
// }
// }
// }
// } catch {
// print("Error processing file location: \(location), error: \(error)")
// }
// }
//
//
//
// private func shouldSkipItem(_ itemL: String, at itemURL: URL) -> Bool {
// var containsItem = false
// collectionAccessQueue.sync {
// containsItem = self.collection.contains(itemURL)
// }
// if containsItem || !isSupportedFileType(at: itemURL.path) {
// return true
// }
//
// for skipCondition in skipConditions {
// if itemL.hasPrefix(skipCondition.skipPrefix) {
// let isAllowed = skipCondition.allowPrefixes.contains(where: itemL.hasPrefix)
// if !isAllowed {
// return true // Skip because it starts with a base prefix but is not in the allowed list
// }
// }
// }
//
// return false
// }
//
//
//
// private func specificCondition(itemL: String, itemURL: URL) -> Bool {
// let bundleIdentifierL = self.appInfo.bundleIdentifier.pearFormat()
// let bundleComponents = self.appInfo.bundleIdentifier.components(separatedBy: ".").compactMap { $0 != "-" ? $0.lowercased() : nil }
// let bundle = bundleComponents.suffix(2).joined()
// let nameL = self.appInfo.appName.pearFormat()
// let nameP = self.appInfo.path.lastPathComponent.replacingOccurrences(of: ".app", with: "")
//
// for condition in conditions {
// if bundleIdentifierL.contains(condition.bundle_id) {
// // Exclude and include keywords
// let hasIncludeKeyword = condition.include.contains(where: itemL.contains)
// let hasExcludeKeyword = condition.exclude.contains(where: itemL.contains)
//
// if hasExcludeKeyword {
// return false
// }
// if hasIncludeKeyword {
// if !condition.exclude.contains(where: itemL.contains) {
// return true
// }
// }
// }
// }
//
//
// if self.appInfo.webApp {
// return itemL.contains(bundleIdentifierL)
// }
//
// return itemL.contains(bundleIdentifierL) || itemL.contains(bundle) || (nameL.count > 3 && itemL.contains(nameL)) || (nameP.count > 3 && itemL.contains(nameP))
//
// }
//
//
// func getGroupContainers(bundleURL: URL) async -> [URL] {
// await withCheckedContinuation { continuation in
// var groupContainers: [URL] = []
// // Assume creating a SecStaticCode is synchronous and quick.
// var staticCode: SecStaticCode?
// let status = SecStaticCodeCreateWithPath(bundleURL as CFURL, [], &staticCode)
//
// if status == errSecSuccess, let staticCode = staticCode {
// var signingInformation: CFDictionary?
//
// // This may involve a blocking call to fetch signing information.
// let status = SecCodeCopySigningInformation(staticCode, SecCSFlags(), &signingInformation)
//
// if status == errSecSuccess, let infoDict = signingInformation as? [String: Any],
// let entitlementsDict = infoDict["entitlements-dict"] as? [String: Any],
// let appGroups = entitlementsDict["com.apple.security.application-groups"] as? [String] {
//
// let fileManager = FileManager.default
// let home = NSHomeDirectory() // Assuming `home` is derived earlier or statically known.
//
// for groupID in appGroups {
// let groupURL = URL(fileURLWithPath: "\(home)/Library/Group Containers/\(groupID)")
// if fileManager.fileExists(atPath: groupURL.path) {
// groupContainers.append(groupURL)
// }
// }
// }
// }
//
// // Continue once all operations are complete.
// continuation.resume(returning: groupContainers)
// }
// }
//
//
// private func handleOutliers() -> [URL] {
// var outliers: [URL] = []
// let bundleIdentifier = self.appInfo.bundleIdentifier.pearFormat()
//
// // Find conditions that match the current app's bundle identifier
// let matchingConditions = conditions.filter { condition in
// bundleIdentifier.contains(condition.bundle_id)
// }
//
// for condition in matchingConditions {
// if let forceIncludes = condition.includeForce {
// for path in forceIncludes {
// if let url = URL(string: path), FileManager.default.fileExists(atPath: url.path) {
// outliers.append(url)
// }
// }
// }
// }
//
// return outliers
// }
//
// private func finalizeCollection() async {
// let groupContainers = await getGroupContainers(bundleURL: self.appInfo.path)
// let outliers = handleOutliers()
// var tempCollection: [URL] = []
//
// collectionAccessQueue.sync {
// tempCollection = self.collection
// }
//
// tempCollection.append(contentsOf: groupContainers)
// tempCollection.append(contentsOf: outliers)
//
// // Sort and standardize URLs to ensure consistent comparisons
// let sortedCollection = tempCollection.map { $0.standardizedFileURL }.sorted(by: { $0.path < $1.path })
// var filteredCollection: [URL] = []
// var previousUrl: URL?
//
// for url in sortedCollection {
// if let previous = previousUrl, url.path.hasPrefix(previous.path + "/") {
// // Current URL is a subdirectory of the previous one, so skip it
// continue
// }
// // This URL is not a subdirectory of the previous one, so keep it and set it as the previous URL
// filteredCollection.append(url)
// previousUrl = url
// }
//
// await handlePostProcessing(sortedCollection: filteredCollection)
// }
//
// private func handlePostProcessing(sortedCollection: [URL]) async {
// for path in sortedCollection {
// let size = totalSizeOnDisk(for: path)
// if let icon = getIconForFileOrFolderNS(atPath: path) { // Retrieve NSImage
// let iconData = serializeImage(icon) // Convert NSImage to Data
// await state.setFileDetails(for: path, size: size, icon: iconData) // Pass Data to actor
// } else {
// await state.setFileDetails(for: path, size: size, icon: nil)
// }
// }
// await updateAppState(with: sortedCollection)
// }
//
// private func updateAppState(with sortedCollection: [URL]) async {
// // Retrieve state from the actor
// let fileSize = await self.state.getFileSize()
// let fileIconData = await self.state.getFileIconData()
// let fileIcons = fileIconData.mapValues { deserializeImage($0) }
//
// // Assume updating of appInfo needs to happen on the main thread
// self.appInfo.fileSize = fileSize
// self.appInfo.fileIcon = fileIcons
//
// // Execute UI related updates on the main thread using MainActor
// await MainActor.run {
// if !self.backgroundRun {
// self.appState.appInfo = self.appInfo
// self.appState.selectedItems = Set(sortedCollection)
// }
//
//
// }
//
// self.completion() // Call the completion handler
// }
//}
//
//actor PathFinderState {
// var fileSize: [URL: Int64] = [:]
// var fileIconData: [URL: Data?] = [:]
//
// func setFileDetails(for path: URL, size: Int64, icon: Data?) {
// fileSize[path] = size
// fileIconData[path] = icon
// }
//
// func getFileSize() -> [URL: Int64] {
// return fileSize
// }
//
// func getFileIconData() -> [URL: Data?] {
// return fileIconData
// }
//}
//
//
//func serializeImage(_ image: NSImage?) -> Data? {
// guard let image = image else { return nil }
// guard let tiffData = image.tiffRepresentation else { return nil }
// let bitmapImage = NSBitmapImageRep(data: tiffData)
// return bitmapImage?.representation(using: .png, properties: [:])
//}
//
//func deserializeImage(_ data: Data?) -> NSImage? {
// guard let data = data else { return nil }
// return NSImage(data: data)
//}
+14 -7
View File
@@ -29,9 +29,6 @@ class AppState: ObservableObject
@Published var progressBar: (String, Double) = ("Ready", 0.0) @Published var progressBar: (String, Double) = ("Ready", 0.0)
@Published var reload: Bool = false @Published var reload: Bool = false
@Published var showProgress: Bool = false @Published var showProgress: Bool = false
@Published var popCount: Int = 0
@Published var instantProgress: Double = 0.0
@Published var instantTotal: Double = 0.0
@Published var finderExtensionEnabled: Bool = false @Published var finderExtensionEnabled: Bool = false
@@ -48,12 +45,14 @@ class AppState: ObservableObject
system: false, system: false,
files: [], files: [],
fileSize: [:], fileSize: [:],
fileSizeLogical: [:],
fileIcon: [:] fileIcon: [:]
) )
self.zombieFile = ZombieFile( self.zombieFile = ZombieFile(
id: UUID(), id: UUID(),
fileSize: [:], fileSize: [:],
fileSizeLogical: [:],
fileIcon: [:] fileIcon: [:]
) )
updateExtensionStatus() updateExtensionStatus()
@@ -91,14 +90,19 @@ struct AppInfo: Identifiable, Equatable, Hashable {
let system: Bool let system: Bool
var files: [URL] var files: [URL]
var fileSize: [URL:Int64] var fileSize: [URL:Int64]
var fileSizeLogical: [URL:Int64]
var fileIcon: [URL:NSImage?] var fileIcon: [URL:NSImage?]
var totalSize: Int64 var totalSize: Int64
{ {
return fileSize.values.reduce(0, +) return fileSize.values.reduce(0, +)
} }
var totalSizeLogical: Int64
{
return fileSizeLogical.values.reduce(0, +)
}
static let empty = AppInfo(id: UUID(), path: URL(fileURLWithPath: ""), bundleIdentifier: "", appName: "", appVersion: "", appIcon: nil, webApp: false, wrapped: false, system: false, files: [], fileSize: [:], fileIcon: [:]) static let empty = AppInfo(id: UUID(), path: URL(fileURLWithPath: ""), bundleIdentifier: "", appName: "", appVersion: "", appIcon: nil, webApp: false, wrapped: false, system: false, files: [], fileSize: [:], fileSizeLogical: [:], fileIcon: [:])
} }
@@ -107,14 +111,19 @@ struct AppInfo: Identifiable, Equatable, Hashable {
struct ZombieFile: Identifiable, Equatable, Hashable { struct ZombieFile: Identifiable, Equatable, Hashable {
let id: UUID let id: UUID
var fileSize: [URL:Int64] var fileSize: [URL:Int64]
var fileSizeLogical: [URL:Int64]
var fileIcon: [URL:NSImage?] var fileIcon: [URL:NSImage?]
var totalSize: Int64 var totalSize: Int64
{ {
return fileSize.values.reduce(0, +) return fileSize.values.reduce(0, +)
} }
var totalSizeLogical: Int64
{
return fileSizeLogical.values.reduce(0, +)
}
static let empty = ZombieFile(id: UUID(), fileSize: [:], fileIcon: [:]) static let empty = ZombieFile(id: UUID(), fileSize: [:], fileSizeLogical: [:], fileIcon: [:])
} }
@@ -201,5 +210,3 @@ enum DisplayMode: Int, CaseIterable {
} }
} }
} }
+149
View File
@@ -0,0 +1,149 @@
//
// Conditions.swift
// Pearcleaner
//
// Created by Alin Lupascu on 4/15/24.
//
import Foundation
struct Condition: Decodable {
var bundle_id: String
var include: [String]
var exclude: [String]
var includeForce: [String]?
}
struct SkipCondition {
var skipPrefix: String
var allowPrefixes: [String]
}
// Conditions for some apps that need to include/exclude certain files/folders when names are more complicated
var conditions: [Condition] = [
Condition(
bundle_id: "comappledtxcode",
include: ["comappledt", "xcode", "simulator"],
exclude: ["comrobotsandpencilsxcodesapp", "comoneminutegamesxcodecleaner", "iohyperappxcodecleaner", "xcodesjson"],
includeForce: ["\(home)/Library/Containers/com.apple.iphonesimulator.ShareExtension"]
),
Condition(
bundle_id: "comrobotsandpencilsxcodesapp",
include: [],
exclude: ["comappledtxcode", "comoneminutegamesxcodecleaner", "iohyperappxcodecleaner"]
),
Condition(
bundle_id: "iohyperappxcodecleaner",
include: [],
exclude: ["comrobotsandpencilsxcodesapp", "comoneminutegamesxcodecleaner", "comappledtxcode", "xcodesjson"]
),
Condition(
bundle_id: "uszoomxos",
include: ["zoom"],
exclude: []
),
Condition(
bundle_id: "combravebrowser",
include: ["brave"],
exclude: []
),
Condition(
bundle_id: "comoktamobile",
include: ["okta"],
exclude: []
),
Condition(
bundle_id: "comgooglechrome",
include: ["google", "chrome"],
exclude: ["iterm", "chromefeaturestate"]
),
Condition(
bundle_id: "commicrosoftedgemac",
include: ["microsoft"],
exclude: ["vscode", "rdc", "appcenter", "office", "oneauth"]
),
Condition(
bundle_id: "orgmozillafirefox",
include: ["mozilla"],
exclude: []
),
Condition(
bundle_id: "comlogioptionsplus",
include: ["logi"],
exclude: ["login", "logic"],
includeForce: []
),
Condition(
bundle_id: "commicrosoftvscode",
include: ["vscode"],
exclude: [],
includeForce: ["\(home)/Library/Application Support/Code"]
),
Condition(
bundle_id: "comfacebookarchondeveloperid",
include: ["archonloginhelper"],
exclude: []
),
Condition(
bundle_id: "euexelbanstats",
include: [],
exclude: ["video"]
),
Condition(
bundle_id: "jetbrains",
include: ["jetbrains", "jcef"],
exclude: []
),
]
// Skip com.apple files/folders since most are system originated, allow some for apps
let skipConditions: [SkipCondition] = [
SkipCondition(
skipPrefix: "comapple",
allowPrefixes: ["comappleconfigurator", "comappledt", "comappleiwork", "comapplesfsymbols", "comappletestflight"]
)
]
// Skip files/folders during reverse file search
let skipReverse = ["apple", "temporary", "btserver", "proapps", "scripteditor", "ilife", "livefsd", "siritoday", "addressbook", "animoji", "appstore", "askpermission", "callhistory", "clouddocs", "diskimages", "dock", "facetime", "fileprovider", "instruments", "knowledge", "mobilesync", "syncservices", "homeenergyd", "icloud", "icdd", "networkserviceproxy", "familycircle", "geoservices", "installation", "passkit", "sharedimagecache", "desktop", "mbuseragent", "swiftpm", "baseband", "coresimulator", "photoslegacyupgrade", "photosupgrade", "siritts", "ipod", "globalpreferences", "apmanalytics", "apmexperiment", "avatarcache", "byhost", "contextstoreagent", "mobilemeaccounts", "intentbuilderc", "loginwindow", "momc", "replayd", "sharedfilelistd", "clang", "audiocomponent", "csexattrcryptoservice", "livetranscriptionagent", "sandboxhelper", "statuskitagent", "betaenrollmentd", "contentlinkingd", "diagnosticextensionsd", "gamed", "heard", "homed", "itunescloudd", "lldb", "mds", "mediaanalysisd", "metrickitd", "mobiletimerd", "proactived", "ptpcamerad", "studentd", "talagent", "watchlistd", "apptranslocation", "xcrun", "ds_store", "caches", "crashreporter", "trash", "pearcleaner", "amsdatamigratortool", "arfilecache", "assistant", "chromium", "cloudkit", "webkit", "databases", "diagnostic", "cache", "gamekit", "homebrew", "logi", "microsoft", "mozilla", "sync", "google", "sentinel", "hexnode", "sentry", "tvappservices"]
// Function to load additional conditions from a GitHub JSON file
func loadConditionsFromGitHub() {
let url = URL(string: "https://api.github.com/repos/alienator88/Pearcleaner/contents/conditions.json")!
var request = URLRequest(url: url)
request.setValue("application/vnd.github.VERSION.raw", forHTTPHeaderField: "Accept")
request.setValue("2022-11-28", forHTTPHeaderField: "X-GitHub-Api-Version")
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
do {
// Assuming the JSON structure directly maps to an array of Condition
let jsonObject = try JSONSerialization.jsonObject(with: data, options: [])
if let conditionArray = jsonObject as? [[String: Any]] {
let jsonData = try JSONSerialization.data(withJSONObject: conditionArray, options: [])
let additionalConditions = try JSONDecoder().decode([Condition].self, from: jsonData)
if !additionalConditions.isEmpty {
DispatchQueue.main.async {
conditions += additionalConditions
}
}
} else {
printOS("The data format is incorrect or empty for GitHub conditions processing.")
}
} catch {
printOS("Failed to decode conditions JSON: \(error.localizedDescription)")
}
} else {
printOS("Failed to fetch conditions data: \(error?.localizedDescription ?? "Unknown error")")
}
}.resume()
}
-32
View File
@@ -19,8 +19,6 @@ class Locations: ObservableObject {
var apps: Category var apps: Category
var reverse: Category var reverse: Category
// var widgets: Category
// var plugins: Category
init() { init() {
let (cacheDir, tempDir) = darwinCT() let (cacheDir, tempDir) = darwinCT()
@@ -103,35 +101,5 @@ class Locations: ObservableObject {
cacheDir, cacheDir,
tempDir tempDir
]) ])
// self.widgets = Category(name: "Widgets", paths: [
// // User
// "~/Library/Widgets",
// // System
// "/Library/Widgets"
// ])
//
// self.plugins = Category(name: "Plugins", paths: [
// // User
// "~/Library/Contextual Menu Items",
// "~/Library/InputManagers",
// "~/Library/Internet Plug-Ins",
// "~/Library/Mail/Bundles",
// "~/Library/PreferencePanes",
// "~/Library/QuickLook",
// "~/Library/QuickTime",
// "~/Library/Screen Savers",
// "~/Library/Spotlight",
// // System
// "/Library/Contextual Menu Items",
// "/Library/InputManagers",
// "/Library/Internet Plug-Ins",
// "/Library/Mail/Bundles",
// "/Library/PreferencePanes",
// "/Library/QuickLook",
// "/Library/QuickTime",
// "/Library/Screen Savers",
// "/Library/Spotlight",
// ])
} }
} }
+6 -24
View File
@@ -11,7 +11,6 @@ import SwiftUI
// Get all apps from /Applications and ~/Applications // Get all apps from /Applications and ~/Applications
func getSortedApps(paths: [String], appState: AppState) -> [AppInfo] { func getSortedApps(paths: [String], appState: AppState) -> [AppInfo] {
@AppStorage("settings.general.instant") var instantSearch: Bool = true
let fileManager = FileManager.default let fileManager = FileManager.default
var apps: [URL] = [] var apps: [URL] = []
@@ -42,11 +41,6 @@ func getSortedApps(paths: [String], appState: AppState) -> [AppInfo] {
.compactMap { AppInfoFetcher.getAppInfo(atPath: $0) } .compactMap { AppInfoFetcher.getAppInfo(atPath: $0) }
.sorted { $0.appName.replacingOccurrences(of: ".", with: "").lowercased() < $1.appName.replacingOccurrences(of: ".", with: "").lowercased() } .sorted { $0.appName.replacingOccurrences(of: ".", with: "").lowercased() < $1.appName.replacingOccurrences(of: ".", with: "").lowercased() }
if instantSearch {
updateOnMain {
appState.instantTotal = Double(sortedApps.count)
}
}
return sortedApps return sortedApps
} }
@@ -116,15 +110,14 @@ func listAppSupportDirectories() -> [String] {
// Load app paths on launch // Load app paths on launch
func loadAllPaths(allApps: [AppInfo], appState: AppState, locations: Locations, reverseAddon: Bool = false, completion: @escaping () -> Void = {}) { func reversePreloader(allApps: [AppInfo], appState: AppState, locations: Locations, reverseAddon: Bool = false, completion: @escaping () -> Void = {}) {
let dispatchGroup = DispatchGroup() let dispatchGroup = DispatchGroup()
appState.appInfoStore.removeAll() appState.appInfoStore.removeAll()
for app in allApps { for app in allApps {
dispatchGroup.enter() dispatchGroup.enter()
DispatchQueue.global(qos: .background).async { DispatchQueue.global(qos: .background).async {
let pathFinder = AppPathFinder(appInfo: app, appState: appState, locations: locations, backgroundRun: true, reverseAddon: reverseAddon) AppPathFinder(appInfo: app, appState: appState, locations: locations, backgroundRun: true, reverseAddon: reverseAddon).findPaths()
pathFinder.findPaths()
dispatchGroup.leave() dispatchGroup.leave()
} }
} }
@@ -134,28 +127,17 @@ func loadAllPaths(allApps: [AppInfo], appState: AppState, locations: Locations,
func checkAllAppsProcessed(retryCount: Int = 0, maxRetry: Int = 120) { func checkAllAppsProcessed(retryCount: Int = 0, maxRetry: Int = 120) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
if appState.appInfoStore.count == allApps.count { if appState.appInfoStore.count == allApps.count {
if reverseAddon { ReversePathsSearcher(appState: appState, locations: locations).reversePathsSearch() {
let reverse = ReversePathsSearcher(appState: appState, locations: locations)
reverse.reversePathsSearch() {
updateOnMain { updateOnMain {
appState.showProgress = false appState.showProgress = false
} }
} }
}
// Reset progress values to 0
updateOnMain {
appState.instantProgress = 0
appState.instantTotal = 0
appState.showProgress = false
}
completion() completion()
} else if retryCount < maxRetry { } else if retryCount < maxRetry {
checkAllAppsProcessed(retryCount: retryCount + 1, maxRetry: maxRetry) checkAllAppsProcessed(retryCount: retryCount + 1, maxRetry: maxRetry)
} else { } else {
// Reset progress values to 0 // Reset progress values to 0
updateOnMain { updateOnMain {
appState.instantProgress = 0
appState.instantTotal = 0
appState.showProgress = false appState.showProgress = false
} }
printOS("loadAllPaths - Retry limit reached. Not all paths were loaded.") printOS("loadAllPaths - Retry limit reached. Not all paths were loaded.")
@@ -196,13 +178,13 @@ func showAppInFiles(appInfo: AppInfo, appState: AppState, locations: Locations,
appState.showProgress = true appState.showProgress = true
// Initialize the path finder and execute its search. // Initialize the path finder and execute its search.
let pathFinder = AppPathFinder(appInfo: appInfo, appState: appState, locations: locations) { AppPathFinder(appInfo: appInfo, appState: appState, locations: locations) {
updateOnMain { updateOnMain {
// Update the progress indicator on the main thread once the search completes. // Update the progress indicator on the main thread once the search completes.
appState.showProgress = false appState.showProgress = false
} }
} }.findPaths()
pathFinder.findPaths()
appState.appInfo = appInfo appState.appInfo = appInfo
// Animate the view change and popover display. // Animate the view change and popover display.
+11 -6
View File
@@ -14,16 +14,17 @@ class ReversePathsSearcher {
private let fileManager = FileManager.default private let fileManager = FileManager.default
private var collection: [URL] = [] private var collection: [URL] = []
private var fileSize: [URL: Int64] = [:] private var fileSize: [URL: Int64] = [:]
private var fileSizeLogical: [URL: Int64] = [:]
private var fileIcon: [URL: NSImage?] = [:] private var fileIcon: [URL: NSImage?] = [:]
private var skipped: Set<String>
private let dispatchGroup = DispatchGroup() private let dispatchGroup = DispatchGroup()
init(appState: AppState, locations: Locations) { init(appState: AppState, locations: Locations) {
self.appState = appState self.appState = appState
self.locations = locations self.locations = locations
self.skipped = ["apple", "temporary", "btserver", "proapps", "scripteditor", "ilife", "livefsd", "siritoday", "addressbook", "animoji", "appstore", "askpermission", "callhistory", "clouddocs", "diskimages", "dock", "facetime", "fileprovider", "instruments", "knowledge", "mobilesync", "syncservices", "homeenergyd", "icloud", "icdd", "networkserviceproxy", "familycircle", "geoservices", "installation", "passkit", "sharedimagecache", "desktop", "mbuseragent", "swiftpm", "baseband", "coresimulator", "photoslegacyupgrade", "photosupgrade", "siritts", "ipod", "globalpreferences", "apmanalytics", "apmexperiment", "avatarcache", "byhost", "contextstoreagent", "mobilemeaccounts", "intentbuilderc", "loginwindow", "momc", "replayd", "sharedfilelistd", "clang", "audiocomponent", "csexattrcryptoservice", "livetranscriptionagent", "sandboxhelper", "statuskitagent", "betaenrollmentd", "contentlinkingd", "diagnosticextensionsd", "gamed", "heard", "homed", "itunescloudd", "lldb", "mds", "mediaanalysisd", "metrickitd", "mobiletimerd", "proactived", "ptpcamerad", "studentd", "talagent", "watchlistd", "apptranslocation", "xcrun", "ds_store", "caches", "crashreporter", "trash", "pearcleaner", "amsdatamigratortool", "arfilecache", "assistant", "chromium", "cloudkit", "webkit", "databases", "diagnostic", "cache", "gamekit", "homebrew", "logi", "microsoft", "mozilla", "sync", "google", "sentinel", "hexnode", "sentry", "tvappservices"]
} }
func reversePathsSearch(completion: @escaping () -> Void = {}) { func reversePathsSearch(completion: @escaping () -> Void = {}) {
Task(priority: .high) { Task(priority: .high) {
self.processLocations() self.processLocations()
@@ -34,17 +35,19 @@ class ReversePathsSearcher {
} }
private func processLocations() { private func processLocations() {
let allPaths = appState.appInfoStore.flatMap { $0.files.map { $0.path.pearFormat() } } let allPaths = appState.appInfoStore.flatMap { $0.fileSize.keys.map { $0.path.pearFormat() } }
let allNames = appState.appInfoStore.map { $0.appName.pearFormat() } let allNames = appState.appInfoStore.map { $0.appName.pearFormat() }
for location in locations.reverse.paths where fileManager.fileExists(atPath: location) { for location in locations.reverse.paths where fileManager.fileExists(atPath: location) {
dispatchGroup.enter() dispatchGroup.enter()
processLocation(location, allPaths: allPaths, allNames: allNames) processLocation(location, allPaths: allPaths, allNames: allNames)
dispatchGroup.leave() dispatchGroup.leave()
} }
} }
private func processLocation(_ location: String, allPaths: [String], allNames: [String]) { private func processLocation(_ location: String, allPaths: [String], allNames: [String]) {
do { do {
let contents = try fileManager.contentsOfDirectory(atPath: location) let contents = try fileManager.contentsOfDirectory(atPath: location)
contents.forEach { itemName in contents.forEach { itemName in
@@ -61,8 +64,7 @@ class ReversePathsSearcher {
let itemPath = itemURL.path.pearFormat() let itemPath = itemURL.path.pearFormat()
let itemLastPathComponent = itemURL.lastPathComponent.pearFormat() let itemLastPathComponent = itemURL.lastPathComponent.pearFormat()
guard !skipped.contains(where: { formattedItemName.contains($0) }), guard !skipReverse.contains(where: { formattedItemName.contains($0) }),
// !allPaths.contains(itemPath),
!allPaths.contains(where: { $0 == itemPath || $0.hasSuffix("/\(itemLastPathComponent)") }), !allPaths.contains(where: { $0 == itemPath || $0.hasSuffix("/\(itemLastPathComponent)") }),
!allNames.contains(formattedItemName), !allNames.contains(formattedItemName),
isSupportedFileType(at: itemURL.path) else { isSupportedFileType(at: itemURL.path) else {
@@ -74,7 +76,9 @@ class ReversePathsSearcher {
private func calculateFileDetails() { private func calculateFileDetails() {
collection.forEach { path in collection.forEach { path in
fileSize[path] = totalSizeOnDisk(for: path) let size = totalSizeOnDisk(for: path)
fileSize[path] = size.real
fileSizeLogical[path] = size.logical
fileIcon[path] = getIconForFileOrFolderNS(atPath: path) fileIcon[path] = getIconForFileOrFolderNS(atPath: path)
} }
} }
@@ -83,6 +87,7 @@ class ReversePathsSearcher {
dispatchGroup.notify(queue: .main) { dispatchGroup.notify(queue: .main) {
var updatedZombieFile = ZombieFile.empty var updatedZombieFile = ZombieFile.empty
updatedZombieFile.fileSize = self.fileSize updatedZombieFile.fileSize = self.fileSize
updatedZombieFile.fileSizeLogical = self.fileSizeLogical
updatedZombieFile.fileIcon = self.fileIcon updatedZombieFile.fileIcon = self.fileIcon
self.appState.zombieFile = updatedZombieFile self.appState.zombieFile = updatedZombieFile
self.appState.showProgress = false self.appState.showProgress = false
-45
View File
@@ -441,56 +441,11 @@ struct SimpleSearchStyle: TextFieldStyle {
} }
} }
.disabled(appState.instantProgress != 0 && appState.instantProgress != appState.instantTotal)
.padding(6) .padding(6)
.overlay( .overlay(
Group {
GeometryReader { geometry in
ZStack(alignment: .leading) {
if appState.instantProgress != 0 && appState.instantProgress != appState.instantTotal {
let totalWidth = geometry.size.width
let progressFraction = CGFloat(appState.instantProgress / appState.instantTotal)
let progressWidth = totalWidth * progressFraction
RoundedRectangle(cornerRadius: 6)
.fill(Color.accentColor)
.padding(4)
.frame(width: progressWidth)
.animation(.linear, value: progressWidth)
.allowsHitTesting(false)
}
RoundedRectangle(cornerRadius: 6) RoundedRectangle(cornerRadius: 6)
.strokeBorder(Color("mode").opacity(0.2), lineWidth: 1) .strokeBorder(Color("mode").opacity(0.2), lineWidth: 1)
.allowsHitTesting(false) .allowsHitTesting(false)
}
}
// ZStack {
// RoundedRectangle(cornerRadius: 6)
// .strokeBorder(Color("mode").opacity(0.2), lineWidth: 1)
// .allowsHitTesting(false)
//
// if appState.instantTotal > 0 {
// let progressWidth = CGFloat(appState.instantProgress / appState.instantTotal)
// * NSScreen.main.bounds.width // Assuming full width for simplicity; adjust as needed
// RoundedRectangle(cornerRadius: 6)
// .fill(Color.accentColor.opacity(0.5)) // Adjust color and opacity as needed
// .frame(width: progressWidth)
// .animation(.linear, value: progressWidth)
// .allowsHitTesting(false)
// }
//// if appState.instantProgress != 0 && appState.instantProgress != appState.instantTotal {
//// ProgressView("", value: appState.instantProgress, total: appState.instantTotal)
//// .progressViewStyle(.linear)
//// .padding()
//// }
// }
}
) )
.onHover { hovering in .onHover { hovering in
withAnimation(Animation.easeInOut(duration: 0.15)) { withAnimation(Animation.easeInOut(duration: 0.15)) {
+46
View File
@@ -28,10 +28,12 @@ class ThemeSettings: ObservableObject {
if let components = UserDefaults.standard.object(forKey: colorKey) as? [CGFloat], components.count >= 4 { if let components = UserDefaults.standard.object(forKey: colorKey) as? [CGFloat], components.count >= 4 {
themeColor = Color(.sRGB, red: components[0], green: components[1], blue: components[2], opacity: components[3]) themeColor = Color(.sRGB, red: components[0], green: components[1], blue: components[2], opacity: components[3])
} else { } else {
themeColor = Color.clear themeColor = Color.clear
} }
} }
func saveThemeColor() { func saveThemeColor() {
let nsColor = NSColor(themeColor) let nsColor = NSColor(themeColor)
if let components = nsColor.cgColor.components { if let components = nsColor.cgColor.components {
@@ -43,17 +45,61 @@ class ThemeSettings: ObservableObject {
themeColor = dark ? Color(.sRGB, red: 0.149, green: 0.149, blue: 0.149, opacity: 1) : Color(.sRGB, red: 1.0, green: 1.0, blue: 1.0, opacity: 1) themeColor = dark ? Color(.sRGB, red: 0.149, green: 0.149, blue: 0.149, opacity: 1) : Color(.sRGB, red: 1.0, green: 1.0, blue: 1.0, opacity: 1)
saveThemeColor() saveThemeColor()
} }
func setPreset(preset: String, colorScheme: DisplayMode) {
themeColor = getColorForPreset(preset: preset, colorScheme: colorScheme)
saveThemeColor()
}
func getColorForPreset(preset: String, colorScheme: DisplayMode) -> Color {
let slate = colorScheme.colorScheme == .light ? Color(.sRGB, red: 0.499549, green: 0.545169, blue: 0.682028, opacity: 1) : Color(.sRGB, red: 0.188143, green: 0.208556, blue: 0.262679, opacity: 1)
let solarized = colorScheme.colorScheme == .light ? Color(.sRGB, red: 0.554372, green: 0.6557, blue: 0.734336, opacity: 1) : Color(.sRGB, red: 0.117257, green: 0.22506, blue: 0.249171, opacity: 1)
let dracula = colorScheme.colorScheme == .light ? Color(.sRGB, red: 0.567094, green: 0.562125, blue: 0.81285, opacity: 1) : Color(.sRGB, red: 0.268614, green: 0.264737, blue: 0.383503, opacity: 1)
switch preset {
case "slate":
return slate
case "solarized":
return solarized
case "dracula":
return dracula
default:
return themeColor
}
}
} }
extension Color { extension Color {
/// Darkens the color by a percentage.
func darker(by percentage: CGFloat = 30.0) -> Color { func darker(by percentage: CGFloat = 30.0) -> Color {
var hsb = (hue: CGFloat(0), saturation: CGFloat(0), brightness: CGFloat(0), alpha: CGFloat(0)) var hsb = (hue: CGFloat(0), saturation: CGFloat(0), brightness: CGFloat(0), alpha: CGFloat(0))
NSColor(self).getHue(&hsb.hue, saturation: &hsb.saturation, brightness: &hsb.brightness, alpha: &hsb.alpha) NSColor(self).getHue(&hsb.hue, saturation: &hsb.saturation, brightness: &hsb.brightness, alpha: &hsb.alpha)
return Color(hue: hsb.hue, saturation: hsb.saturation, brightness: max(hsb.brightness - percentage/100, 0), opacity: hsb.alpha) return Color(hue: hsb.hue, saturation: hsb.saturation, brightness: max(hsb.brightness - percentage/100, 0), opacity: hsb.alpha)
} }
/// Lightens the color by a percentage.
func lighter(by percentage: CGFloat = 30.0) -> Color { func lighter(by percentage: CGFloat = 30.0) -> Color {
var hsb = (hue: CGFloat(0), saturation: CGFloat(0), brightness: CGFloat(0), alpha: CGFloat(0)) var hsb = (hue: CGFloat(0), saturation: CGFloat(0), brightness: CGFloat(0), alpha: CGFloat(0))
NSColor(self).getHue(&hsb.hue, saturation: &hsb.saturation, brightness: &hsb.brightness, alpha: &hsb.alpha) NSColor(self).getHue(&hsb.hue, saturation: &hsb.saturation, brightness: &hsb.brightness, alpha: &hsb.alpha)
return Color(hue: hsb.hue, saturation: hsb.saturation, brightness: min(hsb.brightness + percentage / 100, 1.0), opacity: hsb.alpha) return Color(hue: hsb.hue, saturation: hsb.saturation, brightness: min(hsb.brightness + percentage / 100, 1.0), opacity: hsb.alpha)
} }
} }
extension UserDefaults {
func color(forKey key: String) -> Color? {
guard let components = array(forKey: key) as? [CGFloat], components.count == 4 else {
return nil
}
return Color(.sRGB, red: components[0], green: components[1], blue: components[2], opacity: components[3])
}
func setColor(_ color: Color, forKey key: String) {
let nsColor = NSColor(color)
if let components = nsColor.cgColor.components, components.count == 4 {
set(components, forKey: key)
} else {
removeObject(forKey: key)
}
}
}
+49 -65
View File
@@ -174,20 +174,13 @@ func isCurrentUserAdmin(completion: @escaping (Bool) -> Void) {
} }
// Check if appearance is dark mode // Check if appearance is dark mode
func isDarkModeEnabled() -> Bool { func isDarkMode() -> Bool {
let process = Process() return NSApp.effectiveAppearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua
process.executableURL = URL(fileURLWithPath: "/bin/zsh") }
process.arguments = ["-c", "defaults read -g AppleInterfaceStyle"]
let pipe = Pipe() // Set app color mode
process.standardOutput = pipe func setAppearance(mode: NSAppearance.Name) {
process.standardError = pipe NSApp.appearance = NSAppearance(named: mode)
try? process.run()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
if !data.isEmpty{
return true
} else {
return false
}
} }
// Check if Pearcleaner has any windows open // Check if Pearcleaner has any windows open
@@ -216,6 +209,13 @@ func findAndShowWindows(named titles: [String]) {
} }
} }
// Copy to clipboard
func copyToClipboard(text: String) {
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
pasteboard.setString(text, forType: .string)
}
// Check if appearance is dark mode // Check if appearance is dark mode
//func getCasks() -> [String] { //func getCasks() -> [String] {
@@ -458,86 +458,73 @@ func printOS(_ items: Any..., separator: String = " ", terminator: String = "\n"
} }
// Get size of files
//func totalSizeOnDisk2(for paths: [URL]) -> Int64 { func totalSizeOnDisk(for paths: [URL]) -> (real: Int64, logical: Int64) {
// var totalSize = 0
//
// let process = Process()
// process.launchPath = "/usr/bin/du"
// process.arguments = ["-sk"] + paths.map { (url: URL) -> String in
// return url.path
// }
// let pipe = Pipe()
// process.standardOutput = pipe
// process.standardError = pipe//FileHandle.nullDevice
//
// try? process.run()
// process.waitUntilExit()
//
// let data = pipe.fileHandleForReading.readDataToEndOfFile()
// if let output = String(data: data, encoding: .utf8) {
// let lines = output.components(separatedBy: .newlines)
// for line in lines {
// let components = line.components(separatedBy: "\t")
// if let sizeString = components.first, let size = Int(sizeString) {
// totalSize += size
// }
// }
// }
//
// return Int64(totalSize) * 1024 // Convert the size from kilobytes to bytes
//}
// Get total size of folders and files using DU cli command
func totalSizeOnDisk(for paths: [URL]) -> Int64 {
let fileManager = FileManager.default let fileManager = FileManager.default
var totalSize: Int64 = 0 var totalAllocatedSize: Int64 = 0
var totalFileSize: Int64 = 0
for url in paths { for url in paths {
var isDirectory: ObjCBool = false var isDirectory: ObjCBool = false
if fileManager.fileExists(atPath: url.path, isDirectory: &isDirectory) { if fileManager.fileExists(atPath: url.path, isDirectory: &isDirectory) {
let keys: [URLResourceKey] = [.totalFileAllocatedSizeKey, .fileSizeKey]
if isDirectory.boolValue { if isDirectory.boolValue {
// It's a directory, recurse into it // It's a directory, recurse into it
if let enumerator = fileManager.enumerator(at: url, includingPropertiesForKeys: [.totalFileAllocatedSizeKey], errorHandler: nil) { if let enumerator = fileManager.enumerator(at: url, includingPropertiesForKeys: keys, errorHandler: nil) {
for case let fileURL as URL in enumerator { for case let fileURL as URL in enumerator {
do { do {
let fileAttributes = try fileURL.resourceValues(forKeys: [.totalFileAllocatedSizeKey]) let fileAttributes = try fileURL.resourceValues(forKeys: Set(keys))
if let fileSize = fileAttributes.totalFileAllocatedSize { if let allocatedSize = fileAttributes.totalFileAllocatedSize {
totalSize += Int64(fileSize) totalAllocatedSize += Int64(allocatedSize)
}
if let fileSize = fileAttributes.fileSize {
totalFileSize += Int64(fileSize)
} }
} catch { } catch {
printOS("Error getting file size for \(fileURL): \(error)") print("Error getting file attributes for \(fileURL): \(error)")
} }
} }
} }
} else { } else {
// It's a file // It's a file
do { do {
let fileAttributes = try url.resourceValues(forKeys: [.totalFileAllocatedSizeKey]) let fileAttributes = try url.resourceValues(forKeys: Set(keys))
if let fileSize = fileAttributes.totalFileAllocatedSize { if let allocatedSize = fileAttributes.totalFileAllocatedSize {
totalSize += Int64(fileSize) totalAllocatedSize += Int64(allocatedSize)
}
if let fileSize = fileAttributes.fileSize {
totalFileSize += Int64(fileSize)
} }
} catch { } catch {
printOS("Error getting file size for \(url): \(error)") print("Error getting file attributes for \(url): \(error)")
} }
} }
} }
} }
return totalSize return (real: totalAllocatedSize, logical: totalFileSize)
} }
func totalSizeOnDisk(for path: URL) -> Int64 {
func totalSizeOnDisk(for path: URL) -> (real: Int64, logical: Int64) {
return totalSizeOnDisk(for: [path]) return totalSizeOnDisk(for: [path])
} }
// ByteFormatter // ByteFormatter
func formatByte(size: Int64) -> String { func formatByte(size: Int64) -> (human: String, byte: String) {
let byteCountFormatter = ByteCountFormatter() let byteCountFormatter = ByteCountFormatter()
byteCountFormatter.countStyle = .file byteCountFormatter.countStyle = .file
byteCountFormatter.allowedUnits = [.useAll] byteCountFormatter.allowedUnits = [.useAll]
return byteCountFormatter.string(fromByteCount: size) let human = byteCountFormatter.string(fromByteCount: size)
let numberformatter = NumberFormatter()
numberformatter.numberStyle = .decimal
let formattedNumber = numberformatter.string(from: NSNumber(value: size)) ?? "\(size)"
let byte = "\(formattedNumber)"
return (human: human, byte: byte)
} }
// Only process supported files // Only process supported files
@@ -603,12 +590,9 @@ func uninstallPearcleaner(appState: AppState, locations: Locations) {
// Get app info for Pearcleaner // Get app info for Pearcleaner
let appInfo = AppInfoFetcher.getAppInfo(atPath: Bundle.main.bundleURL) let appInfo = AppInfoFetcher.getAppInfo(atPath: Bundle.main.bundleURL)
// appState.appInfo = appInfo!
// Find application files for Pearcleaner // Find application files for Pearcleaner
let pathFinder = AppPathFinder(appInfo: appInfo!, appState: appState, locations: locations) AppPathFinder(appInfo: appInfo!, appState: appState, locations: locations).findPaths()
pathFinder.findPaths()
// findPathsForApp(appInfo: appInfo!,appState: appState, locations: locations)
// Kill Pearcleaner and tell Finder to trash the files // Kill Pearcleaner and tell Finder to trash the files
DispatchQueue.main.asyncAfter(deadline: .now() + 2) { DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
+43 -9
View File
@@ -23,7 +23,6 @@ struct PearcleanerApp: App {
@AppStorage("displayMode") var displayMode: DisplayMode = .system @AppStorage("displayMode") var displayMode: DisplayMode = .system
@AppStorage("settings.general.mini") private var mini: Bool = false @AppStorage("settings.general.mini") private var mini: Bool = false
@AppStorage("settings.general.miniview") private var miniView: Bool = true @AppStorage("settings.general.miniview") private var miniView: Bool = true
@AppStorage("settings.general.instant") private var instantSearch: Bool = true
@AppStorage("settings.general.features") private var features: String = "" @AppStorage("settings.general.features") private var features: String = ""
@AppStorage("settings.general.brew") private var brew: Bool = false @AppStorage("settings.general.brew") private var brew: Bool = false
@AppStorage("settings.menubar.enabled") private var menubarEnabled: Bool = false @AppStorage("settings.menubar.enabled") private var menubarEnabled: Bool = false
@@ -102,13 +101,7 @@ struct PearcleanerApp: App {
let sortedApps = getSortedApps(paths: fsm.folderPaths, appState: appState) let sortedApps = getSortedApps(paths: fsm.folderPaths, appState: appState)
appState.sortedApps = sortedApps appState.sortedApps = sortedApps
// Enable menubar item
// Find all app paths/information on load if instantSearch is enabled
if instantSearch {
loadAllPaths(allApps: sortedApps, appState: appState, locations: locations)
}
if menubarEnabled { if menubarEnabled {
MenuBarExtraManager.shared.addMenuBarExtra(withView: { MenuBarExtraManager.shared.addMenuBarExtra(withView: {
MenuBarMiniAppView(search: $search, showPopover: $showPopover) MenuBarMiniAppView(search: $search, showPopover: $showPopover)
@@ -140,6 +133,9 @@ struct PearcleanerApp: App {
hasLaunched = true hasLaunched = true
} }
// Load extra conditions from GitHub
loadConditionsFromGitHub()
// TIMERS //////////////////////////////////////////////////////////////////////////////////// // TIMERS ////////////////////////////////////////////////////////////////////////////////////
@@ -183,6 +179,7 @@ struct PearcleanerApp: App {
class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate { class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
var observer: NSObjectProtocol?
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
let menubarEnabled = UserDefaults.standard.bool(forKey: "settings.menubar.enabled") let menubarEnabled = UserDefaults.standard.bool(forKey: "settings.menubar.enabled")
@@ -191,11 +188,49 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
func applicationDidFinishLaunching(_ notification: Notification) { func applicationDidFinishLaunching(_ notification: Notification) {
let menubarEnabled = UserDefaults.standard.bool(forKey: "settings.menubar.enabled") let menubarEnabled = UserDefaults.standard.bool(forKey: "settings.menubar.enabled")
if UserDefaults.standard.object(forKey: "themeColor") == nil {
self.appearanceChanged()
}
if menubarEnabled { if menubarEnabled {
findAndHideWindows(named: ["Pearcleaner"]) findAndHideWindows(named: ["Pearcleaner"])
NSApplication.shared.setActivationPolicy(.accessory) NSApplication.shared.setActivationPolicy(.accessory)
} }
// Start observing the appearance change
observer = DistributedNotificationCenter.default().addObserver(forName: NSNotification.Name(rawValue: "AppleInterfaceThemeChangedNotification"), object: nil, queue: OperationQueue.main) { [weak self] _ in
let themeMode = UserDefaults.standard.string(forKey: "settings.general.selectedTheme")
if themeMode == "Auto" {
self?.appearanceChanged()
}
}
}
func appearanceChanged() {
let dm = UserDefaults.standard.integer(forKey: "displayMode")
var displayMode = DisplayMode(rawValue: dm)
let dark = isDarkMode()
if dark {
displayMode?.colorScheme = .dark
} else {
displayMode?.colorScheme = .light
}
UserDefaults.standard.set(displayMode?.rawValue, forKey: "displayMode")
ThemeSettings.shared.themeColor = isDarkMode() ? Color(.sRGB, red: 0.149, green: 0.149, blue: 0.149, opacity: 1) : Color(.sRGB, red: 1.0, green: 1.0, blue: 1.0, opacity: 1)
ThemeSettings.shared.saveThemeColor()
}
func applicationWillTerminate(_ notification: Notification) {
// Stop observing the appearance change
if let observer = observer {
DistributedNotificationCenter.default().removeObserver(observer)
}
} }
@@ -220,4 +255,3 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
} }
} }
+84 -35
View File
@@ -23,16 +23,15 @@ struct GeneralSettingsTab: View {
@AppStorage("displayMode") var displayMode: DisplayMode = .system @AppStorage("displayMode") var displayMode: DisplayMode = .system
@AppStorage("settings.sentinel.enable") private var sentinel: Bool = false @AppStorage("settings.sentinel.enable") private var sentinel: Bool = false
@AppStorage("settings.general.brew") private var brew: Bool = false @AppStorage("settings.general.brew") private var brew: Bool = false
@AppStorage("settings.general.instant") private var instantSearch: Bool = true
@AppStorage("settings.general.selectedTheme") var selectedTheme: String = "Auto"
@AppStorage("settings.general.selectedSort") var selectedSortAlpha: Bool = true @AppStorage("settings.general.selectedSort") var selectedSortAlpha: Bool = true
// @State private var finderExtensionEnabled: Bool = false @AppStorage("settings.general.sizeType") var sizeType: String = "Real"
@State private var diskStatus: Bool = false @State private var diskStatus: Bool = false
@State private var accessStatus: Bool = false @State private var accessStatus: Bool = false
private let themes = ["Auto", "Dark", "Light"]
@Binding var showPopover: Bool @Binding var showPopover: Bool
@Binding var search: String @Binding var search: String
@State var selectedIndex: Int?
var body: some View { var body: some View {
Form { Form {
VStack { VStack {
@@ -44,30 +43,6 @@ struct GeneralSettingsTab: View {
.padding(.leading) .padding(.leading)
HStack(spacing: 0) {
Image(systemName: instantSearch ? "bolt" : "bolt.slash")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(Color("mode").opacity(0.5))
VStack(alignment: .leading, spacing: 5) {
Text("\(instantSearch ? "Instant search is enabled" : "Instant search is disabled")")
.font(.callout)
.foregroundStyle(Color("mode").opacity(0.5))
}
InfoButton(text: "When instant search is enabled, all application files are gathered and cached for later use on startup instead of on each app click. There might be a slight delay of a few seconds when launching Pearcleaner depending on the amount of apps you have installed.", color: nil, label: "")
Spacer()
Toggle(isOn: $instantSearch, label: {
})
.toggleStyle(.switch)
}
.padding(5)
.padding(.leading)
HStack(spacing: 0) { HStack(spacing: 0) {
Image(systemName: brew ? "mug.fill" : "mug") Image(systemName: brew ? "mug.fill" : "mug")
.resizable() .resizable()
@@ -104,14 +79,88 @@ struct GeneralSettingsTab: View {
.font(.callout) .font(.callout)
.foregroundStyle(Color("mode").opacity(0.5)) .foregroundStyle(Color("mode").opacity(0.5))
} }
InfoButton(text: "When searching for app files or leftover files, the list will be sorted based on this choice", color: nil, label: "") InfoButton(text: "When searching for app files or leftover files, the list will be sorted either alphabetically or by size(large to small)", color: nil, label: "")
Spacer() Spacer()
Picker("", selection: $selectedSortAlpha) { SegmentedPicker(
Text("Alphabetical").tag(true) ["Alpha", "Size"],
Text("Size").tag(false) selectedIndex: Binding(
get: { selectedSortAlpha ? 0 : 1 },
set: { newIndex in
withAnimation(.easeInOut(duration: 0.3)) {
selectedSortAlpha = (newIndex == 0)
} }
.pickerStyle(SegmentedPickerStyle()) }),
.frame(width: 200) selectionAlignment: .bottom,
content: { item, isSelected in
Text(item)
.font(.callout)
.foregroundColor(isSelected ? Color("mode") : Color("mode").opacity(0.5))
.padding(.horizontal)
.padding(.bottom, 5)
.frame(width: 75)
},
selection: {
VStack(spacing: 0) {
Spacer()
Color("pear").frame(height: 1)
}
})
}
.padding(5)
.padding(.leading)
HStack(spacing: 0) {
Image(systemName: "plus.forwardslash.minus")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(Color("mode").opacity(0.5))
VStack(alignment: .leading, spacing: 5) {
Text("File size display")
.font(.callout)
.foregroundStyle(Color("mode").opacity(0.5))
}
InfoButton(text: "Real size type will show how much actual allocated space the file has on disk. Logical type shows the binary size. The filesystem can compress and deduplicate sectors on disk, so real size is sometimes smaller(or bigger) than logical size. Finder size is similar to if you right click > Get Info on a file in Finder, which will show both the logical and real sizes together.", color: nil, label: "")
Spacer()
SegmentedPicker(
["Real", "Logical", "Finder"],
selectedIndex: Binding(
get: {
switch sizeType {
case "Real": return 0
case "Logical": return 1
case "Finder": return 2
default: return 0
}
},
set: { newIndex in
withAnimation(.easeInOut(duration: 0.3)) {
switch newIndex {
case 0: sizeType = "Real"
case 1: sizeType = "Logical"
case 2: sizeType = "Finder"
default: sizeType = "Real"
}
}
}),
selectionAlignment: .bottom,
content: { item, isSelected in
Text(item)
.font(.callout)
.foregroundColor(isSelected ? Color("mode") : Color("mode").opacity(0.5) )
.padding(.horizontal)
.padding(.bottom, 5)
.frame(width: 75)
},
selection: {
VStack(spacing: 0) {
Spacer()
Color("pear").frame(height: 1)
}
})
} }
.padding(5) .padding(5)
.padding(.leading) .padding(.leading)
@@ -275,7 +324,7 @@ struct GeneralSettingsTab: View {
} }
.padding(20) .padding(20)
.frame(width: 500, height: 560) .frame(width: 500, height: 600)
} }
+47 -26
View File
@@ -31,18 +31,9 @@ struct InterfaceSettingsTab: View {
@Binding var showPopover: Bool @Binding var showPopover: Bool
@Binding var search: String @Binding var search: String
let icons = ["externaldrive", "trash", "folder", "pear-1", "pear-1.5", "pear-2", "pear-3", "pear-4"] let icons = ["externaldrive", "trash", "folder", "pear-1", "pear-1.5", "pear-2", "pear-3", "pear-4"]
private let themes = ["Auto", "Dark", "Light"]
// @State private var slate = Color(.sRGB, red: 0.188143, green: 0.208556, blue: 0.262679, opacity: 1)
// @State private var solarized = Color(.sRGB, red: 0.117257, green: 0.22506, blue: 0.249171, opacity: 1)
// @State private var dracula = Color(.sRGB, red: 0.268614, green: 0.264737, blue: 0.383503, opacity: 1)
var body: some View { var body: some View {
let slate = displayMode.colorScheme == .light ? Color(.sRGB, red: 0.499549, green: 0.545169, blue: 0.682028, opacity: 1) : Color(.sRGB, red: 0.188143, green: 0.208556, blue: 0.262679, opacity: 1)
let solarized = displayMode.colorScheme == .light ? Color(.sRGB, red: 0.554372, green: 0.6557, blue: 0.734336, opacity: 1) : Color(.sRGB, red: 0.117257, green: 0.22506, blue: 0.249171, opacity: 1)
let dracula = displayMode.colorScheme == .light ? Color(.sRGB, red: 0.567094, green: 0.562125, blue: 0.81285, opacity: 1) : Color(.sRGB, red: 0.268614, green: 0.264737, blue: 0.383503, opacity: 1)
Form { Form {
VStack { VStack {
@@ -92,23 +83,21 @@ struct InterfaceSettingsTab: View {
Spacer() Spacer()
Button("") { Button("") {
themeSettings.themeColor = slate themeSettings.setPreset(preset: "slate", colorScheme: displayMode)
themeSettings.saveThemeColor()
} }
.buttonStyle(PresetColor(fillColor: slate, label: "Slate")) .buttonStyle(PresetColor(fillColor: themeSettings.getColorForPreset(preset: "slate", colorScheme: displayMode), label: "Slate"))
Button("") { Button("") {
themeSettings.themeColor = dracula themeSettings.setPreset(preset: "dracula", colorScheme: displayMode)
themeSettings.saveThemeColor()
} }
.buttonStyle(PresetColor(fillColor: dracula, label: "Dracula")) .buttonStyle(PresetColor(fillColor: themeSettings.getColorForPreset(preset: "dracula", colorScheme: displayMode), label: "Dracula"))
.padding(.horizontal) .padding(.horizontal)
Button("") { Button("") {
themeSettings.themeColor = solarized themeSettings.setPreset(preset: "solarized", colorScheme: displayMode)
themeSettings.saveThemeColor() themeSettings.saveThemeColor()
} }
.buttonStyle(PresetColor(fillColor: solarized, label: "Solarized")) .buttonStyle(PresetColor(fillColor: themeSettings.getColorForPreset(preset: "solarized", colorScheme: displayMode), label: "Solarized"))
Spacer() Spacer()
@@ -151,18 +140,50 @@ struct InterfaceSettingsTab: View {
} }
InfoButton(text: "Changing the color mode will reset the base color to defaults", color: nil, label: "") InfoButton(text: "Changing the color mode will reset the base color to defaults", color: nil, label: "")
Spacer() Spacer()
Picker("", selection: $selectedTheme) { SegmentedPicker(
ForEach(themes, id: \.self) { theme in ["Auto", "Dark", "Light"],
Text(theme) selectedIndex: Binding(
get: {
switch selectedTheme {
case "Dark": return 1
case "Light": return 2
default: return 0
}
},
set: { newIndex in
withAnimation(.easeInOut(duration: 0.3)) {
switch newIndex {
case 1: selectedTheme = "Dark"
case 2: selectedTheme = "Light"
default: selectedTheme = "Auto"
} }
} }
.pickerStyle(SegmentedPickerStyle()) }),
.frame(width: 200) selectionAlignment: .bottom,
content: { item, isSelected in
Text(item)
.font(.callout)
.foregroundColor(isSelected ? Color("mode") : Color("mode").opacity(0.5) )
.padding(.horizontal)
.padding(.bottom, 5)
.frame(width: 65)
},
selection: {
VStack(spacing: 0) {
Spacer()
Color("pear").frame(height: 1)
}
})
.onChange(of: selectedTheme) { newTheme in .onChange(of: selectedTheme) { newTheme in
switch newTheme { switch newTheme {
case "Auto": case "Auto":
displayMode.colorScheme = nil if isDarkMode() {
themeSettings.resetToDefault(dark: isDarkModeEnabled()) displayMode.colorScheme = .dark
} else {
displayMode.colorScheme = .light
}
// displayMode.colorScheme = nil
themeSettings.resetToDefault(dark: isDarkMode())
// Refresh foreground colors // Refresh foreground colors
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.selectedTab = .interface self.selectedTab = .interface
@@ -172,7 +193,7 @@ struct InterfaceSettingsTab: View {
} }
case "Dark": case "Dark":
displayMode.colorScheme = .dark displayMode.colorScheme = .dark
themeSettings.resetToDefault(dark: true) // themeSettings.resetToDefault(dark: true)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.selectedTab = .interface self.selectedTab = .interface
} }
@@ -181,7 +202,7 @@ struct InterfaceSettingsTab: View {
} }
case "Light": case "Light":
displayMode.colorScheme = .light displayMode.colorScheme = .light
themeSettings.resetToDefault(dark: false) // themeSettings.resetToDefault(dark: false)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.selectedTab = .interface self.selectedTab = .interface
} }
+1 -2
View File
@@ -88,7 +88,7 @@ struct AppListItems: View {
if (isHovered || isSelected) && !mini { if (isHovered || isSelected) && !mini {
Triangle() Triangle()
.fill(isSelected ? themeSettings.themeColor : Color("mode").opacity(0.1)) .fill(themeSettings.themeColor)
.frame(width: isSelected ? 16 : 8, height: 30) .frame(width: isSelected ? 16 : 8, height: 30)
.padding(.leading, 5) .padding(.leading, 5)
.offset(x: 22) .offset(x: 22)
@@ -115,7 +115,6 @@ struct AppListItems: View {
} }
} }
} }
// .grayscale(opacityForItem(appInfo.path))
} }
+60 -40
View File
@@ -14,24 +14,36 @@ struct FilesView: View {
@State private var showPop: Bool = false @State private var showPop: Bool = false
@AppStorage("settings.general.mini") private var mini: Bool = false @AppStorage("settings.general.mini") private var mini: Bool = false
@AppStorage("settings.sentinel.enable") private var sentinel: Bool = false @AppStorage("settings.sentinel.enable") private var sentinel: Bool = false
@AppStorage("settings.general.instant") var instantSearch: Bool = true
@AppStorage("settings.general.brew") private var brew: Bool = false @AppStorage("settings.general.brew") private var brew: Bool = false
@AppStorage("settings.menubar.enabled") private var menubarEnabled: Bool = false @AppStorage("settings.menubar.enabled") private var menubarEnabled: Bool = false
@AppStorage("settings.general.selectedSort") var selectedSortAlpha: Bool = true @AppStorage("settings.general.selectedSort") var selectedSortAlpha: Bool = true
@AppStorage("settings.general.sizeType") var sizeType: String = "Real"
@State private var localKey = UUID()
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme
@Binding var showPopover: Bool @Binding var showPopover: Bool
@Binding var search: String @Binding var search: String
var regularWin: Bool var regularWin: Bool
// @State private var selectedOption = "Alpha"
@State private var elapsedTime = 0 @State private var elapsedTime = 0
@State private var timer: Timer? = nil @State private var timer: Timer? = nil
var body: some View { var body: some View {
let totalSelectedSize = appState.selectedItems.reduce(Int64(0)) { (result, url) in var totalSelectedSize: (real: String, logical: String, finder: String) {
result + (appState.appInfo.fileSize[url] ?? 0) var totalReal: Int64 = 0
var totalLogical: Int64 = 0
for url in appState.selectedItems {
let realSize = appState.appInfo.fileSize[url] ?? 0
let logicalSize = appState.appInfo.fileSizeLogical[url] ?? 0
totalReal += realSize
totalLogical += logicalSize
} }
return (real: formatByte(size: totalReal).human, logical: formatByte(size: totalLogical).human, finder: "\(formatByte(size: totalLogical).byte) (\(formatByte(size: totalReal).human))")
}
let displaySizeTotal = sizeType == "Real" ? formatByte(size: appState.appInfo.totalSize).human :
sizeType == "Logical" ? formatByte(size: appState.appInfo.totalSizeLogical).human :
"\(formatByte(size: appState.appInfo.totalSizeLogical).byte) (\(formatByte(size: appState.appInfo.totalSize).human))"
VStack(alignment: .center) { VStack(alignment: .center) {
if appState.showProgress { if appState.showProgress {
@@ -73,22 +85,6 @@ struct FilesView: View {
Spacer() Spacer()
if instantSearch {
Button("Rescan") {
updateOnMain {
appState.showProgress.toggle()
let pathFinder = AppPathFinder(appInfo: appState.appInfo, appState: appState, locations: locations) {
updateOnMain {
appState.showProgress = false
}
}
pathFinder.findPaths()
}
}
.buttonStyle(SimpleButtonStyle(icon: "arrow.counterclockwise.circle.fill", help: "Rescan files"))
}
Button("Close") { Button("Close") {
updateOnMain { updateOnMain {
appState.appInfo = AppInfo.empty appState.appInfo = AppInfo.empty
@@ -144,7 +140,8 @@ struct FilesView: View {
Spacer() Spacer()
VStack(alignment: .trailing, spacing: 5) { VStack(alignment: .trailing, spacing: 5) {
Text("\(formatByte(size: appState.appInfo.totalSize))").font(.title).fontWeight(.bold)
Text("\(displaySizeTotal)").font(.title).fontWeight(.bold).help("Total size on disk")
Text("\(appState.appInfo.fileSize.count == 1 ? "\(appState.selectedItems.count) / \(appState.appInfo.fileSize.count) item" : "\(appState.selectedItems.count) / \(appState.appInfo.fileSize.count) items")") Text("\(appState.appInfo.fileSize.count == 1 ? "\(appState.selectedItems.count) / \(appState.appInfo.fileSize.count) item" : "\(appState.selectedItems.count) / \(appState.appInfo.fileSize.count) items")")
.font(.callout).foregroundStyle((Color("mode").opacity(0.5))) .font(.callout).foregroundStyle((Color("mode").opacity(0.5)))
} }
@@ -161,10 +158,10 @@ struct FilesView: View {
// Item selection and sorting toolbar // Item selection and sorting toolbar
HStack() { HStack() {
Toggle("", isOn: Binding( Toggle("", isOn: Binding(
get: { self.appState.selectedItems.count == self.appState.appInfo.files.count }, get: { self.appState.selectedItems.count == self.appState.appInfo.fileSize.keys.count },
set: { newValue in set: { newValue in
updateOnMain { updateOnMain {
self.appState.selectedItems = newValue ? Set(self.appState.appInfo.files) : [] self.appState.selectedItems = newValue ? Set(self.appState.appInfo.fileSize.keys) : []
} }
} }
)) ))
@@ -212,7 +209,6 @@ struct FilesView: View {
Button("") { Button("") {
selectedSortAlpha.toggle() selectedSortAlpha.toggle()
// selectedOption = selectedOption == "Alpha" ? "Size" : "Alpha"
} }
.buttonStyle(SimpleButtonStyle(icon: selectedSortAlpha ? "textformat.abc" : "textformat.123", help: selectedSortAlpha ? "Sorted alphabetically" : "Sorted by size")) .buttonStyle(SimpleButtonStyle(icon: selectedSortAlpha ? "textformat.abc" : "textformat.123", help: selectedSortAlpha ? "Sorted alphabetically" : "Sorted by size"))
@@ -228,10 +224,9 @@ struct FilesView: View {
ScrollView() { ScrollView() {
LazyVStack { LazyVStack {
let sortedFilesSize = appState.appInfo.files.sorted(by: { appState.appInfo.fileSize[$0, default: 0] > appState.appInfo.fileSize[$1, default: 0] }) let sortedFilesSize = appState.appInfo.fileSize.keys.sorted(by: { appState.appInfo.fileSize[$0, default: 0] > appState.appInfo.fileSize[$1, default: 0] })
// let sortedFilesAlpha = appState.appInfo.files let sortedFilesAlpha = appState.appInfo.fileSize.keys.sorted { firstURL, secondURL in
let sortedFilesAlpha = appState.appInfo.files.sorted { firstURL, secondURL in
let isFirstPathApp = firstURL.pathExtension == "app" let isFirstPathApp = firstURL.pathExtension == "app"
let isSecondPathApp = secondURL.pathExtension == "app" let isSecondPathApp = secondURL.pathExtension == "app"
if isFirstPathApp, !isSecondPathApp { if isFirstPathApp, !isSecondPathApp {
@@ -247,10 +242,10 @@ struct FilesView: View {
let sort = selectedSortAlpha ? sortedFilesAlpha : sortedFilesSize let sort = selectedSortAlpha ? sortedFilesAlpha : sortedFilesSize
ForEach(Array(sort.enumerated()), id: \.element) { index, path in ForEach(Array(sort.enumerated()), id: \.element) { index, path in
if let fileSize = appState.appInfo.fileSize[path], let fileIcon = appState.appInfo.fileIcon[path] { if let fileSize = appState.appInfo.fileSize[path], let fileSizeL = appState.appInfo.fileSizeLogical[path], let fileIcon = appState.appInfo.fileIcon[path] {
let iconImage = fileIcon.map(Image.init(nsImage:)) let iconImage = fileIcon.map(Image.init(nsImage:))
VStack { VStack {
FileDetailsItem(size: fileSize, icon: iconImage, path: path) FileDetailsItem(size: fileSize, sizeL: fileSizeL, icon: iconImage, path: path)
if index < sort.count - 1 { if index < sort.count - 1 {
Divider().padding(.leading, 40).opacity(0.5) Divider().padding(.leading, 40).opacity(0.5)
} }
@@ -260,7 +255,12 @@ struct FilesView: View {
} }
.padding() .padding()
.onChange(of: sizeType) { _ in
localKey = UUID()
} }
.id(localKey)
}
Spacer() Spacer()
@@ -269,7 +269,7 @@ struct FilesView: View {
Spacer() Spacer()
Button("\(formatByte(size: totalSelectedSize))") { Button("\(sizeType == "Logical" ? totalSelectedSize.logical : sizeType == "Finder" ? totalSelectedSize.finder : totalSelectedSize.real)") {
Task { Task {
updateOnMain { updateOnMain {
search = "" search = ""
@@ -305,7 +305,6 @@ struct FilesView: View {
removeApp(appState: appState, withId: appState.appInfo.id) removeApp(appState: appState, withId: appState.appInfo.id)
} else { } else {
// Add deleted appInfo object to trashed array // Add deleted appInfo object to trashed array
appState.appInfo.files = []
appState.appInfo.fileSize = [:] appState.appInfo.fileSize = [:]
appState.trashedFiles.append(appState.appInfo) appState.trashedFiles.append(appState.appInfo)
@@ -346,7 +345,10 @@ struct FilesView: View {
struct FileDetailsItem: View { struct FileDetailsItem: View {
@EnvironmentObject var appState: AppState @EnvironmentObject var appState: AppState
@State private var isHovered = false
@AppStorage("settings.general.sizeType") var sizeType: String = "Real"
let size: Int64? let size: Int64?
let sizeL: Int64?
let icon: Image? let icon: Image?
let path: URL let path: URL
@@ -372,6 +374,8 @@ struct FileDetailsItem: View {
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.frame(width: 30, height: 30) .frame(width: 30, height: 30)
.clipShape(RoundedRectangle(cornerRadius: 8)) .clipShape(RoundedRectangle(cornerRadius: 8))
.brightness(isHovered ? 0.2 : 0)
.shadow(color: Color("mode"), radius: isHovered ? 2 : 0)
} }
VStack(alignment: .leading, spacing: 5) { VStack(alignment: .leading, spacing: 5) {
@@ -396,19 +400,35 @@ struct FileDetailsItem: View {
Spacer() Spacer()
Text(formatByte(size:size!)) let displaySize = sizeType == "Real" ? formatByte(size: size!).human :
sizeType == "Logical" ? formatByte(size: sizeL!).human :
"\(formatByte(size: sizeL!).byte) (\(formatByte(size: size!).human))"
Text("\(displaySize)")
}
Button("") { .onHover { hovering in
withAnimation(Animation.easeIn(duration: 0.2)) {
self.isHovered = hovering
}
}
.onTapGesture {
NSWorkspace.shared.selectFile(path.path, inFileViewerRootedAtPath: path.deletingLastPathComponent().path) NSWorkspace.shared.selectFile(path.path, inFileViewerRootedAtPath: path.deletingLastPathComponent().path)
} }
.buttonStyle(SimpleButtonStyle(icon: "folder.fill", help: "Show in Finder")) .contextMenu {
if path.pathExtension == "app" {
Button("Open \(path.deletingPathExtension().lastPathComponent)") {
NSWorkspace.shared.open(path)
}
Divider()
}
Button("Copy Path") {
copyToClipboard(text: path.path)
}
Button("View in Finder") {
NSWorkspace.shared.selectFile(path.path, inFileViewerRootedAtPath: path.deletingLastPathComponent().path)
}
}
} }
.background(
RoundedRectangle(cornerRadius: 10)
.fill(Color.white.opacity(0))
)
}
} }
+2 -17
View File
@@ -18,7 +18,6 @@ struct MenuBarMiniAppView: View {
@State private var showSys: Bool = true @State private var showSys: Bool = true
@State private var showUsr: Bool = true @State private var showUsr: Bool = true
@AppStorage("settings.general.mini") private var mini: Bool = false @AppStorage("settings.general.mini") private var mini: Bool = false
@AppStorage("settings.general.instant") private var instantSearch: Bool = true
@AppStorage("settings.general.glass") private var glass: Bool = true @AppStorage("settings.general.glass") private var glass: Bool = true
@AppStorage("settings.general.popover") private var popoverStay: Bool = true @AppStorage("settings.general.popover") private var popoverStay: Bool = true
@Binding var showPopover: Bool @Binding var showPopover: Bool
@@ -40,15 +39,7 @@ struct MenuBarMiniAppView: View {
if appState.reload { if appState.reload {
VStack { VStack {
Spacer() Spacer()
ProgressView(instantSearch ? "Refreshing app list and caching files" : "Refreshing app list") ProgressView("Refreshing app list")
if instantSearch {
Image(systemName: "bolt.fill")
.resizable()
.scaledToFit()
.foregroundStyle(Color("mode").opacity(0.5))
.frame(width: 30, height: 30)
.padding()
}
Spacer() Spacer()
} }
.padding(.vertical) .padding(.vertical)
@@ -70,13 +61,7 @@ struct MenuBarMiniAppView: View {
appState.currentView = .zombie appState.currentView = .zombie
appState.showProgress.toggle() appState.showProgress.toggle()
showPopover.toggle() showPopover.toggle()
if instantSearch { reversePreloader(allApps: appState.sortedApps, appState: appState, locations: locations, reverseAddon: true)
let reverse = ReversePathsSearcher(appState: appState, locations: locations)
reverse.reversePathsSearch()
// reversePathsSearch(appState: appState, locations: locations)
} else {
loadAllPaths(allApps: appState.sortedApps, appState: appState, locations: locations, reverseAddon: true)
}
} else { } else {
appState.currentView = .zombie appState.currentView = .zombie
showPopover.toggle() showPopover.toggle()
+1 -10
View File
@@ -155,7 +155,6 @@ struct MiniAppView: View {
@State private var showSys: Bool = true @State private var showSys: Bool = true
@State private var showUsr: Bool = true @State private var showUsr: Bool = true
@AppStorage("settings.general.mini") private var mini: Bool = false @AppStorage("settings.general.mini") private var mini: Bool = false
@AppStorage("settings.general.instant") private var instantSearch: Bool = true
@AppStorage("settings.general.glass") private var glass: Bool = true @AppStorage("settings.general.glass") private var glass: Bool = true
@Binding var showPopover: Bool @Binding var showPopover: Bool
@@ -175,15 +174,7 @@ struct MiniAppView: View {
if appState.reload { if appState.reload {
VStack { VStack {
Spacer() Spacer()
ProgressView(instantSearch ? "Refreshing app list and caching files" : "Refreshing app list") ProgressView("Refreshing app list")
if instantSearch {
Image(systemName: "bolt.fill")
.resizable()
.scaledToFit()
.foregroundStyle(Color("mode").opacity(0.5))
.frame(width: 30, height: 30)
.padding()
}
Spacer() Spacer()
} }
.padding(.vertical) .padding(.vertical)
@@ -0,0 +1,109 @@
//
// SegmentedPicker.swift
// Pearcleaner
//
// Created by Alin Lupascu on 4/17/24.
//
import SwiftUI
public struct SegmentedPicker<Element, Content, Selection>: View
where
Content: View,
Selection: View {
public typealias Data = [Element]
@State private var frames: [CGRect]
@Binding private var selectedIndex: Data.Index?
private let data: Data
private let selection: () -> Selection
private let content: (Data.Element, Bool) -> Content
private let selectionAlignment: VerticalAlignment
public init(_ data: Data,
selectedIndex: Binding<Data.Index?>,
selectionAlignment: VerticalAlignment = .center,
@ViewBuilder content: @escaping (Data.Element, Bool) -> Content,
@ViewBuilder selection: @escaping () -> Selection) {
self.data = data
self.content = content
self.selection = selection
self._selectedIndex = selectedIndex
self._frames = State(wrappedValue: Array(repeating: .zero,
count: data.count))
self.selectionAlignment = selectionAlignment
}
public var body: some View {
ZStack(alignment: Alignment(horizontal: .horizontalCenterAlignment,
vertical: selectionAlignment)) {
if let selectedIndex = selectedIndex {
selection()
.frame(width: frames[selectedIndex].width,
height: frames[selectedIndex].height)
.alignmentGuide(.horizontalCenterAlignment) { dimensions in
dimensions[HorizontalAlignment.center]
}
}
HStack(spacing: 0) {
ForEach(data.indices, id: \.self) { index in
Button(action: { selectedIndex = index },
label: { content(data[index], selectedIndex == index) }
)
.buttonStyle(PlainButtonStyle())
.background(GeometryReader { proxy in
Color.clear.onAppear { frames[index] = proxy.frame(in: .global) }
})
.alignmentGuide(.horizontalCenterAlignment,
isActive: selectedIndex == index) { dimensions in
dimensions[HorizontalAlignment.center]
}
}
}
}
}
}
extension HorizontalAlignment {
private enum CenterAlignmentID: AlignmentID {
static func defaultValue(in dimension: ViewDimensions) -> CGFloat {
return dimension[HorizontalAlignment.center]
}
}
static var horizontalCenterAlignment: HorizontalAlignment {
HorizontalAlignment(CenterAlignmentID.self)
}
}
extension View {
@ViewBuilder
@inlinable func alignmentGuide(_ alignment: HorizontalAlignment,
isActive: Bool,
computeValue: @escaping (ViewDimensions) -> CGFloat) -> some View {
if isActive {
alignmentGuide(alignment, computeValue: computeValue)
} else {
self
}
}
@ViewBuilder
@inlinable func alignmentGuide(_ alignment: VerticalAlignment,
isActive: Bool,
computeValue: @escaping (ViewDimensions) -> CGFloat) -> some View {
if isActive {
alignmentGuide(alignment, computeValue: computeValue)
} else {
self
}
}
}
+1 -19
View File
@@ -13,13 +13,10 @@ struct RegularMode: View {
@EnvironmentObject var themeSettings: ThemeSettings @EnvironmentObject var themeSettings: ThemeSettings
@AppStorage("settings.general.glass") private var glass: Bool = false @AppStorage("settings.general.glass") private var glass: Bool = false
@AppStorage("settings.general.sidebarWidth") private var sidebarWidth: Double = 280 @AppStorage("settings.general.sidebarWidth") private var sidebarWidth: Double = 280
@AppStorage("settings.general.instant") private var instantSearch: Bool = true
@Binding var search: String @Binding var search: String
@State private var showSys: Bool = true @State private var showSys: Bool = true
@State private var showUsr: Bool = true @State private var showUsr: Bool = true
@Binding var showPopover: Bool @Binding var showPopover: Bool
// @State private var isHovering = false
// @State private var isHovering2 = false
var filteredApps: [AppInfo] { var filteredApps: [AppInfo] {
@@ -30,7 +27,6 @@ struct RegularMode: View {
} }
} }
// let slate = Color(.sRGB, red: 0.188143, green: 0.208556, blue: 0.262679, opacity: 1)
var body: some View { var body: some View {
@@ -41,13 +37,7 @@ struct RegularMode: View {
if appState.reload { if appState.reload {
VStack { VStack {
Spacer() Spacer()
ProgressView(instantSearch ? "Refreshing app list and caching files" : "Refreshing app list") ProgressView("Refreshing app list")
if instantSearch {
Image(systemName: "bolt")
.resizable()
.scaledToFit()
.frame(width: 30, height: 30)
}
Spacer() Spacer()
} }
.frame(width: sidebarWidth) .frame(width: sidebarWidth)
@@ -159,7 +149,6 @@ struct Header: View {
@EnvironmentObject var locations: Locations @EnvironmentObject var locations: Locations
@EnvironmentObject var fsm: FolderSettingsManager @EnvironmentObject var fsm: FolderSettingsManager
@Binding var showPopover: Bool @Binding var showPopover: Bool
@AppStorage("settings.general.instant") private var instantSearch: Bool = true
@AppStorage("settings.general.glass") private var glass: Bool = true @AppStorage("settings.general.glass") private var glass: Bool = true
@@ -198,15 +187,8 @@ struct Header: View {
let sortedApps = getSortedApps(paths: fsm.folderPaths, appState: appState) let sortedApps = getSortedApps(paths: fsm.folderPaths, appState: appState)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
appState.sortedApps = sortedApps appState.sortedApps = sortedApps
if instantSearch {
loadAllPaths(allApps: sortedApps, appState: appState, locations: locations) {
appState.reload.toggle() appState.reload.toggle()
} }
} else {
appState.reload.toggle()
}
}
} }
} }
.frame(minHeight: 20) .frame(minHeight: 20)
+2 -14
View File
@@ -13,7 +13,6 @@ struct TopBar: View {
@AppStorage("settings.general.glass") private var glass: Bool = false @AppStorage("settings.general.glass") private var glass: Bool = false
@AppStorage("settings.sentinel.enable") private var sentinel: Bool = false @AppStorage("settings.sentinel.enable") private var sentinel: Bool = false
@AppStorage("settings.general.mini") private var mini: Bool = false @AppStorage("settings.general.mini") private var mini: Bool = false
@AppStorage("settings.general.instant") private var instantSearch: Bool = true
@EnvironmentObject var appState: AppState @EnvironmentObject var appState: AppState
@Binding var showPopover: Bool @Binding var showPopover: Bool
@EnvironmentObject var locations: Locations @EnvironmentObject var locations: Locations
@@ -47,12 +46,7 @@ struct TopBar: View {
updateOnMain { updateOnMain {
appState.zombieFile = .empty appState.zombieFile = .empty
appState.showProgress.toggle() appState.showProgress.toggle()
if instantSearch { reversePreloader(allApps: appState.sortedApps, appState: appState, locations: locations, reverseAddon: true)
let reverse = ReversePathsSearcher(appState: appState, locations: locations)
reverse.reversePathsSearch()
} else {
loadAllPaths(allApps: appState.sortedApps, appState: appState, locations: locations, reverseAddon: true)
}
} }
} }
.buttonStyle(SimpleButtonStyle(icon: "arrow.counterclockwise.circle.fill", help: "Rescan files")) .buttonStyle(SimpleButtonStyle(icon: "arrow.counterclockwise.circle.fill", help: "Rescan files"))
@@ -68,13 +62,7 @@ struct TopBar: View {
appState.currentView = .zombie appState.currentView = .zombie
appState.showProgress.toggle() appState.showProgress.toggle()
showPopover.toggle() showPopover.toggle()
if instantSearch { reversePreloader(allApps: appState.sortedApps, appState: appState, locations: locations, reverseAddon: true)
let reverse = ReversePathsSearcher(appState: appState, locations: locations)
reverse.reversePathsSearch()
// reversePathsSearch(appState: appState, locations: locations)
} else {
loadAllPaths(allApps: appState.sortedApps, appState: appState, locations: locations, reverseAddon: true)
}
} else { } else {
appState.currentView = .zombie appState.currentView = .zombie
showPopover.toggle() showPopover.toggle()
+3 -15
View File
@@ -12,7 +12,6 @@ struct TopBarMini: View {
@AppStorage("displayMode") var displayMode: DisplayMode = .system @AppStorage("displayMode") var displayMode: DisplayMode = .system
@AppStorage("settings.sentinel.enable") private var sentinel: Bool = false @AppStorage("settings.sentinel.enable") private var sentinel: Bool = false
@AppStorage("settings.general.mini") private var mini: Bool = false @AppStorage("settings.general.mini") private var mini: Bool = false
@AppStorage("settings.general.instant") private var instantSearch: Bool = true
@Binding var search: String @Binding var search: String
@Binding var showPopover: Bool @Binding var showPopover: Bool
@EnvironmentObject var appState: AppState @EnvironmentObject var appState: AppState
@@ -36,13 +35,7 @@ struct TopBarMini: View {
appState.currentView = .zombie appState.currentView = .zombie
appState.showProgress.toggle() appState.showProgress.toggle()
showPopover.toggle() showPopover.toggle()
if instantSearch { reversePreloader(allApps: appState.sortedApps, appState: appState, locations: locations, reverseAddon: true)
let reverse = ReversePathsSearcher(appState: appState, locations: locations)
reverse.reversePathsSearch()
// reversePathsSearch(appState: appState, locations: locations)
} else {
loadAllPaths(allApps: appState.sortedApps, appState: appState, locations: locations, reverseAddon: true)
}
} else { } else {
appState.currentView = .zombie appState.currentView = .zombie
showPopover.toggle() showPopover.toggle()
@@ -79,12 +72,7 @@ struct TopBarMini: View {
appState.currentView = .zombie appState.currentView = .zombie
appState.showProgress.toggle() appState.showProgress.toggle()
showPopover.toggle() showPopover.toggle()
if instantSearch { reversePreloader(allApps: appState.sortedApps, appState: appState, locations: locations, reverseAddon: true)
let reverse = ReversePathsSearcher(appState: appState, locations: locations)
reverse.reversePathsSearch()
} else {
loadAllPaths(allApps: appState.sortedApps, appState: appState, locations: locations, reverseAddon: true)
}
} else { } else {
appState.currentView = .zombie appState.currentView = .zombie
showPopover.toggle() showPopover.toggle()
@@ -133,7 +121,7 @@ struct SearchBarMiniBottom: View {
var body: some View { var body: some View {
HStack { HStack {
TextField("\(appState.instantTotal > appState.instantProgress ? " Searching the file system" : " Search")", text: $search) TextField(" Search", text: $search)
.textFieldStyle(SimpleSearchStyle(trash: true, text: $search)) .textFieldStyle(SimpleSearchStyle(trash: true, text: $search))
} }
} }
+71 -33
View File
@@ -14,9 +14,10 @@ struct ZombieView: View {
@State private var showPop: Bool = false @State private var showPop: Bool = false
@AppStorage("settings.general.mini") private var mini: Bool = false @AppStorage("settings.general.mini") private var mini: Bool = false
@AppStorage("settings.sentinel.enable") private var sentinel: Bool = false @AppStorage("settings.sentinel.enable") private var sentinel: Bool = false
@AppStorage("settings.general.instant") private var instantSearch: Bool = true
@AppStorage("settings.menubar.enabled") private var menubarEnabled: Bool = false @AppStorage("settings.menubar.enabled") private var menubarEnabled: Bool = false
@AppStorage("settings.general.selectedSort") var selectedSortAlpha: Bool = true @AppStorage("settings.general.selectedSort") var selectedSortAlpha: Bool = true
@AppStorage("settings.general.sizeType") var sizeType: String = "Real"
@State private var localKey = UUID()
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme
@Binding var showPopover: Bool @Binding var showPopover: Bool
@Binding var search: String @Binding var search: String
@@ -27,28 +28,45 @@ struct ZombieView: View {
var body: some View { var body: some View {
let totalSelectedZombieSize = appState.selectedZombieItems.reduce(Int64(0)) { (result, url) in let totalSelectedZombieSize: (real: String, logical: String, finder: String) = {
result + (appState.zombieFile.fileSize[url] ?? 0) var totalReal: Int64 = 0
} var totalLogical: Int64 = 0
let filteredAndSortedFiles: ([URL], Int64) = { for url in appState.selectedZombieItems {
let filteredFiles = appState.zombieFile.fileSize.filter { (url, _) in let realSize = appState.zombieFile.fileSize[url] ?? 0
let logicalSize = appState.zombieFile.fileSizeLogical[url] ?? 0
totalReal += realSize
totalLogical += logicalSize
}
return (formatByte(size: totalReal).human, formatByte(size:totalLogical).human, "\(formatByte(size: totalLogical).byte) (\(formatByte(size: totalReal).human))")
}()
let filteredAndSortedFiles: ([URL], Int64, Int64) = {
let fileSizeReal = appState.zombieFile.fileSize
let fileSizeLogical = appState.zombieFile.fileSizeLogical
let filteredFilesReal = fileSizeReal.filter { (url, _) in
searchZ.isEmpty || url.lastPathComponent.localizedCaseInsensitiveContains(searchZ) searchZ.isEmpty || url.lastPathComponent.localizedCaseInsensitiveContains(searchZ)
} }
let filteredFilesLogical = fileSizeLogical.filter { (url, _) in
let sortedFilteredFiles = filteredFiles.sorted(by: { searchZ.isEmpty || url.lastPathComponent.localizedCaseInsensitiveContains(searchZ)
}
let filesToSort = (sizeType == "Real" || sizeType == "Finder" ? filteredFilesReal : filteredFilesLogical)
let sortedFilteredFiles = filesToSort.sorted(by: {
if selectedSortAlpha { if selectedSortAlpha {
return $0.key.lastPathComponent.pearFormat() < $1.key.lastPathComponent.pearFormat() return $0.key.lastPathComponent.pearFormat() < $1.key.lastPathComponent.pearFormat()
} else { } else {
return $0.value > $1.value return $0.value > $1.value
} }
}).map { $0.key } }).map { $0.key }
let totalSize = filteredFilesReal.values.reduce(0, +)
let totalSize = filteredFiles.values.reduce(0, +) let totalSizeL = filteredFilesLogical.values.reduce(0, +)
return (sortedFilteredFiles, totalSize, totalSizeL)
return (sortedFilteredFiles, totalSize)
}() }()
let displaySizeTotal = sizeType == "Real" ? formatByte(size: filteredAndSortedFiles.1).human :
sizeType == "Logical" ? formatByte(size: filteredAndSortedFiles.2).human :
"\(formatByte(size: filteredAndSortedFiles.2).byte) (\(formatByte(size: filteredAndSortedFiles.1).human))"
VStack(alignment: .center) { VStack(alignment: .center) {
if appState.showProgress { if appState.showProgress {
VStack { VStack {
@@ -62,6 +80,7 @@ struct ZombieView: View {
.progressViewStyle(.linear) .progressViewStyle(.linear)
.frame(width: 400, height: 10) .frame(width: 400, height: 10)
Text("\(elapsedTime)") Text("\(elapsedTime)")
.font(.title).monospacedDigit() .font(.title).monospacedDigit()
.foregroundStyle(Color("mode").opacity(0.5)) .foregroundStyle(Color("mode").opacity(0.5))
@@ -97,13 +116,7 @@ struct ZombieView: View {
updateOnMain { updateOnMain {
appState.zombieFile = .empty appState.zombieFile = .empty
appState.showProgress.toggle() appState.showProgress.toggle()
if instantSearch { reversePreloader(allApps: appState.sortedApps, appState: appState, locations: locations, reverseAddon: true)
let reverse = ReversePathsSearcher(appState: appState, locations: locations)
reverse.reversePathsSearch()
// reversePathsSearch(appState: appState, locations: locations)
} else {
loadAllPaths(allApps: appState.sortedApps, appState: appState, locations: locations, reverseAddon: true)
}
} }
} }
.buttonStyle(SimpleButtonStyle(icon: "arrow.counterclockwise.circle.fill", help: "Rescan files")) .buttonStyle(SimpleButtonStyle(icon: "arrow.counterclockwise.circle.fill", help: "Rescan files"))
@@ -142,14 +155,14 @@ struct ZombieView: View {
Spacer() Spacer()
} }
Text("Files and folders remaining from previously installed applications") Text("Remaining files and folders from previous installs")
.font(.callout).foregroundStyle(Color("mode").opacity(0.5)) .font(.callout).foregroundStyle(Color("mode").opacity(0.5))
} }
Spacer() Spacer()
VStack(alignment: .trailing, spacing: 5) { VStack(alignment: .trailing, spacing: 5) {
Text("\(formatByte(size: filteredAndSortedFiles.1))").font(.title).fontWeight(.bold) Text("\(displaySizeTotal)").font(.title).fontWeight(.bold).help("Total size on disk")
Text("\(appState.zombieFile.fileSize.count == 1 ? "\(appState.selectedZombieItems.count) / \(appState.zombieFile.fileSize.count) item" : "\(appState.selectedZombieItems.count) / \(appState.zombieFile.fileSize.count) items")") Text("\(appState.zombieFile.fileSize.count == 1 ? "\(appState.selectedZombieItems.count) / \(appState.zombieFile.fileSize.count) item" : "\(appState.selectedZombieItems.count) / \(appState.zombieFile.fileSize.count) items")")
.font(.callout).foregroundStyle(Color("mode").opacity(0.5)) .font(.callout).foregroundStyle(Color("mode").opacity(0.5))
@@ -195,10 +208,10 @@ struct ZombieView: View {
ScrollView() { ScrollView() {
LazyVStack { LazyVStack {
ForEach(Array(filteredAndSortedFiles.0.enumerated()), id: \.element) { index, file in ForEach(Array(filteredAndSortedFiles.0.enumerated()), id: \.element) { index, file in
if let fileSize = appState.zombieFile.fileSize[file], let fileIcon = appState.zombieFile.fileIcon[file] { if let fileSize = appState.zombieFile.fileSize[file], let fileSizeL = appState.zombieFile.fileSizeLogical[file], let fileIcon = appState.zombieFile.fileIcon[file] {
let iconImage = fileIcon.map(Image.init(nsImage:)) let iconImage = fileIcon.map(Image.init(nsImage:))
VStack { VStack {
ZombieFileDetailsItem(size: fileSize, icon: iconImage, path: file) ZombieFileDetailsItem(size: fileSize, sizeL: fileSizeL, icon: iconImage, path: file)
.padding(.trailing) .padding(.trailing)
if index < filteredAndSortedFiles.0.count - 1 { if index < filteredAndSortedFiles.0.count - 1 {
@@ -210,7 +223,12 @@ struct ZombieView: View {
} }
.padding() .padding()
.onChange(of: sizeType) { _ in
localKey = UUID()
} }
.id(localKey)
}
Spacer() Spacer()
@@ -218,7 +236,7 @@ struct ZombieView: View {
Spacer() Spacer()
Button("\(formatByte(size: totalSelectedZombieSize))") { Button("\(sizeType == "Logical" ? totalSelectedZombieSize.logical : sizeType == "Finder" ? totalSelectedZombieSize.finder : totalSelectedZombieSize.real)") {
Task { Task {
updateOnMain { updateOnMain {
appState.zombieFile = .empty appState.zombieFile = .empty
@@ -271,7 +289,10 @@ struct ZombieView: View {
struct ZombieFileDetailsItem: View { struct ZombieFileDetailsItem: View {
@EnvironmentObject var appState: AppState @EnvironmentObject var appState: AppState
@State private var isHovered = false
@AppStorage("settings.general.sizeType") var sizeType: String = "Real"
let size: Int64? let size: Int64?
let sizeL: Int64?
let icon: Image? let icon: Image?
let path: URL let path: URL
@@ -295,6 +316,8 @@ struct ZombieFileDetailsItem: View {
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.frame(width: 30, height: 30) .frame(width: 30, height: 30)
.clipShape(RoundedRectangle(cornerRadius: 8)) .clipShape(RoundedRectangle(cornerRadius: 8))
.brightness(isHovered ? 0.2 : 0)
.shadow(color: Color("mode"), radius: isHovered ? 2 : 0)
} }
VStack(alignment: .leading, spacing: 5) { VStack(alignment: .leading, spacing: 5) {
@@ -313,20 +336,35 @@ struct ZombieFileDetailsItem: View {
Spacer() Spacer()
Text(formatByte(size:size!)) let displaySize = sizeType == "Real" ? formatByte(size: size!).human :
sizeType == "Logical" ? formatByte(size: sizeL!).human :
"\(formatByte(size: sizeL!).byte) (\(formatByte(size: size!).human))"
Text("\(displaySize)")
}
Button("") { .onHover { hovering in
withAnimation(Animation.easeIn(duration: 0.2)) {
self.isHovered = hovering
}
}
.onTapGesture {
NSWorkspace.shared.selectFile(path.path, inFileViewerRootedAtPath: path.deletingLastPathComponent().path)
}
.contextMenu {
if path.pathExtension == "app" {
Button("Open \(path.deletingPathExtension().lastPathComponent)") {
NSWorkspace.shared.open(path)
}
Divider()
}
Button("Copy Path") {
copyToClipboard(text: path.path)
}
Button("View in Finder") {
NSWorkspace.shared.selectFile(path.path, inFileViewerRootedAtPath: path.deletingLastPathComponent().path) NSWorkspace.shared.selectFile(path.path, inFileViewerRootedAtPath: path.deletingLastPathComponent().path)
} }
.buttonStyle(SimpleButtonStyle(icon: "folder.fill", help: "Show in Finder"))
} }
.background(
RoundedRectangle(cornerRadius: 10)
.fill(Color.white.opacity(0))
)
} }
} }