mirror of
https://github.com/exituser/Pearcleaner.git
synced 2026-09-17 04:58:56 +00:00
3.8.7 - almost there
This commit is contained in:
@@ -12,7 +12,6 @@
|
||||
C72893052AFD42E600C8C1CD /* DeepLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = C72893042AFD42E600C8C1CD /* DeepLink.swift */; };
|
||||
C72893122AFD51EA00C8C1CD /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = C72893112AFD51EA00C8C1CD /* main.swift */; };
|
||||
C72F4B6C2C5DF4ED00B04F60 /* SunburstChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = C72F4B6B2C5DF4ED00B04F60 /* SunburstChart.swift */; };
|
||||
C736E7652C2DF1C9009EDB6A /* AppPathsFetch-Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = C736E7642C2DF1C9009EDB6A /* AppPathsFetch-Async.swift */; };
|
||||
C74412C72BBF249000DDFCA8 /* AppPathsFetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C74412C62BBF249000DDFCA8 /* AppPathsFetch.swift */; };
|
||||
C74D76B72C5C4ABC00748DD1 /* TreeMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = C74D76B62C5C4ABC00748DD1 /* TreeMap.swift */; };
|
||||
C74D76BD2C5D2DF300748DD1 /* FileSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C74D76B92C5D2DF300748DD1 /* FileSearch.swift */; };
|
||||
@@ -104,7 +103,6 @@
|
||||
C728930F2AFD51EA00C8C1CD /* PearcleanerSentinel */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = PearcleanerSentinel; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
C72893112AFD51EA00C8C1CD /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
|
||||
C72F4B6B2C5DF4ED00B04F60 /* SunburstChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SunburstChart.swift; sourceTree = "<group>"; };
|
||||
C736E7642C2DF1C9009EDB6A /* AppPathsFetch-Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppPathsFetch-Async.swift"; sourceTree = "<group>"; };
|
||||
C74412C62BBF249000DDFCA8 /* AppPathsFetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPathsFetch.swift; sourceTree = "<group>"; };
|
||||
C74D76B62C5C4ABC00748DD1 /* TreeMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TreeMap.swift; sourceTree = "<group>"; };
|
||||
C74D76B92C5D2DF300748DD1 /* FileSearch.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileSearch.swift; sourceTree = "<group>"; };
|
||||
@@ -183,7 +181,6 @@
|
||||
C77B90192AF1938F009CC655 /* Logic.swift */,
|
||||
C7DD49E92BAB4D8100CCBA16 /* AppInfoFetch.swift */,
|
||||
C74412C62BBF249000DDFCA8 /* AppPathsFetch.swift */,
|
||||
C736E7642C2DF1C9009EDB6A /* AppPathsFetch-Async.swift */,
|
||||
C7DD49ED2BAB7F6000CCBA16 /* ReversePathsFetch.swift */,
|
||||
C77B90172AF19382009CC655 /* AppCommands.swift */,
|
||||
C77B90152AF19377009CC655 /* AppState.swift */,
|
||||
@@ -384,7 +381,7 @@
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = 1;
|
||||
LastSwiftUpdateCheck = 1530;
|
||||
LastUpgradeCheck = 1510;
|
||||
LastUpgradeCheck = 1600;
|
||||
TargetAttributes = {
|
||||
C728930E2AFD51EA00C8C1CD = {
|
||||
CreatedOnToolsVersion = 15.0.1;
|
||||
@@ -472,7 +469,6 @@
|
||||
C7045A282B03E71D00376976 /* MiniMode.swift in Sources */,
|
||||
C7ED86B12BA8A35500F13DE4 /* Interface.swift in Sources */,
|
||||
C72893052AFD42E600C8C1CD /* DeepLink.swift in Sources */,
|
||||
C736E7652C2DF1C9009EDB6A /* AppPathsFetch-Async.swift in Sources */,
|
||||
C71848442B8D2D600046CB13 /* ZombieView.swift in Sources */,
|
||||
C7DE672B2BA6343D00EB1633 /* MenuBarItem.swift in Sources */,
|
||||
C76D084A2AF83C6E00D07867 /* General.swift in Sources */,
|
||||
@@ -561,8 +557,8 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
APP_BUILD = 60;
|
||||
APP_VERSION = 3.8.6;
|
||||
APP_BUILD = 61;
|
||||
APP_VERSION = 3.8.7;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
@@ -631,8 +627,8 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
APP_BUILD = 60;
|
||||
APP_VERSION = 3.8.6;
|
||||
APP_BUILD = 61;
|
||||
APP_VERSION = 3.8.7;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
@@ -693,7 +689,6 @@
|
||||
C77B90102AF18E2F009CC655 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
APP_BUILD = "$(APP_BUILD)";
|
||||
APP_VERSION = "$(APP_VERSION)";
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
@@ -733,7 +728,6 @@
|
||||
C77B90112AF18E2F009CC655 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
APP_BUILD = "$(APP_BUILD)";
|
||||
APP_VERSION = "$(APP_VERSION)";
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1530"
|
||||
LastUpgradeVersion = "1600"
|
||||
wasCreatedForAppExtension = "YES"
|
||||
version = "2.0">
|
||||
<BuildAction
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1510"
|
||||
LastUpgradeVersion = "1600"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1510"
|
||||
LastUpgradeVersion = "1600"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1510"
|
||||
LastUpgradeVersion = "1600"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1510"
|
||||
LastUpgradeVersion = "1600"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
@@ -65,7 +65,7 @@ struct AppCommands: Commands {
|
||||
undoTrash(appState: appState) {
|
||||
reloadAppsList(appState: appState, fsm: fsm)
|
||||
for app in appState.trashedFiles {
|
||||
AppPathFinder(appInfo: app, appState: appState, locations: locations, undo: true).findPaths()
|
||||
AppPathFinder(appInfo: app, locations: locations, appState: appState, undo: true).findPaths()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,334 +0,0 @@
|
||||
////
|
||||
//// AppPathsFetch-Async.swift
|
||||
//// Pearcleaner
|
||||
////
|
||||
//// Created by Alin Lupascu on 6/27/24.
|
||||
////
|
||||
//
|
||||
//import Foundation
|
||||
//import AppKit
|
||||
//import SwiftUI
|
||||
//
|
||||
//actor PathFinderState {
|
||||
// var fileSize: [URL: Int64] = [:]
|
||||
// var fileIconData: [URL: Data?] = [:]
|
||||
//
|
||||
// func setFileDetails(for path: URL, size: Int64, icon: Data?) {
|
||||
// fileSize[path] = size
|
||||
// fileIconData[path] = icon
|
||||
// }
|
||||
//
|
||||
// func getFileSize() -> [URL: Int64] {
|
||||
// return fileSize
|
||||
// }
|
||||
//
|
||||
// func getFileIconData() -> [URL: Data?] {
|
||||
// return fileIconData
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//class AppPathFinderAsync {
|
||||
// private var appInfo: AppInfo
|
||||
// private var appState: AppState
|
||||
// private var locations: Locations
|
||||
// private var backgroundRun: Bool
|
||||
// private var reverseAddon: Bool
|
||||
// private var undo: Bool
|
||||
// private var completion: () -> Void = {}
|
||||
// private var collection: [URL] = []
|
||||
// private let collectionAccessQueue = DispatchQueue(label: "com.alienator88.Pearcleaner.appPathFinder.collectionAccess")
|
||||
// private var state = PathFinderState() // Actor instance
|
||||
//
|
||||
// init(appInfo: AppInfo = .empty, appState: AppState, locations: Locations, backgroundRun: Bool = false, reverseAddon: Bool = false, undo: Bool = false, completion: @escaping () -> Void = {}) {
|
||||
// self.appInfo = appInfo
|
||||
// self.appState = appState
|
||||
// self.locations = locations
|
||||
// self.backgroundRun = backgroundRun
|
||||
// self.reverseAddon = reverseAddon
|
||||
// self.undo = undo
|
||||
// self.completion = completion
|
||||
// }
|
||||
//
|
||||
// func findPaths() async {
|
||||
// await initialURLProcessing()
|
||||
// await withTaskGroup(of: Void.self) { group in
|
||||
// for location in self.locations.apps.paths {
|
||||
// group.addTask {
|
||||
// await self.processDirectoryLocation(location)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// await withTaskGroup(of: Void.self) { group in
|
||||
// for location in self.locations.apps.paths {
|
||||
// group.addTask {
|
||||
// await self.processFileLocation(location)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// await finalizeCollection()
|
||||
// }
|
||||
//
|
||||
// private func initialURLProcessing() async {
|
||||
// if let url = URL(string: self.appInfo.path.absoluteString), !url.path.contains(".Trash") {
|
||||
// let modifiedUrl = url.path.contains("Wrapper") ? url.deletingLastPathComponent().deletingLastPathComponent() : url
|
||||
// collectionAccessQueue.sync {
|
||||
// self.collection.append(modifiedUrl)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private func processDirectoryLocation(_ location: String) async {
|
||||
// do {
|
||||
// let contents = try FileManager.default.contentsOfDirectory(atPath: location)
|
||||
// for item in contents {
|
||||
// let itemURL = URL(fileURLWithPath: location).appendingPathComponent(item)
|
||||
// let itemL = item.replacingOccurrences(of: ".", with: "").replacingOccurrences(of: " ", with: "").lowercased()
|
||||
//
|
||||
// var isDirectory: ObjCBool = false
|
||||
// if FileManager.default.fileExists(atPath: itemURL.path, isDirectory: &isDirectory), isDirectory.boolValue {
|
||||
// if shouldSkipItem(itemL, at: itemURL) {
|
||||
// continue
|
||||
// }
|
||||
//
|
||||
// collectionAccessQueue.sync {
|
||||
// let alreadyIncluded = self.collection.contains { existingURL in
|
||||
// itemURL.path.hasPrefix(existingURL.path)
|
||||
// }
|
||||
//
|
||||
// if !alreadyIncluded && specificCondition(itemL: itemL, itemURL: itemURL) {
|
||||
// self.collection.append(itemURL)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// } catch {
|
||||
// print("Error processing directory location: \(location), error: \(error)")
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private func processFileLocation(_ location: String) async {
|
||||
// do {
|
||||
// let contents = try FileManager.default.contentsOfDirectory(atPath: location)
|
||||
// for item in contents {
|
||||
// let itemURL = URL(fileURLWithPath: location).appendingPathComponent(item)
|
||||
// let itemL = item.replacingOccurrences(of: ".", with: "").replacingOccurrences(of: " ", with: "").lowercased()
|
||||
//
|
||||
// if FileManager.default.fileExists(atPath: itemURL.path),
|
||||
// !shouldSkipItem(itemL, at: itemURL),
|
||||
// specificCondition(itemL: itemL, itemURL: itemURL) {
|
||||
// collectionAccessQueue.sync {
|
||||
// self.collection.append(itemURL)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// } catch {
|
||||
// print("Error processing file location: \(location), error: \(error)")
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private func shouldSkipItem(_ itemL: String, at itemURL: URL) -> Bool {
|
||||
// var containsItem = false
|
||||
// collectionAccessQueue.sync {
|
||||
// containsItem = self.collection.contains(itemURL)
|
||||
// }
|
||||
// if containsItem || !isSupportedFileType(at: itemURL.path) {
|
||||
// return true
|
||||
// }
|
||||
//
|
||||
// for skipCondition in skipConditions {
|
||||
// if itemL.hasPrefix(skipCondition.skipPrefix) {
|
||||
// let isAllowed = skipCondition.allowPrefixes.contains(where: itemL.hasPrefix)
|
||||
// if !isAllowed {
|
||||
// return true
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return false
|
||||
// }
|
||||
//
|
||||
// private func specificCondition(itemL: String, itemURL: URL) -> Bool {
|
||||
// let bundleIdentifierL = self.appInfo.bundleIdentifier.pearFormat()
|
||||
// let bundleComponents = self.appInfo.bundleIdentifier.components(separatedBy: ".").compactMap { $0 != "-" ? $0.lowercased() : nil }
|
||||
// let bundle = bundleComponents.suffix(2).joined()
|
||||
// let nameL = self.appInfo.appName.pearFormat()
|
||||
// let nameP = self.appInfo.path.lastPathComponent.replacingOccurrences(of: ".app", with: "")
|
||||
//
|
||||
// for condition in conditions {
|
||||
// if bundleIdentifierL.contains(condition.bundle_id) {
|
||||
// let hasIncludeKeyword = condition.include.contains(where: itemL.contains)
|
||||
// let hasExcludeKeyword = condition.exclude.contains(where: itemL.contains)
|
||||
//
|
||||
// if hasExcludeKeyword {
|
||||
// return false
|
||||
// }
|
||||
// if hasIncludeKeyword {
|
||||
// if !condition.exclude.contains(where: itemL.contains) {
|
||||
// return true
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if self.appInfo.webApp {
|
||||
// return itemL.contains(bundleIdentifierL)
|
||||
// }
|
||||
//
|
||||
// return itemL.contains(bundleIdentifierL) || itemL.contains(bundle) || (nameL.count > 3 && itemL.contains(nameL)) || (nameP.count > 3 && itemL.contains(nameP))
|
||||
// }
|
||||
//
|
||||
// func getAllContainers(bundleURL: URL) async -> [URL] {
|
||||
// await withCheckedContinuation { continuation in
|
||||
// var containers: [URL] = []
|
||||
//
|
||||
// let bundleIdentifier = Bundle(url: bundleURL)?.bundleIdentifier
|
||||
//
|
||||
// guard let containerBundleIdentifier = bundleIdentifier else {
|
||||
// printOS("Get Containers: No bundle identifier found for the given bundle URL.")
|
||||
// continuation.resume(returning: containers)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// if let groupContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: containerBundleIdentifier) {
|
||||
// if FileManager.default.fileExists(atPath: groupContainer.path) {
|
||||
// containers.append(groupContainer)
|
||||
// }
|
||||
// } else {
|
||||
// printOS("Get Containers: Failed to retrieve container URL for bundle identifier: \(containerBundleIdentifier)")
|
||||
// }
|
||||
//
|
||||
// let containersPath = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first?.appendingPathComponent("Containers")
|
||||
//
|
||||
// do {
|
||||
// let containerDirectories = try FileManager.default.contentsOfDirectory(at: containersPath!, includingPropertiesForKeys: nil, options: .skipsHiddenFiles)
|
||||
//
|
||||
// let uuidRegex = try NSRegularExpression(pattern: "^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$", options: .caseInsensitive)
|
||||
//
|
||||
// for directory in containerDirectories {
|
||||
// let directoryName = directory.lastPathComponent
|
||||
//
|
||||
// if uuidRegex.firstMatch(in: directoryName, options: [], range: NSRange(location: 0, length: directoryName.utf16.count)) != nil {
|
||||
// let metadataPlistURL = directory.appendingPathComponent(".com.apple.containermanagerd.metadata.plist")
|
||||
// if let metadataDict = NSDictionary(contentsOf: metadataPlistURL), let applicationBundleID = metadataDict["MCMMetadataIdentifier"] as? String {
|
||||
// if applicationBundleID == self.appInfo.bundleIdentifier {
|
||||
// containers.append(directory)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// } catch {
|
||||
// printOS("Error accessing the Containers directory: \(error)")
|
||||
// }
|
||||
//
|
||||
// continuation.resume(returning: containers)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private func handleOutliers(include: Bool = true) -> [URL] {
|
||||
// var outliers: [URL] = []
|
||||
// let bundleIdentifier = self.appInfo.bundleIdentifier.pearFormat()
|
||||
//
|
||||
// let matchingConditions = conditions.filter { condition in
|
||||
// bundleIdentifier.contains(condition.bundle_id)
|
||||
// }
|
||||
//
|
||||
// for condition in matchingConditions {
|
||||
// if include {
|
||||
// if let forceIncludes = condition.includeForce {
|
||||
// for path in forceIncludes {
|
||||
// outliers.append(path)
|
||||
// }
|
||||
// }
|
||||
// } else {
|
||||
// if let excludeForce = condition.excludeForce {
|
||||
// for path in excludeForce {
|
||||
// outliers.append(path)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return outliers
|
||||
// }
|
||||
//
|
||||
// private func finalizeCollection() async {
|
||||
// let allContainers = await getAllContainers(bundleURL: self.appInfo.path)
|
||||
// let outliers = handleOutliers()
|
||||
// let outliersEx = handleOutliers(include: false)
|
||||
// var tempCollection: [URL] = []
|
||||
// collectionAccessQueue.sync {
|
||||
// tempCollection = self.collection
|
||||
// }
|
||||
// tempCollection.append(contentsOf: allContainers)
|
||||
// tempCollection.append(contentsOf: outliers)
|
||||
//
|
||||
// let excludePaths = outliersEx.map { $0.path }
|
||||
// tempCollection.removeAll { url in
|
||||
// excludePaths.contains(url.path)
|
||||
// }
|
||||
//
|
||||
// let sortedCollection = tempCollection.map { $0.standardizedFileURL }.sorted(by: { $0.path < $1.path })
|
||||
// var filteredCollection: [URL] = []
|
||||
// var previousUrl: URL?
|
||||
// for url in sortedCollection {
|
||||
// if let previousUrl = previousUrl, url.path.hasPrefix(previousUrl.path + "/") {
|
||||
// continue
|
||||
// }
|
||||
// filteredCollection.append(url)
|
||||
// previousUrl = url
|
||||
// }
|
||||
//
|
||||
// await handlePostProcessing(sortedCollection: filteredCollection)
|
||||
// }
|
||||
//
|
||||
// private func handlePostProcessing(sortedCollection: [URL]) async {
|
||||
// for path in sortedCollection {
|
||||
// let size = totalSizeOnDisk(for: path)
|
||||
// if let icon = getIconForFileOrFolderNS(atPath: path) {
|
||||
// let iconData = serializeImage(icon)
|
||||
// await state.setFileDetails(for: path, size: size.real, icon: iconData)
|
||||
// } else {
|
||||
// await state.setFileDetails(for: path, size: size.real, icon: nil)
|
||||
// }
|
||||
// }
|
||||
// await updateAppState(with: sortedCollection)
|
||||
// }
|
||||
//
|
||||
// private func updateAppState(with sortedCollection: [URL]) async {
|
||||
// let fileSize = await self.state.getFileSize()
|
||||
// let fileIconData = await self.state.getFileIconData()
|
||||
// let fileIcons = fileIconData.mapValues { deserializeImage($0) }
|
||||
// let arch = checkAppBundleArchitecture(at: self.appInfo.path.path)
|
||||
//
|
||||
// self.appInfo.fileSize = fileSize
|
||||
// self.appInfo.fileIcon = fileIcons
|
||||
// self.appInfo.arch = arch
|
||||
//
|
||||
// await MainActor.run {
|
||||
// if !self.backgroundRun {
|
||||
// self.appState.appInfo = self.appInfo
|
||||
// if !self.undo {
|
||||
// self.appState.selectedItems = Set(sortedCollection)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if self.reverseAddon {
|
||||
// self.appState.appInfoStore.append(self.appInfo)
|
||||
// }
|
||||
//
|
||||
// self.completion()
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//func serializeImage(_ image: NSImage?) -> Data? {
|
||||
// guard let image = image else { return nil }
|
||||
// guard let tiffData = image.tiffRepresentation else { return nil }
|
||||
// let bitmapImage = NSBitmapImageRep(data: tiffData)
|
||||
// return bitmapImage?.representation(using: .png, properties: [:])
|
||||
//}
|
||||
//
|
||||
//func deserializeImage(_ data: Data?) -> NSImage? {
|
||||
// guard let data = data else { return nil }
|
||||
// return NSImage(data: data)
|
||||
//}
|
||||
@@ -11,42 +11,28 @@ import SwiftUI
|
||||
import AlinFoundation
|
||||
|
||||
class AppPathFinder {
|
||||
// Shared properties
|
||||
private var appInfo: AppInfo
|
||||
private var appState: AppState
|
||||
private var locations: Locations
|
||||
private var backgroundRun: Bool
|
||||
private var undo: Bool
|
||||
private var completion: () -> Void = {}
|
||||
private var collection: [URL] = []
|
||||
private var containerCollection: [URL] = []
|
||||
private let collectionAccessQueue = DispatchQueue(label: "com.alienator88.Pearcleaner.appPathFinder.collectionAccess")
|
||||
|
||||
init(appInfo: AppInfo = .empty, appState: AppState, locations: Locations, backgroundRun: Bool = false, undo: Bool = false, completion: @escaping () -> Void = {}) {
|
||||
// GUI-specific properties (can be nil for CLI)
|
||||
private var appState: AppState?
|
||||
private var undo: Bool = false
|
||||
private var completion: (() -> Void)?
|
||||
|
||||
// Initializer for both CLI and GUI
|
||||
init(appInfo: AppInfo, locations: Locations, appState: AppState? = nil, undo: Bool = false, completion: (() -> Void)? = nil) {
|
||||
self.appInfo = appInfo
|
||||
self.appState = appState
|
||||
self.locations = locations
|
||||
self.backgroundRun = backgroundRun
|
||||
self.appState = appState
|
||||
self.undo = undo
|
||||
self.completion = completion
|
||||
}
|
||||
|
||||
func findPaths() {
|
||||
Task(priority: .background) {
|
||||
if self.appInfo.webApp {
|
||||
containerCollection = self.getAllContainers(bundleURL: self.appInfo.path)
|
||||
self.initialURLProcessing()
|
||||
self.finalizeCollection()
|
||||
} else {
|
||||
containerCollection = self.getAllContainers(bundleURL: self.appInfo.path)
|
||||
self.initialURLProcessing()
|
||||
self.collectDirectories()
|
||||
self.collectFiles()
|
||||
self.finalizeCollection()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Shared Methods
|
||||
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
|
||||
@@ -54,18 +40,55 @@ class AppPathFinder {
|
||||
}
|
||||
}
|
||||
|
||||
private func collectDirectories() {
|
||||
let dispatchGroup = DispatchGroup()
|
||||
private func getAllContainers(bundleURL: URL) -> [URL] {
|
||||
var containers: [URL] = []
|
||||
|
||||
for location in self.locations.apps.paths {
|
||||
dispatchGroup.enter()
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
self.processDirectoryLocation(location)
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
// Extract bundle identifier from bundleURL
|
||||
let bundleIdentifier = Bundle(url: bundleURL)?.bundleIdentifier
|
||||
|
||||
// Ensure the bundleIdentifier is not nil
|
||||
guard let containerBundleIdentifier = bundleIdentifier else {
|
||||
printOS("Get Containers: No bundle identifier found for the given bundle URL.")
|
||||
return containers // Returns whatever was found so far, possibly empty
|
||||
}
|
||||
|
||||
dispatchGroup.wait()
|
||||
// Get the regular container URL for the extracted bundle identifier
|
||||
if let groupContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: containerBundleIdentifier) {
|
||||
if FileManager.default.fileExists(atPath: groupContainer.path) {
|
||||
containers.append(groupContainer)
|
||||
}
|
||||
} else {
|
||||
printOS("Get Containers: Failed to retrieve container URL for bundle identifier: \(containerBundleIdentifier)")
|
||||
}
|
||||
|
||||
let containersPath = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first?.appendingPathComponent("Containers")
|
||||
|
||||
do {
|
||||
let containerDirectories = try FileManager.default.contentsOfDirectory(at: containersPath!, includingPropertiesForKeys: nil, options: .skipsHiddenFiles)
|
||||
|
||||
// Define a regular expression to match UUID format
|
||||
let uuidRegex = try NSRegularExpression(pattern: "^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$", options: .caseInsensitive)
|
||||
|
||||
for directory in containerDirectories {
|
||||
let directoryName = directory.lastPathComponent
|
||||
|
||||
// Check if the directory name matches the UUID pattern
|
||||
if uuidRegex.firstMatch(in: directoryName, options: [], range: NSRange(location: 0, length: directoryName.utf16.count)) != nil {
|
||||
// Attempt to read the metadata plist file
|
||||
let metadataPlistURL = directory.appendingPathComponent(".com.apple.containermanagerd.metadata.plist")
|
||||
if let metadataDict = NSDictionary(contentsOf: metadataPlistURL), let applicationBundleID = metadataDict["MCMMetadataIdentifier"] as? String {
|
||||
if applicationBundleID == self.appInfo.bundleIdentifier {
|
||||
containers.append(directory)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
printOS("Error accessing the Containers directory: \(error)")
|
||||
}
|
||||
|
||||
// Return all found containers
|
||||
return containers
|
||||
}
|
||||
|
||||
private func processDirectoryLocation(_ location: String) {
|
||||
@@ -95,20 +118,6 @@ class AppPathFinder {
|
||||
}
|
||||
}
|
||||
|
||||
private func collectFiles() {
|
||||
let dispatchGroup = DispatchGroup()
|
||||
|
||||
for location in self.locations.apps.paths {
|
||||
dispatchGroup.enter()
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
self.processFileLocation(location)
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
}
|
||||
|
||||
dispatchGroup.wait()
|
||||
}
|
||||
|
||||
private func processFileLocation(_ location: String) {
|
||||
if let contents = try? FileManager.default.contentsOfDirectory(atPath: location) {
|
||||
for item in contents {
|
||||
@@ -126,6 +135,114 @@ class AppPathFinder {
|
||||
}
|
||||
}
|
||||
|
||||
private func handleOutliers(include: Bool = true) -> [URL] {
|
||||
var outliers: [URL] = []
|
||||
let bundleIdentifier = self.appInfo.bundleIdentifier.pearFormat()
|
||||
|
||||
// Find conditions that match the current app's bundle identifier
|
||||
let matchingConditions = conditions.filter { condition in
|
||||
bundleIdentifier.contains(condition.bundle_id)
|
||||
}
|
||||
|
||||
|
||||
for condition in matchingConditions {
|
||||
if include {
|
||||
// Handle includeForce
|
||||
if let forceIncludes = condition.includeForce {
|
||||
for path in forceIncludes {
|
||||
outliers.append(path)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Handle excludeForce
|
||||
if let excludeForce = condition.excludeForce {
|
||||
for path in excludeForce {
|
||||
outliers.append(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return outliers
|
||||
}
|
||||
|
||||
|
||||
// MARK: - findPaths Methods
|
||||
func findPaths() {
|
||||
Task(priority: .background) {
|
||||
if self.appInfo.webApp {
|
||||
containerCollection = getAllContainers(bundleURL: self.appInfo.path)
|
||||
self.initialURLProcessing()
|
||||
self.finalizeCollection()
|
||||
} else {
|
||||
containerCollection = getAllContainers(bundleURL: self.appInfo.path)
|
||||
self.initialURLProcessing()
|
||||
self.collectDirectories()
|
||||
self.collectFiles()
|
||||
self.finalizeCollection()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func findPathsCLI() -> Set<URL> {
|
||||
if self.appInfo.webApp {
|
||||
containerCollection = getAllContainers(bundleURL: self.appInfo.path)
|
||||
self.initialURLProcessing()
|
||||
return finalizeCollectionCLI() // Return the collected paths
|
||||
} else {
|
||||
containerCollection = getAllContainers(bundleURL: self.appInfo.path)
|
||||
self.initialURLProcessing()
|
||||
self.collectDirectoriesCLI() // Synchronous version
|
||||
self.collectFilesCLI() // Synchronous version
|
||||
return finalizeCollectionCLI() // Return the collected paths
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Unique Methods
|
||||
|
||||
|
||||
private func collectDirectories() {
|
||||
let dispatchGroup = DispatchGroup()
|
||||
|
||||
for location in self.locations.apps.paths {
|
||||
dispatchGroup.enter()
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
self.processDirectoryLocation(location)
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
}
|
||||
|
||||
dispatchGroup.wait()
|
||||
}
|
||||
|
||||
private func collectFiles() {
|
||||
let dispatchGroup = DispatchGroup()
|
||||
|
||||
for location in self.locations.apps.paths {
|
||||
dispatchGroup.enter()
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
self.processFileLocation(location)
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
}
|
||||
|
||||
dispatchGroup.wait()
|
||||
}
|
||||
|
||||
|
||||
private func collectDirectoriesCLI() {
|
||||
for location in self.locations.apps.paths {
|
||||
processDirectoryLocation(location)
|
||||
}
|
||||
}
|
||||
|
||||
private func collectFilesCLI() {
|
||||
for location in self.locations.apps.paths {
|
||||
processFileLocation(location)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func shouldSkipItem(_ itemL: String, at itemURL: URL) -> Bool {
|
||||
@@ -185,93 +302,9 @@ class AppPathFinder {
|
||||
}
|
||||
|
||||
|
||||
private func getAllContainers(bundleURL: URL) -> [URL] {
|
||||
var containers: [URL] = []
|
||||
|
||||
// Extract bundle identifier from bundleURL
|
||||
let bundleIdentifier = Bundle(url: bundleURL)?.bundleIdentifier
|
||||
|
||||
// Ensure the bundleIdentifier is not nil
|
||||
guard let containerBundleIdentifier = bundleIdentifier else {
|
||||
printOS("Get Containers: No bundle identifier found for the given bundle URL.")
|
||||
return containers // Returns whatever was found so far, possibly empty
|
||||
}
|
||||
|
||||
// Get the regular container URL for the extracted bundle identifier
|
||||
if let groupContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: containerBundleIdentifier) {
|
||||
if FileManager.default.fileExists(atPath: groupContainer.path) {
|
||||
containers.append(groupContainer)
|
||||
}
|
||||
} else {
|
||||
printOS("Get Containers: Failed to retrieve container URL for bundle identifier: \(containerBundleIdentifier)")
|
||||
}
|
||||
|
||||
let containersPath = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first?.appendingPathComponent("Containers")
|
||||
|
||||
do {
|
||||
let containerDirectories = try FileManager.default.contentsOfDirectory(at: containersPath!, includingPropertiesForKeys: nil, options: .skipsHiddenFiles)
|
||||
|
||||
// Define a regular expression to match UUID format
|
||||
let uuidRegex = try NSRegularExpression(pattern: "^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$", options: .caseInsensitive)
|
||||
|
||||
for directory in containerDirectories {
|
||||
let directoryName = directory.lastPathComponent
|
||||
|
||||
// Check if the directory name matches the UUID pattern
|
||||
if uuidRegex.firstMatch(in: directoryName, options: [], range: NSRange(location: 0, length: directoryName.utf16.count)) != nil {
|
||||
// Attempt to read the metadata plist file
|
||||
let metadataPlistURL = directory.appendingPathComponent(".com.apple.containermanagerd.metadata.plist")
|
||||
if let metadataDict = NSDictionary(contentsOf: metadataPlistURL), let applicationBundleID = metadataDict["MCMMetadataIdentifier"] as? String {
|
||||
if applicationBundleID == self.appInfo.bundleIdentifier {
|
||||
containers.append(directory)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
printOS("Error accessing the Containers directory: \(error)")
|
||||
}
|
||||
|
||||
// Return all found containers
|
||||
return containers
|
||||
}
|
||||
|
||||
|
||||
private func handleOutliers(include: Bool = true) -> [URL] {
|
||||
var outliers: [URL] = []
|
||||
let bundleIdentifier = self.appInfo.bundleIdentifier.pearFormat()
|
||||
|
||||
// Find conditions that match the current app's bundle identifier
|
||||
let matchingConditions = conditions.filter { condition in
|
||||
bundleIdentifier.contains(condition.bundle_id)
|
||||
}
|
||||
|
||||
|
||||
for condition in matchingConditions {
|
||||
if include {
|
||||
// Handle includeForce
|
||||
if let forceIncludes = condition.includeForce {
|
||||
for path in forceIncludes {
|
||||
outliers.append(path)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Handle excludeForce
|
||||
if let excludeForce = condition.excludeForce {
|
||||
for path in excludeForce {
|
||||
outliers.append(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return outliers
|
||||
}
|
||||
|
||||
private func finalizeCollection() {
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
// let allContainers = self.getAllContainers(bundleURL: self.appInfo.path)
|
||||
let outliers = self.handleOutliers()
|
||||
let outliersEx = self.handleOutliers(include: false)
|
||||
var tempCollection: [URL] = []
|
||||
@@ -302,10 +335,48 @@ class AppPathFinder {
|
||||
}
|
||||
|
||||
self.handlePostProcessing(sortedCollection: filteredCollection)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private func finalizeCollectionCLI() -> Set<URL> {
|
||||
let outliers = handleOutliers()
|
||||
let outliersEx = handleOutliers(include: false)
|
||||
var tempCollection: [URL] = []
|
||||
self.collectionAccessQueue.sync {
|
||||
tempCollection = self.collection
|
||||
}
|
||||
tempCollection.append(contentsOf: self.containerCollection)
|
||||
tempCollection.append(contentsOf: outliers)
|
||||
|
||||
// Remove excluded paths
|
||||
let excludePaths = outliersEx.map { $0.path }
|
||||
tempCollection.removeAll { url in
|
||||
excludePaths.contains(url.path)
|
||||
}
|
||||
|
||||
// Sort and filter subdirectories
|
||||
let sortedCollection = tempCollection.map { $0.standardizedFileURL }.sorted(by: { $0.path < $1.path })
|
||||
var filteredCollection: [URL] = []
|
||||
var previousUrl: URL?
|
||||
|
||||
for url in sortedCollection {
|
||||
if let previousUrl = previousUrl, url.path.hasPrefix(previousUrl.path + "/") {
|
||||
continue
|
||||
}
|
||||
filteredCollection.append(url)
|
||||
previousUrl = url
|
||||
}
|
||||
|
||||
// Remove Trash paths if necessary
|
||||
if filteredCollection.count == 1, let firstURL = filteredCollection.first, firstURL.path.contains(".Trash") {
|
||||
filteredCollection.removeAll()
|
||||
}
|
||||
|
||||
return Set(filteredCollection)
|
||||
}
|
||||
|
||||
private func handlePostProcessing(sortedCollection: [URL]) {
|
||||
// Calculate file details (sizes and icons), update app state, and call completion
|
||||
var fileSize: [URL: Int64] = [:]
|
||||
@@ -321,11 +392,12 @@ class AppPathFinder {
|
||||
|
||||
let arch = checkAppBundleArchitecture(at: self.appInfo.path.path)
|
||||
|
||||
var updatedCollection = sortedCollection
|
||||
if updatedCollection.count == 1, let firstURL = updatedCollection.first, firstURL.path.contains(".Trash") {
|
||||
updatedCollection.removeAll()
|
||||
}
|
||||
|
||||
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.fileSize = fileSize
|
||||
@@ -333,19 +405,285 @@ class AppPathFinder {
|
||||
self.appInfo.fileIcon = fileIcon
|
||||
self.appInfo.arch = arch
|
||||
|
||||
if !self.backgroundRun {
|
||||
self.appState.appInfo = self.appInfo
|
||||
if !self.undo {
|
||||
self.appState.selectedItems = Set(updatedCollection)
|
||||
}
|
||||
self.appState?.appInfo = self.appInfo
|
||||
if !self.undo {
|
||||
self.appState?.selectedItems = Set(updatedCollection)
|
||||
}
|
||||
|
||||
// Append object to store if running reverse search with empty store
|
||||
// if self.reverseAddon {
|
||||
// self.appState.appInfoStore.append(self.appInfo)
|
||||
// }
|
||||
|
||||
self.completion()
|
||||
self.completion?()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// CLI // =========================================================================================================================
|
||||
|
||||
|
||||
//class AppPathFinderCLI {
|
||||
// private var appInfo: AppInfo
|
||||
// private var locations: Locations
|
||||
// private var collection: [URL] = []
|
||||
// private var containerCollection: [URL] = []
|
||||
// private let collectionAccessQueue = DispatchQueue(label: "com.alienator88.Pearcleaner.appPathFinderCLI.collectionAccess")
|
||||
//
|
||||
// init(appInfo: AppInfo = .empty, locations: Locations) {
|
||||
// self.appInfo = appInfo
|
||||
// self.locations = locations
|
||||
// }
|
||||
//
|
||||
// // Make this function synchronous for the CLI
|
||||
// func findPaths() -> Set<URL> {
|
||||
// if self.appInfo.webApp {
|
||||
// containerCollection = getAllContainers(bundleURL: self.appInfo.path)
|
||||
// self.initialURLProcessing()
|
||||
// return finalizeCollection() // Return the collected paths
|
||||
// } else {
|
||||
// containerCollection = getAllContainers(bundleURL: self.appInfo.path)
|
||||
// self.initialURLProcessing()
|
||||
// self.collectDirectories() // Synchronous version
|
||||
// self.collectFiles() // Synchronous version
|
||||
// return finalizeCollection() // Return the collected paths
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// 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 shouldSkipItem(_ itemL: String, at itemURL: URL) -> Bool {
|
||||
// var containsItem = false
|
||||
// collectionAccessQueue.sync {
|
||||
// containsItem = self.collection.contains(itemURL)
|
||||
// }
|
||||
// if containsItem || !isSupportedFileType(at: itemURL.path) {
|
||||
// return true
|
||||
// }
|
||||
//
|
||||
// for skipCondition in skipConditions {
|
||||
// if itemL.hasPrefix(skipCondition.skipPrefix) {
|
||||
// let isAllowed = skipCondition.allowPrefixes.contains(where: itemL.hasPrefix)
|
||||
// if !isAllowed {
|
||||
// return true // Skip because it starts with a base prefix but is not in the allowed list
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return false
|
||||
// }
|
||||
//
|
||||
// private func processDirectoryLocation(_ location: String) {
|
||||
// if let contents = try? FileManager.default.contentsOfDirectory(atPath: location) {
|
||||
// for item in contents {
|
||||
// let itemURL = URL(fileURLWithPath: location).appendingPathComponent(item)
|
||||
// let itemL = item.replacingOccurrences(of: ".", with: "").replacingOccurrences(of: " ", with: "").lowercased()
|
||||
//
|
||||
// var isDirectory: ObjCBool = false
|
||||
// if FileManager.default.fileExists(atPath: itemURL.path, isDirectory: &isDirectory), isDirectory.boolValue {
|
||||
// // Perform the check to skip the item if needed
|
||||
// if shouldSkipItem(itemL, at: itemURL) {
|
||||
// continue
|
||||
// }
|
||||
//
|
||||
// collectionAccessQueue.sync {
|
||||
// let alreadyIncluded = self.collection.contains { existingURL in
|
||||
// itemURL.path.hasPrefix(existingURL.path)
|
||||
// }
|
||||
//
|
||||
// if !alreadyIncluded && specificCondition(itemL: itemL, itemURL: itemURL) {
|
||||
// self.collection.append(itemURL)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// 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 nameLFiltered = nameL.filter { $0.isLetter }
|
||||
//
|
||||
// let nameP = self.appInfo.path.lastPathComponent.replacingOccurrences(of: ".app", with: "")
|
||||
//
|
||||
// for condition in conditions {
|
||||
// if bundleIdentifierL.contains(condition.bundle_id) {
|
||||
// // Exclude keywords
|
||||
// let hasExcludeKeyword = condition.exclude.contains(where: itemL.contains)
|
||||
// if hasExcludeKeyword {
|
||||
// return false
|
||||
// }
|
||||
// // Include keywords
|
||||
// let hasIncludeKeyword = condition.include.contains(where: itemL.contains)
|
||||
// if hasIncludeKeyword {
|
||||
// return true
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
// if self.appInfo.webApp {
|
||||
// return itemL.contains(bundleIdentifierL)
|
||||
// }
|
||||
//
|
||||
// return itemL.contains(bundleIdentifierL) || itemL.contains(bundle) || (nameL.count > 3 && itemL.contains(nameL)) || (nameP.count > 3 && itemL.contains(nameP) || (nameLFiltered.count > 3 && itemL.contains(nameLFiltered)))
|
||||
//
|
||||
// }
|
||||
//
|
||||
// // Make collectDirectories synchronous
|
||||
// private func collectDirectories() {
|
||||
// for location in self.locations.apps.paths {
|
||||
// self.processDirectoryLocation(location) // Synchronous call
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private func processFileLocation(_ location: String) {
|
||||
// if let contents = try? FileManager.default.contentsOfDirectory(atPath: location) {
|
||||
// for item in contents {
|
||||
// let itemURL = URL(fileURLWithPath: location).appendingPathComponent(item)
|
||||
// let itemL = item.replacingOccurrences(of: ".", with: "").replacingOccurrences(of: " ", with: "").lowercased()
|
||||
//
|
||||
// if FileManager.default.fileExists(atPath: itemURL.path),
|
||||
// !shouldSkipItem(itemL, at: itemURL),
|
||||
// specificCondition(itemL: itemL, itemURL: itemURL) {
|
||||
// collectionAccessQueue.sync {
|
||||
// self.collection.append(itemURL)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private func getAllContainers(bundleURL: URL) -> [URL] {
|
||||
// var containers: [URL] = []
|
||||
//
|
||||
// // Extract bundle identifier from bundleURL
|
||||
// let bundleIdentifier = Bundle(url: bundleURL)?.bundleIdentifier
|
||||
//
|
||||
// // Ensure the bundleIdentifier is not nil
|
||||
// guard let containerBundleIdentifier = bundleIdentifier else {
|
||||
// printOS("Get Containers: No bundle identifier found for the given bundle URL.")
|
||||
// return containers // Returns whatever was found so far, possibly empty
|
||||
// }
|
||||
//
|
||||
// // Get the regular container URL for the extracted bundle identifier
|
||||
// if let groupContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: containerBundleIdentifier) {
|
||||
// if FileManager.default.fileExists(atPath: groupContainer.path) {
|
||||
// containers.append(groupContainer)
|
||||
// }
|
||||
// } else {
|
||||
// printOS("Get Containers: Failed to retrieve container URL for bundle identifier: \(containerBundleIdentifier)")
|
||||
// }
|
||||
//
|
||||
// let containersPath = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first?.appendingPathComponent("Containers")
|
||||
//
|
||||
// do {
|
||||
// let containerDirectories = try FileManager.default.contentsOfDirectory(at: containersPath!, includingPropertiesForKeys: nil, options: .skipsHiddenFiles)
|
||||
//
|
||||
// // Define a regular expression to match UUID format
|
||||
// let uuidRegex = try NSRegularExpression(pattern: "^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$", options: .caseInsensitive)
|
||||
//
|
||||
// for directory in containerDirectories {
|
||||
// let directoryName = directory.lastPathComponent
|
||||
//
|
||||
// // Check if the directory name matches the UUID pattern
|
||||
// if uuidRegex.firstMatch(in: directoryName, options: [], range: NSRange(location: 0, length: directoryName.utf16.count)) != nil {
|
||||
// // Attempt to read the metadata plist file
|
||||
// let metadataPlistURL = directory.appendingPathComponent(".com.apple.containermanagerd.metadata.plist")
|
||||
// if let metadataDict = NSDictionary(contentsOf: metadataPlistURL), let applicationBundleID = metadataDict["MCMMetadataIdentifier"] as? String {
|
||||
// if applicationBundleID == self.appInfo.bundleIdentifier {
|
||||
// containers.append(directory)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// } catch {
|
||||
// printOS("Error accessing the Containers directory: \(error)")
|
||||
// }
|
||||
//
|
||||
// // Return all found containers
|
||||
// return containers
|
||||
// }
|
||||
//
|
||||
// // Make collectFiles synchronous
|
||||
// private func collectFiles() {
|
||||
// for location in self.locations.apps.paths {
|
||||
// self.processFileLocation(location) // Synchronous call
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private func handleOutliers(include: Bool = true) -> [URL] {
|
||||
// var outliers: [URL] = []
|
||||
// let bundleIdentifier = self.appInfo.bundleIdentifier.pearFormat()
|
||||
//
|
||||
// // Find conditions that match the current app's bundle identifier
|
||||
// let matchingConditions = conditions.filter { condition in
|
||||
// bundleIdentifier.contains(condition.bundle_id)
|
||||
// }
|
||||
//
|
||||
//
|
||||
// for condition in matchingConditions {
|
||||
// if include {
|
||||
// // Handle includeForce
|
||||
// if let forceIncludes = condition.includeForce {
|
||||
// for path in forceIncludes {
|
||||
// outliers.append(path)
|
||||
// }
|
||||
// }
|
||||
// } else {
|
||||
// // Handle excludeForce
|
||||
// if let excludeForce = condition.excludeForce {
|
||||
// for path in excludeForce {
|
||||
// outliers.append(path)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// }
|
||||
//
|
||||
// return outliers
|
||||
// }
|
||||
//
|
||||
// // No need to dispatch asynchronously here
|
||||
// private func finalizeCollection() -> Set<URL> {
|
||||
// let outliers = self.handleOutliers()
|
||||
// let outliersEx = self.handleOutliers(include: false)
|
||||
// var tempCollection: [URL] = []
|
||||
// self.collectionAccessQueue.sync {
|
||||
// tempCollection = self.collection
|
||||
// }
|
||||
// tempCollection.append(contentsOf: self.containerCollection)
|
||||
// tempCollection.append(contentsOf: outliers)
|
||||
//
|
||||
// let excludePaths = outliersEx.map { $0.path }
|
||||
// tempCollection.removeAll { url in
|
||||
// excludePaths.contains(url.path)
|
||||
// }
|
||||
//
|
||||
// let sortedCollection = tempCollection.map { $0.standardizedFileURL }.sorted(by: { $0.path < $1.path })
|
||||
// var filteredCollection: [URL] = []
|
||||
// var previousUrl: URL?
|
||||
//
|
||||
// for url in sortedCollection {
|
||||
// if let previousUrl = previousUrl, url.path.hasPrefix(previousUrl.path + "/") {
|
||||
// continue
|
||||
// }
|
||||
// filteredCollection.append(url)
|
||||
// previousUrl = url
|
||||
// }
|
||||
//
|
||||
// // Handle Trash directory case
|
||||
// var updatedCollection = filteredCollection
|
||||
// if updatedCollection.count == 1, let firstURL = updatedCollection.first, firstURL.path.contains(".Trash") {
|
||||
// // If only one item exists and it's in the Trash, remove it
|
||||
// updatedCollection.removeAll()
|
||||
// }
|
||||
//
|
||||
// return Set(updatedCollection)
|
||||
// }
|
||||
//
|
||||
//}
|
||||
|
||||
@@ -145,7 +145,7 @@ func showAppInFiles(appInfo: AppInfo, appState: AppState, locations: Locations,
|
||||
appState.showProgress = true
|
||||
|
||||
// Initialize the path finder and execute its search.
|
||||
AppPathFinder(appInfo: appInfo, appState: appState, locations: locations) {
|
||||
AppPathFinder(appInfo: appInfo, locations: locations, appState: appState) {
|
||||
updateOnMain {
|
||||
// Update the progress indicator on the main thread once the search completes.
|
||||
appState.showProgress = false
|
||||
@@ -210,7 +210,42 @@ func moveFilesToTrash(appState: AppState, at fileURLs: [URL], completion: @escap
|
||||
|
||||
}
|
||||
|
||||
func moveFilesToTrashCLI(at fileURLs: [URL]) -> Bool {
|
||||
// Stop Sentinel FileWatcher momentarily to ignore .app bundle being sent to Trash
|
||||
sendStopNotificationFW()
|
||||
|
||||
// Create the AppleScript for moving files to the Trash
|
||||
let posixFiles = fileURLs.map { item in
|
||||
return "POSIX file \"\(item.path)\"" + (item == fileURLs.last ? "" : ", ")}.joined()
|
||||
|
||||
let scriptSource = """
|
||||
tell application \"Finder\" to delete { \(posixFiles) }
|
||||
"""
|
||||
|
||||
var error: NSDictionary?
|
||||
if let scriptObject = NSAppleScript(source: scriptSource) {
|
||||
let output: NSAppleEventDescriptor = scriptObject.executeAndReturnError(&error)
|
||||
|
||||
// Handle any AppleScript errors
|
||||
if let error = error {
|
||||
print("Trash Error: \(error)") // Synchronous error reporting
|
||||
return false // Indicate failure
|
||||
}
|
||||
|
||||
// Check if output is null, indicating the user canceled the operation
|
||||
if output.descriptorType == typeNull {
|
||||
print("Trash Error: operation canceled by the user") // Synchronous cancellation reporting
|
||||
return false // Indicate failure due to cancellation
|
||||
}
|
||||
|
||||
// Process output if it exists
|
||||
if let outputString = output.stringValue {
|
||||
print("Trash: \(outputString)")
|
||||
}
|
||||
}
|
||||
|
||||
return true // Indicate success
|
||||
}
|
||||
|
||||
|
||||
// Undo trash action
|
||||
|
||||
@@ -56,13 +56,215 @@ func findAndSetWindowFrame(named titles: [String], windowSettings: WindowSetting
|
||||
}
|
||||
|
||||
|
||||
// Process CLI // ========================================================================================================
|
||||
func processCLI(arguments: [String], appState: AppState, locations: Locations) {
|
||||
let options = Array(arguments.dropFirst()) // Remove the first argument (binary path)
|
||||
|
||||
// Private function to list files for uninstall, using the provided path
|
||||
func listFiles(at path: String) {
|
||||
// Convert the provided string path to a URL
|
||||
let url = URL(fileURLWithPath: path)
|
||||
|
||||
// print("[BETA] Pearcleaner CLI | List Files:\n")
|
||||
|
||||
// Fetch the app info and safely unwrap
|
||||
guard let appInfo = AppInfoFetcher.getAppInfo(atPath: url) else {
|
||||
print("Error: Invalid path or unable to fetch app info at path: \(path)")
|
||||
exit(1) // Exit with non-zero code to indicate failure
|
||||
}
|
||||
|
||||
// Use the AppPathFinderCLI to find paths synchronously
|
||||
let appPathFinder = AppPathFinder(appInfo: appInfo, locations: locations)
|
||||
|
||||
// Call findPaths to get the Set of URLs
|
||||
let foundPaths = appPathFinder.findPathsCLI()
|
||||
|
||||
// Print each path in the Set to the console
|
||||
for path in foundPaths {
|
||||
print(path.path)
|
||||
}
|
||||
}
|
||||
|
||||
// Private function to uninstall the application bundle at a given path
|
||||
func uninstallApp(at path: String) {
|
||||
// Convert the provided string path to a URL
|
||||
let url = URL(fileURLWithPath: path)
|
||||
print("[BETA] Pearcleaner CLI | Uninstall Application:\n")
|
||||
|
||||
// Fetch the app info and safely unwrap
|
||||
guard let appInfo = AppInfoFetcher.getAppInfo(atPath: url) else {
|
||||
print("Error: Invalid path or unable to fetch app info at path: \(path)")
|
||||
exit(1) // Exit with non-zero code to indicate failure
|
||||
}
|
||||
|
||||
killApp(appId: appInfo.bundleIdentifier) {
|
||||
let success = moveFilesToTrashCLI(at: [appInfo.path])
|
||||
if success {
|
||||
print("Application moved to the trash successfully.")
|
||||
exit(0)
|
||||
} else {
|
||||
print("Failed to move application to trash.")
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Private function to uninstall the application and all related files at a given path
|
||||
func uninstallAll(at path: String) {
|
||||
// Convert the provided string path to a URL
|
||||
let url = URL(fileURLWithPath: path)
|
||||
print("[BETA] Pearcleaner CLI | Uninstall Application + Related Files:\n")
|
||||
|
||||
// Fetch the app info and safely unwrap
|
||||
guard let appInfo = AppInfoFetcher.getAppInfo(atPath: url) else {
|
||||
print("Error: Invalid path or unable to fetch app info at path: \(path)")
|
||||
exit(1) // Exit with non-zero code to indicate failure
|
||||
}
|
||||
|
||||
// Use the AppPathFinderCLI to find paths synchronously
|
||||
let appPathFinder = AppPathFinder(appInfo: appInfo, locations: locations)
|
||||
|
||||
// Call findPaths to get the Set of URLs
|
||||
let foundPaths = appPathFinder.findPathsCLI()
|
||||
|
||||
killApp(appId: appInfo.bundleIdentifier) {
|
||||
let success = moveFilesToTrashCLI(at: Array(foundPaths))
|
||||
if success {
|
||||
print("The following files have been moved to the trash successfully:\n")
|
||||
// Print each path in the Set to the console
|
||||
for path in foundPaths {
|
||||
print(path.path)
|
||||
}
|
||||
exit(0)
|
||||
} else {
|
||||
print("Failed to move application and related files to trash.")
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle help option (-h or --help)
|
||||
if options.contains("-h") || options.contains("--help") {
|
||||
displayHelp()
|
||||
exit(0)
|
||||
}
|
||||
|
||||
// Handle --list or -l option with a path argument
|
||||
if let listIndex = options.firstIndex(where: { $0 == "--list" || $0 == "-l" }), listIndex + 1 < options.count {
|
||||
let path = options[listIndex + 1] // Path provided after --list or -l
|
||||
listFiles(at: path)
|
||||
exit(0)
|
||||
}
|
||||
|
||||
// Handle --uninstall or -u option with a path argument
|
||||
if let uninstallIndex = options.firstIndex(where: { $0 == "--uninstall" || $0 == "-u" }), uninstallIndex + 1 < options.count {
|
||||
let path = options[uninstallIndex + 1] // Path provided after --uninstall or -u
|
||||
uninstallApp(at: path)
|
||||
exit(0)
|
||||
}
|
||||
|
||||
// Handle --uninstall-all or -ua option with a path argument
|
||||
if let uninstallAllIndex = options.firstIndex(where: { $0 == "--uninstall-all" || $0 == "-ua" }), uninstallAllIndex + 1 < options.count {
|
||||
let path = options[uninstallAllIndex + 1] // Path provided after --uninstall-all or -ua
|
||||
uninstallAll(at: path)
|
||||
exit(0)
|
||||
}
|
||||
|
||||
// If no valid option was provided, show the help menu by default
|
||||
displayHelp()
|
||||
exit(0)
|
||||
}
|
||||
|
||||
// Private function to display help message
|
||||
func displayHelp() {
|
||||
print("""
|
||||
|
||||
[BETA] Pearcleaner CLI | Usage:
|
||||
|
||||
--list <path>, -l <path> Find application files available for uninstall at the specified path
|
||||
--uninstall <path>, -u <path> Remove only the application bundle at the specified path
|
||||
--uninstall-all <path>, -ua <path> Remove the application bundle and all related files at the specified path
|
||||
--help, -h Show this help message
|
||||
|
||||
""")
|
||||
}
|
||||
|
||||
|
||||
// Check if pearcleaner symlink exists
|
||||
func checkCLISymlink() -> Bool {
|
||||
let filePath = "/usr/local/bin/pearcleaner"
|
||||
let fileManager = FileManager.default
|
||||
|
||||
// Check if the file exists at the given path
|
||||
return fileManager.fileExists(atPath: filePath)
|
||||
}
|
||||
|
||||
// Install/uninstall symlink for CLI
|
||||
func manageSymlink(install: Bool) {
|
||||
// Get the current running application's bundle binary path
|
||||
guard let appPath = Bundle.main.executablePath else {
|
||||
printOS("Error: Unable to get the executable path.")
|
||||
return
|
||||
}
|
||||
|
||||
// Path where the symlink should be created
|
||||
let symlinkPath = "/usr/local/bin/pearcleaner"
|
||||
|
||||
// Check if the symlink already exists
|
||||
let symlinkExists = checkCLISymlink()
|
||||
|
||||
// If we are installing the symlink and it already exists, skip creating it
|
||||
if install && symlinkExists {
|
||||
printOS("Symlink already exists at \(symlinkPath). No action needed.")
|
||||
return
|
||||
}
|
||||
|
||||
// If we are uninstalling the symlink and it doesn't exist, skip removing it
|
||||
if !install && !symlinkExists {
|
||||
printOS("Symlink does not exist at \(symlinkPath). No action needed.")
|
||||
return
|
||||
}
|
||||
|
||||
// Create AppleScript commands for installing or uninstalling the symlink
|
||||
let script: String
|
||||
|
||||
if install {
|
||||
// AppleScript to create a symlink with admin privileges
|
||||
script = """
|
||||
do shell script "ln -s '\(appPath)' '\(symlinkPath)'" with administrator privileges
|
||||
"""
|
||||
} else {
|
||||
// AppleScript to remove the symlink with admin privileges
|
||||
script = """
|
||||
do shell script "rm '\(symlinkPath)'" with administrator privileges
|
||||
"""
|
||||
}
|
||||
|
||||
// Execute the AppleScript
|
||||
var error: NSDictionary?
|
||||
if let scriptObject = NSAppleScript(source: script) {
|
||||
scriptObject.executeAndReturnError(&error)
|
||||
|
||||
if let error = error {
|
||||
printOS("AppleScript Error: \(error)")
|
||||
} else {
|
||||
if install {
|
||||
printOS("Symlink created successfully at \(symlinkPath).")
|
||||
} else {
|
||||
printOS("Symlink removed successfully from \(symlinkPath).")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
printOS("Error: Unable to create the AppleScript object.")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Brew cleanup
|
||||
func caskCleanup(app: String) {
|
||||
Task(priority: .high) {
|
||||
let formattedApp = app.lowercased().replacingOccurrences(of: " ", with: "-")
|
||||
print(formattedApp)
|
||||
|
||||
#if arch(x86_64)
|
||||
let cmd = "/usr/local/bin/brew"
|
||||
@@ -85,6 +287,8 @@ func caskCleanup(app: String) {
|
||||
end tell
|
||||
"""
|
||||
|
||||
print(script)
|
||||
|
||||
var error: NSDictionary?
|
||||
if let appleScript = NSAppleScript(source: script) {
|
||||
appleScript.executeAndReturnError(&error)
|
||||
@@ -98,7 +302,6 @@ func caskCleanup(app: String) {
|
||||
|
||||
|
||||
|
||||
|
||||
// Print list of files locally
|
||||
func saveURLsToFile(urls: Set<URL>, appState: AppState, copy: Bool = false) {
|
||||
|
||||
@@ -426,7 +629,7 @@ func uninstallPearcleaner(appState: AppState, locations: Locations) {
|
||||
let appInfo = AppInfoFetcher.getAppInfo(atPath: Bundle.main.bundleURL)
|
||||
|
||||
// Find application files for Pearcleaner
|
||||
AppPathFinder(appInfo: appInfo!, appState: appState, locations: locations, completion: {
|
||||
AppPathFinder(appInfo: appInfo!, locations: locations, appState: appState, completion: {
|
||||
// Kill Pearcleaner and tell Finder to trash the files
|
||||
let selectedItemsArray = Array(appState.selectedItems).filter { !$0.path.contains(".Trash") }
|
||||
let posixFiles = selectedItemsArray.map { item in
|
||||
|
||||
@@ -29,6 +29,23 @@ struct PearcleanerApp: App {
|
||||
@State private var showPopover: Bool = false
|
||||
let conditionManager = ConditionManager.shared
|
||||
|
||||
init() {
|
||||
let arguments = CommandLine.arguments
|
||||
let filteredArguments = arguments.filter { !["-NSDocumentRevisionsDebugMode", "YES"].contains($0) }
|
||||
let isRunningInTerminal = isatty(STDIN_FILENO) != 0
|
||||
|
||||
// If running from terminal and no arguments are provided
|
||||
if isRunningInTerminal && arguments.count == 1 {
|
||||
displayHelp()
|
||||
exit(0) // Exit without launching the GUI
|
||||
}
|
||||
|
||||
// The first argument is always the binary path, so check if there are more than 1 arguments
|
||||
if filteredArguments.count > 1 {
|
||||
// Process the CLI options
|
||||
processCLI(arguments: arguments, appState: appState, locations: locations)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
|
||||
@@ -9,6 +9,7 @@ import Foundation
|
||||
import SwiftUI
|
||||
import FinderSync
|
||||
import AlinFoundation
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
struct GeneralSettingsTab: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
@@ -20,7 +21,9 @@ struct GeneralSettingsTab: View {
|
||||
@AppStorage("settings.general.confirmAlert") private var confirmAlert: Bool = false
|
||||
@AppStorage("settings.general.selectedSort") var selectedSortAlpha: Bool = true
|
||||
@AppStorage("settings.general.sizeType") var sizeType: String = "Real"
|
||||
// @AppStorage("settings.general.brewTerminal") var brewTerm: String = "Terminal"
|
||||
@State private var isResetting = false
|
||||
@State private var isCLISymlinked = false
|
||||
|
||||
var body: some View {
|
||||
VStack() {
|
||||
@@ -39,13 +42,33 @@ struct GeneralSettingsTab: View {
|
||||
.frame(width: 20, height: 20)
|
||||
.padding(.trailing)
|
||||
.foregroundStyle(.primary.opacity(0.5))
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
Text("\(brew ? "Homebrew cleanup is enabled" : "Homebrew cleanup is disabled")")
|
||||
.font(.callout)
|
||||
.foregroundStyle(.primary.opacity(0.5))
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
HStack(spacing: 0) {
|
||||
Text("\(brew ? "Homebrew cleanup is enabled" : "Homebrew cleanup is disabled")")
|
||||
.font(.callout)
|
||||
.foregroundStyle(.primary.opacity(0.5))
|
||||
InfoButton(text: "When homebrew cleanup is enabled, Pearcleaner will check if the app you are removing was installed via homebrew and launch Terminal.app to execute a brew uninstall and cleanup command to let homebrew know that the app is removed. This way your homebrew list will be synced up correctly and caching will be removed. Terminal.app is required since some apps need sudo permissions to remove services and files placed in system folders. Since other terminal apps don't support applescript and/or the 'do script' command, I opted to use the default macOS Terminal app for this.\n\nNOTE: If you undo the file delete with CMD+Z, the files will be put back but homebrew will not be aware of it. To get the homebrew list back in sync you'd need to run:\n brew install APPNAME --force")
|
||||
}
|
||||
|
||||
// HStack(spacing: 0) {
|
||||
// Text("Cleanup application: \(brewTerm)")
|
||||
// .font(.footnote)
|
||||
// .foregroundStyle(.primary.opacity(0.4))
|
||||
//
|
||||
// Button("") {
|
||||
// selectBrewTerm()
|
||||
// }
|
||||
// .buttonStyle(SimpleButtonStyle(icon: "gear", help: "Set terminal application, right click to Reset", size: 14))
|
||||
// .contextMenu{
|
||||
// Button("Reset") {
|
||||
// brewTerm = "Terminal"
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
InfoButton(text: "When homebrew cleanup is enabled, Pearcleaner will check if the app you are removing was installed via homebrew and launch Terminal.app to execute a brew uninstall and cleanup command to let homebrew know that the app is removed. This way your homebrew list will be synced up correctly and caching will be removed. Terminal is required since some apps need sudo permissions to remove services and files placed in system folders.\n\nNOTE: If you undo the file delete with CMD+Z, the files will be put back but homebrew will not be aware of it. To get the homebrew list back in sync you'd need to run:\n brew install APPNAME --force")
|
||||
|
||||
|
||||
Spacer()
|
||||
Toggle(isOn: $brew, label: {
|
||||
@@ -116,12 +139,12 @@ struct GeneralSettingsTab: View {
|
||||
.font(.callout)
|
||||
.foregroundStyle(.primary.opacity(0.5))
|
||||
}
|
||||
InfoButton(text: "When searching for app files or leftover files, the list will be sorted either alphabetically or by size(large to small)")
|
||||
// InfoButton(text: "When searching for app files or leftover files, the list will be sorted either alphabetically or by size(large to small)")
|
||||
Spacer()
|
||||
Picker("", selection: $selectedSortAlpha) {
|
||||
Text("Alpha")
|
||||
Text("Alphabetical")
|
||||
.tag(true)
|
||||
Text("Size")
|
||||
Text("File Size")
|
||||
.tag(false)
|
||||
}
|
||||
.buttonStyle(.borderless)
|
||||
@@ -237,6 +260,50 @@ struct GeneralSettingsTab: View {
|
||||
.padding(5)
|
||||
.padding(.leading)
|
||||
|
||||
|
||||
// === CLI =============================================================================================
|
||||
|
||||
Divider()
|
||||
.padding()
|
||||
|
||||
HStack() {
|
||||
Text("Command Line").font(.title2)
|
||||
Spacer()
|
||||
}
|
||||
.padding(.leading)
|
||||
|
||||
HStack(spacing: 0) {
|
||||
Image(systemName: "terminal")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 20, height: 20)
|
||||
.padding(.trailing)
|
||||
.foregroundStyle(isCLISymlinked ? .green : .red)
|
||||
.saturation(themeManager.displayMode.colorScheme == .dark ? 0.8 : 1)
|
||||
Text(isCLISymlinked ? "Pearcleaner CLI is installed" : "Pearcleaner CLI is **not** installed")
|
||||
.font(.callout)
|
||||
.foregroundStyle(.primary.opacity(0.5))
|
||||
InfoButton(text: "Enabling the CLI will allow you to execute Pearcleaner actions from the Terminal. This will add pearcleaner command into /usr/local/bin so it's available directly from your PATH environment variable. Try it after enabling:\n\n > pearcleaner --help")
|
||||
Spacer()
|
||||
|
||||
Toggle(isOn: $isCLISymlinked, label: {
|
||||
})
|
||||
.toggleStyle(.switch)
|
||||
.onChange(of: isCLISymlinked) { newValue in
|
||||
if newValue {
|
||||
manageSymlink(install: true)
|
||||
} else {
|
||||
manageSymlink(install: false)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
.padding(5)
|
||||
.padding(.leading)
|
||||
|
||||
|
||||
|
||||
|
||||
// === Reset Settings =============================================================================================
|
||||
|
||||
HStack() {
|
||||
@@ -250,16 +317,17 @@ struct GeneralSettingsTab: View {
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(.vertical, 5)
|
||||
.padding(.vertical, 10)
|
||||
|
||||
Spacer()
|
||||
|
||||
}
|
||||
.onAppear {
|
||||
appState.updateExtensionStatus()
|
||||
isCLISymlinked = checkCLISymlink()
|
||||
}
|
||||
.padding(20)
|
||||
.frame(width: 500, height: 520)
|
||||
.frame(width: 500, height: 640)
|
||||
|
||||
}
|
||||
|
||||
@@ -273,6 +341,20 @@ struct GeneralSettingsTab: View {
|
||||
}
|
||||
}
|
||||
|
||||
// private func selectBrewTerm() {
|
||||
// let dialog = NSOpenPanel()
|
||||
// dialog.title = "Select a Terminal application"
|
||||
// dialog.allowedContentTypes = [UTType("com.apple.application-bundle")!] // Limit to .app bundles
|
||||
// dialog.allowsMultipleSelection = false
|
||||
// dialog.canChooseFiles = true
|
||||
// dialog.canChooseDirectories = false
|
||||
//
|
||||
// if dialog.runModal() == .OK, let url = dialog.url {
|
||||
// let appNameWithExtension = url.lastPathComponent
|
||||
// brewTerm = appNameWithExtension.replacingOccurrences(of: ".app", with: "")
|
||||
// }
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -44,14 +44,26 @@ struct SectionView: View {
|
||||
var apps: [AppInfo]
|
||||
@Binding var search: String
|
||||
@Binding var showPopover: Bool
|
||||
@State private var showItems: Bool = true
|
||||
@AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true
|
||||
|
||||
var body: some View {
|
||||
LazyVStack(spacing: 0) {
|
||||
Header(title: title, count: count, showPopover: $showPopover)
|
||||
.padding(.leading, 5)
|
||||
ForEach(apps, id: \.self) { appInfo in
|
||||
AppListItems(search: $search, showPopover: $showPopover, appInfo: appInfo)
|
||||
.onTapGesture {
|
||||
withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) {
|
||||
showItems.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
if showItems {
|
||||
ForEach(apps, id: \.self) { appInfo in
|
||||
AppListItems(search: $search, showPopover: $showPopover, appInfo: appInfo)
|
||||
.transition(.opacity)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,7 +78,7 @@ struct Header: View {
|
||||
@EnvironmentObject var fsm: FolderSettingsManager
|
||||
@Binding var showPopover: Bool
|
||||
@AppStorage("settings.general.glass") private var glass: Bool = true
|
||||
@AppStorage("settings.general.selectedSortAppsList") var selectedSortAlpha: Bool = true
|
||||
// @AppStorage("settings.general.selectedSortAppsList") var selectedSortAlpha: Bool = true
|
||||
|
||||
|
||||
var body: some View {
|
||||
@@ -87,9 +99,9 @@ struct Header: View {
|
||||
.frame(minHeight: 20)
|
||||
.padding(5)
|
||||
.help("Click header to change sorting order")
|
||||
.onTapGesture {
|
||||
selectedSortAlpha.toggle()
|
||||
}
|
||||
// .onTapGesture {
|
||||
// selectedSortAlpha.toggle()
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user