mirror of
https://github.com/exituser/Pearcleaner.git
synced 2026-09-17 09:38:51 +00:00
updates
This commit is contained in:
@@ -8,7 +8,6 @@
|
||||
import SwiftUI
|
||||
|
||||
struct AppCommands: Commands {
|
||||
@AppStorage("settings.general.instant") private var instantSearch: Bool = true
|
||||
|
||||
let appState: AppState
|
||||
let locations: Locations
|
||||
@@ -42,15 +41,7 @@ struct AppCommands: Commands {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
appState.sortedApps = []
|
||||
appState.sortedApps = sortedApps
|
||||
if instantSearch {
|
||||
Task(priority: .high){
|
||||
loadAllPaths(allApps: sortedApps, appState: appState, locations: locations) {
|
||||
appState.reload.toggle()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
appState.reload.toggle()
|
||||
}
|
||||
appState.reload.toggle()
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
@@ -78,8 +69,7 @@ struct AppCommands: Commands {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||
appState.sortedApps = sortedApps
|
||||
for app in appState.trashedFiles {
|
||||
let pathFinder = AppPathFinder(appInfo: app, appState: appState, locations: locations)
|
||||
pathFinder.findPaths()
|
||||
AppPathFinder(appInfo: app, appState: appState, locations: locations).findPaths()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ class AppInfoFetcher {
|
||||
let system = !path.path.contains(NSHomeDirectory())
|
||||
|
||||
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? {
|
||||
|
||||
@@ -9,6 +9,7 @@ import Foundation
|
||||
import AppKit
|
||||
import SwiftUI
|
||||
|
||||
|
||||
class AppPathFinder {
|
||||
private var appInfo: AppInfo
|
||||
private var appState: AppState
|
||||
@@ -18,7 +19,6 @@ class AppPathFinder {
|
||||
private var completion: () -> Void = {}
|
||||
private var collection: [URL] = []
|
||||
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 = {}) {
|
||||
self.appInfo = appInfo
|
||||
@@ -124,13 +124,24 @@ class AppPathFinder {
|
||||
collectionAccessQueue.sync {
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
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 }
|
||||
@@ -138,85 +149,20 @@ class AppPathFinder {
|
||||
let nameL = self.appInfo.appName.pearFormat()
|
||||
let nameP = self.appInfo.path.lastPathComponent.replacingOccurrences(of: ".app", with: "")
|
||||
|
||||
let exclusions = [
|
||||
"comappledtxcode": ["comrobotsandpencilsxcodesapp", "comoneminutegamesxcodecleaner", "iohyperappxcodecleaner", "xcodesjson"],
|
||||
"comrobotsandpencilsxcodesapp": ["comappledtxcode", "comoneminutegamesxcodecleaner", "iohyperappxcodecleaner"],
|
||||
"iohyperappxcodecleaner": ["comrobotsandpencilsxcodesapp", "comoneminutegamesxcodecleaner", "comappledtxcode", "xcodesjson"]
|
||||
]
|
||||
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 let excludedApps = exclusions[bundleIdentifierL], excludedApps.contains(where: { itemL.contains($0) }) {
|
||||
return false
|
||||
}
|
||||
|
||||
if bundleIdentifierL.contains("comappledtxcode") {
|
||||
// Include items that are part of the Xcode ecosystem
|
||||
if itemL.contains("comappledt") || itemL.contains("xcode") || itemL.contains("simulator") {
|
||||
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
|
||||
if hasExcludeKeyword {
|
||||
return false
|
||||
}
|
||||
if hasIncludeKeyword {
|
||||
if !condition.exclude.contains(where: itemL.contains) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,37 +209,35 @@ class AppPathFinder {
|
||||
|
||||
private func handleOutliers() -> [URL] {
|
||||
var outliers: [URL] = []
|
||||
let bundleIdentifier = self.appInfo.bundleIdentifier.pearFormat()
|
||||
|
||||
// Handle VSCode folder
|
||||
if self.appInfo.bundleIdentifier.pearFormat().contains("commicrosoftvscode") {
|
||||
let appSupportCodePath = "\(home)/Library/Application Support/Code"
|
||||
if let codeDirectoryURL = URL(string: appSupportCodePath), FileManager.default.fileExists(atPath: codeDirectoryURL.path) {
|
||||
outliers.append(codeDirectoryURL)
|
||||
}
|
||||
// Find conditions that match the current app's bundle identifier
|
||||
let matchingConditions = conditions.filter { condition in
|
||||
bundleIdentifier.contains(condition.bundle_id)
|
||||
}
|
||||
|
||||
// Handle Xcode folder
|
||||
if self.appInfo.bundleIdentifier.pearFormat().contains("comappledtxcode") {
|
||||
let simulators = "\(home)/Library/Containers/com.apple.iphonesimulator.ShareExtension"
|
||||
if let simulatorsURL = URL(string: simulators), FileManager.default.fileExists(atPath: simulatorsURL.path) {
|
||||
outliers.append(simulatorsURL)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add other outlier cases here as needed
|
||||
|
||||
return outliers
|
||||
}
|
||||
|
||||
private func finalizeCollection() {
|
||||
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()
|
||||
var tempCollection: [URL] = []
|
||||
self.collectionAccessQueue.sync {
|
||||
tempCollection = self.collection
|
||||
}
|
||||
tempCollection.append(contentsOf: groupContainers)
|
||||
// tempCollection.append(contentsOf: groupContainers)
|
||||
tempCollection.append(contentsOf: outliers)
|
||||
|
||||
// Sort and standardize URLs to ensure consistent comparisons
|
||||
@@ -318,10 +262,13 @@ class AppPathFinder {
|
||||
private func handlePostProcessing(sortedCollection: [URL]) {
|
||||
// Calculate file details (sizes and icons), update app state, and call completion
|
||||
var fileSize: [URL: Int64] = [:]
|
||||
var fileSizeLogical: [URL: Int64] = [:]
|
||||
var fileIcon: [URL: NSImage?] = [:]
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -332,8 +279,8 @@ class AppPathFinder {
|
||||
}
|
||||
|
||||
// Update appInfo and appState with the new values
|
||||
self.appInfo.files = updatedCollection
|
||||
self.appInfo.fileSize = fileSize
|
||||
self.appInfo.fileSizeLogical = fileSizeLogical
|
||||
self.appInfo.fileIcon = fileIcon
|
||||
|
||||
if !self.backgroundRun {
|
||||
@@ -341,12 +288,6 @@ class AppPathFinder {
|
||||
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
|
||||
if self.reverseAddon {
|
||||
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)
|
||||
//}
|
||||
|
||||
|
||||
@@ -29,9 +29,6 @@ class AppState: ObservableObject
|
||||
@Published var progressBar: (String, Double) = ("Ready", 0.0)
|
||||
@Published var reload: 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
|
||||
|
||||
|
||||
@@ -48,12 +45,14 @@ class AppState: ObservableObject
|
||||
system: false,
|
||||
files: [],
|
||||
fileSize: [:],
|
||||
fileSizeLogical: [:],
|
||||
fileIcon: [:]
|
||||
)
|
||||
|
||||
self.zombieFile = ZombieFile(
|
||||
id: UUID(),
|
||||
fileSize: [:],
|
||||
fileSizeLogical: [:],
|
||||
fileIcon: [:]
|
||||
)
|
||||
updateExtensionStatus()
|
||||
@@ -91,14 +90,19 @@ struct AppInfo: Identifiable, Equatable, Hashable {
|
||||
let system: Bool
|
||||
var files: [URL]
|
||||
var fileSize: [URL:Int64]
|
||||
var fileSizeLogical: [URL:Int64]
|
||||
var fileIcon: [URL:NSImage?]
|
||||
var totalSize: Int64
|
||||
{
|
||||
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 {
|
||||
let id: UUID
|
||||
var fileSize: [URL:Int64]
|
||||
var fileSizeLogical: [URL:Int64]
|
||||
var fileIcon: [URL:NSImage?]
|
||||
var totalSize: Int64
|
||||
{
|
||||
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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -19,8 +19,6 @@ class Locations: ObservableObject {
|
||||
|
||||
var apps: Category
|
||||
var reverse: Category
|
||||
// var widgets: Category
|
||||
// var plugins: Category
|
||||
|
||||
init() {
|
||||
let (cacheDir, tempDir) = darwinCT()
|
||||
@@ -103,35 +101,5 @@ class Locations: ObservableObject {
|
||||
cacheDir,
|
||||
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",
|
||||
// ])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import SwiftUI
|
||||
|
||||
// Get all apps from /Applications and ~/Applications
|
||||
func getSortedApps(paths: [String], appState: AppState) -> [AppInfo] {
|
||||
@AppStorage("settings.general.instant") var instantSearch: Bool = true
|
||||
let fileManager = FileManager.default
|
||||
var apps: [URL] = []
|
||||
|
||||
@@ -42,11 +41,6 @@ func getSortedApps(paths: [String], appState: AppState) -> [AppInfo] {
|
||||
.compactMap { AppInfoFetcher.getAppInfo(atPath: $0) }
|
||||
.sorted { $0.appName.replacingOccurrences(of: ".", with: "").lowercased() < $1.appName.replacingOccurrences(of: ".", with: "").lowercased() }
|
||||
|
||||
if instantSearch {
|
||||
updateOnMain {
|
||||
appState.instantTotal = Double(sortedApps.count)
|
||||
}
|
||||
}
|
||||
|
||||
return sortedApps
|
||||
}
|
||||
@@ -116,15 +110,14 @@ func listAppSupportDirectories() -> [String] {
|
||||
|
||||
|
||||
// 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()
|
||||
appState.appInfoStore.removeAll()
|
||||
|
||||
for app in allApps {
|
||||
dispatchGroup.enter()
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
let pathFinder = AppPathFinder(appInfo: app, appState: appState, locations: locations, backgroundRun: true, reverseAddon: reverseAddon)
|
||||
pathFinder.findPaths()
|
||||
AppPathFinder(appInfo: app, appState: appState, locations: locations, backgroundRun: true, reverseAddon: reverseAddon).findPaths()
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
}
|
||||
@@ -134,28 +127,17 @@ func loadAllPaths(allApps: [AppInfo], appState: AppState, locations: Locations,
|
||||
func checkAllAppsProcessed(retryCount: Int = 0, maxRetry: Int = 120) {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
if appState.appInfoStore.count == allApps.count {
|
||||
if reverseAddon {
|
||||
let reverse = ReversePathsSearcher(appState: appState, locations: locations)
|
||||
reverse.reversePathsSearch() {
|
||||
updateOnMain {
|
||||
appState.showProgress = false
|
||||
}
|
||||
ReversePathsSearcher(appState: appState, locations: locations).reversePathsSearch() {
|
||||
updateOnMain {
|
||||
appState.showProgress = false
|
||||
}
|
||||
}
|
||||
// Reset progress values to 0
|
||||
updateOnMain {
|
||||
appState.instantProgress = 0
|
||||
appState.instantTotal = 0
|
||||
appState.showProgress = false
|
||||
}
|
||||
completion()
|
||||
} else if retryCount < maxRetry {
|
||||
checkAllAppsProcessed(retryCount: retryCount + 1, maxRetry: maxRetry)
|
||||
} else {
|
||||
// Reset progress values to 0
|
||||
updateOnMain {
|
||||
appState.instantProgress = 0
|
||||
appState.instantTotal = 0
|
||||
appState.showProgress = false
|
||||
}
|
||||
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
|
||||
|
||||
// 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 {
|
||||
// Update the progress indicator on the main thread once the search completes.
|
||||
appState.showProgress = false
|
||||
}
|
||||
}
|
||||
pathFinder.findPaths()
|
||||
}.findPaths()
|
||||
|
||||
appState.appInfo = appInfo
|
||||
|
||||
// Animate the view change and popover display.
|
||||
|
||||
@@ -14,16 +14,17 @@ class ReversePathsSearcher {
|
||||
private let fileManager = FileManager.default
|
||||
private var collection: [URL] = []
|
||||
private var fileSize: [URL: Int64] = [:]
|
||||
private var fileSizeLogical: [URL: Int64] = [:]
|
||||
private var fileIcon: [URL: NSImage?] = [:]
|
||||
private var skipped: Set<String>
|
||||
private let dispatchGroup = DispatchGroup()
|
||||
|
||||
init(appState: AppState, locations: Locations) {
|
||||
self.appState = appState
|
||||
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 = {}) {
|
||||
Task(priority: .high) {
|
||||
self.processLocations()
|
||||
@@ -34,17 +35,19 @@ class ReversePathsSearcher {
|
||||
}
|
||||
|
||||
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() }
|
||||
|
||||
for location in locations.reverse.paths where fileManager.fileExists(atPath: location) {
|
||||
dispatchGroup.enter()
|
||||
processLocation(location, allPaths: allPaths, allNames: allNames)
|
||||
dispatchGroup.leave()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private func processLocation(_ location: String, allPaths: [String], allNames: [String]) {
|
||||
|
||||
do {
|
||||
let contents = try fileManager.contentsOfDirectory(atPath: location)
|
||||
contents.forEach { itemName in
|
||||
@@ -61,8 +64,7 @@ class ReversePathsSearcher {
|
||||
let itemPath = itemURL.path.pearFormat()
|
||||
let itemLastPathComponent = itemURL.lastPathComponent.pearFormat()
|
||||
|
||||
guard !skipped.contains(where: { formattedItemName.contains($0) }),
|
||||
// !allPaths.contains(itemPath),
|
||||
guard !skipReverse.contains(where: { formattedItemName.contains($0) }),
|
||||
!allPaths.contains(where: { $0 == itemPath || $0.hasSuffix("/\(itemLastPathComponent)") }),
|
||||
!allNames.contains(formattedItemName),
|
||||
isSupportedFileType(at: itemURL.path) else {
|
||||
@@ -74,7 +76,9 @@ class ReversePathsSearcher {
|
||||
|
||||
private func calculateFileDetails() {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -83,6 +87,7 @@ class ReversePathsSearcher {
|
||||
dispatchGroup.notify(queue: .main) {
|
||||
var updatedZombieFile = ZombieFile.empty
|
||||
updatedZombieFile.fileSize = self.fileSize
|
||||
updatedZombieFile.fileSizeLogical = self.fileSizeLogical
|
||||
updatedZombieFile.fileIcon = self.fileIcon
|
||||
self.appState.zombieFile = updatedZombieFile
|
||||
self.appState.showProgress = false
|
||||
|
||||
@@ -441,56 +441,11 @@ struct SimpleSearchStyle: TextFieldStyle {
|
||||
}
|
||||
|
||||
}
|
||||
.disabled(appState.instantProgress != 0 && appState.instantProgress != appState.instantTotal)
|
||||
.padding(6)
|
||||
.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)
|
||||
.strokeBorder(Color("mode").opacity(0.2), lineWidth: 1)
|
||||
.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()
|
||||
//// }
|
||||
// }
|
||||
|
||||
|
||||
}
|
||||
RoundedRectangle(cornerRadius: 6)
|
||||
.strokeBorder(Color("mode").opacity(0.2), lineWidth: 1)
|
||||
.allowsHitTesting(false)
|
||||
)
|
||||
.onHover { hovering in
|
||||
withAnimation(Animation.easeInOut(duration: 0.15)) {
|
||||
|
||||
@@ -28,10 +28,12 @@ class ThemeSettings: ObservableObject {
|
||||
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])
|
||||
} else {
|
||||
|
||||
themeColor = Color.clear
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func saveThemeColor() {
|
||||
let nsColor = NSColor(themeColor)
|
||||
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)
|
||||
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 {
|
||||
/// Darkens the color by a percentage.
|
||||
func darker(by percentage: CGFloat = 30.0) -> Color {
|
||||
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)
|
||||
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 {
|
||||
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)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,20 +174,13 @@ func isCurrentUserAdmin(completion: @escaping (Bool) -> Void) {
|
||||
}
|
||||
|
||||
// Check if appearance is dark mode
|
||||
func isDarkModeEnabled() -> Bool {
|
||||
let process = Process()
|
||||
process.executableURL = URL(fileURLWithPath: "/bin/zsh")
|
||||
process.arguments = ["-c", "defaults read -g AppleInterfaceStyle"]
|
||||
let pipe = Pipe()
|
||||
process.standardOutput = pipe
|
||||
process.standardError = pipe
|
||||
try? process.run()
|
||||
let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
||||
if !data.isEmpty{
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
func isDarkMode() -> Bool {
|
||||
return NSApp.effectiveAppearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua
|
||||
}
|
||||
|
||||
// Set app color mode
|
||||
func setAppearance(mode: NSAppearance.Name) {
|
||||
NSApp.appearance = NSAppearance(named: mode)
|
||||
}
|
||||
|
||||
// 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
|
||||
//func getCasks() -> [String] {
|
||||
@@ -458,86 +458,73 @@ func printOS(_ items: Any..., separator: String = " ", terminator: String = "\n"
|
||||
}
|
||||
|
||||
|
||||
|
||||
//func totalSizeOnDisk2(for paths: [URL]) -> 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 {
|
||||
// Get size of files
|
||||
func totalSizeOnDisk(for paths: [URL]) -> (real: Int64, logical: Int64) {
|
||||
let fileManager = FileManager.default
|
||||
var totalSize: Int64 = 0
|
||||
var totalAllocatedSize: Int64 = 0
|
||||
var totalFileSize: Int64 = 0
|
||||
|
||||
for url in paths {
|
||||
var isDirectory: ObjCBool = false
|
||||
if fileManager.fileExists(atPath: url.path, isDirectory: &isDirectory) {
|
||||
let keys: [URLResourceKey] = [.totalFileAllocatedSizeKey, .fileSizeKey]
|
||||
if isDirectory.boolValue {
|
||||
// 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 {
|
||||
do {
|
||||
let fileAttributes = try fileURL.resourceValues(forKeys: [.totalFileAllocatedSizeKey])
|
||||
if let fileSize = fileAttributes.totalFileAllocatedSize {
|
||||
totalSize += Int64(fileSize)
|
||||
let fileAttributes = try fileURL.resourceValues(forKeys: Set(keys))
|
||||
if let allocatedSize = fileAttributes.totalFileAllocatedSize {
|
||||
totalAllocatedSize += Int64(allocatedSize)
|
||||
}
|
||||
if let fileSize = fileAttributes.fileSize {
|
||||
totalFileSize += Int64(fileSize)
|
||||
}
|
||||
} catch {
|
||||
printOS("Error getting file size for \(fileURL): \(error)")
|
||||
print("Error getting file attributes for \(fileURL): \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// It's a file
|
||||
do {
|
||||
let fileAttributes = try url.resourceValues(forKeys: [.totalFileAllocatedSizeKey])
|
||||
if let fileSize = fileAttributes.totalFileAllocatedSize {
|
||||
totalSize += Int64(fileSize)
|
||||
let fileAttributes = try url.resourceValues(forKeys: Set(keys))
|
||||
if let allocatedSize = fileAttributes.totalFileAllocatedSize {
|
||||
totalAllocatedSize += Int64(allocatedSize)
|
||||
}
|
||||
if let fileSize = fileAttributes.fileSize {
|
||||
totalFileSize += Int64(fileSize)
|
||||
}
|
||||
} 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])
|
||||
}
|
||||
|
||||
// ByteFormatter
|
||||
func formatByte(size: Int64) -> String {
|
||||
func formatByte(size: Int64) -> (human: String, byte: String) {
|
||||
let byteCountFormatter = ByteCountFormatter()
|
||||
byteCountFormatter.countStyle = .file
|
||||
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
|
||||
@@ -603,12 +590,9 @@ func uninstallPearcleaner(appState: AppState, locations: Locations) {
|
||||
|
||||
// Get app info for Pearcleaner
|
||||
let appInfo = AppInfoFetcher.getAppInfo(atPath: Bundle.main.bundleURL)
|
||||
// appState.appInfo = appInfo!
|
||||
|
||||
// Find application files for Pearcleaner
|
||||
let pathFinder = AppPathFinder(appInfo: appInfo!, appState: appState, locations: locations)
|
||||
pathFinder.findPaths()
|
||||
// findPathsForApp(appInfo: appInfo!,appState: appState, locations: locations)
|
||||
AppPathFinder(appInfo: appInfo!, appState: appState, locations: locations).findPaths()
|
||||
|
||||
// Kill Pearcleaner and tell Finder to trash the files
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
|
||||
|
||||
Reference in New Issue
Block a user