mirror of
https://github.com/exituser/Pearcleaner.git
synced 2026-09-17 04:58:56 +00:00
v3.3.0
This commit is contained in:
@@ -37,6 +37,10 @@
|
||||
C7D31D4A2AFEB26700C7ED9E /* AppListItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7D31D492AFEB26700C7ED9E /* AppListItems.swift */; };
|
||||
C7D31D512AFF00F300C7ED9E /* Locations.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7D31D502AFF00F300C7ED9E /* Locations.swift */; };
|
||||
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 */; };
|
||||
C7DE672D2BA6356500EB1633 /* MenuBarMiniAppView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7DE672C2BA6356500EB1633 /* MenuBarMiniAppView.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>"; };
|
||||
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; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@@ -144,6 +152,9 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C77B90192AF1938F009CC655 /* Logic.swift */,
|
||||
C7DD49E92BAB4D8100CCBA16 /* AppInfoFetch.swift */,
|
||||
C7DD49EB2BAB58AA00CCBA16 /* AppPathsFetch.swift */,
|
||||
C7DD49ED2BAB7F6000CCBA16 /* ReversePathsFetch.swift */,
|
||||
C77B90172AF19382009CC655 /* AppCommands.swift */,
|
||||
C77B90152AF19377009CC655 /* AppState.swift */,
|
||||
C7F539372AF60865007DF1B2 /* Utilities.swift */,
|
||||
@@ -179,6 +190,7 @@
|
||||
C77B901D2AF1A9DB009CC655 /* SettingsWindow.swift */,
|
||||
C76D08492AF83C6E00D07867 /* General.swift */,
|
||||
C7ED86B02BA8A35500F13DE4 /* Interface.swift */,
|
||||
C7DD49EF2BABA14400CCBA16 /* Folders.swift */,
|
||||
C76D08472AF83C3F00D07867 /* Update.swift */,
|
||||
C76D08502AF850F300D07867 /* About.swift */,
|
||||
);
|
||||
@@ -354,11 +366,14 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
C7DD49EA2BAB4D8100CCBA16 /* AppInfoFetch.swift in Sources */,
|
||||
C76D08482AF83C3F00D07867 /* Update.swift in Sources */,
|
||||
C7FB173B2B96321300B96F9A /* AppsListView.swift in Sources */,
|
||||
C7045A342B068B2900376976 /* UpdateView.swift in Sources */,
|
||||
C77B90162AF19377009CC655 /* AppState.swift in Sources */,
|
||||
C7DD49EE2BAB7F6000CCBA16 /* ReversePathsFetch.swift in Sources */,
|
||||
C7045A322B068AD700376976 /* NewWin.swift in Sources */,
|
||||
C7DD49F02BABA14400CCBA16 /* Folders.swift in Sources */,
|
||||
C7DE672D2BA6356500EB1633 /* MenuBarMiniAppView.swift in Sources */,
|
||||
C7ED86B32BA8ED8800F13DE4 /* Trash.swift in Sources */,
|
||||
C7D31D512AFF00F300C7ED9E /* Locations.swift in Sources */,
|
||||
@@ -378,6 +393,7 @@
|
||||
C7D31D4A2AFEB26700C7ED9E /* AppListItems.swift in Sources */,
|
||||
C77B901E2AF1A9DB009CC655 /* SettingsWindow.swift in Sources */,
|
||||
C7D31D482AFEB23700C7ED9E /* TopBar.swift in Sources */,
|
||||
C7DD49EC2BAB58AA00CCBA16 /* AppPathsFetch.swift in Sources */,
|
||||
C7ACADE62B92A737000B5845 /* Features.swift in Sources */,
|
||||
C7F539382AF60865007DF1B2 /* Utilities.swift in Sources */,
|
||||
C76D08552AF89CDE00D07867 /* RegularMode.swift in Sources */,
|
||||
@@ -559,7 +575,7 @@
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 25;
|
||||
CURRENT_PROJECT_VERSION = 26;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_ASSET_PATHS = "";
|
||||
DEVELOPMENT_TEAM = BK8443AXLU;
|
||||
@@ -577,7 +593,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||
MARKETING_VERSION = 3.2.1;
|
||||
MARKETING_VERSION = 3.3.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.alienator88.Pearcleaner;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
@@ -594,7 +610,7 @@
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 25;
|
||||
CURRENT_PROJECT_VERSION = 26;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_ASSET_PATHS = "";
|
||||
DEVELOPMENT_TEAM = BK8443AXLU;
|
||||
@@ -612,7 +628,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||
MARKETING_VERSION = 3.2.1;
|
||||
MARKETING_VERSION = 3.3.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.alienator88.Pearcleaner;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
|
||||
@@ -12,10 +12,12 @@ struct AppCommands: Commands {
|
||||
|
||||
let appState: AppState
|
||||
let locations: Locations
|
||||
let fsm: FolderSettingsManager
|
||||
|
||||
init(appState: AppState, locations: Locations) {
|
||||
init(appState: AppState, locations: Locations, fsm: FolderSettingsManager) {
|
||||
self.appState = appState
|
||||
self.locations = locations
|
||||
self.fsm = fsm
|
||||
}
|
||||
|
||||
var body: some Commands {
|
||||
@@ -36,7 +38,7 @@ struct AppCommands: Commands {
|
||||
updateOnMain {
|
||||
appState.reload.toggle()
|
||||
}
|
||||
let sortedApps = getSortedApps()
|
||||
let sortedApps = getSortedApps(paths: fsm.folderPaths, appState: appState)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
appState.sortedApps = []
|
||||
// appState.sortedApps.systemApps = []
|
||||
@@ -71,7 +73,7 @@ struct AppCommands: Commands {
|
||||
Button
|
||||
{
|
||||
undoTrash(appState: appState) {
|
||||
let sortedApps = getSortedApps()
|
||||
let sortedApps = getSortedApps(paths: fsm.folderPaths, appState: appState)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
appState.sortedApps = sortedApps
|
||||
// if instantSearch {
|
||||
|
||||
@@ -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
|
||||
//}
|
||||
@@ -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")
|
||||
@@ -28,6 +28,8 @@ class AppState: ObservableObject
|
||||
@Published var reload: Bool = false
|
||||
@Published var showProgress: Bool = false
|
||||
@Published var popCount: Int = 0
|
||||
@Published var instantProgress: Double = 0.0
|
||||
@Published var instantTotal: Double = 0.0
|
||||
|
||||
|
||||
init() {
|
||||
@@ -106,6 +108,7 @@ enum CurrentTabView:Int
|
||||
{
|
||||
case general
|
||||
case interface
|
||||
case folders
|
||||
case update
|
||||
case about
|
||||
|
||||
@@ -113,6 +116,7 @@ enum CurrentTabView:Int
|
||||
switch self {
|
||||
case .general: return "General"
|
||||
case .interface: return "Interface"
|
||||
case .folders: return "Folders"
|
||||
case .update: return "Update"
|
||||
case .about: return "About"
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ class DeeplinkManager {
|
||||
let queryItems = components.queryItems {
|
||||
if let path = queryItems.first(where: { $0.name == DeepLinkConstants.query })?.value {
|
||||
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)
|
||||
} else {
|
||||
printOS("No path query parameter found in the URL")
|
||||
@@ -45,7 +45,7 @@ class DeeplinkManager {
|
||||
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ class Locations: ObservableObject {
|
||||
var paths: [String]
|
||||
}
|
||||
|
||||
let home: String
|
||||
let cacheDir: String
|
||||
let tempDir: String
|
||||
|
||||
@@ -24,7 +23,6 @@ class Locations: ObservableObject {
|
||||
// var plugins: Category
|
||||
|
||||
init() {
|
||||
self.home = FileManager.default.homeDirectoryForCurrentUser.path
|
||||
let (cacheDir, tempDir) = darwinCT()
|
||||
self.cacheDir = cacheDir
|
||||
self.tempDir = tempDir
|
||||
|
||||
+95
-550
@@ -8,14 +8,11 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
|
||||
// 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
|
||||
|
||||
// Define the paths for system and user applications
|
||||
let systemAppsPath = "/Applications"
|
||||
let userAppsPath = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Applications").path
|
||||
|
||||
var apps: [URL] = []
|
||||
|
||||
func collectAppPaths(at directoryPath: String) {
|
||||
@@ -37,214 +34,78 @@ func getSortedApps() -> [AppInfo] {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Collect system applications
|
||||
collectAppPaths(at: systemAppsPath)
|
||||
|
||||
// Collect user applications
|
||||
collectAppPaths(at: userAppsPath)
|
||||
paths.forEach { collectAppPaths(at: $0) }
|
||||
|
||||
// Get app info and sort
|
||||
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() }
|
||||
|
||||
if instantSearch {
|
||||
updateOnMain {
|
||||
appState.instantTotal = Double(sortedApps.count)
|
||||
}
|
||||
}
|
||||
|
||||
return sortedApps
|
||||
}
|
||||
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Get directory path for darwin cache and temp directories
|
||||
func darwinCT() -> (String, String) {
|
||||
let task = Process()
|
||||
task.launchPath = "/bin/bash"
|
||||
task.arguments = ["-c", "getconf DARWIN_USER_CACHE_DIR; getconf DARWIN_USER_TEMP_DIR"]
|
||||
let command = "echo $(getconf DARWIN_USER_CACHE_DIR) $(getconf DARWIN_USER_TEMP_DIR)"
|
||||
let process = Process()
|
||||
process.launchPath = "/bin/bash"
|
||||
process.arguments = ["-c", command]
|
||||
|
||||
let pipe = Pipe()
|
||||
task.standardOutput = pipe
|
||||
task.launch()
|
||||
process.standardOutput = pipe
|
||||
process.launch()
|
||||
|
||||
let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
||||
if let output = String(data: data, encoding: .utf8) {
|
||||
let components = output.components(separatedBy: "\n")
|
||||
if components.count >= 2 {
|
||||
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)
|
||||
}
|
||||
}
|
||||
guard let output = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) else {
|
||||
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] {
|
||||
let home = FileManager.default.homeDirectoryForCurrentUser
|
||||
let appSupportLocation = home.appendingPathComponent("Library/Application Support/")
|
||||
let exclusions = ["MobileSync", ".DS_Store", "Xcode", "SyncServices", "networkserviceproxy", "DiskImages", "CallHistoryTransactions", "App Store", "CloudDocs", "icdd", "iCloud", "Instruments", "AddressBook", "FaceTime", "AskPermission", "CallHistoryDB"]
|
||||
let fileManager = FileManager.default
|
||||
let home = fileManager.homeDirectoryForCurrentUser
|
||||
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: [])
|
||||
|
||||
do {
|
||||
let fileManager = FileManager.default
|
||||
let directoryContents = try fileManager.contentsOfDirectory(at: appSupportLocation, includingPropertiesForKeys: [.isDirectoryKey], options: .skipsHiddenFiles)
|
||||
let directoryContents = try fileManager.contentsOfDirectory(atPath: appSupportLocation)
|
||||
|
||||
let filteredDirectories: [String] = directoryContents.compactMap { url in
|
||||
return directoryContents.compactMap { directoryName in
|
||||
let fullPath = appSupportLocation.appending("/\(directoryName)")
|
||||
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
|
||||
}
|
||||
|
||||
let directoryName = url.lastPathComponent
|
||||
|
||||
// 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 excludeByList = exclusions.contains(directoryName)
|
||||
|
||||
return isDirectory.boolValue && !excludeByRegex && !excludeByList ? directoryName : nil
|
||||
if exclusions.contains(directoryName) || excludeByRegex {
|
||||
return nil
|
||||
}
|
||||
return directoryName
|
||||
}
|
||||
|
||||
return filteredDirectories
|
||||
} catch {
|
||||
printOS("Error listing AppSupport directories: \(error.localizedDescription)")
|
||||
return []
|
||||
@@ -253,381 +114,96 @@ 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
|
||||
func loadAllPaths(allApps: [AppInfo], appState: AppState, locations: Locations, reverseAddon: Bool = false, completion: @escaping () -> Void = {}) {
|
||||
|
||||
let dispatchGroup = DispatchGroup()
|
||||
let retryLimit = 120
|
||||
var currentRetryCount = 0
|
||||
|
||||
func checkCompletion() {
|
||||
if appState.appInfoStore.count == allApps.count {
|
||||
if reverseAddon {
|
||||
reversePathsSearch(appState: appState, locations: locations)
|
||||
}
|
||||
completion()
|
||||
} 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()
|
||||
for app in allApps {
|
||||
dispatchGroup.enter()
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
let pathFinder = AppPathFinder(appInfo: app, appState: appState, locations: locations, backgroundRun: true)
|
||||
pathFinder.findPaths()
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
}
|
||||
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
for app in allApps {
|
||||
findPathsForApp(appInfo: app, appState: appState, locations: locations, backgroundRun: true)
|
||||
}
|
||||
dispatchGroup.leave()
|
||||
_ = dispatchGroup.wait(timeout: .now() + 60)
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checkAllAppsProcessed()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// Load item in Files view
|
||||
func showAppInFiles(appInfo: AppInfo, appState: AppState, locations: Locations, showPopover: Binding<Bool>) {
|
||||
showPopover.wrappedValue = false
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||
|
||||
updateOnMain {
|
||||
appState.appInfo = .empty
|
||||
appState.selectedItems = []
|
||||
|
||||
// Check if the appInfo exists in the appState.appInfoStore
|
||||
if let storedAppInfo = appState.appInfoStore.first(where: { $0.path == appInfo.path }) {
|
||||
// Update appState with the stored app info and selected items.
|
||||
appState.appInfo = storedAppInfo
|
||||
appState.selectedItems = Set(storedAppInfo.files)
|
||||
|
||||
// Trigger the animation for changing views and showing the popover.
|
||||
withAnimation(Animation.easeIn(duration: 0.4)) {
|
||||
appState.currentView = .files
|
||||
showPopover.wrappedValue.toggle()
|
||||
}
|
||||
} else {
|
||||
// Handle the case where the appInfo is not found in the store
|
||||
withAnimation(Animation.easeIn(duration: 0.4)) {
|
||||
// When the appInfo is not found, show progress, and search for paths.
|
||||
appState.showProgress = true
|
||||
|
||||
// Initialize the path finder and execute its search.
|
||||
let pathFinder = AppPathFinder(appInfo: appInfo, appState: appState, locations: locations) {
|
||||
updateOnMain {
|
||||
// Update the progress indicator on the main thread once the search completes.
|
||||
appState.showProgress = false
|
||||
}
|
||||
}
|
||||
pathFinder.findPaths()
|
||||
appState.appInfo = appInfo
|
||||
|
||||
// Animate the view change and popover display.
|
||||
withAnimation(Animation.easeIn(duration: 0.4)) {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 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) {
|
||||
|
||||
updateOnMain {
|
||||
updatedZombieFile.fileSize = fileSize
|
||||
updatedZombieFile.fileIcon = fileIcon
|
||||
appState.zombieFile = updatedZombieFile
|
||||
appState.showProgress = false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
completion()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// Move files to trash using applescript/Finder so it asks for user password if needed
|
||||
func moveFilesToTrash(at fileURLs: [URL], completion: @escaping () -> Void = {}) {
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -308,6 +308,7 @@ struct AnimatedSearchStyle: TextFieldStyle {
|
||||
@FocusState private var isFocused: Bool
|
||||
@Binding var text: String
|
||||
@EnvironmentObject var appState: AppState
|
||||
@EnvironmentObject var fsm: FolderSettingsManager
|
||||
|
||||
func _body(configuration: TextField<Self._Label>) -> some View {
|
||||
|
||||
@@ -332,7 +333,7 @@ struct AnimatedSearchStyle: TextFieldStyle {
|
||||
withAnimation {
|
||||
// Refresh Apps list
|
||||
appState.reload.toggle()
|
||||
let sortedApps = getSortedApps()
|
||||
let sortedApps = getSortedApps(paths: fsm.folderPaths, appState: appState)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
appState.sortedApps = sortedApps
|
||||
appState.reload.toggle()
|
||||
@@ -435,13 +436,55 @@ struct SimpleSearchStyle: TextFieldStyle {
|
||||
}
|
||||
|
||||
}
|
||||
.disabled(appState.instantProgress != 0 && appState.instantProgress != appState.instantTotal)
|
||||
.padding(6)
|
||||
.overlay(
|
||||
Group {
|
||||
GeometryReader { geometry in
|
||||
ZStack(alignment: .leading) {
|
||||
|
||||
|
||||
if appState.instantProgress != 0 && appState.instantProgress != appState.instantTotal {
|
||||
let totalWidth = geometry.size.width
|
||||
let progressFraction = CGFloat(appState.instantProgress / appState.instantTotal)
|
||||
let progressWidth = totalWidth * progressFraction
|
||||
|
||||
RoundedRectangle(cornerRadius: 6)
|
||||
.strokeBorder(Color("mode").opacity(0.2), lineWidth: 1)
|
||||
.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()
|
||||
//// }
|
||||
// }
|
||||
|
||||
|
||||
}
|
||||
)
|
||||
.onHover { hovering in
|
||||
|
||||
@@ -312,7 +312,7 @@ func startEnd(_ function: @escaping () -> Void) {
|
||||
function()
|
||||
let endTime = Date()
|
||||
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)
|
||||
}
|
||||
|
||||
// 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
|
||||
func removeApp(appState: AppState, withId id: UUID) {
|
||||
DispatchQueue.main.async {
|
||||
@@ -379,41 +390,80 @@ extension FileManager {
|
||||
// --- Extend print command to also output to the Console ---
|
||||
func printOS(_ items: Any..., separator: String = " ", terminator: String = "\n") {
|
||||
let message = items.map { "\($0)" }.joined(separator: separator)
|
||||
let log = OSLog(subsystem: "com.alienator88.Pearcleaner", category: "Application")
|
||||
os_log("%@", log: log, type: .debug, message)
|
||||
let log = OSLog(subsystem: "pearcleaner", category: "Application")
|
||||
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
|
||||
func totalSizeOnDisk(for paths: [URL]) -> Int64 {
|
||||
var totalSize = 0
|
||||
let fileManager = FileManager.default
|
||||
var totalSize: Int64 = 0
|
||||
|
||||
let process = Process()
|
||||
process.launchPath = "/usr/bin/du"
|
||||
process.arguments = ["-sk"] + paths.map { (url: URL) -> String in
|
||||
return url.path
|
||||
for url in paths {
|
||||
var isDirectory: ObjCBool = false
|
||||
if fileManager.fileExists(atPath: url.path, isDirectory: &isDirectory) {
|
||||
if isDirectory.boolValue {
|
||||
// It's a directory, recurse into it
|
||||
if let enumerator = fileManager.enumerator(at: url, includingPropertiesForKeys: [.totalFileAllocatedSizeKey], errorHandler: nil) {
|
||||
for case let fileURL as URL in enumerator {
|
||||
do {
|
||||
let fileAttributes = try fileURL.resourceValues(forKeys: [.totalFileAllocatedSizeKey])
|
||||
if let fileSize = fileAttributes.totalFileAllocatedSize {
|
||||
totalSize += Int64(fileSize)
|
||||
}
|
||||
} catch {
|
||||
printOS("Error getting file size for \(fileURL): \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 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)")
|
||||
}
|
||||
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
|
||||
return totalSize
|
||||
}
|
||||
|
||||
func totalSizeOnDisk(for path: URL) -> Int64 {
|
||||
@@ -428,53 +478,25 @@ func formatByte(size: Int64) -> String {
|
||||
return byteCountFormatter.string(fromByteCount: size)
|
||||
}
|
||||
|
||||
|
||||
// Get total size of folders and files using straight swift
|
||||
extension URL {
|
||||
func totalAllocatedSize(includingSubfolders: Bool = false) throws -> Int? {
|
||||
return try [self].totalAllocatedSize(includingSubfolders: includingSubfolders)
|
||||
}
|
||||
|
||||
func totalSizeOnDisk(includingSubfolders: Bool = false) throws -> String? {
|
||||
return try [self].totalSizeOnDisk(includingSubfolders: includingSubfolders)
|
||||
}
|
||||
}
|
||||
|
||||
extension Array where Element == URL {
|
||||
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)
|
||||
// Only process supported files
|
||||
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
|
||||
}
|
||||
}
|
||||
} else {
|
||||
totalSize += resourceValues.totalFileAllocatedSize ?? 0
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return totalSize
|
||||
}
|
||||
|
||||
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
|
||||
} catch {
|
||||
printOS("Error getting file attributes: \(error)")
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -518,11 +540,13 @@ func uninstallPearcleaner(appState: AppState, locations: Locations) {
|
||||
launchctl(load: false)
|
||||
|
||||
// Get app info for Pearcleaner
|
||||
let appInfo = getAppInfo(atPath: Bundle.main.bundleURL)
|
||||
let appInfo = AppInfoFetcher.getAppInfo(atPath: Bundle.main.bundleURL)
|
||||
// appState.appInfo = appInfo!
|
||||
|
||||
// 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
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
|
||||
@@ -587,6 +611,7 @@ func writeLog(string: String) {
|
||||
// --- Load Plist file with launchctl ---
|
||||
func launchctl(load: Bool, completion: @escaping () -> Void = {}) {
|
||||
let cmd = load ? "load" : "unload"
|
||||
|
||||
if let plistPath = Bundle.main.path(forResource: "com.alienator88.PearcleanerSentinel", ofType: "plist") {
|
||||
var plistContent = try! String(contentsOfFile: plistPath)
|
||||
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")
|
||||
|
||||
do {
|
||||
try plistContent.write(to: temporaryPlistURL, atomically: false, encoding: .utf8)
|
||||
try plistContent.write(to: temporaryPlistURL, atomically: true, encoding: .utf8)
|
||||
} catch {
|
||||
printOS("Error writing the temporary plist file: \(error)")
|
||||
return
|
||||
}
|
||||
|
||||
let task = Process()
|
||||
task.launchPath = "/bin/launchctl"
|
||||
task.arguments = [cmd, "-w", temporaryPlistURL.path]
|
||||
|
||||
let pipe = Pipe()
|
||||
task.standardOutput = pipe
|
||||
task.standardError = pipe
|
||||
|
||||
task.standardOutput = FileHandle.nullDevice
|
||||
task.standardError = FileHandle.nullDevice
|
||||
task.launch()
|
||||
|
||||
completion()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ struct PearcleanerApp: App {
|
||||
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
|
||||
@StateObject var appState = AppState()
|
||||
@StateObject var locations = Locations()
|
||||
@StateObject var fsm = FolderSettingsManager()
|
||||
@State private var windowSettings = WindowSettings()
|
||||
@AppStorage("settings.updater.updateTimeframe") private var updateTimeframe: Int = 1
|
||||
@AppStorage("settings.permissions.disk") private var diskP: Bool = false
|
||||
@@ -57,6 +58,7 @@ struct PearcleanerApp: App {
|
||||
}
|
||||
.environmentObject(appState)
|
||||
.environmentObject(locations)
|
||||
.environmentObject(fsm)
|
||||
.preferredColorScheme(displayMode.colorScheme)
|
||||
.handlesExternalEvents(preferring: Set(arrayLiteral: "pear"), allowing: Set(arrayLiteral: "*"))
|
||||
.onOpenURL(perform: { url in
|
||||
@@ -96,7 +98,7 @@ struct PearcleanerApp: App {
|
||||
NSApplication.shared.windows.first?.setFrame(frame, display: true)
|
||||
|
||||
// Get Apps
|
||||
let sortedApps = getSortedApps()
|
||||
let sortedApps = getSortedApps(paths: fsm.folderPaths, appState: appState)
|
||||
appState.sortedApps = sortedApps
|
||||
|
||||
|
||||
@@ -105,16 +107,17 @@ struct PearcleanerApp: App {
|
||||
loadAllPaths(allApps: sortedApps, appState: appState, locations: locations)
|
||||
}
|
||||
|
||||
|
||||
if menubarEnabled {
|
||||
MenuBarExtraManager.shared.addMenuBarExtra(withView: {
|
||||
MenuBarMiniAppView(search: $search, showPopover: $showPopover)
|
||||
.environmentObject(locations)
|
||||
.environmentObject(appState)
|
||||
.environmentObject(fsm)
|
||||
.preferredColorScheme(displayMode.colorScheme)
|
||||
}, icon: selectedMenubarIcon)
|
||||
}
|
||||
|
||||
|
||||
#if !DEBUG
|
||||
Task {
|
||||
|
||||
@@ -153,7 +156,7 @@ struct PearcleanerApp: App {
|
||||
.windowStyle(.hiddenTitleBar)
|
||||
.windowResizability(.contentMinSize)
|
||||
.commands {
|
||||
AppCommands(appState: appState, locations: locations)
|
||||
AppCommands(appState: appState, locations: locations, fsm: fsm)
|
||||
CommandGroup(replacing: .newItem, addition: { })
|
||||
|
||||
}
|
||||
@@ -165,6 +168,7 @@ struct PearcleanerApp: App {
|
||||
SettingsView(showPopover: $showPopover, search: $search, showFeature: $showFeature)
|
||||
.environmentObject(appState)
|
||||
.environmentObject(locations)
|
||||
.environmentObject(fsm)
|
||||
.toolbarBackground(.clear)
|
||||
.preferredColorScheme(displayMode.colorScheme)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import ServiceManagement
|
||||
struct InterfaceSettingsTab: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
@EnvironmentObject var locations: Locations
|
||||
@EnvironmentObject var fsm: FolderSettingsManager
|
||||
@State private var windowSettings = WindowSettings()
|
||||
@AppStorage("settings.menubar.enabled") private var menubarEnabled: Bool = false
|
||||
@AppStorage("settings.general.mini") private var mini: Bool = false
|
||||
@@ -151,9 +152,12 @@ struct InterfaceSettingsTab: View {
|
||||
MiniMode(search: $search, showPopover: $showPopover)
|
||||
.environmentObject(locations)
|
||||
.environmentObject(appState)
|
||||
.environmentObject(fsm)
|
||||
.preferredColorScheme(displayMode.colorScheme)
|
||||
}
|
||||
updateOnMain(after: 0.1, {
|
||||
resizeWindowAuto(windowSettings: windowSettings, title: "Pearcleaner")
|
||||
})
|
||||
} else {
|
||||
if appState.appInfo.appName.isEmpty {
|
||||
appState.currentView = .empty
|
||||
@@ -164,9 +168,12 @@ struct InterfaceSettingsTab: View {
|
||||
RegularMode(search: $search, showPopover: $showPopover)
|
||||
.environmentObject(locations)
|
||||
.environmentObject(appState)
|
||||
.environmentObject(fsm)
|
||||
.preferredColorScheme(displayMode.colorScheme)
|
||||
}
|
||||
updateOnMain(after: 0.1, {
|
||||
resizeWindowAuto(windowSettings: windowSettings, title: "Pearcleaner")
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -262,6 +269,7 @@ struct InterfaceSettingsTab: View {
|
||||
MenuBarMiniAppView(search: $search, showPopover: $showPopover)
|
||||
.environmentObject(locations)
|
||||
.environmentObject(appState)
|
||||
.environmentObject(fsm)
|
||||
.preferredColorScheme(displayMode.colorScheme)
|
||||
}, icon: selectedMenubarIcon)
|
||||
NSApplication.shared.setActivationPolicy(.accessory)
|
||||
@@ -276,6 +284,7 @@ struct InterfaceSettingsTab: View {
|
||||
MiniMode(search: $search, showPopover: $showPopover)
|
||||
.environmentObject(locations)
|
||||
.environmentObject(appState)
|
||||
.environmentObject(fsm)
|
||||
.preferredColorScheme(displayMode.colorScheme)
|
||||
}
|
||||
resizeWindowAuto(windowSettings: windowSettings, title: "Pearcleaner")
|
||||
@@ -284,6 +293,7 @@ struct InterfaceSettingsTab: View {
|
||||
RegularMode(search: $search, showPopover: $showPopover)
|
||||
.environmentObject(locations)
|
||||
.environmentObject(appState)
|
||||
.environmentObject(fsm)
|
||||
.preferredColorScheme(displayMode.colorScheme)
|
||||
}
|
||||
resizeWindowAuto(windowSettings: windowSettings, title: "Pearcleaner")
|
||||
|
||||
@@ -9,6 +9,7 @@ import SwiftUI
|
||||
|
||||
struct SettingsView: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
@EnvironmentObject var fsm: FolderSettingsManager
|
||||
@Binding var showPopover: Bool
|
||||
@Binding var search: String
|
||||
@Binding var showFeature: Bool
|
||||
@@ -29,6 +30,12 @@ struct SettingsView: View {
|
||||
}
|
||||
.tag(CurrentTabView.interface)
|
||||
|
||||
FolderSettingsTab()
|
||||
.tabItem {
|
||||
Label(CurrentTabView.folders.title, systemImage: "folder")
|
||||
}
|
||||
.tag(CurrentTabView.interface)
|
||||
|
||||
UpdateSettingsTab(showFeature: $showFeature)
|
||||
.tabItem {
|
||||
Label(CurrentTabView.update.title, systemImage: "cloud")
|
||||
|
||||
@@ -22,6 +22,8 @@ struct FilesView: View {
|
||||
@Binding var search: String
|
||||
var regularWin: Bool
|
||||
@State private var selectedOption = "Default"
|
||||
@State private var elapsedTime = 0
|
||||
@State private var timer: Timer? = nil
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .center) {
|
||||
@@ -30,14 +32,33 @@ struct FilesView: View {
|
||||
Spacer()
|
||||
Text("Searching the file system").font(.title3)
|
||||
.foregroundStyle((.gray.opacity(0.8)))
|
||||
|
||||
HStack {
|
||||
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()
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.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 {
|
||||
// Titlebar
|
||||
if !regularWin {
|
||||
@@ -69,8 +90,14 @@ struct FilesView: View {
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 50, height: 50)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||
.padding(.trailing)
|
||||
// .clipShape(RoundedRectangle(cornerRadius: 8))
|
||||
// .padding(.trailing)
|
||||
.padding()
|
||||
.background{
|
||||
RoundedRectangle(cornerRadius: 16)
|
||||
.fill(Color((appState.appInfo.appIcon?.averageColor)!))
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 5){
|
||||
@@ -86,6 +113,7 @@ struct FilesView: View {
|
||||
Text("\(appState.appInfo.bundleIdentifier)").font(.title3)
|
||||
.foregroundStyle((.gray.opacity(0.8)))
|
||||
}
|
||||
.padding(.leading)
|
||||
|
||||
Spacer()
|
||||
|
||||
@@ -97,6 +125,7 @@ struct FilesView: View {
|
||||
|
||||
}
|
||||
|
||||
|
||||
HStack(alignment: .center, spacing: 10) {
|
||||
|
||||
Spacer()
|
||||
@@ -157,7 +186,7 @@ struct FilesView: View {
|
||||
VStack {
|
||||
FileDetailsItem(size: fileSize, icon: iconImage, path: path)
|
||||
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)
|
||||
Text(path.path)
|
||||
.font(.footnote)
|
||||
.lineLimit(2)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
.opacity(0.5)
|
||||
.help(path.path)
|
||||
|
||||
@@ -70,7 +70,9 @@ struct MenuBarMiniAppView: View {
|
||||
appState.showProgress.toggle()
|
||||
showPopover.toggle()
|
||||
if instantSearch {
|
||||
reversePathsSearch(appState: appState, locations: locations)
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ struct MiniMode: View {
|
||||
} else {
|
||||
TopBarMini(search: $search, showPopover: $showPopover)
|
||||
MiniAppView(search: $search, showPopover: $showPopover)
|
||||
|
||||
}
|
||||
}
|
||||
.transition(.opacity)
|
||||
|
||||
@@ -178,6 +178,7 @@ struct Header: View {
|
||||
@State private var hovered = false
|
||||
@EnvironmentObject var appState: AppState
|
||||
@EnvironmentObject var locations: Locations
|
||||
@EnvironmentObject var fsm: FolderSettingsManager
|
||||
@Binding var showPopover: Bool
|
||||
@AppStorage("settings.general.instant") private var instantSearch: Bool = true
|
||||
@AppStorage("settings.general.glass") private var glass: Bool = true
|
||||
@@ -199,7 +200,7 @@ struct Header: View {
|
||||
// Refresh Apps list
|
||||
appState.reload.toggle()
|
||||
showPopover = false
|
||||
let sortedApps = getSortedApps()
|
||||
let sortedApps = getSortedApps(paths: fsm.folderPaths, appState: appState)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
appState.sortedApps = sortedApps
|
||||
if instantSearch {
|
||||
|
||||
@@ -42,6 +42,23 @@ struct TopBar: View {
|
||||
HStack() {
|
||||
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 {
|
||||
Button("") {
|
||||
withAnimation(.easeInOut(duration: 0.5)) {
|
||||
@@ -53,7 +70,9 @@ struct TopBar: View {
|
||||
appState.showProgress.toggle()
|
||||
showPopover.toggle()
|
||||
if instantSearch {
|
||||
reversePathsSearch(appState: appState, locations: locations)
|
||||
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)
|
||||
}
|
||||
@@ -81,6 +100,8 @@ struct TopBar: View {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -37,7 +37,9 @@ struct TopBarMini: View {
|
||||
appState.showProgress.toggle()
|
||||
showPopover.toggle()
|
||||
if instantSearch {
|
||||
reversePathsSearch(appState: appState, locations: locations)
|
||||
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)
|
||||
}
|
||||
@@ -78,7 +80,9 @@ struct TopBarMini: View {
|
||||
appState.showProgress.toggle()
|
||||
showPopover.toggle()
|
||||
if instantSearch {
|
||||
reversePathsSearch(appState: appState, locations: locations)
|
||||
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)
|
||||
}
|
||||
@@ -130,10 +134,8 @@ struct SearchBarMiniBottom: View {
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
TextField(" Search", text: $search)
|
||||
TextField("\(appState.instantTotal > appState.instantProgress ? " Searching the file system" : " Search")", text: $search)
|
||||
.textFieldStyle(SimpleSearchStyle(trash: true, text: $search))
|
||||
}
|
||||
// .padding(.horizontal)
|
||||
// .padding(.bottom, 0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ struct ZombieView: View {
|
||||
@State private var searchZ: String = ""
|
||||
@State private var selectedOption = "Default"
|
||||
var regularWin: Bool
|
||||
@State private var elapsedTime = 0
|
||||
@State private var timer: Timer? = nil
|
||||
|
||||
var body: some View {
|
||||
|
||||
@@ -49,9 +51,19 @@ struct ZombieView: View {
|
||||
|
||||
Text("Searching the file system").font(.title3)
|
||||
.foregroundStyle((.gray.opacity(0.8)))
|
||||
|
||||
HStack {
|
||||
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()
|
||||
}
|
||||
@@ -59,6 +71,16 @@ struct ZombieView: View {
|
||||
|
||||
}
|
||||
.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 {
|
||||
// Titlebar
|
||||
if !regularWin {
|
||||
@@ -70,7 +92,9 @@ struct ZombieView: View {
|
||||
appState.zombieFile = .empty
|
||||
appState.showProgress.toggle()
|
||||
if instantSearch {
|
||||
reversePathsSearch(appState: appState, locations: locations)
|
||||
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)
|
||||
}
|
||||
@@ -131,6 +155,7 @@ struct ZombieView: View {
|
||||
}
|
||||
|
||||
SearchBarMiniBottom(search: $searchZ)
|
||||
.padding(.top)
|
||||
|
||||
Divider()
|
||||
.padding()
|
||||
@@ -146,7 +171,7 @@ struct ZombieView: View {
|
||||
.padding(.trailing)
|
||||
|
||||
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)
|
||||
Text(path.path)
|
||||
.font(.footnote)
|
||||
.lineLimit(2)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
.opacity(0.5)
|
||||
// .foregroundStyle(Color("AccentColor"))
|
||||
|
||||
@@ -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.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"
|
||||
|
||||
Reference in New Issue
Block a user