mirror of
https://github.com/exituser/Pearcleaner.git
synced 2026-09-17 09:38:51 +00:00
progress
This commit is contained in:
@@ -15,18 +15,18 @@ class AppPathFinder {
|
||||
private var appState: AppState
|
||||
private var locations: Locations
|
||||
private var backgroundRun: Bool
|
||||
private var reverseAddon: Bool
|
||||
// private var reverseAddon: Bool
|
||||
private var undo: Bool
|
||||
private var completion: () -> Void = {}
|
||||
private var collection: [URL] = []
|
||||
private let collectionAccessQueue = DispatchQueue(label: "com.alienator88.Pearcleaner.appPathFinder.collectionAccess")
|
||||
|
||||
init(appInfo: AppInfo = .empty, appState: AppState, locations: Locations, backgroundRun: Bool = false, reverseAddon: Bool = false, undo: Bool = false, completion: @escaping () -> Void = {}) {
|
||||
init(appInfo: AppInfo = .empty, appState: AppState, locations: Locations, backgroundRun: Bool = false, undo: Bool = false, completion: @escaping () -> Void = {}) {
|
||||
self.appInfo = appInfo
|
||||
self.appState = appState
|
||||
self.locations = locations
|
||||
self.backgroundRun = backgroundRun
|
||||
self.reverseAddon = reverseAddon
|
||||
// self.reverseAddon = reverseAddon
|
||||
self.undo = undo
|
||||
self.completion = completion
|
||||
}
|
||||
@@ -338,9 +338,9 @@ class AppPathFinder {
|
||||
}
|
||||
|
||||
// Append object to store if running reverse search with empty store
|
||||
if self.reverseAddon {
|
||||
self.appState.appInfoStore.append(self.appInfo)
|
||||
}
|
||||
// if self.reverseAddon {
|
||||
// self.appState.appInfoStore.append(self.appInfo)
|
||||
// }
|
||||
|
||||
self.completion()
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ let home = FileManager.default.homeDirectoryForCurrentUser.path
|
||||
|
||||
class AppState: ObservableObject {
|
||||
@Published var appInfo: AppInfo
|
||||
@Published var appInfoStore: [AppInfo] = []
|
||||
// @Published var appInfoStore: [AppInfo] = []
|
||||
@Published var trashedFiles: [AppInfo] = []
|
||||
@Published var zombieFile: ZombieFile
|
||||
@Published var sortedApps: [AppInfo] = []
|
||||
@@ -23,12 +23,24 @@ class AppState: ObservableObject {
|
||||
@Published var sidebar: Bool = true
|
||||
@Published var reload: Bool = false
|
||||
@Published var showProgress: Bool = false
|
||||
@Published var leftoverProgress: (String, Double) = ("", 0.0)
|
||||
@Published var finderExtensionEnabled: Bool = false
|
||||
@Published var showUninstallAlert: Bool = false
|
||||
@Published var oneShotMode: Bool = false
|
||||
@Published var showConditionBuilder: Bool = false
|
||||
|
||||
var operationQueueLeftover = OperationQueue()
|
||||
@Published var shouldCancelOperations = false
|
||||
|
||||
func cancelQueueOperations() {
|
||||
operationQueueLeftover.cancelAllOperations()
|
||||
shouldCancelOperations = true
|
||||
DispatchQueue.main.async {
|
||||
self.leftoverProgress = ("Search canceled", 0.0)
|
||||
self.showProgress = false
|
||||
self.currentView = .empty
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
self.appInfo = AppInfo(
|
||||
|
||||
@@ -52,53 +52,62 @@ var conditions: [Condition] = [
|
||||
Condition(
|
||||
bundle_id: "com.robotsandpencils.xcodesapp",
|
||||
include: [],
|
||||
exclude: ["com.apple.dt.xcode", "com.oneminutegames.xcodecleaner", "io.hyperapp.xcodecleaner"]
|
||||
exclude: ["com.apple.dt.xcode", "com.oneminutegames.xcodecleaner", "io.hyperapp.xcodecleaner"],
|
||||
includeForce: nil
|
||||
),
|
||||
Condition(
|
||||
bundle_id: "io.hyperapp.xcodecleaner",
|
||||
include: [],
|
||||
exclude: ["com.robotsandpencils.xcodesapp", "com.oneminutegames.xcodecleaner", "com.apple.dt.xcode", "xcodes.json"]
|
||||
exclude: ["com.robotsandpencils.xcodesapp", "com.oneminutegames.xcodecleaner", "com.apple.dt.xcode", "xcodes.json"],
|
||||
includeForce: nil
|
||||
),
|
||||
Condition(
|
||||
bundle_id: "us.zoom.xos",
|
||||
include: ["zoom"],
|
||||
exclude: []
|
||||
exclude: [],
|
||||
includeForce: nil
|
||||
),
|
||||
Condition(
|
||||
bundle_id: "com.brave.browser",
|
||||
include: ["brave"],
|
||||
exclude: []
|
||||
exclude: [],
|
||||
includeForce: nil
|
||||
),
|
||||
Condition(
|
||||
bundle_id: "com.okta.mobile",
|
||||
include: ["okta"],
|
||||
exclude: []
|
||||
exclude: [],
|
||||
includeForce: nil
|
||||
),
|
||||
Condition(
|
||||
bundle_id: "com.google.chrome",
|
||||
include: ["google", "chrome"],
|
||||
exclude: ["iterm", "chromefeaturestate"]
|
||||
exclude: ["iterm", "chromefeaturestate"],
|
||||
includeForce: nil
|
||||
),
|
||||
Condition(
|
||||
bundle_id: "com.microsoft.edgemac",
|
||||
include: ["microsoft"],
|
||||
exclude: ["vscode", "rdc", "appcenter", "office", "oneauth"]
|
||||
exclude: ["vscode", "rdc", "appcenter", "office", "oneauth"],
|
||||
includeForce: nil
|
||||
),
|
||||
Condition(
|
||||
bundle_id: "org.mozilla.firefox",
|
||||
include: ["mozilla", "firefox"],
|
||||
exclude: []
|
||||
exclude: [],
|
||||
includeForce: nil
|
||||
),
|
||||
Condition(
|
||||
bundle_id: "org.mozilla.firefox.nightly",
|
||||
include: ["mozilla", "firefox"],
|
||||
exclude: []
|
||||
exclude: [],
|
||||
includeForce: nil
|
||||
),
|
||||
Condition(
|
||||
bundle_id: "com.logi.optionsplus",
|
||||
include: ["logi"],
|
||||
exclude: ["login", "logic"],
|
||||
includeForce: []
|
||||
includeForce: nil
|
||||
),
|
||||
Condition(
|
||||
bundle_id: "com.microsoft.vscode",
|
||||
@@ -109,17 +118,20 @@ var conditions: [Condition] = [
|
||||
Condition(
|
||||
bundle_id: "com.facebook.archon.developerid",
|
||||
include: ["archon.loginhelper"],
|
||||
exclude: []
|
||||
exclude: [],
|
||||
includeForce: nil
|
||||
),
|
||||
Condition(
|
||||
bundle_id: "eu.exelban.stats",
|
||||
include: [],
|
||||
exclude: ["video"]
|
||||
exclude: ["video"],
|
||||
includeForce: nil
|
||||
),
|
||||
Condition(
|
||||
bundle_id: "jetbrains",
|
||||
include: ["jetbrains", "jcef"],
|
||||
exclude: []
|
||||
exclude: [],
|
||||
includeForce: nil
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
+142
-60
@@ -109,45 +109,108 @@ func listAppSupportDirectories() -> [String] {
|
||||
|
||||
|
||||
// Load app paths on launch
|
||||
func reversePreloader(allApps: [AppInfo], appState: AppState, locations: Locations, fsm: FolderSettingsManager, reverseAddon: Bool = false, completion: @escaping () -> Void = {}) {
|
||||
let dispatchGroup = DispatchGroup()
|
||||
appState.appInfoStore.removeAll()
|
||||
func reversePreloader(allApps: [AppInfo], appState: AppState, locations: Locations, fsm: FolderSettingsManager, completion: @escaping () -> Void = {}) {
|
||||
// appState.operationQueueLeftover.maxConcurrentOperationCount = 10 // Adjust this value as needed
|
||||
// appState.shouldCancelOperations = false
|
||||
// appState.appInfoStore.removeAll()
|
||||
// let sortedAllApps = allApps.sorted { $0.appName.localizedCompare($1.appName) == .orderedAscending }
|
||||
|
||||
for app in allApps {
|
||||
dispatchGroup.enter()
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
AppPathFinder(appInfo: app, appState: appState, locations: locations, backgroundRun: true, reverseAddon: reverseAddon).findPaths()
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
updateOnMain {
|
||||
appState.leftoverProgress.0 = "Finding leftover files, please wait..."
|
||||
}
|
||||
|
||||
dispatchGroup.notify(queue: DispatchQueue.global(qos: .background)) {
|
||||
|
||||
func checkAllAppsProcessed(retryCount: Int = 0, maxRetry: Int = 120) {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
if appState.appInfoStore.count == allApps.count {
|
||||
ReversePathsSearcher(appState: appState, locations: locations, fsm: fsm).reversePathsSearch() {
|
||||
updateOnMain {
|
||||
appState.showProgress = false
|
||||
}
|
||||
}
|
||||
completion()
|
||||
} else if retryCount < maxRetry {
|
||||
checkAllAppsProcessed(retryCount: retryCount + 1, maxRetry: maxRetry)
|
||||
} else {
|
||||
// Reset progress values to 0
|
||||
updateOnMain {
|
||||
appState.showProgress = false
|
||||
}
|
||||
printOS("loadAllPaths - Retry limit reached. Not all paths were loaded.")
|
||||
completion()
|
||||
}
|
||||
ReversePathsSearcher(appState: appState, locations: locations, fsm: fsm, sortedApps: allApps).reversePathsSearch {
|
||||
updateOnMain {
|
||||
printOS("Reverse search processed successfully")
|
||||
appState.showProgress = false
|
||||
withAnimation {
|
||||
appState.leftoverProgress.1 = 0.0
|
||||
}
|
||||
appState.leftoverProgress.0 = "Reverse search completed successfully"
|
||||
}
|
||||
|
||||
checkAllAppsProcessed()
|
||||
|
||||
completion()
|
||||
}
|
||||
|
||||
// let totalApps = sortedAllApps.count
|
||||
// var processedApps = 0
|
||||
//
|
||||
// DispatchQueue.global(qos: .userInitiated).async {
|
||||
// for (index, app) in sortedAllApps.enumerated() {
|
||||
// autoreleasepool {
|
||||
// if appState.shouldCancelOperations {
|
||||
// printOS("Operations cancelled.") // Debug print
|
||||
// return
|
||||
// }
|
||||
// printOS("Processing app \(index + 1) of \(totalApps): \(app.appName)")
|
||||
//
|
||||
// let semaphore = DispatchSemaphore(value: 0)
|
||||
// appState.operationQueueLeftover.addOperation {
|
||||
// if appState.shouldCancelOperations {
|
||||
// printOS("Operations cancelled.") // Debug print
|
||||
// return
|
||||
// }
|
||||
// let pathFinder = AppPathFinder(
|
||||
// appInfo: app,
|
||||
// appState: appState,
|
||||
// locations: locations,
|
||||
// backgroundRun: true,
|
||||
// reverseAddon: reverseAddon,
|
||||
// completion: {
|
||||
// semaphore.signal()
|
||||
// }
|
||||
// )
|
||||
// pathFinder.findPaths()
|
||||
// }
|
||||
//
|
||||
// semaphore.wait()
|
||||
//
|
||||
// DispatchQueue.main.async {
|
||||
// processedApps += 1
|
||||
// let progress = Double(processedApps) / Double(totalApps)
|
||||
// withAnimation {
|
||||
// appState.leftoverProgress.1 = progress
|
||||
// }
|
||||
// appState.leftoverProgress.0 = "Excluding application files for \(app.appName)"
|
||||
// printOS("Progress: \(Int(progress * 100))%") // Debug print
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// }
|
||||
//
|
||||
// appState.operationQueueLeftover.waitUntilAllOperationsAreFinished()
|
||||
//
|
||||
// DispatchQueue.main.async {
|
||||
// if appState.shouldCancelOperations {
|
||||
// completion()
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// if appState.appInfoStore.count == sortedAllApps.count {
|
||||
// printOS("All apps processed successfully")
|
||||
// appState.leftoverProgress.0 = "Finding leftover files, please wait..."
|
||||
// ReversePathsSearcher(appState: appState, locations: locations, fsm: fsm, sortedApps: sortedAllApps).reversePathsSearch {
|
||||
// updateOnMain {
|
||||
// printOS("Reverse search processed successfully")
|
||||
// appState.showProgress = false
|
||||
// withAnimation {
|
||||
// appState.leftoverProgress.1 = 0.0
|
||||
// }
|
||||
// appState.leftoverProgress.0 = "Reverse search completed successfully"
|
||||
// }
|
||||
// completion()
|
||||
// }
|
||||
// } else {
|
||||
// printOS("reversePreloader - Not all paths were loaded. Expected: \(sortedAllApps.count), Actual: \(appState.appInfoStore.count)")
|
||||
// updateOnMain {
|
||||
// appState.showProgress = false
|
||||
// withAnimation {
|
||||
// appState.leftoverProgress.1 = 0.0
|
||||
// }
|
||||
// appState.leftoverProgress.0 = "Reverse search failed to process existing application files (\(sortedAllApps.count)/\(appState.appInfoStore.count))"
|
||||
// }
|
||||
// completion()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
@@ -161,37 +224,56 @@ func showAppInFiles(appInfo: AppInfo, appState: AppState, locations: Locations,
|
||||
appState.appInfo = .empty
|
||||
appState.selectedItems = []
|
||||
|
||||
// Check if the appInfo exists in the appState.appInfoStore
|
||||
if let storedAppInfo = appState.appInfoStore.first(where: { $0.path == appInfo.path }) {
|
||||
// Update appState with the stored app info and selected items.
|
||||
appState.appInfo = storedAppInfo
|
||||
appState.selectedItems = Set(storedAppInfo.files)
|
||||
// When the appInfo is not found, show progress, and search for paths.
|
||||
appState.showProgress = true
|
||||
|
||||
// Trigger the animation for changing views and showing the popover.
|
||||
withAnimation(Animation.easeIn(duration: 0.4)) {
|
||||
appState.currentView = .files
|
||||
showPopover.wrappedValue.toggle()
|
||||
// Initialize the path finder and execute its search.
|
||||
AppPathFinder(appInfo: appInfo, appState: appState, locations: locations) {
|
||||
updateOnMain {
|
||||
// Update the progress indicator on the main thread once the search completes.
|
||||
appState.showProgress = false
|
||||
}
|
||||
} else {
|
||||
// When the appInfo is not found, show progress, and search for paths.
|
||||
appState.showProgress = true
|
||||
}.findPaths()
|
||||
|
||||
// Initialize the path finder and execute its search.
|
||||
AppPathFinder(appInfo: appInfo, appState: appState, locations: locations) {
|
||||
updateOnMain {
|
||||
// Update the progress indicator on the main thread once the search completes.
|
||||
appState.showProgress = false
|
||||
}
|
||||
}.findPaths()
|
||||
appState.appInfo = appInfo
|
||||
|
||||
appState.appInfo = appInfo
|
||||
|
||||
// Animate the view change and popover display.
|
||||
withAnimation(Animation.easeIn(duration: 0.4)) {
|
||||
appState.currentView = .files
|
||||
showPopover.wrappedValue.toggle()
|
||||
}
|
||||
// Animate the view change and popover display.
|
||||
withAnimation(Animation.easeIn(duration: 0.4)) {
|
||||
appState.currentView = .files
|
||||
showPopover.wrappedValue.toggle()
|
||||
}
|
||||
|
||||
// Check if the appInfo exists in the appState.appInfoStore
|
||||
// if let storedAppInfo = appState.appInfoStore.first(where: { $0.path == appInfo.path }) {
|
||||
// // Update appState with the stored app info and selected items.
|
||||
// appState.appInfo = storedAppInfo
|
||||
// appState.selectedItems = Set(storedAppInfo.files)
|
||||
//
|
||||
// // Trigger the animation for changing views and showing the popover.
|
||||
// withAnimation(Animation.easeIn(duration: 0.4)) {
|
||||
// appState.currentView = .files
|
||||
// showPopover.wrappedValue.toggle()
|
||||
// }
|
||||
// } else {
|
||||
// // When the appInfo is not found, show progress, and search for paths.
|
||||
// appState.showProgress = true
|
||||
//
|
||||
// // Initialize the path finder and execute its search.
|
||||
// AppPathFinder(appInfo: appInfo, appState: appState, locations: locations) {
|
||||
// updateOnMain {
|
||||
// // Update the progress indicator on the main thread once the search completes.
|
||||
// appState.showProgress = false
|
||||
// }
|
||||
// }.findPaths()
|
||||
//
|
||||
// appState.appInfo = appInfo
|
||||
//
|
||||
// // Animate the view change and popover display.
|
||||
// withAnimation(Animation.easeIn(duration: 0.4)) {
|
||||
// appState.currentView = .files
|
||||
// showPopover.wrappedValue.toggle()
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,15 +19,15 @@ class ReversePathsSearcher {
|
||||
private var fileSizeLogical: [URL: Int64] = [:]
|
||||
private var fileIcon: [URL: NSImage?] = [:]
|
||||
private let dispatchGroup = DispatchGroup()
|
||||
private let sortedApps: [AppInfo]
|
||||
|
||||
init(appState: AppState, locations: Locations, fsm: FolderSettingsManager) {
|
||||
init(appState: AppState, locations: Locations, fsm: FolderSettingsManager, sortedApps: [AppInfo]) {
|
||||
self.appState = appState
|
||||
self.locations = locations
|
||||
self.fsm = fsm
|
||||
self.sortedApps = sortedApps
|
||||
}
|
||||
|
||||
|
||||
|
||||
func reversePathsSearch(completion: @escaping () -> Void = {}) {
|
||||
Task(priority: .high) {
|
||||
self.processLocations()
|
||||
@@ -38,51 +38,74 @@ class ReversePathsSearcher {
|
||||
}
|
||||
|
||||
private func processLocations() {
|
||||
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)
|
||||
processLocation(location)
|
||||
dispatchGroup.leave()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private func processLocation(_ location: String, allPaths: [String], allNames: [String]) {
|
||||
private func processLocation(_ location: String) {
|
||||
|
||||
do {
|
||||
let contents = try fileManager.contentsOfDirectory(atPath: location)
|
||||
contents.forEach { itemName in
|
||||
let itemURL = URL(fileURLWithPath: location).appendingPathComponent(itemName)
|
||||
processItem(itemName, itemURL: itemURL, allPaths: allPaths, allNames: allNames)
|
||||
processItem(itemName, itemURL: itemURL)
|
||||
}
|
||||
} catch {
|
||||
printOS("Error processing location: \(location), error: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
private func processItem(_ itemName: String, itemURL: URL, allPaths: [String], allNames: [String]) {
|
||||
let formattedItemName = itemName.pearFormat()
|
||||
private func processItem(_ itemName: String, itemURL: URL) {
|
||||
let itemPath = itemURL.path.pearFormat()
|
||||
let itemLastPathComponent = itemURL.lastPathComponent.pearFormat()
|
||||
let exclusionList = fsm.fileFolderPathsZ.map { $0.pearFormat() }
|
||||
|
||||
if exclusionList.contains(itemPath) || itemPath.contains("dsstore") || itemPath.contains("daemonnameoridentifierhere") {
|
||||
return
|
||||
}
|
||||
|
||||
guard !skipReverse.contains(where: { formattedItemName.contains($0) }),
|
||||
!allPaths.contains(where: { $0 == itemPath || $0.hasSuffix("/\(itemLastPathComponent)") }),
|
||||
!allNames.contains(formattedItemName),
|
||||
isSupportedFileType(at: itemURL.path) else {
|
||||
guard !skipReverse.contains(where: { itemName.pearFormat().contains($0) }),
|
||||
isSupportedFileType(at: itemURL.path),
|
||||
!isRelatedToInstalledApp(itemPath: itemPath),
|
||||
!isExcludedByConditions(itemPath: itemPath) else {
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
collection.append(itemURL)
|
||||
}
|
||||
|
||||
private func isRelatedToInstalledApp(itemPath: String) -> Bool {
|
||||
for app in sortedApps {
|
||||
if itemPath.contains(app.bundleIdentifier.pearFormat()) || itemPath.contains(app.appName.pearFormat()) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private func isExcludedByConditions(itemPath: String) -> Bool {
|
||||
|
||||
for condition in conditions {
|
||||
// Include keywords
|
||||
if condition.include.contains(where: { itemPath.contains($0.pearFormat()) }) {
|
||||
return true
|
||||
}
|
||||
// Include force
|
||||
if let includeForce = condition.includeForce,
|
||||
includeForce.contains(where: { itemPath.contains($0.path.pearFormat()) }) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private func calculateFileDetails() {
|
||||
collection.forEach { path in
|
||||
let size = totalSizeOnDisk(for: path)
|
||||
@@ -104,3 +127,5 @@ class ReversePathsSearcher {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -208,9 +208,9 @@ func removeApp(appState: AppState, withPath path: URL) {
|
||||
// return // Exit the function if the app was found and removed
|
||||
}
|
||||
// Remove from appInfoStore if found
|
||||
if let index = appState.appInfoStore.firstIndex(where: { $0.path == path }) {
|
||||
appState.appInfoStore.remove(at: index)
|
||||
}
|
||||
// if let index = appState.appInfoStore.firstIndex(where: { $0.path == path }) {
|
||||
// appState.appInfoStore.remove(at: index)
|
||||
// }
|
||||
// Brew cleanup if enabled
|
||||
if brew {
|
||||
caskCleanup(app: appState.appInfo.appName)
|
||||
|
||||
Reference in New Issue
Block a user