This commit is contained in:
Alin
2024-03-21 11:24:56 -06:00
parent 6b71708561
commit 422463e76d
23 changed files with 1110 additions and 696 deletions
+20 -4
View File
@@ -37,6 +37,10 @@
C7D31D4A2AFEB26700C7ED9E /* AppListItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7D31D492AFEB26700C7ED9E /* AppListItems.swift */; }; C7D31D4A2AFEB26700C7ED9E /* AppListItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7D31D492AFEB26700C7ED9E /* AppListItems.swift */; };
C7D31D512AFF00F300C7ED9E /* Locations.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7D31D502AFF00F300C7ED9E /* Locations.swift */; }; C7D31D512AFF00F300C7ED9E /* Locations.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7D31D502AFF00F300C7ED9E /* Locations.swift */; };
C7DC1FEE2B9F7D3D009AC317 /* features.json in Resources */ = {isa = PBXBuildFile; fileRef = C7DC1FED2B9F7D3D009AC317 /* features.json */; }; C7DC1FEE2B9F7D3D009AC317 /* features.json in Resources */ = {isa = PBXBuildFile; fileRef = C7DC1FED2B9F7D3D009AC317 /* features.json */; };
C7DD49EA2BAB4D8100CCBA16 /* AppInfoFetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7DD49E92BAB4D8100CCBA16 /* AppInfoFetch.swift */; };
C7DD49EC2BAB58AA00CCBA16 /* AppPathsFetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7DD49EB2BAB58AA00CCBA16 /* AppPathsFetch.swift */; };
C7DD49EE2BAB7F6000CCBA16 /* ReversePathsFetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7DD49ED2BAB7F6000CCBA16 /* ReversePathsFetch.swift */; };
C7DD49F02BABA14400CCBA16 /* Folders.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7DD49EF2BABA14400CCBA16 /* Folders.swift */; };
C7DE672B2BA6343D00EB1633 /* MenuBarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7DE672A2BA6343D00EB1633 /* MenuBarItem.swift */; }; C7DE672B2BA6343D00EB1633 /* MenuBarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7DE672A2BA6343D00EB1633 /* MenuBarItem.swift */; };
C7DE672D2BA6356500EB1633 /* MenuBarMiniAppView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7DE672C2BA6356500EB1633 /* MenuBarMiniAppView.swift */; }; C7DE672D2BA6356500EB1633 /* MenuBarMiniAppView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7DE672C2BA6356500EB1633 /* MenuBarMiniAppView.swift */; };
C7ED86B12BA8A35500F13DE4 /* Interface.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7ED86B02BA8A35500F13DE4 /* Interface.swift */; }; C7ED86B12BA8A35500F13DE4 /* Interface.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7ED86B02BA8A35500F13DE4 /* Interface.swift */; };
@@ -101,6 +105,10 @@
C7D31D492AFEB26700C7ED9E /* AppListItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppListItems.swift; sourceTree = "<group>"; }; C7D31D492AFEB26700C7ED9E /* AppListItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppListItems.swift; sourceTree = "<group>"; };
C7D31D502AFF00F300C7ED9E /* Locations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Locations.swift; sourceTree = "<group>"; }; C7D31D502AFF00F300C7ED9E /* Locations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Locations.swift; sourceTree = "<group>"; };
C7DC1FED2B9F7D3D009AC317 /* features.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = features.json; sourceTree = SOURCE_ROOT; }; C7DC1FED2B9F7D3D009AC317 /* features.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = features.json; sourceTree = SOURCE_ROOT; };
C7DD49E92BAB4D8100CCBA16 /* AppInfoFetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppInfoFetch.swift; sourceTree = "<group>"; };
C7DD49EB2BAB58AA00CCBA16 /* AppPathsFetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPathsFetch.swift; sourceTree = "<group>"; };
C7DD49ED2BAB7F6000CCBA16 /* ReversePathsFetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReversePathsFetch.swift; sourceTree = "<group>"; };
C7DD49EF2BABA14400CCBA16 /* Folders.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Folders.swift; sourceTree = "<group>"; };
C7DE672A2BA6343D00EB1633 /* MenuBarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarItem.swift; sourceTree = "<group>"; }; C7DE672A2BA6343D00EB1633 /* MenuBarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarItem.swift; sourceTree = "<group>"; };
C7DE672C2BA6356500EB1633 /* MenuBarMiniAppView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarMiniAppView.swift; sourceTree = "<group>"; }; C7DE672C2BA6356500EB1633 /* MenuBarMiniAppView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarMiniAppView.swift; sourceTree = "<group>"; };
C7ED86B02BA8A35500F13DE4 /* Interface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Interface.swift; sourceTree = "<group>"; }; C7ED86B02BA8A35500F13DE4 /* Interface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Interface.swift; sourceTree = "<group>"; };
@@ -144,6 +152,9 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
C77B90192AF1938F009CC655 /* Logic.swift */, C77B90192AF1938F009CC655 /* Logic.swift */,
C7DD49E92BAB4D8100CCBA16 /* AppInfoFetch.swift */,
C7DD49EB2BAB58AA00CCBA16 /* AppPathsFetch.swift */,
C7DD49ED2BAB7F6000CCBA16 /* ReversePathsFetch.swift */,
C77B90172AF19382009CC655 /* AppCommands.swift */, C77B90172AF19382009CC655 /* AppCommands.swift */,
C77B90152AF19377009CC655 /* AppState.swift */, C77B90152AF19377009CC655 /* AppState.swift */,
C7F539372AF60865007DF1B2 /* Utilities.swift */, C7F539372AF60865007DF1B2 /* Utilities.swift */,
@@ -179,6 +190,7 @@
C77B901D2AF1A9DB009CC655 /* SettingsWindow.swift */, C77B901D2AF1A9DB009CC655 /* SettingsWindow.swift */,
C76D08492AF83C6E00D07867 /* General.swift */, C76D08492AF83C6E00D07867 /* General.swift */,
C7ED86B02BA8A35500F13DE4 /* Interface.swift */, C7ED86B02BA8A35500F13DE4 /* Interface.swift */,
C7DD49EF2BABA14400CCBA16 /* Folders.swift */,
C76D08472AF83C3F00D07867 /* Update.swift */, C76D08472AF83C3F00D07867 /* Update.swift */,
C76D08502AF850F300D07867 /* About.swift */, C76D08502AF850F300D07867 /* About.swift */,
); );
@@ -354,11 +366,14 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
C7DD49EA2BAB4D8100CCBA16 /* AppInfoFetch.swift in Sources */,
C76D08482AF83C3F00D07867 /* Update.swift in Sources */, C76D08482AF83C3F00D07867 /* Update.swift in Sources */,
C7FB173B2B96321300B96F9A /* AppsListView.swift in Sources */, C7FB173B2B96321300B96F9A /* AppsListView.swift in Sources */,
C7045A342B068B2900376976 /* UpdateView.swift in Sources */, C7045A342B068B2900376976 /* UpdateView.swift in Sources */,
C77B90162AF19377009CC655 /* AppState.swift in Sources */, C77B90162AF19377009CC655 /* AppState.swift in Sources */,
C7DD49EE2BAB7F6000CCBA16 /* ReversePathsFetch.swift in Sources */,
C7045A322B068AD700376976 /* NewWin.swift in Sources */, C7045A322B068AD700376976 /* NewWin.swift in Sources */,
C7DD49F02BABA14400CCBA16 /* Folders.swift in Sources */,
C7DE672D2BA6356500EB1633 /* MenuBarMiniAppView.swift in Sources */, C7DE672D2BA6356500EB1633 /* MenuBarMiniAppView.swift in Sources */,
C7ED86B32BA8ED8800F13DE4 /* Trash.swift in Sources */, C7ED86B32BA8ED8800F13DE4 /* Trash.swift in Sources */,
C7D31D512AFF00F300C7ED9E /* Locations.swift in Sources */, C7D31D512AFF00F300C7ED9E /* Locations.swift in Sources */,
@@ -378,6 +393,7 @@
C7D31D4A2AFEB26700C7ED9E /* AppListItems.swift in Sources */, C7D31D4A2AFEB26700C7ED9E /* AppListItems.swift in Sources */,
C77B901E2AF1A9DB009CC655 /* SettingsWindow.swift in Sources */, C77B901E2AF1A9DB009CC655 /* SettingsWindow.swift in Sources */,
C7D31D482AFEB23700C7ED9E /* TopBar.swift in Sources */, C7D31D482AFEB23700C7ED9E /* TopBar.swift in Sources */,
C7DD49EC2BAB58AA00CCBA16 /* AppPathsFetch.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 */,
@@ -559,7 +575,7 @@
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 25; CURRENT_PROJECT_VERSION = 26;
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = BK8443AXLU; DEVELOPMENT_TEAM = BK8443AXLU;
@@ -577,7 +593,7 @@
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 13.0; MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 3.2.1; MARKETING_VERSION = 3.3.0;
PRODUCT_BUNDLE_IDENTIFIER = com.alienator88.Pearcleaner; PRODUCT_BUNDLE_IDENTIFIER = com.alienator88.Pearcleaner;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
@@ -594,7 +610,7 @@
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 25; CURRENT_PROJECT_VERSION = 26;
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = BK8443AXLU; DEVELOPMENT_TEAM = BK8443AXLU;
@@ -612,7 +628,7 @@
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 13.0; MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 3.2.1; MARKETING_VERSION = 3.3.0;
PRODUCT_BUNDLE_IDENTIFIER = com.alienator88.Pearcleaner; PRODUCT_BUNDLE_IDENTIFIER = com.alienator88.Pearcleaner;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
+5 -3
View File
@@ -12,10 +12,12 @@ struct AppCommands: Commands {
let appState: AppState let appState: AppState
let locations: Locations let locations: Locations
let fsm: FolderSettingsManager
init(appState: AppState, locations: Locations) { init(appState: AppState, locations: Locations, fsm: FolderSettingsManager) {
self.appState = appState self.appState = appState
self.locations = locations self.locations = locations
self.fsm = fsm
} }
var body: some Commands { var body: some Commands {
@@ -36,7 +38,7 @@ struct AppCommands: Commands {
updateOnMain { updateOnMain {
appState.reload.toggle() appState.reload.toggle()
} }
let sortedApps = getSortedApps() let sortedApps = getSortedApps(paths: fsm.folderPaths, appState: appState)
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
appState.sortedApps = [] appState.sortedApps = []
// appState.sortedApps.systemApps = [] // appState.sortedApps.systemApps = []
@@ -71,7 +73,7 @@ struct AppCommands: Commands {
Button Button
{ {
undoTrash(appState: appState) { undoTrash(appState: appState) {
let sortedApps = getSortedApps() let sortedApps = getSortedApps(paths: fsm.folderPaths, appState: appState)
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
appState.sortedApps = sortedApps appState.sortedApps = sortedApps
// if instantSearch { // if instantSearch {
+174
View File
@@ -0,0 +1,174 @@
//
// AppInfoFetch.swift
// Pearcleaner
//
// Created by Alin Lupascu on 3/20/24.
//
import Foundation
import SwiftUI
class AppInfoFetcher {
static let fileManager = FileManager.default
static func getAppInfo(atPath path: URL, wrapped: Bool = false) -> AppInfo? {
if isDirectoryWrapped(path: path) {
return handleWrappedDirectory(atPath: path)
} else {
return createAppInfoFromBundle(atPath: path, wrapped: wrapped)
}
}
private static func isDirectoryWrapped(path: URL) -> Bool {
let wrapperURL = path.appendingPathComponent("Wrapper")
return fileManager.fileExists(atPath: wrapperURL.path)
}
private static func handleWrappedDirectory(atPath path: URL) -> AppInfo? {
let wrapperURL = path.appendingPathComponent("Wrapper")
do {
let contents = try fileManager.contentsOfDirectory(at: wrapperURL, includingPropertiesForKeys: nil)
guard let firstAppFile = contents.first(where: { $0.pathExtension == "app" }) else {
printOS("No .app files found in the 'Wrapper' directory: \(wrapperURL)")
return nil
}
let fullPath = wrapperURL.appendingPathComponent(firstAppFile.lastPathComponent)
return getAppInfo(atPath: fullPath, wrapped: true)
} catch {
printOS("Error reading contents of 'Wrapper' directory: \(error.localizedDescription)\n\(wrapperURL)")
return nil
}
}
private static func createAppInfoFromBundle(atPath path: URL, wrapped: Bool) -> AppInfo? {
guard let bundle = Bundle(url: path), let bundleIdentifier = bundle.bundleIdentifier else {
printOS("Bundle not found or missing bundle identifier at path: \(path)")
return nil
}
let appName = bundle.localizedInfoDictionary?[kCFBundleNameKey as String] as? String
?? bundle.infoDictionary?["CFBundleName"] as? String
?? path.deletingPathExtension().lastPathComponent
let appVersion = bundle.infoDictionary?["CFBundleShortVersionString"] as? String
?? bundle.infoDictionary?["CFBundleVersion"] as? String
?? ""
let appIcon = fetchAppIcon(for: path, wrapped: wrapped)
let webApp = bundle.infoDictionary?["LSTemplateApplication"] as? Bool ?? false
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: [:])
}
private static func fetchAppIcon(for path: URL, wrapped: Bool) -> NSImage? {
let iconPath = wrapped ? path.deletingLastPathComponent().deletingLastPathComponent() : path
if let appIcon = getIconForFileOrFolderNS(atPath: iconPath) {
return convertICNSToPNG(icon: appIcon, size: NSSize(width: 100, height: 100))
} else {
printOS("App Icon not found for app at path: \(path)")
return nil
}
}
}
// OLD FUNCTION ============================================================
//func getAppInfo(atPath path: URL, wrapped: Bool = false) -> AppInfo? {
// let filemanager = FileManager.default
// let wrapperURL = path.appendingPathComponent("Wrapper")
// if filemanager.fileExists(atPath: wrapperURL.path) {
// do {
// let contents = try filemanager.contentsOfDirectory(at: wrapperURL, includingPropertiesForKeys: nil, options: [])
// let appFiles = contents.filter { $0.pathExtension == "app" }
//
// if let firstAppFile = appFiles.first {
// let fullPath = wrapperURL.appendingPathComponent(firstAppFile.lastPathComponent)
// if let wrappedAppInfo = getAppInfo(atPath: fullPath, wrapped: true) {
// return wrappedAppInfo
// }
// } else {
// printOS("No .app files found in the 'Wrapper' directory: \(wrapperURL)")
// }
// } catch {
// printOS("Error reading contents of 'Wrapper' directory: \(error.localizedDescription)\n\(wrapperURL)")
// }
// } else {
// if let bundle = Bundle(url: path) {
// if let bundleIdentifier = bundle.bundleIdentifier {
// var appVersion: String?
// var appIcon: NSImage?
// var appName: String?
// var webApp: Bool?
// var wrappedApp: Bool?
// var system: Bool?
//
// if let shortVersion = bundle.infoDictionary?["CFBundleShortVersionString"] as? String, !shortVersion.isEmpty {
// appVersion = shortVersion
// } else {
// if let bundleVersion = bundle.infoDictionary?["CFBundleVersion"] as? String, !bundleVersion.isEmpty {
// appVersion = bundleVersion
// } else {
// printOS("Failed to retrieve bundle version")
// }
// }
//
// if let localizedName = bundle.localizedInfoDictionary?[kCFBundleNameKey as String] as? String {
// appName = localizedName
// } else if let bundleName = bundle.infoDictionary?["CFBundleName"] as? String {
// appName = bundleName
// } else {
// appName = path.deletingPathExtension().lastPathComponent
// }
//
// // Get icon from app
// if path.absoluteString.contains("Wrapper") {
// appIcon = getIconForFileOrFolderNS(atPath: path.deletingLastPathComponent().deletingLastPathComponent())
// } else {
// appIcon = getIconForFileOrFolderNS(atPath: path)
// }
//
// // Convert the icon to a 100x100 PNG image
// if let pngIcon = appIcon.flatMap({ convertICNSToPNG(icon: $0, size: NSSize(width: 100, height: 100)) }) {
// appIcon = pngIcon
// }
//
// if appIcon == nil {
// printOS("App Icon not found for app at path: \(path)")
// }
//
// if bundle.infoDictionary?["LSTemplateApplication"] is Bool {
// webApp = true
// } else {
// webApp = false
// }
//
// if wrapped {
// wrappedApp = true
// } else {
// wrappedApp = false
// }
//
// if !path.path.contains(home) {
// system = true
// } else {
// system = false
// }
//
//
// return AppInfo(id: UUID(), path: path, bundleIdentifier: bundleIdentifier, appName: appName ?? "", appVersion: appVersion ?? "", appIcon: appIcon, webApp: webApp ?? false, wrapped: wrappedApp ?? false, system: system ?? false, files: [], fileSize: [:], fileIcon: [:])
//
// } else {
// printOS("Bundle identifier not found at path: \(path)")
// }
// } else {
// printOS("Bundle not found at path: \(path)")
// }
// }
// return nil
//}
+226
View File
@@ -0,0 +1,226 @@
//
// AppPathsFetch.swift
// Pearcleaner
//
// Created by Alin Lupascu on 3/20/24.
//
import Foundation
import AppKit
import SwiftUI
class AppPathFinder {
private var appInfo: AppInfo
private var appState: AppState
private var locations: Locations
private var backgroundRun: Bool
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, completion: @escaping () -> Void = {}) {
self.appInfo = appInfo
self.appState = appState
self.locations = locations
self.backgroundRun = backgroundRun
self.completion = completion
}
func findPaths() {
Task(priority: .background) {
self.initialURLProcessing()
let dispatchGroup = DispatchGroup()
for location in self.locations.apps.paths {
dispatchGroup.enter()
DispatchQueue.global(qos: .userInitiated).async {
self.processLocation(location, with: dispatchGroup)
dispatchGroup.leave()
}
}
dispatchGroup.notify(queue: .main) {
self.finalizeCollection()
}
}
}
private func initialURLProcessing() {
if let url = URL(string: self.appInfo.path.absoluteString), !url.path.contains(".Trash") {
let modifiedUrl = url.path.contains("Wrapper") ? url.deletingLastPathComponent().deletingLastPathComponent() : url
self.collection.append(modifiedUrl)
}
}
private func processLocation(_ location: String, with dispatchGroup: DispatchGroup) {
// Check if the directory exists before attempting to read its contents
if FileManager.default.fileExists(atPath: location) {
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 shouldSkipItem(itemL, at: itemURL) { continue }
if specificCondition(itemL: itemL, itemURL: itemURL) {
collectionAccessQueue.async {
self.collection.append(itemURL)
}
}
}
} catch {
// If an error occurs while trying to read the directory's contents, log or handle it here if needed
printOS("Error processing location: \(location), \(error)")
}
} else {
// The directory does not exist; skip it
}
}
private func shouldSkipItem(_ itemL: String, at itemURL: URL) -> Bool {
var containsItem = false
collectionAccessQueue.sync {
containsItem = self.collection.contains(itemURL)
}
return itemL.hasPrefix("comapple") && !["comappleconfigurator", "comappledt", "comappleiwork"].contains(where: itemL.hasPrefix) || containsItem || !isSupportedFileType(at: itemURL.path)
}
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: "")
if self.appInfo.webApp {
return itemL.contains(bundleIdentifierL)
} else {
if itemL.contains("xcode") && bundleIdentifierL.contains("comappledt") {
return !(itemURL.path.contains("comrobotsandpencilsxcodesapp") || itemURL.path.contains("comoneminutegamesxcodecleaner") || itemURL.path.contains("iohyperappxcodecleaner") || itemURL.path.contains("xcodesjson")) && (itemL.contains(bundle) || itemL.contains(bundleIdentifierL) || (nameL.count < 6 && itemL.contains(nameL)))
} else if itemL.contains("xcodes") && bundleIdentifierL.contains("comrobotsandpencilsxcodesapp") {
return !(itemURL.path.contains("comappledt")) && (itemL.contains(bundle) || itemL.contains(bundleIdentifierL) || (nameL.count > 4 && itemL.contains(nameL)))
} else {
return itemL.contains(bundleIdentifierL) || itemL.contains(bundle) || (nameL.count > 3 && itemL.contains(nameL)) || (nameP.count > 3 && itemL.contains(nameP))
}
}
}
private func filterParentDirectories(in collection: [URL]) -> [URL] {
// Normalize URLs by removing percent encoding and trailing slashes for accurate comparison
var filteredURLs: [URL] = []
let normalizedURLs = collection.map { url -> URL in
let path = url.path.removingPercentEncoding?.trimmingCharacters(in: CharacterSet(charactersIn: "/")) ?? ""
return URL(fileURLWithPath: path)
}
for url in normalizedURLs {
// Determine if 'url' is a parent of any path in 'filteredURLs'
if !filteredURLs.contains(where: { $0.absoluteString.hasPrefix(url.absoluteString) }) {
filteredURLs.append(url)
}
}
// Remove URLs that are parents of another URL in the list
filteredURLs = filteredURLs.filter { parentUrl in
!filteredURLs.contains { childUrl in
childUrl != parentUrl && childUrl.absoluteString.hasPrefix(parentUrl.absoluteString)
}
}
return filteredURLs
}
private func getGroupContainers(bundleURL: URL) -> [URL] {
// Get group containers
var staticCode: SecStaticCode?
guard SecStaticCodeCreateWithPath(bundleURL as CFURL, [], &staticCode) == errSecSuccess else {
return []
}
var signingInformation: CFDictionary?
let status = SecCodeCopySigningInformation(staticCode!, SecCSFlags(), &signingInformation)
if status != errSecSuccess {
printOS("Failed to copy signing information. Status: \(status)")
return []
}
guard let topDict = signingInformation as? [String: Any],
let entitlementsDict = topDict["entitlements-dict"] as? [String: Any],
let appGroups = entitlementsDict["com.apple.security.application-groups"] as? [String] else {
// printOS("No application groups to extract from entitlements for this app.")
return []
}
let groupContainersPath = appGroups.map { URL(fileURLWithPath: "\(home)/Library/Group Containers/" + $0) }
let existingGroupContainers = groupContainersPath.filter { FileManager.default.fileExists(atPath: $0.path) }
return existingGroupContainers
}
private func finalizeCollection() {
DispatchQueue.global(qos: .userInitiated).async {
let groupContainers = self.getGroupContainers(bundleURL: self.appInfo.path)
var tempCollection: [URL] = []
self.collectionAccessQueue.sync {
tempCollection = self.collection
}
tempCollection.append(contentsOf: groupContainers)
// Apply the filter to remove parent directories
let filteredCollection = self.filterParentDirectories(in: tempCollection)
// Continue with the sorted collection
let sortedCollection = filteredCollection.sorted(by: { $0.absoluteString < $1.absoluteString })
self.handlePostProcessing(sortedCollection: sortedCollection)
}
}
private func handlePostProcessing(sortedCollection: [URL]) {
// Calculate file details (sizes and icons), update app state, and call completion
var fileSize: [URL: Int64] = [:]
var fileIcon: [URL: NSImage?] = [:]
for path in sortedCollection {
fileSize[path] = totalSizeOnDisk(for: path)
fileIcon[path] = getIconForFileOrFolderNS(atPath: path)
}
DispatchQueue.main.async {
var updatedCollection = sortedCollection
if updatedCollection.count == 1, let firstURL = updatedCollection.first, firstURL.path.contains(".Trash") {
updatedCollection.removeAll()
}
// Update appInfo and appState with the new values
self.appInfo.files = updatedCollection
self.appInfo.fileSize = fileSize
self.appInfo.fileIcon = fileIcon
if !self.backgroundRun {
self.appState.appInfo = self.appInfo
self.appState.selectedItems = Set(updatedCollection)
}
self.appState.appInfoStore.append(self.appInfo)
if self.instantSearch {
self.appState.instantProgress += 1
}
self.completion()
}
}
}
//assert(!Thread.isMainThread, "This method should not run on the main thread")
+4
View File
@@ -28,6 +28,8 @@ class AppState: ObservableObject
@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 popCount: Int = 0
@Published var instantProgress: Double = 0.0
@Published var instantTotal: Double = 0.0
init() { init() {
@@ -106,6 +108,7 @@ enum CurrentTabView:Int
{ {
case general case general
case interface case interface
case folders
case update case update
case about case about
@@ -113,6 +116,7 @@ enum CurrentTabView:Int
switch self { switch self {
case .general: return "General" case .general: return "General"
case .interface: return "Interface" case .interface: return "Interface"
case .folders: return "Folders"
case .update: return "Update" case .update: return "Update"
case .about: return "About" case .about: return "About"
} }
+2 -2
View File
@@ -33,7 +33,7 @@ class DeeplinkManager {
let queryItems = components.queryItems { let queryItems = components.queryItems {
if let path = queryItems.first(where: { $0.name == DeepLinkConstants.query })?.value { if let path = queryItems.first(where: { $0.name == DeepLinkConstants.query })?.value {
let pathURL = URL(fileURLWithPath: path) let pathURL = URL(fileURLWithPath: path)
let appInfo = getAppInfo(atPath: pathURL) let appInfo = AppInfoFetcher.getAppInfo(atPath: pathURL)
showAppInFiles(appInfo: appInfo!, appState: appState, locations: locations, showPopover: $showPopover) showAppInFiles(appInfo: appInfo!, appState: appState, locations: locations, showPopover: $showPopover)
} else { } else {
printOS("No path query parameter found in the URL") printOS("No path query parameter found in the URL")
@@ -45,7 +45,7 @@ class DeeplinkManager {
func handleAppBundle(url: URL, appState: AppState, locations: Locations) { func handleAppBundle(url: URL, appState: AppState, locations: Locations) {
let appInfo = getAppInfo(atPath: url) let appInfo = AppInfoFetcher.getAppInfo(atPath: url)
showAppInFiles(appInfo: appInfo!, appState: appState, locations: locations, showPopover: $showPopover) showAppInFiles(appInfo: appInfo!, appState: appState, locations: locations, showPopover: $showPopover)
} }
-2
View File
@@ -14,7 +14,6 @@ class Locations: ObservableObject {
var paths: [String] var paths: [String]
} }
let home: String
let cacheDir: String let cacheDir: String
let tempDir: String let tempDir: String
@@ -24,7 +23,6 @@ class Locations: ObservableObject {
// var plugins: Category // var plugins: Category
init() { init() {
self.home = FileManager.default.homeDirectoryForCurrentUser.path
let (cacheDir, tempDir) = darwinCT() let (cacheDir, tempDir) = darwinCT()
self.cacheDir = cacheDir self.cacheDir = cacheDir
self.tempDir = tempDir self.tempDir = tempDir
+104 -559
View File
@@ -8,14 +8,11 @@
import Foundation import Foundation
import SwiftUI import SwiftUI
// Get all apps from /Applications and ~/Applications // Get all apps from /Applications and ~/Applications
func getSortedApps() -> [AppInfo] { func getSortedApps(paths: [String], appState: AppState) -> [AppInfo] {
@AppStorage("settings.general.instant") var instantSearch: Bool = true
let fileManager = FileManager.default let fileManager = FileManager.default
// Define the paths for system and user applications
let systemAppsPath = "/Applications"
let userAppsPath = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Applications").path
var apps: [URL] = [] var apps: [URL] = []
func collectAppPaths(at directoryPath: String) { func collectAppPaths(at directoryPath: String) {
@@ -37,214 +34,78 @@ func getSortedApps() -> [AppInfo] {
} }
} }
// Collect system applications // Collect system applications
collectAppPaths(at: systemAppsPath) paths.forEach { collectAppPaths(at: $0) }
// Collect user applications
collectAppPaths(at: userAppsPath)
// Get app info and sort // Get app info and sort
let sortedApps = apps let sortedApps = apps
.compactMap { 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() }
return sortedApps if instantSearch {
} updateOnMain {
appState.instantTotal = Double(sortedApps.count)
// Get app bundle information from provided path
func getAppInfo(atPath path: URL, wrapped: Bool = false) -> AppInfo? {
let filemanager = FileManager.default
let wrapperURL = path.appendingPathComponent("Wrapper")
if filemanager.fileExists(atPath: wrapperURL.path) {
do {
let contents = try filemanager.contentsOfDirectory(at: wrapperURL, includingPropertiesForKeys: nil, options: [])
let appFiles = contents.filter { $0.pathExtension == "app" }
if let firstAppFile = appFiles.first {
let fullPath = wrapperURL.appendingPathComponent(firstAppFile.lastPathComponent)
if let wrappedAppInfo = getAppInfo(atPath: fullPath, wrapped: true) {
return wrappedAppInfo
}
} else {
printOS("No .app files found in the 'Wrapper' directory: \(wrapperURL)")
}
} catch {
printOS("Error reading contents of 'Wrapper' directory: \(error.localizedDescription)\n\(wrapperURL)")
}
} else {
if let bundle = Bundle(url: path) {
if let bundleIdentifier = bundle.bundleIdentifier {
var appIconFileName = bundle.infoDictionary?["CFBundleIconFile"] as? String ?? ""
var appVersion: String?
var appIcon: NSImage?
var appName: String?
var webApp: Bool?
var wrappedApp: Bool?
var system: Bool?
if let shortVersion = bundle.infoDictionary?["CFBundleShortVersionString"] as? String, !shortVersion.isEmpty {
appVersion = shortVersion
} else {
if let bundleVersion = bundle.infoDictionary?["CFBundleVersion"] as? String, !bundleVersion.isEmpty {
appVersion = bundleVersion
} else {
printOS("Failed to retrieve bundle version")
}
}
if let localizedName = bundle.localizedInfoDictionary?[kCFBundleNameKey as String] as? String {
appName = localizedName
} else if let bundleName = bundle.infoDictionary?["CFBundleName"] as? String {
appName = bundleName
} else {
appName = path.deletingPathExtension().lastPathComponent
}
if appIconFileName.isEmpty {
if let iconsDict = bundle.infoDictionary?["CFBundleIcons"] as? [String: Any] {
if let primaryIconDict = iconsDict["CFBundlePrimaryIcon"] as? [String: Any],
let iconFiles = primaryIconDict["CFBundleIconFiles"] as? [String],
let primaryIconFile = iconFiles.first {
// Now, you can use the primaryIconFile to construct the path to the icon file
let iconPath = path.appendingPathComponent(primaryIconFile)
appIconFileName = iconPath.path
if let contents = try? FileManager.default.contentsOfDirectory(at: path, includingPropertiesForKeys: nil) {
// Find the first file that matches the specified prefix
if let foundURL = contents.first(where: { $0.lastPathComponent.hasPrefix(primaryIconFile) }),
let image = NSImage(contentsOfFile: foundURL.path) {
appIcon = image
} else {
printOS("No matching image found for \(primaryIconFile) in wrapped app.")
}
} else {
printOS("Unable to access the directory at \(primaryIconFile) for wrapped app.")
}
}
}
}
// Check if the icon file name has an .icns extension, if not, add it
if !appIconFileName.hasSuffix(".icns") {
appIconFileName += ".icns"
}
// Try to find the icon in the main bundle
if let iconPath = bundle.path(forResource: appIconFileName, ofType: nil),
let icon = NSImage(contentsOfFile: iconPath) {
appIcon = icon
}
// If not found, try to find it in the resources directory
if appIcon == nil,
let resourcesPath = bundle.resourcePath {
let iconURL = URL(fileURLWithPath: resourcesPath).appendingPathComponent(appIconFileName)
if let icon = NSImage(contentsOfFile: iconURL.path) {
appIcon = icon
}
}
// Convert the icon to a 100x100 PNG image
if let pngIcon = appIcon.flatMap({ convertICNSToPNG(icon: $0, size: NSSize(width: 100, height: 100)) }) {
appIcon = pngIcon
}
if appIcon == nil {
printOS("App Icon not found for app at path: \(path)")
}
if bundle.infoDictionary?["LSTemplateApplication"] is Bool {
webApp = true
} else {
webApp = false
}
if wrapped {
wrappedApp = true
} else {
wrappedApp = false
}
if !path.path.contains(home) {
system = true
} else {
system = false
}
return AppInfo(id: UUID(), path: path, bundleIdentifier: bundleIdentifier, appName: appName ?? "", appVersion: appVersion ?? "", appIcon: appIcon, webApp: webApp ?? false, wrapped: wrappedApp ?? false, system: system ?? false, files: [], fileSize: [:], fileIcon: [:])
} else {
printOS("Bundle identifier not found at path: \(path)")
}
} else {
printOS("Bundle not found at path: \(path)")
} }
} }
return nil
return sortedApps
} }
// Get directory path for darwin cache and temp directories // Get directory path for darwin cache and temp directories
func darwinCT() -> (String, String) { func darwinCT() -> (String, String) {
let task = Process() let command = "echo $(getconf DARWIN_USER_CACHE_DIR) $(getconf DARWIN_USER_TEMP_DIR)"
task.launchPath = "/bin/bash" let process = Process()
task.arguments = ["-c", "getconf DARWIN_USER_CACHE_DIR; getconf DARWIN_USER_TEMP_DIR"] process.launchPath = "/bin/bash"
process.arguments = ["-c", command]
let pipe = Pipe() let pipe = Pipe()
task.standardOutput = pipe process.standardOutput = pipe
task.launch() process.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile() let data = pipe.fileHandleForReading.readDataToEndOfFile()
if let output = String(data: data, encoding: .utf8) { guard let output = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) else {
let components = output.components(separatedBy: "\n") printOS("Could not get DARWIN_USER_CACHE_DIR or DARWIN_USER_TEMP_DIR")
if components.count >= 2 { return ("", "")
var cacheDir = components[0].trimmingCharacters(in: .whitespacesAndNewlines)
var tempDir = components[1].trimmingCharacters(in: .whitespacesAndNewlines)
if cacheDir.hasSuffix("/") {
cacheDir.removeLast()
}
if tempDir.hasSuffix("/") {
tempDir.removeLast()
}
return (cacheDir, tempDir)
}
} }
printOS("Could not get DARWIN_USER_CACHE_DIR or DARWIN_USER_TEMP_DIR")
return ("", "") let paths = output.split(separator: " ").map(String.init)
guard paths.count >= 2 else {
printOS("Could not parse DARWIN_USER_CACHE_DIR or DARWIN_USER_TEMP_DIR")
return ("", "")
}
return (paths[0].trimmingCharacters(in: .whitespaces), paths[1].trimmingCharacters(in: .whitespaces))
} }
func listAppSupportDirectories() -> [String] { func listAppSupportDirectories() -> [String] {
let home = FileManager.default.homeDirectoryForCurrentUser let fileManager = FileManager.default
let appSupportLocation = home.appendingPathComponent("Library/Application Support/") let home = fileManager.homeDirectoryForCurrentUser
let exclusions = ["MobileSync", ".DS_Store", "Xcode", "SyncServices", "networkserviceproxy", "DiskImages", "CallHistoryTransactions", "App Store", "CloudDocs", "icdd", "iCloud", "Instruments", "AddressBook", "FaceTime", "AskPermission", "CallHistoryDB"] let appSupportLocation = home.appendingPathComponent("Library/Application Support").path
let exclusions = Set(["MobileSync", ".DS_Store", "Xcode", "SyncServices", "networkserviceproxy", "DiskImages", "CallHistoryTransactions", "App Store", "CloudDocs", "icdd", "iCloud", "Instruments", "AddressBook", "FaceTime", "AskPermission", "CallHistoryDB"])
let exclusionRegex = try! NSRegularExpression(pattern: "\\bcom\\.apple\\b", options: []) let exclusionRegex = try! NSRegularExpression(pattern: "\\bcom\\.apple\\b", options: [])
do { do {
let fileManager = FileManager.default let directoryContents = try fileManager.contentsOfDirectory(atPath: appSupportLocation)
let directoryContents = try fileManager.contentsOfDirectory(at: appSupportLocation, includingPropertiesForKeys: [.isDirectoryKey], options: .skipsHiddenFiles)
let filteredDirectories: [String] = directoryContents.compactMap { url in return directoryContents.compactMap { directoryName in
let fullPath = appSupportLocation.appending("/\(directoryName)")
var isDirectory: ObjCBool = false var isDirectory: ObjCBool = false
guard fileManager.fileExists(atPath: url.path, isDirectory: &isDirectory) else {
guard fileManager.fileExists(atPath: fullPath, isDirectory: &isDirectory),
isDirectory.boolValue else {
return nil return nil
} }
let directoryName = url.lastPathComponent
// Check for exclusions using regex and provided list // Check for exclusions using regex and provided list
let excludeByRegex = exclusionRegex.firstMatch(in: directoryName, options: [], range: NSRange(location: 0, length: directoryName.utf16.count)) != nil let excludeByRegex = exclusionRegex.firstMatch(in: directoryName, options: [], range: NSRange(location: 0, length: directoryName.utf16.count)) != nil
let excludeByList = exclusions.contains(directoryName) if exclusions.contains(directoryName) || excludeByRegex {
return nil
return isDirectory.boolValue && !excludeByRegex && !excludeByList ? directoryName : nil }
return directoryName
} }
return filteredDirectories
} catch { } catch {
printOS("Error listing AppSupport directories: \(error.localizedDescription)") printOS("Error listing AppSupport directories: \(error.localizedDescription)")
return [] return []
@@ -253,382 +114,97 @@ func listAppSupportDirectories() -> [String] {
// Check if app is running before deleting app files
func killApp(appId: String, completion: @escaping () -> Void = {}) {
let runningApps = NSWorkspace.shared.runningApplications
for app in runningApps {
if app.bundleIdentifier == appId {
app.terminate()
}
}
completion()
}
// Find all possible paths for an app based on name/bundle id
func findPathsForApp(appInfo: AppInfo = .empty, appState: AppState, locations: Locations, backgroundRun: Bool = false, completion: @escaping () -> Void = {}) {
Task(priority: .high) {
var collection: [URL] = []
if let url = URL(string: appInfo.path.absoluteString) {
if !url.path.contains(".Trash") {
if url.path.contains("Wrapper") {
let modifiedUrl = url.deletingLastPathComponent().deletingLastPathComponent()
collection.insert(modifiedUrl, at: 0)
} else {
collection.insert(url, at: 0)
}
}
}
let fileManager = FileManager.default
let dispatchGroup = DispatchGroup()
var bundleComponents = appInfo.bundleIdentifier.components(separatedBy: ".")
if let lastComponent = bundleComponents.last, let rangeOfDash = lastComponent.range(of: "-") {
let updatedLastComponent = String(lastComponent[..<rangeOfDash.lowerBound])
bundleComponents[bundleComponents.count - 1] = updatedLastComponent
}
var bundle: String = ""
if bundleComponents.count >= 3 { // get last 2 or middle 2 components
bundle = bundleComponents[1...2].joined(separator: "").lowercased()
}
let nameL = appInfo.appName.pearFormat()
let nameP = appInfo.path.lastPathComponent.replacingOccurrences(of: ".app", with: "")
let bundleIdentifierL = appInfo.bundleIdentifier.pearFormat()
for location in locations.apps.paths {
if !fileManager.fileExists(atPath: location) {
continue
}
dispatchGroup.enter() // Enter the dispatch group
do {
let contents = try fileManager.contentsOfDirectory(atPath: location)
for item in contents {
let itemURL = URL(fileURLWithPath: location).appendingPathComponent(item)
let itemL = ("\(item)").replacingOccurrences(of: ".", with: "").replacingOccurrences(of: " ", with: "").lowercased()
let filterItem = "\(location)/\(itemL)"
// Skip directories that start with com.apple. except for a few
if itemL.hasPrefix("comapple") && !itemL.hasPrefix("comappleconfigurator") && !itemL.hasPrefix("comappledt") && !itemL.hasPrefix("comappleiwork") {
continue
}
// Skip adding item if the collection already has it to prevent duplicates
if collection.contains(itemURL) {
continue
}
// Skip if not a regular file, directory or symlink
if !isSupportedFileType(at: itemURL.path) {
continue
}
// Catch web app plist files
if appInfo.webApp {
if itemL.contains(bundleIdentifierL) {
if collection.contains(itemURL) {
continue
}
collection.append(itemURL)
}
} else {
// Catch Xcode files for Xcodes app
if itemL.contains("xcode") && bundleIdentifierL.contains("comappledt") {
if filterItem.contains("comrobotsandpencilsxcodesapp") || filterItem.contains("comoneminutegamesxcodecleaner") || filterItem.contains("iohyperappxcodecleaner") || filterItem.contains("xcodesjson") {
continue
}
if itemL.contains(bundle) || itemL.contains(bundleIdentifierL) || (nameL.count < 6 && itemL.contains(nameL)) {
collection.append(itemURL)
}
} else if itemL.contains("xcodes") && bundleIdentifierL.contains("comrobotsandpencilsxcodesapp") {
if filterItem.contains("comappledt") {
continue
}
if itemL.contains(bundle) || itemL.contains(bundleIdentifierL) || (nameL.count > 4 && itemL.contains(nameL)) {
collection.append(itemURL)
}
}
else {
if itemL.contains(bundleIdentifierL) || itemL.contains(bundle) || (nameL.count > 3 && itemL.contains(nameL) || (nameP.count > 3 && itemL.contains(nameP))) {
collection.append(itemURL)
}
}
}
}
} catch {
printOS("Error processing location:", location, error)
continue
}
dispatchGroup.leave() // Leave the dispatch group
}
// Append group containers
let groupContainers = getGroupContainers(bundleURL: appState.appInfo.path)
collection.append(contentsOf: groupContainers)
var sortedCollection = collection.sorted(by: { $0.absoluteString < $1.absoluteString })
// Calculate file details (sizes and icons)
var fileSize: [URL: Int64] = [:]
var fileIcon: [URL: NSImage?] = [:]
var updatedAppInfo = appInfo
for path in collection {
var size: Int64
var icon: NSImage? = nil
size = totalSizeOnDisk(for: path)
icon = getIconForFileOrFolderNS(atPath: path)
fileSize[path] = size
fileIcon[path] = icon
}
if sortedCollection.count == 1 {
if let firstURL = sortedCollection.first, firstURL.path.contains(".Trash") {
sortedCollection = []
}
}
// Save to appState
dispatchGroup.notify(queue: .main) {
updateOnMain {
updatedAppInfo.files = sortedCollection
updatedAppInfo.fileSize = fileSize
updatedAppInfo.fileIcon = fileIcon
if !backgroundRun {
appState.appInfo = updatedAppInfo
appState.selectedItems = Set(sortedCollection)
}
appState.appInfoStore.append(updatedAppInfo)
}
}
completion()
}
}
// 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 loadAllPaths(allApps: [AppInfo], appState: AppState, locations: Locations, reverseAddon: Bool = false, completion: @escaping () -> Void = {}) {
let dispatchGroup = DispatchGroup() let dispatchGroup = DispatchGroup()
let retryLimit = 120 appState.appInfoStore.removeAll()
var currentRetryCount = 0
func checkCompletion() { for app in allApps {
if appState.appInfoStore.count == allApps.count { dispatchGroup.enter()
if reverseAddon { DispatchQueue.global(qos: .background).async {
reversePathsSearch(appState: appState, locations: locations) let pathFinder = AppPathFinder(appInfo: app, appState: appState, locations: locations, backgroundRun: true)
} pathFinder.findPaths()
completion() dispatchGroup.leave()
} else {
if currentRetryCount < retryLimit {
currentRetryCount += 1
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
checkCompletion()
}
} else {
printOS("loadAllPaths - Retry limit timed out. Unable to load all paths within 1 minute.")
completion()
}
} }
} }
dispatchGroup.enter()
updateOnMain {
appState.appInfoStore.removeAll()
}
dispatchGroup.notify(queue: .main) {
checkCompletion()
}
DispatchQueue.global(qos: .background).async { DispatchQueue.global(qos: .background).async {
for app in allApps { _ = dispatchGroup.wait(timeout: .now() + 60)
findPathsForApp(appInfo: app, appState: appState, locations: locations, backgroundRun: true)
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()
}
// Reset progress values to 0
appState.instantProgress = 0
appState.instantTotal = 0
completion()
} else if retryCount < maxRetry {
checkAllAppsProcessed(retryCount: retryCount + 1, maxRetry: maxRetry)
} else {
printOS("loadAllPaths - Retry limit reached. Not all paths were loaded.")
completion()
}
}
} }
dispatchGroup.leave()
checkAllAppsProcessed()
} }
} }
// Load item in Files view // Load item in Files view
func showAppInFiles(appInfo: AppInfo, appState: AppState, locations: Locations, showPopover: Binding<Bool>) { func showAppInFiles(appInfo: AppInfo, appState: AppState, locations: Locations, showPopover: Binding<Bool>) {
showPopover.wrappedValue = false showPopover.wrappedValue = false
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
updateOnMain { updateOnMain {
appState.appInfo = .empty appState.appInfo = .empty
appState.selectedItems = [] appState.selectedItems = []
if let storedAppInfo = appState.appInfoStore.first(where: { $0.path == appInfo.path }) {
appState.appInfo = storedAppInfo // Check if the appInfo exists in the appState.appInfoStore
appState.selectedItems = Set(storedAppInfo.files) if let storedAppInfo = appState.appInfoStore.first(where: { $0.path == appInfo.path }) {
withAnimation(Animation.easeIn(duration: 0.4)) { // Update appState with the stored app info and selected items.
appState.currentView = .files appState.appInfo = storedAppInfo
showPopover.wrappedValue.toggle() appState.selectedItems = Set(storedAppInfo.files)
}
} else { // Trigger the animation for changing views and showing the popover.
// Handle the case where the appInfo is not found in the store withAnimation(Animation.easeIn(duration: 0.4)) {
withAnimation(Animation.easeIn(duration: 0.4)) { appState.currentView = .files
appState.showProgress = true showPopover.wrappedValue.toggle()
appState.currentView = .files
showPopover.wrappedValue.toggle()
}
appState.appInfo = appInfo
findPathsForApp(appInfo: appInfo, appState: appState, locations: locations) {
withAnimation(Animation.easeIn(duration: 0.4)) {
updateOnMain {
appState.showProgress = false
}
}
}
} }
} } else {
// When the appInfo is not found, show progress, and search for paths.
} appState.showProgress = true
}
// Get group containers
func getGroupContainers(bundleURL: URL) -> [URL] {
let fileManager = FileManager.default
var staticCode: SecStaticCode?
guard SecStaticCodeCreateWithPath(bundleURL as CFURL, [], &staticCode) == errSecSuccess else {
return []
}
var signingInformation: CFDictionary?
let status = SecCodeCopySigningInformation(staticCode!, SecCSFlags(), &signingInformation)
if status != errSecSuccess {
printOS("Failed to copy signing information. Status: \(status)")
return []
}
guard let topDict = signingInformation as? [String: Any],
let entitlementsDict = topDict["entitlements-dict"] as? [String: Any],
let appGroups = entitlementsDict["com.apple.security.application-groups"] as? [String] else {
// printOS("No application groups to extract from entitlements for this app.")
return []
}
let groupContainersPath = appGroups.map { URL(fileURLWithPath: "\(home)/Library/Group Containers/" + $0) }
let existingGroupContainers = groupContainersPath.filter { fileManager.fileExists(atPath: $0.path) }
return existingGroupContainers
}
// Reverse search for leftover zombie files
func reversePathsSearch(appState: AppState, locations: Locations, completion: @escaping () -> Void = {}) {
Task(priority: .high) {
var collection: [URL] = []
let fileManager = FileManager.default
let dispatchGroup = DispatchGroup()
let allPaths = appState.appInfoStore.flatMap { $0.files.map { $0.path.pearFormat() } }
let allNames = appState.appInfoStore.map { $0.appName.pearFormat() }
let 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"] // Skip system folders
// Skip locations that might not exist
for location in locations.reverse.paths {
if !fileManager.fileExists(atPath: location) {
continue
}
dispatchGroup.enter()
do {
let contents = try fileManager.contentsOfDirectory(atPath: location)
for item in contents {
let itemURL = URL(fileURLWithPath: location).appendingPathComponent(item)
let itemName = ("\(item)").pearFormat()
let itemPath = "\(location)/\(itemName)".pearFormat()
if skipped.contains(where: { itemName.contains($0) }) {
continue
}
if allPaths.contains(where: { $0 == itemPath }) || allNames.contains(where: { $0 == itemName }) {
continue
}
if !isSupportedFileType(at: itemURL.path) {
continue
}
collection.append(itemURL)
}
} catch {
printOS("Error processing location:", location, error)
continue
}
dispatchGroup.leave() // Leave the dispatch group
}
// Calculate file details (sizes and icons)
var fileSize: [URL: Int64] = [:]
var fileIcon: [URL: NSImage?] = [:]
var updatedZombieFile = ZombieFile.empty
for path in collection {
var size: Int64
var icon: NSImage? = nil
size = totalSizeOnDisk(for: path)
icon = getIconForFileOrFolderNS(atPath: path)
fileSize[path] = size
fileIcon[path] = icon
}
// Save to appState
dispatchGroup.notify(queue: .main) {
// Initialize the path finder and execute its search.
let pathFinder = AppPathFinder(appInfo: appInfo, appState: appState, locations: locations) {
updateOnMain { updateOnMain {
updatedZombieFile.fileSize = fileSize // Update the progress indicator on the main thread once the search completes.
updatedZombieFile.fileIcon = fileIcon
appState.zombieFile = updatedZombieFile
appState.showProgress = false appState.showProgress = false
} }
} }
pathFinder.findPaths()
appState.appInfo = appInfo
completion() // Animate the view change and popover display.
withAnimation(Animation.easeIn(duration: 0.4)) {
appState.currentView = .files
showPopover.wrappedValue.toggle()
}
}
} }
} }
// Move files to trash using applescript/Finder so it asks for user password if needed // Move files to trash using applescript/Finder so it asks for user password if needed
func moveFilesToTrash(at fileURLs: [URL], completion: @escaping () -> Void = {}) { func moveFilesToTrash(at fileURLs: [URL], completion: @escaping () -> Void = {}) {
@AppStorage("settings.sentinel.enable") var sentinel: Bool = false @AppStorage("settings.sentinel.enable") var sentinel: Bool = false
@@ -658,37 +234,6 @@ func moveFilesToTrash(at fileURLs: [URL], completion: @escaping () -> Void = {})
} }
func isSocketFile(at url: URL) -> Bool {
do {
let attributes = try FileManager.default.attributesOfItem(atPath: url.path)
if let fileType = attributes[FileAttributeKey.type] as? FileAttributeType {
return fileType == .typeSocket
}
} catch {
print("Error: \(error)")
}
return false
}
func isSupportedFileType(at path: String) -> Bool {
let fileManager = FileManager.default
do {
let attributes = try fileManager.attributesOfItem(atPath: path)
if let fileType = attributes[FileAttributeKey.type] as? FileAttributeType {
switch fileType {
case .typeRegular, .typeDirectory, .typeSymbolicLink:
// The file is a regular file, directory, or symbolic link
return true
default:
// The file is a socket, pipe, or another type not supported
return false
}
}
} catch {
print("Error getting file attributes: \(error)")
}
return false
}
// Undo trash action // Undo trash action
+90
View File
@@ -0,0 +1,90 @@
//
// ReversePathsFetch.swift
// Pearcleaner
//
// Created by Alin Lupascu on 3/20/24.
//
import Foundation
import AppKit
class ReversePathsSearcher {
private let appState: AppState
private let locations: Locations
private let fileManager = FileManager.default
private var collection: [URL] = []
private var fileSize: [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()
self.calculateFileDetails()
self.updateAppState()
completion()
}
}
private func processLocations() {
let allPaths = appState.appInfoStore.flatMap { $0.files.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
let itemURL = URL(fileURLWithPath: location).appendingPathComponent(itemName)
processItem(itemName, itemURL: itemURL, allPaths: allPaths, allNames: allNames)
}
} catch {
printOS("Error processing location: \(location), error: \(error)")
}
}
private func processItem(_ itemName: String, itemURL: URL, allPaths: [String], allNames: [String]) {
let formattedItemName = itemName.pearFormat()
let itemPath = itemURL.path.pearFormat()
guard !skipped.contains(where: { formattedItemName.contains($0) }),
!allPaths.contains(itemPath),
!allNames.contains(formattedItemName),
isSupportedFileType(at: itemURL.path) else {
return
}
collection.append(itemURL)
}
private func calculateFileDetails() {
collection.forEach { path in
fileSize[path] = totalSizeOnDisk(for: path)
fileIcon[path] = getIconForFileOrFolderNS(atPath: path)
}
}
private func updateAppState() {
dispatchGroup.notify(queue: .main) {
var updatedZombieFile = ZombieFile.empty
updatedZombieFile.fileSize = self.fileSize
updatedZombieFile.fileIcon = self.fileIcon
self.appState.zombieFile = updatedZombieFile
self.appState.showProgress = false
}
}
}
+47 -4
View File
@@ -308,6 +308,7 @@ struct AnimatedSearchStyle: TextFieldStyle {
@FocusState private var isFocused: Bool @FocusState private var isFocused: Bool
@Binding var text: String @Binding var text: String
@EnvironmentObject var appState: AppState @EnvironmentObject var appState: AppState
@EnvironmentObject var fsm: FolderSettingsManager
func _body(configuration: TextField<Self._Label>) -> some View { func _body(configuration: TextField<Self._Label>) -> some View {
@@ -332,7 +333,7 @@ struct AnimatedSearchStyle: TextFieldStyle {
withAnimation { withAnimation {
// Refresh Apps list // Refresh Apps list
appState.reload.toggle() appState.reload.toggle()
let sortedApps = getSortedApps() let sortedApps = getSortedApps(paths: fsm.folderPaths, appState: appState)
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
appState.sortedApps = sortedApps appState.sortedApps = sortedApps
appState.reload.toggle() appState.reload.toggle()
@@ -435,12 +436,54 @@ struct SimpleSearchStyle: TextFieldStyle {
} }
} }
.disabled(appState.instantProgress != 0 && appState.instantProgress != appState.instantTotal)
.padding(6) .padding(6)
.overlay( .overlay(
Group { Group {
RoundedRectangle(cornerRadius: 6) GeometryReader { geometry in
.strokeBorder(Color("mode").opacity(0.2), lineWidth: 1) ZStack(alignment: .leading) {
.allowsHitTesting(false)
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.gray.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()
//// }
// }
} }
) )
+100 -78
View File
@@ -312,7 +312,7 @@ func startEnd(_ function: @escaping () -> Void) {
function() function()
let endTime = Date() let endTime = Date()
let executionTime = endTime.timeIntervalSince(startTime) let executionTime = endTime.timeIntervalSince(startTime)
print("Function executed in: \n\(executionTime) seconds") printOS("Function executed in: \n\(executionTime) seconds")
} }
@@ -327,6 +327,17 @@ func relaunchApp(afterDelay seconds: TimeInterval = 0.5) -> Never {
exit(0) exit(0)
} }
// Check if app is running before deleting app files
func killApp(appId: String, completion: @escaping () -> Void = {}) {
let runningApps = NSWorkspace.shared.runningApplications
for app in runningApps {
if app.bundleIdentifier == appId {
app.terminate()
}
}
completion()
}
// Remove app from cache // Remove app from cache
func removeApp(appState: AppState, withId id: UUID) { func removeApp(appState: AppState, withId id: UUID) {
DispatchQueue.main.async { DispatchQueue.main.async {
@@ -379,41 +390,80 @@ extension FileManager {
// --- Extend print command to also output to the Console --- // --- Extend print command to also output to the Console ---
func printOS(_ items: Any..., separator: String = " ", terminator: String = "\n") { func printOS(_ items: Any..., separator: String = " ", terminator: String = "\n") {
let message = items.map { "\($0)" }.joined(separator: separator) let message = items.map { "\($0)" }.joined(separator: separator)
let log = OSLog(subsystem: "com.alienator88.Pearcleaner", category: "Application") let log = OSLog(subsystem: "pearcleaner", category: "Application")
os_log("%@", log: log, type: .debug, message) os_log("%@", log: log, type: .default, message)
} }
//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 // Get total size of folders and files using DU cli command
func totalSizeOnDisk(for paths: [URL]) -> Int64 { func totalSizeOnDisk(for paths: [URL]) -> Int64 {
var totalSize = 0 let fileManager = FileManager.default
var totalSize: Int64 = 0
let process = Process() for url in paths {
process.launchPath = "/usr/bin/du" var isDirectory: ObjCBool = false
process.arguments = ["-sk"] + paths.map { (url: URL) -> String in if fileManager.fileExists(atPath: url.path, isDirectory: &isDirectory) {
return url.path if isDirectory.boolValue {
} // It's a directory, recurse into it
let pipe = Pipe() if let enumerator = fileManager.enumerator(at: url, includingPropertiesForKeys: [.totalFileAllocatedSizeKey], errorHandler: nil) {
process.standardOutput = pipe for case let fileURL as URL in enumerator {
process.standardError = pipe//FileHandle.nullDevice do {
let fileAttributes = try fileURL.resourceValues(forKeys: [.totalFileAllocatedSizeKey])
try? process.run() if let fileSize = fileAttributes.totalFileAllocatedSize {
process.waitUntilExit() totalSize += Int64(fileSize)
}
let data = pipe.fileHandleForReading.readDataToEndOfFile() } catch {
if let output = String(data: data, encoding: .utf8) { printOS("Error getting file size for \(fileURL): \(error)")
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) { } else {
totalSize += size // It's a file
do {
let fileAttributes = try url.resourceValues(forKeys: [.totalFileAllocatedSizeKey])
if let fileSize = fileAttributes.totalFileAllocatedSize {
totalSize += Int64(fileSize)
}
} catch {
printOS("Error getting file size for \(url): \(error)")
}
} }
} }
} }
return Int64(totalSize) * 1024 // Convert the size from kilobytes to bytes return totalSize
} }
func totalSizeOnDisk(for path: URL) -> Int64 { func totalSizeOnDisk(for path: URL) -> Int64 {
@@ -428,53 +478,25 @@ func formatByte(size: Int64) -> String {
return byteCountFormatter.string(fromByteCount: size) return byteCountFormatter.string(fromByteCount: size)
} }
// Only process supported files
// Get total size of folders and files using straight swift func isSupportedFileType(at path: String) -> Bool {
extension URL { let fileManager = FileManager.default
func totalAllocatedSize(includingSubfolders: Bool = false) throws -> Int? { do {
return try [self].totalAllocatedSize(includingSubfolders: includingSubfolders) let attributes = try fileManager.attributesOfItem(atPath: path)
} if let fileType = attributes[FileAttributeKey.type] as? FileAttributeType {
switch fileType {
func totalSizeOnDisk(includingSubfolders: Bool = false) throws -> String? { case .typeRegular, .typeDirectory, .typeSymbolicLink:
return try [self].totalSizeOnDisk(includingSubfolders: includingSubfolders) // The file is a regular file, directory, or symbolic link
} return true
} default:
// The file is a socket, pipe, or another type not supported
extension Array where Element == URL { return false
func totalAllocatedSize(includingSubfolders: Bool = false) throws -> Int? { }
var totalSize = 0
for path in self {
let resourceValues = try path.resourceValues(forKeys: [.isDirectoryKey, .isPackageKey, .totalFileAllocatedSizeKey])
if resourceValues.isDirectory == true || path.pathExtension == "app" {
if includingSubfolders {
let filePaths = FileManager.default.subpaths(atPath: path.path) ?? []
for filePath in filePaths {
let fileUrl = path.appendingPathComponent(filePath)
let fileAttributes = try FileManager.default.attributesOfItem(atPath: fileUrl.path)
let fileSize = fileAttributes[.size] as? Int64 ?? 0
totalSize += Int(fileSize)
}
}
} else {
totalSize += resourceValues.totalFileAllocatedSize ?? 0
}
} }
} catch {
return totalSize printOS("Error getting file attributes: \(error)")
}
func totalSizeOnDisk(includingSubfolders: Bool = false) throws -> String? {
if let totalSize = try self.totalAllocatedSize(includingSubfolders: includingSubfolders) {
let byteCountFormatter = ByteCountFormatter()
byteCountFormatter.countStyle = .file
byteCountFormatter.allowedUnits = [.useBytes, .useKB, .useMB, .useTB]
return byteCountFormatter.string(fromByteCount: Int64(totalSize))
}
return nil
} }
return false
} }
@@ -518,11 +540,13 @@ func uninstallPearcleaner(appState: AppState, locations: Locations) {
launchctl(load: false) launchctl(load: false)
// Get app info for Pearcleaner // Get app info for Pearcleaner
let appInfo = getAppInfo(atPath: Bundle.main.bundleURL) let appInfo = AppInfoFetcher.getAppInfo(atPath: Bundle.main.bundleURL)
// appState.appInfo = appInfo! // appState.appInfo = appInfo!
// Find application files for Pearcleaner // Find application files for Pearcleaner
findPathsForApp(appInfo: appInfo!,appState: appState, locations: locations) let pathFinder = AppPathFinder(appInfo: appInfo!, appState: appState, locations: locations)
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) {
@@ -587,6 +611,7 @@ func writeLog(string: String) {
// --- Load Plist file with launchctl --- // --- Load Plist file with launchctl ---
func launchctl(load: Bool, completion: @escaping () -> Void = {}) { func launchctl(load: Bool, completion: @escaping () -> Void = {}) {
let cmd = load ? "load" : "unload" let cmd = load ? "load" : "unload"
if let plistPath = Bundle.main.path(forResource: "com.alienator88.PearcleanerSentinel", ofType: "plist") { if let plistPath = Bundle.main.path(forResource: "com.alienator88.PearcleanerSentinel", ofType: "plist") {
var plistContent = try! String(contentsOfFile: plistPath) var plistContent = try! String(contentsOfFile: plistPath)
let executableURL = Bundle.main.bundleURL.appendingPathComponent("Contents/MacOS/PearcleanerSentinel") let executableURL = Bundle.main.bundleURL.appendingPathComponent("Contents/MacOS/PearcleanerSentinel")
@@ -598,23 +623,20 @@ func launchctl(load: Bool, completion: @escaping () -> Void = {}) {
let temporaryPlistURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("com.alienator88.PearcleanerSentinel.plist") let temporaryPlistURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("com.alienator88.PearcleanerSentinel.plist")
do { do {
try plistContent.write(to: temporaryPlistURL, atomically: false, encoding: .utf8) try plistContent.write(to: temporaryPlistURL, atomically: true, encoding: .utf8)
} catch { } catch {
printOS("Error writing the temporary plist file: \(error)") printOS("Error writing the temporary plist file: \(error)")
return return
} }
let task = Process() let task = Process()
task.launchPath = "/bin/launchctl" task.launchPath = "/bin/launchctl"
task.arguments = [cmd, "-w", temporaryPlistURL.path] task.arguments = [cmd, "-w", temporaryPlistURL.path]
task.standardOutput = FileHandle.nullDevice
let pipe = Pipe() task.standardError = FileHandle.nullDevice
task.standardOutput = pipe
task.standardError = pipe
task.launch() task.launch()
completion() completion()
} }
} }
+7 -3
View File
@@ -14,6 +14,7 @@ struct PearcleanerApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
@StateObject var appState = AppState() @StateObject var appState = AppState()
@StateObject var locations = Locations() @StateObject var locations = Locations()
@StateObject var fsm = FolderSettingsManager()
@State private var windowSettings = WindowSettings() @State private var windowSettings = WindowSettings()
@AppStorage("settings.updater.updateTimeframe") private var updateTimeframe: Int = 1 @AppStorage("settings.updater.updateTimeframe") private var updateTimeframe: Int = 1
@AppStorage("settings.permissions.disk") private var diskP: Bool = false @AppStorage("settings.permissions.disk") private var diskP: Bool = false
@@ -57,6 +58,7 @@ struct PearcleanerApp: App {
} }
.environmentObject(appState) .environmentObject(appState)
.environmentObject(locations) .environmentObject(locations)
.environmentObject(fsm)
.preferredColorScheme(displayMode.colorScheme) .preferredColorScheme(displayMode.colorScheme)
.handlesExternalEvents(preferring: Set(arrayLiteral: "pear"), allowing: Set(arrayLiteral: "*")) .handlesExternalEvents(preferring: Set(arrayLiteral: "pear"), allowing: Set(arrayLiteral: "*"))
.onOpenURL(perform: { url in .onOpenURL(perform: { url in
@@ -96,7 +98,7 @@ struct PearcleanerApp: App {
NSApplication.shared.windows.first?.setFrame(frame, display: true) NSApplication.shared.windows.first?.setFrame(frame, display: true)
// Get Apps // Get Apps
let sortedApps = getSortedApps() let sortedApps = getSortedApps(paths: fsm.folderPaths, appState: appState)
appState.sortedApps = sortedApps appState.sortedApps = sortedApps
@@ -105,16 +107,17 @@ struct PearcleanerApp: App {
loadAllPaths(allApps: sortedApps, appState: appState, locations: locations) 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)
.environmentObject(locations) .environmentObject(locations)
.environmentObject(appState) .environmentObject(appState)
.environmentObject(fsm)
.preferredColorScheme(displayMode.colorScheme) .preferredColorScheme(displayMode.colorScheme)
}, icon: selectedMenubarIcon) }, icon: selectedMenubarIcon)
} }
#if !DEBUG #if !DEBUG
Task { Task {
@@ -153,7 +156,7 @@ struct PearcleanerApp: App {
.windowStyle(.hiddenTitleBar) .windowStyle(.hiddenTitleBar)
.windowResizability(.contentMinSize) .windowResizability(.contentMinSize)
.commands { .commands {
AppCommands(appState: appState, locations: locations) AppCommands(appState: appState, locations: locations, fsm: fsm)
CommandGroup(replacing: .newItem, addition: { }) CommandGroup(replacing: .newItem, addition: { })
} }
@@ -165,6 +168,7 @@ struct PearcleanerApp: App {
SettingsView(showPopover: $showPopover, search: $search, showFeature: $showFeature) SettingsView(showPopover: $showPopover, search: $search, showFeature: $showFeature)
.environmentObject(appState) .environmentObject(appState)
.environmentObject(locations) .environmentObject(locations)
.environmentObject(fsm)
.toolbarBackground(.clear) .toolbarBackground(.clear)
.preferredColorScheme(displayMode.colorScheme) .preferredColorScheme(displayMode.colorScheme)
} }
+202
View File
@@ -0,0 +1,202 @@
//
// Folders.swift
// Pearcleaner
//
// Created by Alin Lupascu on 3/20/24.
//
//
// General.swift
// Pearcleaner
//
// Created by Alin Lupascu on 11/5/23.
//
import Foundation
import SwiftUI
import AppKit
struct FolderSettingsTab: View {
@EnvironmentObject var appState: AppState
@EnvironmentObject var locations: Locations
@EnvironmentObject var fsm: FolderSettingsManager
@State private var isHovered = false
@State private var isHoveredPlus = false
var body: some View {
Form {
VStack {
HStack(spacing: 0) {
Text("Apps").font(.title2)
InfoButton(text: "Locations that will be searched for .app files. Click a non-default path to remove it. Add new folders below or drag/drop a folder over the list.", color: nil, label: "")
Spacer()
}
ScrollView {
VStack(spacing: 5) {
ForEach(fsm.folderPaths.indices, id: \.self) { index in
HStack {
Text(fsm.folderPaths[index])
.font(.callout)
.opacity(fsm.defaultPaths.contains(fsm.folderPaths[index]) ? 0.5 : 1)
.padding(5)
Spacer()
}
.disabled(fsm.defaultPaths.contains(fsm.folderPaths[index]))
.onHover { hovering in
withAnimation(Animation.easeInOut(duration: 0.4)) {
isHovered = hovering
}
if isHovered && !fsm.defaultPaths.contains(fsm.folderPaths[index]) {
NSCursor.disappearingItem.push()
} else {
NSCursor.pop()
}
}
.onTapGesture {
if !fsm.defaultPaths.contains(fsm.folderPaths[index]) {
fsm.removePath(at: index)
}
}
if index != fsm.folderPaths.indices.last {
Divider().opacity(0.5)
}
}
}
}
.scrollIndicators(.automatic)
.padding()
.background(Color("mode").opacity(0.05))
.clipShape(RoundedRectangle(cornerRadius: 10))
.onDrop(of: ["public.file-url"], isTargeted: nil) { providers -> Bool in
providers.forEach { provider in
provider.loadDataRepresentation(forTypeIdentifier: "public.file-url") { (data, error) in
guard let data = data, error == nil,
let url = URL(dataRepresentation: data, relativeTo: nil),
url.hasDirectoryPath else {
printOS("FSM: Failed to load URL or the item is not a folder")
return
}
updateOnMain {
fsm.addPath(url.path)
}
}
}
return true
}
// === OTHER ================================================================================================
ZStack {
RoundedRectangle(cornerRadius: 10)
.fill(Color("mode").opacity(0.1))
// .strokeBorder(Color("mode").opacity(0.1), lineWidth: 2)
.frame(width: 300, height: 100)
Image(systemName: "plus")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 30, height: 30)
.foregroundStyle(isHoveredPlus ? Color("mode") : Color("mode").opacity(0.5))
}
.padding(.top)
.onTapGesture {
selectFolder()
}
.onHover { hovering in
withAnimation(Animation.easeInOut(duration: 0.4)) {
isHoveredPlus = hovering
}
if isHoveredPlus {
NSCursor.pointingHand.push()
} else {
NSCursor.pop()
}
}
Spacer()
}
}
.padding(20)
.frame(width: 500, height: 420)
}
private func selectFolder() {
let dialog = NSOpenPanel()
dialog.title = "Choose a folder"
dialog.showsResizeIndicator = false
dialog.showsHiddenFiles = false
dialog.canChooseDirectories = true
dialog.canCreateDirectories = true
dialog.canChooseFiles = false
if dialog.runModal() == NSApplication.ModalResponse.OK {
if let result = dialog.url {
fsm.addPath(result.path)
}
} else {
return
}
}
}
class FolderSettingsManager: ObservableObject {
@Published var folderPaths: [String] = []
private let userDefaultsKey = "settings.folders.apps"
let defaultPaths = ["/Applications", "\(NSHomeDirectory())/Applications"]
init() {
loadDefaultPathsIfNeeded()
}
private func loadDefaultPathsIfNeeded() {
var paths = UserDefaults.standard.stringArray(forKey: userDefaultsKey) ?? defaultPaths
if paths.count < 2 {
paths = defaultPaths
}
UserDefaults.standard.set(paths, forKey: userDefaultsKey)
self.folderPaths = paths
}
func addPath(_ path: String) {
if !self.folderPaths.contains(path) {
self.folderPaths.append(path)
UserDefaults.standard.set(self.folderPaths, forKey: userDefaultsKey)
}
}
func removePath(at index: Int) {
guard self.folderPaths.indices.contains(index) else { return }
self.folderPaths.remove(at: index) // Update local state
UserDefaults.standard.set(self.folderPaths, forKey: userDefaultsKey)
}
func refreshPaths() {
self.folderPaths = UserDefaults.standard.stringArray(forKey: userDefaultsKey) ?? defaultPaths
}
func getPaths() -> [String] {
return UserDefaults.standard.stringArray(forKey: userDefaultsKey) ?? defaultPaths
}
}
+12 -2
View File
@@ -13,6 +13,7 @@ import ServiceManagement
struct InterfaceSettingsTab: View { struct InterfaceSettingsTab: View {
@EnvironmentObject var appState: AppState @EnvironmentObject var appState: AppState
@EnvironmentObject var locations: Locations @EnvironmentObject var locations: Locations
@EnvironmentObject var fsm: FolderSettingsManager
@State private var windowSettings = WindowSettings() @State private var windowSettings = WindowSettings()
@AppStorage("settings.menubar.enabled") private var menubarEnabled: Bool = false @AppStorage("settings.menubar.enabled") private var menubarEnabled: Bool = false
@AppStorage("settings.general.mini") private var mini: Bool = false @AppStorage("settings.general.mini") private var mini: Bool = false
@@ -151,9 +152,12 @@ struct InterfaceSettingsTab: View {
MiniMode(search: $search, showPopover: $showPopover) MiniMode(search: $search, showPopover: $showPopover)
.environmentObject(locations) .environmentObject(locations)
.environmentObject(appState) .environmentObject(appState)
.environmentObject(fsm)
.preferredColorScheme(displayMode.colorScheme) .preferredColorScheme(displayMode.colorScheme)
} }
resizeWindowAuto(windowSettings: windowSettings, title: "Pearcleaner") updateOnMain(after: 0.1, {
resizeWindowAuto(windowSettings: windowSettings, title: "Pearcleaner")
})
} else { } else {
if appState.appInfo.appName.isEmpty { if appState.appInfo.appName.isEmpty {
appState.currentView = .empty appState.currentView = .empty
@@ -164,9 +168,12 @@ struct InterfaceSettingsTab: View {
RegularMode(search: $search, showPopover: $showPopover) RegularMode(search: $search, showPopover: $showPopover)
.environmentObject(locations) .environmentObject(locations)
.environmentObject(appState) .environmentObject(appState)
.environmentObject(fsm)
.preferredColorScheme(displayMode.colorScheme) .preferredColorScheme(displayMode.colorScheme)
} }
resizeWindowAuto(windowSettings: windowSettings, title: "Pearcleaner") updateOnMain(after: 0.1, {
resizeWindowAuto(windowSettings: windowSettings, title: "Pearcleaner")
})
} }
@@ -262,6 +269,7 @@ struct InterfaceSettingsTab: View {
MenuBarMiniAppView(search: $search, showPopover: $showPopover) MenuBarMiniAppView(search: $search, showPopover: $showPopover)
.environmentObject(locations) .environmentObject(locations)
.environmentObject(appState) .environmentObject(appState)
.environmentObject(fsm)
.preferredColorScheme(displayMode.colorScheme) .preferredColorScheme(displayMode.colorScheme)
}, icon: selectedMenubarIcon) }, icon: selectedMenubarIcon)
NSApplication.shared.setActivationPolicy(.accessory) NSApplication.shared.setActivationPolicy(.accessory)
@@ -276,6 +284,7 @@ struct InterfaceSettingsTab: View {
MiniMode(search: $search, showPopover: $showPopover) MiniMode(search: $search, showPopover: $showPopover)
.environmentObject(locations) .environmentObject(locations)
.environmentObject(appState) .environmentObject(appState)
.environmentObject(fsm)
.preferredColorScheme(displayMode.colorScheme) .preferredColorScheme(displayMode.colorScheme)
} }
resizeWindowAuto(windowSettings: windowSettings, title: "Pearcleaner") resizeWindowAuto(windowSettings: windowSettings, title: "Pearcleaner")
@@ -284,6 +293,7 @@ struct InterfaceSettingsTab: View {
RegularMode(search: $search, showPopover: $showPopover) RegularMode(search: $search, showPopover: $showPopover)
.environmentObject(locations) .environmentObject(locations)
.environmentObject(appState) .environmentObject(appState)
.environmentObject(fsm)
.preferredColorScheme(displayMode.colorScheme) .preferredColorScheme(displayMode.colorScheme)
} }
resizeWindowAuto(windowSettings: windowSettings, title: "Pearcleaner") resizeWindowAuto(windowSettings: windowSettings, title: "Pearcleaner")
@@ -9,6 +9,7 @@ import SwiftUI
struct SettingsView: View { struct SettingsView: View {
@EnvironmentObject var appState: AppState @EnvironmentObject var appState: AppState
@EnvironmentObject var fsm: FolderSettingsManager
@Binding var showPopover: Bool @Binding var showPopover: Bool
@Binding var search: String @Binding var search: String
@Binding var showFeature: Bool @Binding var showFeature: Bool
@@ -29,6 +30,12 @@ struct SettingsView: View {
} }
.tag(CurrentTabView.interface) .tag(CurrentTabView.interface)
FolderSettingsTab()
.tabItem {
Label(CurrentTabView.folders.title, systemImage: "folder")
}
.tag(CurrentTabView.interface)
UpdateSettingsTab(showFeature: $showFeature) UpdateSettingsTab(showFeature: $showFeature)
.tabItem { .tabItem {
Label(CurrentTabView.update.title, systemImage: "cloud") Label(CurrentTabView.update.title, systemImage: "cloud")
+36 -18
View File
@@ -22,6 +22,8 @@ struct FilesView: View {
@Binding var search: String @Binding var search: String
var regularWin: Bool var regularWin: Bool
@State private var selectedOption = "Default" @State private var selectedOption = "Default"
@State private var elapsedTime = 0
@State private var timer: Timer? = nil
var body: some View { var body: some View {
VStack(alignment: .center) { VStack(alignment: .center) {
@@ -30,14 +32,33 @@ struct FilesView: View {
Spacer() Spacer()
Text("Searching the file system").font(.title3) Text("Searching the file system").font(.title3)
.foregroundStyle((.gray.opacity(0.8))) .foregroundStyle((.gray.opacity(0.8)))
ProgressView()
.progressViewStyle(.linear) HStack {
.frame(width: 400, height: 10) ProgressView()
.progressViewStyle(.linear)
.frame(width: 400, height: 10)
Image(systemName: "\(elapsedTime).circle")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 16, height: 16)
.foregroundStyle((.gray.opacity(0.8)))
.opacity(elapsedTime == 0 ? 0 : 1)
}
Spacer() Spacer()
} }
.frame(maxWidth: .infinity, maxHeight: .infinity) .frame(maxWidth: .infinity, maxHeight: .infinity)
.transition(.opacity) .transition(.opacity)
.onAppear {
self.timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
self.elapsedTime += 1
}
}
.onDisappear {
self.timer?.invalidate()
self.timer = nil
self.elapsedTime = 0
}
} else { } else {
// Titlebar // Titlebar
if !regularWin { if !regularWin {
@@ -69,8 +90,14 @@ struct FilesView: View {
.resizable() .resizable()
.scaledToFit() .scaledToFit()
.frame(width: 50, height: 50) .frame(width: 50, height: 50)
.clipShape(RoundedRectangle(cornerRadius: 8)) // .clipShape(RoundedRectangle(cornerRadius: 8))
.padding(.trailing) // .padding(.trailing)
.padding()
.background{
RoundedRectangle(cornerRadius: 16)
.fill(Color((appState.appInfo.appIcon?.averageColor)!))
}
} }
VStack(alignment: .leading, spacing: 5){ VStack(alignment: .leading, spacing: 5){
@@ -86,6 +113,7 @@ struct FilesView: View {
Text("\(appState.appInfo.bundleIdentifier)").font(.title3) Text("\(appState.appInfo.bundleIdentifier)").font(.title3)
.foregroundStyle((.gray.opacity(0.8))) .foregroundStyle((.gray.opacity(0.8)))
} }
.padding(.leading)
Spacer() Spacer()
@@ -97,6 +125,7 @@ struct FilesView: View {
} }
HStack(alignment: .center, spacing: 10) { HStack(alignment: .center, spacing: 10) {
Spacer() Spacer()
@@ -157,7 +186,7 @@ struct FilesView: View {
VStack { VStack {
FileDetailsItem(size: fileSize, icon: iconImage, path: path) FileDetailsItem(size: fileSize, icon: iconImage, path: path)
if path != appState.appInfo.files.last { if path != appState.appInfo.files.last {
Divider().padding(.leading, 40) Divider().padding(.leading, 40).opacity(0.5)
} }
} }
} }
@@ -268,17 +297,6 @@ struct FilesView: View {
} }
} }
func refreshAppList(_ appInfo: AppInfo) {
showPopover = false
let sortedApps = getSortedApps()
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
appState.sortedApps = sortedApps
// appState.sortedApps.systemApps = sortedApps.systemApps
// if instantSearch {
// loadAllPaths(allApps: sortedApps.userApps + sortedApps.systemApps, appState: appState, locations: locations)
// }
}
}
} }
@@ -321,7 +339,7 @@ struct FileDetailsItem: View {
.help(path.lastPathComponent) .help(path.lastPathComponent)
Text(path.path) Text(path.path)
.font(.footnote) .font(.footnote)
.lineLimit(2) .lineLimit(1)
.truncationMode(.tail) .truncationMode(.tail)
.opacity(0.5) .opacity(0.5)
.help(path.path) .help(path.path)
+3 -1
View File
@@ -70,7 +70,9 @@ struct MenuBarMiniAppView: View {
appState.showProgress.toggle() appState.showProgress.toggle()
showPopover.toggle() showPopover.toggle()
if instantSearch { if instantSearch {
reversePathsSearch(appState: appState, locations: locations) let reverse = ReversePathsSearcher(appState: appState, locations: locations)
reverse.reversePathsSearch()
// reversePathsSearch(appState: appState, locations: locations)
} else { } else {
loadAllPaths(allApps: appState.sortedApps, appState: appState, locations: locations, reverseAddon: true) loadAllPaths(allApps: appState.sortedApps, appState: appState, locations: locations, reverseAddon: true)
} }
+1
View File
@@ -32,6 +32,7 @@ struct MiniMode: View {
} else { } else {
TopBarMini(search: $search, showPopover: $showPopover) TopBarMini(search: $search, showPopover: $showPopover)
MiniAppView(search: $search, showPopover: $showPopover) MiniAppView(search: $search, showPopover: $showPopover)
} }
} }
.transition(.opacity) .transition(.opacity)
+2 -1
View File
@@ -178,6 +178,7 @@ struct Header: View {
@State private var hovered = false @State private var hovered = false
@EnvironmentObject var appState: AppState @EnvironmentObject var appState: AppState
@EnvironmentObject var locations: Locations @EnvironmentObject var locations: Locations
@EnvironmentObject var fsm: FolderSettingsManager
@Binding var showPopover: Bool @Binding var showPopover: Bool
@AppStorage("settings.general.instant") private var instantSearch: Bool = true @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
@@ -199,7 +200,7 @@ struct Header: View {
// Refresh Apps list // Refresh Apps list
appState.reload.toggle() appState.reload.toggle()
showPopover = false showPopover = false
let sortedApps = getSortedApps() let sortedApps = getSortedApps(paths: fsm.folderPaths, appState: appState)
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
appState.sortedApps = sortedApps appState.sortedApps = sortedApps
if instantSearch { if instantSearch {
+22 -1
View File
@@ -42,6 +42,23 @@ struct TopBar: View {
HStack() { HStack() {
Spacer() Spacer()
if appState.currentView == .zombie {
Button("Rescan") {
updateOnMain {
appState.zombieFile = .empty
appState.showProgress.toggle()
if instantSearch {
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(NavButtonBottomBarStyle(image: "arrow.counterclockwise.circle.fill", help: "Rescan files"))
}
if appState.currentView != .zombie { if appState.currentView != .zombie {
Button("") { Button("") {
withAnimation(.easeInOut(duration: 0.5)) { withAnimation(.easeInOut(duration: 0.5)) {
@@ -53,7 +70,9 @@ struct TopBar: View {
appState.showProgress.toggle() appState.showProgress.toggle()
showPopover.toggle() showPopover.toggle()
if instantSearch { if instantSearch {
reversePathsSearch(appState: appState, locations: locations) let reverse = ReversePathsSearcher(appState: appState, locations: locations)
reverse.reversePathsSearch()
// reversePathsSearch(appState: appState, locations: locations)
} else { } else {
loadAllPaths(allApps: appState.sortedApps, appState: appState, locations: locations, reverseAddon: true) loadAllPaths(allApps: appState.sortedApps, appState: appState, locations: locations, reverseAddon: true)
} }
@@ -81,6 +100,8 @@ struct TopBar: View {
} }
} }
+7 -5
View File
@@ -37,7 +37,9 @@ struct TopBarMini: View {
appState.showProgress.toggle() appState.showProgress.toggle()
showPopover.toggle() showPopover.toggle()
if instantSearch { if instantSearch {
reversePathsSearch(appState: appState, locations: locations) let reverse = ReversePathsSearcher(appState: appState, locations: locations)
reverse.reversePathsSearch()
// reversePathsSearch(appState: appState, locations: locations)
} else { } else {
loadAllPaths(allApps: appState.sortedApps, appState: appState, locations: locations, reverseAddon: true) loadAllPaths(allApps: appState.sortedApps, appState: appState, locations: locations, reverseAddon: true)
} }
@@ -78,7 +80,9 @@ struct TopBarMini: View {
appState.showProgress.toggle() appState.showProgress.toggle()
showPopover.toggle() showPopover.toggle()
if instantSearch { if instantSearch {
reversePathsSearch(appState: appState, locations: locations) let reverse = ReversePathsSearcher(appState: appState, locations: locations)
reverse.reversePathsSearch()
// reversePathsSearch(appState: appState, locations: locations)
} else { } else {
loadAllPaths(allApps: appState.sortedApps, appState: appState, locations: locations, reverseAddon: true) loadAllPaths(allApps: appState.sortedApps, appState: appState, locations: locations, reverseAddon: true)
} }
@@ -130,10 +134,8 @@ struct SearchBarMiniBottom: View {
var body: some View { var body: some View {
HStack { HStack {
TextField(" Search", text: $search) TextField("\(appState.instantTotal > appState.instantProgress ? " Searching the file system" : " Search")", text: $search)
.textFieldStyle(SimpleSearchStyle(trash: true, text: $search)) .textFieldStyle(SimpleSearchStyle(trash: true, text: $search))
} }
// .padding(.horizontal)
// .padding(.bottom, 0)
} }
} }
+31 -6
View File
@@ -22,6 +22,8 @@ struct ZombieView: View {
@State private var searchZ: String = "" @State private var searchZ: String = ""
@State private var selectedOption = "Default" @State private var selectedOption = "Default"
var regularWin: Bool var regularWin: Bool
@State private var elapsedTime = 0
@State private var timer: Timer? = nil
var body: some View { var body: some View {
@@ -49,9 +51,19 @@ struct ZombieView: View {
Text("Searching the file system").font(.title3) Text("Searching the file system").font(.title3)
.foregroundStyle((.gray.opacity(0.8))) .foregroundStyle((.gray.opacity(0.8)))
ProgressView()
.progressViewStyle(.linear) HStack {
.frame(width: 400, height: 10) ProgressView()
.progressViewStyle(.linear)
.frame(width: 400, height: 10)
Image(systemName: "\(elapsedTime).circle")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 16, height: 16)
.foregroundStyle((.gray.opacity(0.8)))
.opacity(elapsedTime == 0 ? 0 : 1)
}
Spacer() Spacer()
} }
@@ -59,6 +71,16 @@ struct ZombieView: View {
} }
.frame(maxWidth: .infinity, maxHeight: .infinity) .frame(maxWidth: .infinity, maxHeight: .infinity)
.onAppear {
self.timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
self.elapsedTime += 1
}
}
.onDisappear {
self.timer?.invalidate()
self.timer = nil
self.elapsedTime = 0
}
} else { } else {
// Titlebar // Titlebar
if !regularWin { if !regularWin {
@@ -70,7 +92,9 @@ struct ZombieView: View {
appState.zombieFile = .empty appState.zombieFile = .empty
appState.showProgress.toggle() appState.showProgress.toggle()
if instantSearch { if instantSearch {
reversePathsSearch(appState: appState, locations: locations) let reverse = ReversePathsSearcher(appState: appState, locations: locations)
reverse.reversePathsSearch()
// reversePathsSearch(appState: appState, locations: locations)
} else { } else {
loadAllPaths(allApps: appState.sortedApps, appState: appState, locations: locations, reverseAddon: true) loadAllPaths(allApps: appState.sortedApps, appState: appState, locations: locations, reverseAddon: true)
} }
@@ -131,6 +155,7 @@ struct ZombieView: View {
} }
SearchBarMiniBottom(search: $searchZ) SearchBarMiniBottom(search: $searchZ)
.padding(.top)
Divider() Divider()
.padding() .padding()
@@ -146,7 +171,7 @@ struct ZombieView: View {
.padding(.trailing) .padding(.trailing)
if file != appState.zombieFile.fileSize.keys.sorted(by: { $0.absoluteString < $1.absoluteString }).last { if file != appState.zombieFile.fileSize.keys.sorted(by: { $0.absoluteString < $1.absoluteString }).last {
Divider().padding(.leading, 40) Divider().padding(.leading, 40).opacity(0.5)
} }
} }
} }
@@ -271,7 +296,7 @@ struct ZombieFileDetailsItem: View {
.help(path.lastPathComponent) .help(path.lastPathComponent)
Text(path.path) Text(path.path)
.font(.footnote) .font(.footnote)
.lineLimit(2) .lineLimit(1)
.truncationMode(.tail) .truncationMode(.tail)
.opacity(0.5) .opacity(0.5)
// .foregroundStyle(Color("AccentColor")) // .foregroundStyle(Color("AccentColor"))
+1
View File
@@ -1,4 +1,5 @@
{ {
"3.3.0": "- Folder Settings: Add more directories where Pearcleaner should search for app files|- Progress: Show progress bar on startup when loading all app files with instant search enabled|- App Icon: Show background color behind app icons in FilesView based on icon's average color mapping|- Progress: Show time counter on regular file search views",
"3.2.0": "- Menubar Item: You can choose to access Pearcleaner from the menubar. This will show a slightly modified Mini Mode view.", "3.2.0": "- Menubar Item: You can choose to access Pearcleaner from the menubar. This will show a slightly modified Mini Mode view.",
"3.1.0": "- Homebrew Cleanup: If you install apps using brew, you can now enable homebrew cleanup in the settings to have Pearcleaner alert Homebrew that the app was removed externally from Homebrew and keep the brew app list synced.", "3.1.0": "- Homebrew Cleanup: If you install apps using brew, you can now enable homebrew cleanup in the settings to have Pearcleaner alert Homebrew that the app was removed externally from Homebrew and keep the brew app list synced.",
"3.0.0": "- Instant Search: Enable in settings to load app files on startup|- Semantic Versioning: Going forward will use semver (Ex. v0.0.0)|-Feature Alert: For each version, a feature alert will popup once on startup to show details|- Leftover File Cleaning: Search your mac for files leftover by uninstalled apps|- Sidebar Drag Handle: Resize the sidebar in regular mode|- Redesign UI/Settings/Icon: Some new buttons, layout changes, new official app icon and pear color theme accents|- File Sort: Sort files alphabetically or by size|- Socket File Removal: Finds socket files that aren't even visible in Finder with show hidden files enabled|- OSLog Output: Will print errors to the Console app for easier troubleshooting" "3.0.0": "- Instant Search: Enable in settings to load app files on startup|- Semantic Versioning: Going forward will use semver (Ex. v0.0.0)|-Feature Alert: For each version, a feature alert will popup once on startup to show details|- Leftover File Cleaning: Search your mac for files leftover by uninstalled apps|- Sidebar Drag Handle: Resize the sidebar in regular mode|- Redesign UI/Settings/Icon: Some new buttons, layout changes, new official app icon and pear color theme accents|- File Sort: Sort files alphabetically or by size|- Socket File Removal: Finds socket files that aren't even visible in Finder with show hidden files enabled|- OSLog Output: Will print errors to the Console app for easier troubleshooting"