mirror of
https://github.com/exituser/Pearcleaner.git
synced 2026-09-17 04:58:56 +00:00
updates
This commit is contained in:
@@ -16,11 +16,13 @@
|
||||
C72893052AFD42E600C8C1CD /* DeepLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = C72893042AFD42E600C8C1CD /* DeepLink.swift */; };
|
||||
C72893122AFD51EA00C8C1CD /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = C72893112AFD51EA00C8C1CD /* main.swift */; };
|
||||
C74412C72BBF249000DDFCA8 /* AppPathsFetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C74412C62BBF249000DDFCA8 /* AppPathsFetch.swift */; };
|
||||
C74FDC632BCD833D00B8960F /* Conditions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C74FDC622BCD833D00B8960F /* Conditions.swift */; };
|
||||
C7575D392B0182DE006A600A /* PearcleanerSentinel in CopyFiles */ = {isa = PBXBuildFile; fileRef = C728930F2AFD51EA00C8C1CD /* PearcleanerSentinel */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
|
||||
C76D08482AF83C3F00D07867 /* Update.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76D08472AF83C3F00D07867 /* Update.swift */; };
|
||||
C76D084A2AF83C6E00D07867 /* General.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76D08492AF83C6E00D07867 /* General.swift */; };
|
||||
C76D08512AF850F300D07867 /* About.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76D08502AF850F300D07867 /* About.swift */; };
|
||||
C76D08552AF89CDE00D07867 /* RegularMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76D08542AF89CDE00D07867 /* RegularMode.swift */; };
|
||||
C771CB762BD028AF00C33A18 /* SegmentedPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = C771CB752BD028AF00C33A18 /* SegmentedPicker.swift */; };
|
||||
C77B90042AF18E2E009CC655 /* PearcleanerApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C77B90032AF18E2E009CC655 /* PearcleanerApp.swift */; };
|
||||
C77B90082AF18E2F009CC655 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C77B90072AF18E2F009CC655 /* Assets.xcassets */; };
|
||||
C77B90162AF19377009CC655 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = C77B90152AF19377009CC655 /* AppState.swift */; };
|
||||
@@ -106,10 +108,12 @@
|
||||
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>"; };
|
||||
C74412C62BBF249000DDFCA8 /* AppPathsFetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPathsFetch.swift; sourceTree = "<group>"; };
|
||||
C74FDC622BCD833D00B8960F /* Conditions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Conditions.swift; sourceTree = "<group>"; };
|
||||
C76D08472AF83C3F00D07867 /* Update.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Update.swift; sourceTree = "<group>"; };
|
||||
C76D08492AF83C6E00D07867 /* General.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = General.swift; sourceTree = "<group>"; };
|
||||
C76D08502AF850F300D07867 /* About.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = About.swift; sourceTree = "<group>"; };
|
||||
C76D08542AF89CDE00D07867 /* RegularMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegularMode.swift; sourceTree = "<group>"; };
|
||||
C771CB752BD028AF00C33A18 /* SegmentedPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentedPicker.swift; sourceTree = "<group>"; };
|
||||
C77B90002AF18E2E009CC655 /* Pearcleaner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Pearcleaner.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
C77B90032AF18E2E009CC655 /* PearcleanerApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PearcleanerApp.swift; sourceTree = "<group>"; };
|
||||
C77B90072AF18E2F009CC655 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
@@ -197,6 +201,7 @@
|
||||
C72893042AFD42E600C8C1CD /* DeepLink.swift */,
|
||||
C77B901B2AF193A3009CC655 /* Styles.swift */,
|
||||
C7D31D502AFF00F300C7ED9E /* Locations.swift */,
|
||||
C74FDC622BCD833D00B8960F /* Conditions.swift */,
|
||||
C7A9CE462B89164700EB6E78 /* Authorization.swift */,
|
||||
C7ACADE52B92A737000B5845 /* Features.swift */,
|
||||
C7ED86B22BA8ED8800F13DE4 /* Trash.swift */,
|
||||
@@ -246,6 +251,14 @@
|
||||
path = Resources;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
C771CB742BD028A000C33A18 /* Picker */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C771CB752BD028AF00C33A18 /* SegmentedPicker.swift */,
|
||||
);
|
||||
path = Picker;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
C77B8FF72AF18E2E009CC655 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -283,6 +296,7 @@
|
||||
C77B901F2AF1B390009CC655 /* Views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C771CB742BD028A000C33A18 /* Picker */,
|
||||
C77B90222AF2D616009CC655 /* FilesView.swift */,
|
||||
C71848432B8D2D600046CB13 /* ZombieView.swift */,
|
||||
C76D08542AF89CDE00D07867 /* RegularMode.swift */,
|
||||
@@ -471,9 +485,11 @@
|
||||
C77B90182AF19382009CC655 /* AppCommands.swift in Sources */,
|
||||
C7D31D4A2AFEB26700C7ED9E /* AppListItems.swift in Sources */,
|
||||
C77B901E2AF1A9DB009CC655 /* SettingsWindow.swift in Sources */,
|
||||
C771CB762BD028AF00C33A18 /* SegmentedPicker.swift in Sources */,
|
||||
C74412C72BBF249000DDFCA8 /* AppPathsFetch.swift in Sources */,
|
||||
C79D05202BC99EB40083F976 /* ThemeManager.swift in Sources */,
|
||||
C7D31D482AFEB23700C7ED9E /* TopBar.swift in Sources */,
|
||||
C74FDC632BCD833D00B8960F /* Conditions.swift in Sources */,
|
||||
C7ACADE62B92A737000B5845 /* Features.swift in Sources */,
|
||||
C7F539382AF60865007DF1B2 /* Utilities.swift in Sources */,
|
||||
C76D08552AF89CDE00D07867 /* RegularMode.swift in Sources */,
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
import SwiftUI
|
||||
|
||||
struct AppCommands: Commands {
|
||||
@AppStorage("settings.general.instant") private var instantSearch: Bool = true
|
||||
|
||||
let appState: AppState
|
||||
let locations: Locations
|
||||
@@ -42,17 +41,9 @@ struct AppCommands: Commands {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
appState.sortedApps = []
|
||||
appState.sortedApps = sortedApps
|
||||
if instantSearch {
|
||||
Task(priority: .high){
|
||||
loadAllPaths(allApps: sortedApps, appState: appState, locations: locations) {
|
||||
appState.reload.toggle()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
appState.reload.toggle()
|
||||
}
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Text("Refresh Apps")
|
||||
}
|
||||
@@ -78,8 +69,7 @@ struct AppCommands: Commands {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||
appState.sortedApps = sortedApps
|
||||
for app in appState.trashedFiles {
|
||||
let pathFinder = AppPathFinder(appInfo: app, appState: appState, locations: locations)
|
||||
pathFinder.findPaths()
|
||||
AppPathFinder(appInfo: app, appState: appState, locations: locations).findPaths()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ class AppInfoFetcher {
|
||||
let system = !path.path.contains(NSHomeDirectory())
|
||||
|
||||
return AppInfo(id: UUID(), path: path, bundleIdentifier: bundleIdentifier, appName: appName, appVersion: appVersion, appIcon: appIcon,
|
||||
webApp: webApp, wrapped: wrapped, system: system, files: [], fileSize: [:], fileIcon: [:])
|
||||
webApp: webApp, wrapped: wrapped, system: system, files: [], fileSize: [:], fileSizeLogical: [:], fileIcon: [:])
|
||||
}
|
||||
|
||||
private static func fetchAppIcon(for path: URL, wrapped: Bool) -> NSImage? {
|
||||
|
||||
@@ -9,6 +9,7 @@ import Foundation
|
||||
import AppKit
|
||||
import SwiftUI
|
||||
|
||||
|
||||
class AppPathFinder {
|
||||
private var appInfo: AppInfo
|
||||
private var appState: AppState
|
||||
@@ -18,7 +19,6 @@ class AppPathFinder {
|
||||
private var completion: () -> Void = {}
|
||||
private var collection: [URL] = []
|
||||
private let collectionAccessQueue = DispatchQueue(label: "com.alienator88.Pearcleaner.appPathFinder.collectionAccess")
|
||||
@AppStorage("settings.general.instant") var instantSearch: Bool = true
|
||||
|
||||
init(appInfo: AppInfo = .empty, appState: AppState, locations: Locations, backgroundRun: Bool = false, reverseAddon: Bool = false, completion: @escaping () -> Void = {}) {
|
||||
self.appInfo = appInfo
|
||||
@@ -124,10 +124,21 @@ class AppPathFinder {
|
||||
collectionAccessQueue.sync {
|
||||
containsItem = self.collection.contains(itemURL)
|
||||
}
|
||||
return itemL.hasPrefix("comapple") && !["comappleconfigurator", "comappledt", "comappleiwork", "comapplesfsymbols", "comappletestflight"].contains(where: itemL.hasPrefix) || containsItem || !isSupportedFileType(at: itemURL.path)
|
||||
if containsItem || !isSupportedFileType(at: itemURL.path) {
|
||||
return true
|
||||
}
|
||||
|
||||
for skipCondition in skipConditions {
|
||||
if itemL.hasPrefix(skipCondition.skipPrefix) {
|
||||
let isAllowed = skipCondition.allowPrefixes.contains(where: itemL.hasPrefix)
|
||||
if !isAllowed {
|
||||
return true // Skip because it starts with a base prefix but is not in the allowed list
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -138,85 +149,20 @@ class AppPathFinder {
|
||||
let nameL = self.appInfo.appName.pearFormat()
|
||||
let nameP = self.appInfo.path.lastPathComponent.replacingOccurrences(of: ".app", with: "")
|
||||
|
||||
let exclusions = [
|
||||
"comappledtxcode": ["comrobotsandpencilsxcodesapp", "comoneminutegamesxcodecleaner", "iohyperappxcodecleaner", "xcodesjson"],
|
||||
"comrobotsandpencilsxcodesapp": ["comappledtxcode", "comoneminutegamesxcodecleaner", "iohyperappxcodecleaner"],
|
||||
"iohyperappxcodecleaner": ["comrobotsandpencilsxcodesapp", "comoneminutegamesxcodecleaner", "comappledtxcode", "xcodesjson"]
|
||||
]
|
||||
for condition in conditions {
|
||||
if bundleIdentifierL.contains(condition.bundle_id) {
|
||||
// Exclude and include keywords
|
||||
let hasIncludeKeyword = condition.include.contains(where: itemL.contains)
|
||||
let hasExcludeKeyword = condition.exclude.contains(where: itemL.contains)
|
||||
|
||||
if let excludedApps = exclusions[bundleIdentifierL], excludedApps.contains(where: { itemL.contains($0) }) {
|
||||
if hasExcludeKeyword {
|
||||
return false
|
||||
}
|
||||
|
||||
if bundleIdentifierL.contains("comappledtxcode") {
|
||||
// Include items that are part of the Xcode ecosystem
|
||||
if itemL.contains("comappledt") || itemL.contains("xcode") || itemL.contains("simulator") {
|
||||
if hasIncludeKeyword {
|
||||
if !condition.exclude.contains(where: itemL.contains) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if bundleIdentifierL.contains("uszoomxos") {
|
||||
// Include items for Zoom that are not similar to the app name and/or bundle id
|
||||
if itemL.contains("zoom") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if bundleIdentifierL.contains("combravebrowser") {
|
||||
// Include items for Brave that are not similar to the app name and/or bundle id
|
||||
if itemL.contains("brave") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if bundleIdentifierL.contains("comgooglechrome") {
|
||||
// Include items for Chrome that are not similar to the app name and/or bundle id
|
||||
if (itemL.contains("google") && !itemL.contains("iterm")) || (itemL.contains("chrome") && !itemL.contains("chromefeaturestate")) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if bundleIdentifierL.contains("commicrosoftedgemac") {
|
||||
// Include items for Edge that are not similar to the app name and/or bundle id
|
||||
let exclusions = ["vscode","rdc","appcenter","office","oneauth"]
|
||||
if itemL.contains("microsoft") && !exclusions.contains(where: itemL.contains) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if bundleIdentifierL.contains("orgmozillafirefox") {
|
||||
// Include items for Firefox that are not similar to the app name and/or bundle id
|
||||
if itemL.contains("mozilla") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if bundleIdentifierL.contains("comlogioptionsplus") {
|
||||
// Include items for Zoom that are not similar to the app name and/or bundle id
|
||||
if itemL.contains("logi") && !itemL.contains("login") && !itemL.contains("logic") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if bundleIdentifierL.contains("commicrosoftvscode") {
|
||||
// Include items for vscode
|
||||
if itemL.contains("vscode") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if bundleIdentifierL.contains("comfacebookarchondeveloperid") {
|
||||
// Include items for FB Messenger
|
||||
if itemL.contains("archonloginhelper") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if bundleIdentifierL.contains("euexelbanstats") {
|
||||
// Include items for Stats that are not similar to the app name and/or bundle id
|
||||
if itemL.contains("video") {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,37 +209,35 @@ class AppPathFinder {
|
||||
|
||||
private func handleOutliers() -> [URL] {
|
||||
var outliers: [URL] = []
|
||||
let bundleIdentifier = self.appInfo.bundleIdentifier.pearFormat()
|
||||
|
||||
// Handle VSCode folder
|
||||
if self.appInfo.bundleIdentifier.pearFormat().contains("commicrosoftvscode") {
|
||||
let appSupportCodePath = "\(home)/Library/Application Support/Code"
|
||||
if let codeDirectoryURL = URL(string: appSupportCodePath), FileManager.default.fileExists(atPath: codeDirectoryURL.path) {
|
||||
outliers.append(codeDirectoryURL)
|
||||
}
|
||||
// Find conditions that match the current app's bundle identifier
|
||||
let matchingConditions = conditions.filter { condition in
|
||||
bundleIdentifier.contains(condition.bundle_id)
|
||||
}
|
||||
|
||||
// Handle Xcode folder
|
||||
if self.appInfo.bundleIdentifier.pearFormat().contains("comappledtxcode") {
|
||||
let simulators = "\(home)/Library/Containers/com.apple.iphonesimulator.ShareExtension"
|
||||
if let simulatorsURL = URL(string: simulators), FileManager.default.fileExists(atPath: simulatorsURL.path) {
|
||||
outliers.append(simulatorsURL)
|
||||
for condition in matchingConditions {
|
||||
if let forceIncludes = condition.includeForce {
|
||||
for path in forceIncludes {
|
||||
if let url = URL(string: path), FileManager.default.fileExists(atPath: url.path) {
|
||||
outliers.append(url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add other outlier cases here as needed
|
||||
|
||||
return outliers
|
||||
}
|
||||
|
||||
private func finalizeCollection() {
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
let groupContainers = self.getGroupContainers(bundleURL: self.appInfo.path)
|
||||
// let groupContainers = self.getGroupContainers(bundleURL: self.appInfo.path)
|
||||
let outliers = self.handleOutliers()
|
||||
var tempCollection: [URL] = []
|
||||
self.collectionAccessQueue.sync {
|
||||
tempCollection = self.collection
|
||||
}
|
||||
tempCollection.append(contentsOf: groupContainers)
|
||||
// tempCollection.append(contentsOf: groupContainers)
|
||||
tempCollection.append(contentsOf: outliers)
|
||||
|
||||
// Sort and standardize URLs to ensure consistent comparisons
|
||||
@@ -318,10 +262,13 @@ class AppPathFinder {
|
||||
private func handlePostProcessing(sortedCollection: [URL]) {
|
||||
// Calculate file details (sizes and icons), update app state, and call completion
|
||||
var fileSize: [URL: Int64] = [:]
|
||||
var fileSizeLogical: [URL: Int64] = [:]
|
||||
var fileIcon: [URL: NSImage?] = [:]
|
||||
|
||||
for path in sortedCollection {
|
||||
fileSize[path] = totalSizeOnDisk(for: path)
|
||||
let size = totalSizeOnDisk(for: path)
|
||||
fileSize[path] = size.real
|
||||
fileSizeLogical[path] = size.logical
|
||||
fileIcon[path] = getIconForFileOrFolderNS(atPath: path)
|
||||
}
|
||||
|
||||
@@ -332,8 +279,8 @@ class AppPathFinder {
|
||||
}
|
||||
|
||||
// Update appInfo and appState with the new values
|
||||
self.appInfo.files = updatedCollection
|
||||
self.appInfo.fileSize = fileSize
|
||||
self.appInfo.fileSizeLogical = fileSizeLogical
|
||||
self.appInfo.fileIcon = fileIcon
|
||||
|
||||
if !self.backgroundRun {
|
||||
@@ -341,12 +288,6 @@ class AppPathFinder {
|
||||
self.appState.selectedItems = Set(updatedCollection)
|
||||
}
|
||||
|
||||
// Only append object to store if instant search. Same for calculating progress.
|
||||
if self.instantSearch {
|
||||
self.appState.appInfoStore.append(self.appInfo)
|
||||
self.appState.instantProgress += 1
|
||||
}
|
||||
|
||||
// Append object to store if running reverse search with empty store
|
||||
if self.reverseAddon {
|
||||
self.appState.appInfoStore.append(self.appInfo)
|
||||
@@ -356,3 +297,324 @@ class AppPathFinder {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// Async Test
|
||||
//class AppPathFinder {
|
||||
// private var appInfo: AppInfo
|
||||
// private var appState: AppState
|
||||
// private var locations: Locations
|
||||
// private var backgroundRun: Bool
|
||||
// private var reverseAddon: Bool
|
||||
// private var completion: () -> Void = {}
|
||||
// private var collection: [URL] = []
|
||||
// private let collectionAccessQueue = DispatchQueue(label: "com.alienator88.Pearcleaner.appPathFinder.collectionAccess")
|
||||
// private var state = PathFinderState() // Actor instance
|
||||
//
|
||||
// init(appInfo: AppInfo = .empty, appState: AppState, locations: Locations, backgroundRun: Bool = false, reverseAddon: Bool = false, completion: @escaping () -> Void = {}) {
|
||||
// self.appInfo = appInfo
|
||||
// self.appState = appState
|
||||
// self.locations = locations
|
||||
// self.backgroundRun = backgroundRun
|
||||
// self.reverseAddon = reverseAddon
|
||||
// self.completion = completion
|
||||
// }
|
||||
//
|
||||
// func findPaths() async {
|
||||
// await initialURLProcessing()
|
||||
// await collectDirectories()
|
||||
// await collectFiles()
|
||||
// await finalizeCollection()
|
||||
// }
|
||||
//
|
||||
// private func initialURLProcessing() async {
|
||||
// if let url = URL(string: self.appInfo.path.absoluteString), !url.path.contains(".Trash") {
|
||||
// let modifiedUrl = url.path.contains("Wrapper") ? url.deletingLastPathComponent().deletingLastPathComponent() : url
|
||||
// collectionAccessQueue.sync {
|
||||
// self.collection.append(modifiedUrl)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private func collectDirectories() async {
|
||||
// for location in self.locations.apps.paths {
|
||||
// await processDirectoryLocation(location)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private func processDirectoryLocation(_ location: String) async {
|
||||
// do {
|
||||
// let contents = try FileManager.default.contentsOfDirectory(atPath: location)
|
||||
// for item in contents {
|
||||
// let itemURL = URL(fileURLWithPath: location).appendingPathComponent(item)
|
||||
// let itemL = item.replacingOccurrences(of: ".", with: "").replacingOccurrences(of: " ", with: "").lowercased()
|
||||
//
|
||||
// var isDirectory: ObjCBool = false
|
||||
// if FileManager.default.fileExists(atPath: itemURL.path, isDirectory: &isDirectory), isDirectory.boolValue {
|
||||
// // Perform the check to skip the item if needed
|
||||
// if shouldSkipItem(itemL, at: itemURL) {
|
||||
// continue
|
||||
// }
|
||||
//
|
||||
// collectionAccessQueue.sync {
|
||||
// let alreadyIncluded = self.collection.contains { existingURL in
|
||||
// itemURL.path.hasPrefix(existingURL.path)
|
||||
// }
|
||||
//
|
||||
// if !alreadyIncluded && specificCondition(itemL: itemL, itemURL: itemURL) {
|
||||
// self.collection.append(itemURL)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// } catch {
|
||||
// print("Error processing directory location: \(location), error: \(error)")
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private func collectFiles() async {
|
||||
// for location in self.locations.apps.paths {
|
||||
// await processFileLocation(location)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private func processFileLocation(_ location: String) async {
|
||||
// do {
|
||||
// let contents = try FileManager.default.contentsOfDirectory(atPath: location)
|
||||
// for item in contents {
|
||||
// let itemURL = URL(fileURLWithPath: location).appendingPathComponent(item)
|
||||
// let itemL = item.replacingOccurrences(of: ".", with: "").replacingOccurrences(of: " ", with: "").lowercased()
|
||||
//
|
||||
// if FileManager.default.fileExists(atPath: itemURL.path),
|
||||
// !shouldSkipItem(itemL, at: itemURL),
|
||||
// specificCondition(itemL: itemL, itemURL: itemURL) {
|
||||
// collectionAccessQueue.sync {
|
||||
// self.collection.append(itemURL)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// } catch {
|
||||
// print("Error processing file location: \(location), error: \(error)")
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
//
|
||||
// private func shouldSkipItem(_ itemL: String, at itemURL: URL) -> Bool {
|
||||
// var containsItem = false
|
||||
// collectionAccessQueue.sync {
|
||||
// containsItem = self.collection.contains(itemURL)
|
||||
// }
|
||||
// if containsItem || !isSupportedFileType(at: itemURL.path) {
|
||||
// return true
|
||||
// }
|
||||
//
|
||||
// for skipCondition in skipConditions {
|
||||
// if itemL.hasPrefix(skipCondition.skipPrefix) {
|
||||
// let isAllowed = skipCondition.allowPrefixes.contains(where: itemL.hasPrefix)
|
||||
// if !isAllowed {
|
||||
// return true // Skip because it starts with a base prefix but is not in the allowed list
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return false
|
||||
// }
|
||||
//
|
||||
//
|
||||
//
|
||||
// private func specificCondition(itemL: String, itemURL: URL) -> Bool {
|
||||
// let bundleIdentifierL = self.appInfo.bundleIdentifier.pearFormat()
|
||||
// let bundleComponents = self.appInfo.bundleIdentifier.components(separatedBy: ".").compactMap { $0 != "-" ? $0.lowercased() : nil }
|
||||
// let bundle = bundleComponents.suffix(2).joined()
|
||||
// let nameL = self.appInfo.appName.pearFormat()
|
||||
// let nameP = self.appInfo.path.lastPathComponent.replacingOccurrences(of: ".app", with: "")
|
||||
//
|
||||
// for condition in conditions {
|
||||
// if bundleIdentifierL.contains(condition.bundle_id) {
|
||||
// // Exclude and include keywords
|
||||
// let hasIncludeKeyword = condition.include.contains(where: itemL.contains)
|
||||
// let hasExcludeKeyword = condition.exclude.contains(where: itemL.contains)
|
||||
//
|
||||
// if hasExcludeKeyword {
|
||||
// return false
|
||||
// }
|
||||
// if hasIncludeKeyword {
|
||||
// if !condition.exclude.contains(where: itemL.contains) {
|
||||
// return true
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
// if self.appInfo.webApp {
|
||||
// return itemL.contains(bundleIdentifierL)
|
||||
// }
|
||||
//
|
||||
// return itemL.contains(bundleIdentifierL) || itemL.contains(bundle) || (nameL.count > 3 && itemL.contains(nameL)) || (nameP.count > 3 && itemL.contains(nameP))
|
||||
//
|
||||
// }
|
||||
//
|
||||
//
|
||||
// func getGroupContainers(bundleURL: URL) async -> [URL] {
|
||||
// await withCheckedContinuation { continuation in
|
||||
// var groupContainers: [URL] = []
|
||||
// // Assume creating a SecStaticCode is synchronous and quick.
|
||||
// var staticCode: SecStaticCode?
|
||||
// let status = SecStaticCodeCreateWithPath(bundleURL as CFURL, [], &staticCode)
|
||||
//
|
||||
// if status == errSecSuccess, let staticCode = staticCode {
|
||||
// var signingInformation: CFDictionary?
|
||||
//
|
||||
// // This may involve a blocking call to fetch signing information.
|
||||
// let status = SecCodeCopySigningInformation(staticCode, SecCSFlags(), &signingInformation)
|
||||
//
|
||||
// if status == errSecSuccess, let infoDict = signingInformation as? [String: Any],
|
||||
// let entitlementsDict = infoDict["entitlements-dict"] as? [String: Any],
|
||||
// let appGroups = entitlementsDict["com.apple.security.application-groups"] as? [String] {
|
||||
//
|
||||
// let fileManager = FileManager.default
|
||||
// let home = NSHomeDirectory() // Assuming `home` is derived earlier or statically known.
|
||||
//
|
||||
// for groupID in appGroups {
|
||||
// let groupURL = URL(fileURLWithPath: "\(home)/Library/Group Containers/\(groupID)")
|
||||
// if fileManager.fileExists(atPath: groupURL.path) {
|
||||
// groupContainers.append(groupURL)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Continue once all operations are complete.
|
||||
// continuation.resume(returning: groupContainers)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
// private func handleOutliers() -> [URL] {
|
||||
// var outliers: [URL] = []
|
||||
// let bundleIdentifier = self.appInfo.bundleIdentifier.pearFormat()
|
||||
//
|
||||
// // Find conditions that match the current app's bundle identifier
|
||||
// let matchingConditions = conditions.filter { condition in
|
||||
// bundleIdentifier.contains(condition.bundle_id)
|
||||
// }
|
||||
//
|
||||
// for condition in matchingConditions {
|
||||
// if let forceIncludes = condition.includeForce {
|
||||
// for path in forceIncludes {
|
||||
// if let url = URL(string: path), FileManager.default.fileExists(atPath: url.path) {
|
||||
// outliers.append(url)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return outliers
|
||||
// }
|
||||
//
|
||||
// private func finalizeCollection() async {
|
||||
// let groupContainers = await getGroupContainers(bundleURL: self.appInfo.path)
|
||||
// let outliers = handleOutliers()
|
||||
// var tempCollection: [URL] = []
|
||||
//
|
||||
// collectionAccessQueue.sync {
|
||||
// tempCollection = self.collection
|
||||
// }
|
||||
//
|
||||
// tempCollection.append(contentsOf: groupContainers)
|
||||
// tempCollection.append(contentsOf: outliers)
|
||||
//
|
||||
// // Sort and standardize URLs to ensure consistent comparisons
|
||||
// let sortedCollection = tempCollection.map { $0.standardizedFileURL }.sorted(by: { $0.path < $1.path })
|
||||
// var filteredCollection: [URL] = []
|
||||
// var previousUrl: URL?
|
||||
//
|
||||
// for url in sortedCollection {
|
||||
// if let previous = previousUrl, url.path.hasPrefix(previous.path + "/") {
|
||||
// // Current URL is a subdirectory of the previous one, so skip it
|
||||
// continue
|
||||
// }
|
||||
// // This URL is not a subdirectory of the previous one, so keep it and set it as the previous URL
|
||||
// filteredCollection.append(url)
|
||||
// previousUrl = url
|
||||
// }
|
||||
//
|
||||
// await handlePostProcessing(sortedCollection: filteredCollection)
|
||||
// }
|
||||
//
|
||||
// private func handlePostProcessing(sortedCollection: [URL]) async {
|
||||
// for path in sortedCollection {
|
||||
// let size = totalSizeOnDisk(for: path)
|
||||
// if let icon = getIconForFileOrFolderNS(atPath: path) { // Retrieve NSImage
|
||||
// let iconData = serializeImage(icon) // Convert NSImage to Data
|
||||
// await state.setFileDetails(for: path, size: size, icon: iconData) // Pass Data to actor
|
||||
// } else {
|
||||
// await state.setFileDetails(for: path, size: size, icon: nil)
|
||||
// }
|
||||
// }
|
||||
// await updateAppState(with: sortedCollection)
|
||||
// }
|
||||
//
|
||||
// private func updateAppState(with sortedCollection: [URL]) async {
|
||||
// // Retrieve state from the actor
|
||||
// let fileSize = await self.state.getFileSize()
|
||||
// let fileIconData = await self.state.getFileIconData()
|
||||
// let fileIcons = fileIconData.mapValues { deserializeImage($0) }
|
||||
//
|
||||
// // Assume updating of appInfo needs to happen on the main thread
|
||||
// self.appInfo.fileSize = fileSize
|
||||
// self.appInfo.fileIcon = fileIcons
|
||||
//
|
||||
// // Execute UI related updates on the main thread using MainActor
|
||||
// await MainActor.run {
|
||||
// if !self.backgroundRun {
|
||||
// self.appState.appInfo = self.appInfo
|
||||
// self.appState.selectedItems = Set(sortedCollection)
|
||||
// }
|
||||
//
|
||||
//
|
||||
// }
|
||||
//
|
||||
// self.completion() // Call the completion handler
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//actor PathFinderState {
|
||||
// var fileSize: [URL: Int64] = [:]
|
||||
// var fileIconData: [URL: Data?] = [:]
|
||||
//
|
||||
// func setFileDetails(for path: URL, size: Int64, icon: Data?) {
|
||||
// fileSize[path] = size
|
||||
// fileIconData[path] = icon
|
||||
// }
|
||||
//
|
||||
// func getFileSize() -> [URL: Int64] {
|
||||
// return fileSize
|
||||
// }
|
||||
//
|
||||
// func getFileIconData() -> [URL: Data?] {
|
||||
// return fileIconData
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//
|
||||
//func serializeImage(_ image: NSImage?) -> Data? {
|
||||
// guard let image = image else { return nil }
|
||||
// guard let tiffData = image.tiffRepresentation else { return nil }
|
||||
// let bitmapImage = NSBitmapImageRep(data: tiffData)
|
||||
// return bitmapImage?.representation(using: .png, properties: [:])
|
||||
//}
|
||||
//
|
||||
//func deserializeImage(_ data: Data?) -> NSImage? {
|
||||
// guard let data = data else { return nil }
|
||||
// return NSImage(data: data)
|
||||
//}
|
||||
|
||||
|
||||
@@ -29,9 +29,6 @@ class AppState: ObservableObject
|
||||
@Published var progressBar: (String, Double) = ("Ready", 0.0)
|
||||
@Published var reload: Bool = false
|
||||
@Published var showProgress: Bool = false
|
||||
@Published var popCount: Int = 0
|
||||
@Published var instantProgress: Double = 0.0
|
||||
@Published var instantTotal: Double = 0.0
|
||||
@Published var finderExtensionEnabled: Bool = false
|
||||
|
||||
|
||||
@@ -48,12 +45,14 @@ class AppState: ObservableObject
|
||||
system: false,
|
||||
files: [],
|
||||
fileSize: [:],
|
||||
fileSizeLogical: [:],
|
||||
fileIcon: [:]
|
||||
)
|
||||
|
||||
self.zombieFile = ZombieFile(
|
||||
id: UUID(),
|
||||
fileSize: [:],
|
||||
fileSizeLogical: [:],
|
||||
fileIcon: [:]
|
||||
)
|
||||
updateExtensionStatus()
|
||||
@@ -91,14 +90,19 @@ struct AppInfo: Identifiable, Equatable, Hashable {
|
||||
let system: Bool
|
||||
var files: [URL]
|
||||
var fileSize: [URL:Int64]
|
||||
var fileSizeLogical: [URL:Int64]
|
||||
var fileIcon: [URL:NSImage?]
|
||||
var totalSize: Int64
|
||||
{
|
||||
return fileSize.values.reduce(0, +)
|
||||
}
|
||||
var totalSizeLogical: Int64
|
||||
{
|
||||
return fileSizeLogical.values.reduce(0, +)
|
||||
}
|
||||
|
||||
|
||||
static let empty = AppInfo(id: UUID(), path: URL(fileURLWithPath: ""), bundleIdentifier: "", appName: "", appVersion: "", appIcon: nil, webApp: false, wrapped: false, system: false, files: [], fileSize: [:], fileIcon: [:])
|
||||
static let empty = AppInfo(id: UUID(), path: URL(fileURLWithPath: ""), bundleIdentifier: "", appName: "", appVersion: "", appIcon: nil, webApp: false, wrapped: false, system: false, files: [], fileSize: [:], fileSizeLogical: [:], fileIcon: [:])
|
||||
|
||||
}
|
||||
|
||||
@@ -107,14 +111,19 @@ struct AppInfo: Identifiable, Equatable, Hashable {
|
||||
struct ZombieFile: Identifiable, Equatable, Hashable {
|
||||
let id: UUID
|
||||
var fileSize: [URL:Int64]
|
||||
var fileSizeLogical: [URL:Int64]
|
||||
var fileIcon: [URL:NSImage?]
|
||||
var totalSize: Int64
|
||||
{
|
||||
return fileSize.values.reduce(0, +)
|
||||
}
|
||||
var totalSizeLogical: Int64
|
||||
{
|
||||
return fileSizeLogical.values.reduce(0, +)
|
||||
}
|
||||
|
||||
|
||||
static let empty = ZombieFile(id: UUID(), fileSize: [:], fileIcon: [:])
|
||||
static let empty = ZombieFile(id: UUID(), fileSize: [:], fileSizeLogical: [:], fileIcon: [:])
|
||||
|
||||
}
|
||||
|
||||
@@ -201,5 +210,3 @@ enum DisplayMode: Int, CaseIterable {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
//
|
||||
// Conditions.swift
|
||||
// Pearcleaner
|
||||
//
|
||||
// Created by Alin Lupascu on 4/15/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
struct Condition: Decodable {
|
||||
var bundle_id: String
|
||||
var include: [String]
|
||||
var exclude: [String]
|
||||
var includeForce: [String]?
|
||||
}
|
||||
|
||||
struct SkipCondition {
|
||||
var skipPrefix: String
|
||||
var allowPrefixes: [String]
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Conditions for some apps that need to include/exclude certain files/folders when names are more complicated
|
||||
var conditions: [Condition] = [
|
||||
Condition(
|
||||
bundle_id: "comappledtxcode",
|
||||
include: ["comappledt", "xcode", "simulator"],
|
||||
exclude: ["comrobotsandpencilsxcodesapp", "comoneminutegamesxcodecleaner", "iohyperappxcodecleaner", "xcodesjson"],
|
||||
includeForce: ["\(home)/Library/Containers/com.apple.iphonesimulator.ShareExtension"]
|
||||
),
|
||||
Condition(
|
||||
bundle_id: "comrobotsandpencilsxcodesapp",
|
||||
include: [],
|
||||
exclude: ["comappledtxcode", "comoneminutegamesxcodecleaner", "iohyperappxcodecleaner"]
|
||||
),
|
||||
Condition(
|
||||
bundle_id: "iohyperappxcodecleaner",
|
||||
include: [],
|
||||
exclude: ["comrobotsandpencilsxcodesapp", "comoneminutegamesxcodecleaner", "comappledtxcode", "xcodesjson"]
|
||||
),
|
||||
Condition(
|
||||
bundle_id: "uszoomxos",
|
||||
include: ["zoom"],
|
||||
exclude: []
|
||||
),
|
||||
Condition(
|
||||
bundle_id: "combravebrowser",
|
||||
include: ["brave"],
|
||||
exclude: []
|
||||
),
|
||||
Condition(
|
||||
bundle_id: "comoktamobile",
|
||||
include: ["okta"],
|
||||
exclude: []
|
||||
),
|
||||
Condition(
|
||||
bundle_id: "comgooglechrome",
|
||||
include: ["google", "chrome"],
|
||||
exclude: ["iterm", "chromefeaturestate"]
|
||||
),
|
||||
Condition(
|
||||
bundle_id: "commicrosoftedgemac",
|
||||
include: ["microsoft"],
|
||||
exclude: ["vscode", "rdc", "appcenter", "office", "oneauth"]
|
||||
),
|
||||
Condition(
|
||||
bundle_id: "orgmozillafirefox",
|
||||
include: ["mozilla"],
|
||||
exclude: []
|
||||
),
|
||||
Condition(
|
||||
bundle_id: "comlogioptionsplus",
|
||||
include: ["logi"],
|
||||
exclude: ["login", "logic"],
|
||||
includeForce: []
|
||||
),
|
||||
Condition(
|
||||
bundle_id: "commicrosoftvscode",
|
||||
include: ["vscode"],
|
||||
exclude: [],
|
||||
includeForce: ["\(home)/Library/Application Support/Code"]
|
||||
),
|
||||
Condition(
|
||||
bundle_id: "comfacebookarchondeveloperid",
|
||||
include: ["archonloginhelper"],
|
||||
exclude: []
|
||||
),
|
||||
Condition(
|
||||
bundle_id: "euexelbanstats",
|
||||
include: [],
|
||||
exclude: ["video"]
|
||||
),
|
||||
Condition(
|
||||
bundle_id: "jetbrains",
|
||||
include: ["jetbrains", "jcef"],
|
||||
exclude: []
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
||||
// Skip com.apple files/folders since most are system originated, allow some for apps
|
||||
let skipConditions: [SkipCondition] = [
|
||||
SkipCondition(
|
||||
skipPrefix: "comapple",
|
||||
allowPrefixes: ["comappleconfigurator", "comappledt", "comappleiwork", "comapplesfsymbols", "comappletestflight"]
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
// Skip files/folders during reverse file search
|
||||
let skipReverse = ["apple", "temporary", "btserver", "proapps", "scripteditor", "ilife", "livefsd", "siritoday", "addressbook", "animoji", "appstore", "askpermission", "callhistory", "clouddocs", "diskimages", "dock", "facetime", "fileprovider", "instruments", "knowledge", "mobilesync", "syncservices", "homeenergyd", "icloud", "icdd", "networkserviceproxy", "familycircle", "geoservices", "installation", "passkit", "sharedimagecache", "desktop", "mbuseragent", "swiftpm", "baseband", "coresimulator", "photoslegacyupgrade", "photosupgrade", "siritts", "ipod", "globalpreferences", "apmanalytics", "apmexperiment", "avatarcache", "byhost", "contextstoreagent", "mobilemeaccounts", "intentbuilderc", "loginwindow", "momc", "replayd", "sharedfilelistd", "clang", "audiocomponent", "csexattrcryptoservice", "livetranscriptionagent", "sandboxhelper", "statuskitagent", "betaenrollmentd", "contentlinkingd", "diagnosticextensionsd", "gamed", "heard", "homed", "itunescloudd", "lldb", "mds", "mediaanalysisd", "metrickitd", "mobiletimerd", "proactived", "ptpcamerad", "studentd", "talagent", "watchlistd", "apptranslocation", "xcrun", "ds_store", "caches", "crashreporter", "trash", "pearcleaner", "amsdatamigratortool", "arfilecache", "assistant", "chromium", "cloudkit", "webkit", "databases", "diagnostic", "cache", "gamekit", "homebrew", "logi", "microsoft", "mozilla", "sync", "google", "sentinel", "hexnode", "sentry", "tvappservices"]
|
||||
|
||||
|
||||
// Function to load additional conditions from a GitHub JSON file
|
||||
func loadConditionsFromGitHub() {
|
||||
let url = URL(string: "https://api.github.com/repos/alienator88/Pearcleaner/contents/conditions.json")!
|
||||
var request = URLRequest(url: url)
|
||||
request.setValue("application/vnd.github.VERSION.raw", forHTTPHeaderField: "Accept")
|
||||
request.setValue("2022-11-28", forHTTPHeaderField: "X-GitHub-Api-Version")
|
||||
|
||||
URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
if let data = data {
|
||||
do {
|
||||
// Assuming the JSON structure directly maps to an array of Condition
|
||||
let jsonObject = try JSONSerialization.jsonObject(with: data, options: [])
|
||||
if let conditionArray = jsonObject as? [[String: Any]] {
|
||||
let jsonData = try JSONSerialization.data(withJSONObject: conditionArray, options: [])
|
||||
let additionalConditions = try JSONDecoder().decode([Condition].self, from: jsonData)
|
||||
|
||||
if !additionalConditions.isEmpty {
|
||||
DispatchQueue.main.async {
|
||||
conditions += additionalConditions
|
||||
}
|
||||
}
|
||||
} else {
|
||||
printOS("The data format is incorrect or empty for GitHub conditions processing.")
|
||||
}
|
||||
} catch {
|
||||
printOS("Failed to decode conditions JSON: \(error.localizedDescription)")
|
||||
}
|
||||
} else {
|
||||
printOS("Failed to fetch conditions data: \(error?.localizedDescription ?? "Unknown error")")
|
||||
}
|
||||
}.resume()
|
||||
}
|
||||
|
||||
@@ -19,8 +19,6 @@ class Locations: ObservableObject {
|
||||
|
||||
var apps: Category
|
||||
var reverse: Category
|
||||
// var widgets: Category
|
||||
// var plugins: Category
|
||||
|
||||
init() {
|
||||
let (cacheDir, tempDir) = darwinCT()
|
||||
@@ -103,35 +101,5 @@ class Locations: ObservableObject {
|
||||
cacheDir,
|
||||
tempDir
|
||||
])
|
||||
|
||||
// self.widgets = Category(name: "Widgets", paths: [
|
||||
// // User
|
||||
// "~/Library/Widgets",
|
||||
// // System
|
||||
// "/Library/Widgets"
|
||||
// ])
|
||||
//
|
||||
// self.plugins = Category(name: "Plugins", paths: [
|
||||
// // User
|
||||
// "~/Library/Contextual Menu Items",
|
||||
// "~/Library/InputManagers",
|
||||
// "~/Library/Internet Plug-Ins",
|
||||
// "~/Library/Mail/Bundles",
|
||||
// "~/Library/PreferencePanes",
|
||||
// "~/Library/QuickLook",
|
||||
// "~/Library/QuickTime",
|
||||
// "~/Library/Screen Savers",
|
||||
// "~/Library/Spotlight",
|
||||
// // System
|
||||
// "/Library/Contextual Menu Items",
|
||||
// "/Library/InputManagers",
|
||||
// "/Library/Internet Plug-Ins",
|
||||
// "/Library/Mail/Bundles",
|
||||
// "/Library/PreferencePanes",
|
||||
// "/Library/QuickLook",
|
||||
// "/Library/QuickTime",
|
||||
// "/Library/Screen Savers",
|
||||
// "/Library/Spotlight",
|
||||
// ])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import SwiftUI
|
||||
|
||||
// Get all apps from /Applications and ~/Applications
|
||||
func getSortedApps(paths: [String], appState: AppState) -> [AppInfo] {
|
||||
@AppStorage("settings.general.instant") var instantSearch: Bool = true
|
||||
let fileManager = FileManager.default
|
||||
var apps: [URL] = []
|
||||
|
||||
@@ -42,11 +41,6 @@ func getSortedApps(paths: [String], appState: AppState) -> [AppInfo] {
|
||||
.compactMap { AppInfoFetcher.getAppInfo(atPath: $0) }
|
||||
.sorted { $0.appName.replacingOccurrences(of: ".", with: "").lowercased() < $1.appName.replacingOccurrences(of: ".", with: "").lowercased() }
|
||||
|
||||
if instantSearch {
|
||||
updateOnMain {
|
||||
appState.instantTotal = Double(sortedApps.count)
|
||||
}
|
||||
}
|
||||
|
||||
return sortedApps
|
||||
}
|
||||
@@ -116,15 +110,14 @@ func listAppSupportDirectories() -> [String] {
|
||||
|
||||
|
||||
// Load app paths on launch
|
||||
func loadAllPaths(allApps: [AppInfo], appState: AppState, locations: Locations, reverseAddon: Bool = false, completion: @escaping () -> Void = {}) {
|
||||
func reversePreloader(allApps: [AppInfo], appState: AppState, locations: Locations, reverseAddon: Bool = false, completion: @escaping () -> Void = {}) {
|
||||
let dispatchGroup = DispatchGroup()
|
||||
appState.appInfoStore.removeAll()
|
||||
|
||||
for app in allApps {
|
||||
dispatchGroup.enter()
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
let pathFinder = AppPathFinder(appInfo: app, appState: appState, locations: locations, backgroundRun: true, reverseAddon: reverseAddon)
|
||||
pathFinder.findPaths()
|
||||
AppPathFinder(appInfo: app, appState: appState, locations: locations, backgroundRun: true, reverseAddon: reverseAddon).findPaths()
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
}
|
||||
@@ -134,28 +127,17 @@ func loadAllPaths(allApps: [AppInfo], appState: AppState, locations: Locations,
|
||||
func checkAllAppsProcessed(retryCount: Int = 0, maxRetry: Int = 120) {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
if appState.appInfoStore.count == allApps.count {
|
||||
if reverseAddon {
|
||||
let reverse = ReversePathsSearcher(appState: appState, locations: locations)
|
||||
reverse.reversePathsSearch() {
|
||||
ReversePathsSearcher(appState: appState, locations: locations).reversePathsSearch() {
|
||||
updateOnMain {
|
||||
appState.showProgress = false
|
||||
}
|
||||
}
|
||||
}
|
||||
// Reset progress values to 0
|
||||
updateOnMain {
|
||||
appState.instantProgress = 0
|
||||
appState.instantTotal = 0
|
||||
appState.showProgress = false
|
||||
}
|
||||
completion()
|
||||
} else if retryCount < maxRetry {
|
||||
checkAllAppsProcessed(retryCount: retryCount + 1, maxRetry: maxRetry)
|
||||
} else {
|
||||
// Reset progress values to 0
|
||||
updateOnMain {
|
||||
appState.instantProgress = 0
|
||||
appState.instantTotal = 0
|
||||
appState.showProgress = false
|
||||
}
|
||||
printOS("loadAllPaths - Retry limit reached. Not all paths were loaded.")
|
||||
@@ -196,13 +178,13 @@ func showAppInFiles(appInfo: AppInfo, appState: AppState, locations: Locations,
|
||||
appState.showProgress = true
|
||||
|
||||
// Initialize the path finder and execute its search.
|
||||
let pathFinder = AppPathFinder(appInfo: appInfo, appState: appState, locations: locations) {
|
||||
AppPathFinder(appInfo: appInfo, appState: appState, locations: locations) {
|
||||
updateOnMain {
|
||||
// Update the progress indicator on the main thread once the search completes.
|
||||
appState.showProgress = false
|
||||
}
|
||||
}
|
||||
pathFinder.findPaths()
|
||||
}.findPaths()
|
||||
|
||||
appState.appInfo = appInfo
|
||||
|
||||
// Animate the view change and popover display.
|
||||
|
||||
@@ -14,16 +14,17 @@ class ReversePathsSearcher {
|
||||
private let fileManager = FileManager.default
|
||||
private var collection: [URL] = []
|
||||
private var fileSize: [URL: Int64] = [:]
|
||||
private var fileSizeLogical: [URL: Int64] = [:]
|
||||
private var fileIcon: [URL: NSImage?] = [:]
|
||||
private var skipped: Set<String>
|
||||
private let dispatchGroup = DispatchGroup()
|
||||
|
||||
init(appState: AppState, locations: Locations) {
|
||||
self.appState = appState
|
||||
self.locations = locations
|
||||
self.skipped = ["apple", "temporary", "btserver", "proapps", "scripteditor", "ilife", "livefsd", "siritoday", "addressbook", "animoji", "appstore", "askpermission", "callhistory", "clouddocs", "diskimages", "dock", "facetime", "fileprovider", "instruments", "knowledge", "mobilesync", "syncservices", "homeenergyd", "icloud", "icdd", "networkserviceproxy", "familycircle", "geoservices", "installation", "passkit", "sharedimagecache", "desktop", "mbuseragent", "swiftpm", "baseband", "coresimulator", "photoslegacyupgrade", "photosupgrade", "siritts", "ipod", "globalpreferences", "apmanalytics", "apmexperiment", "avatarcache", "byhost", "contextstoreagent", "mobilemeaccounts", "intentbuilderc", "loginwindow", "momc", "replayd", "sharedfilelistd", "clang", "audiocomponent", "csexattrcryptoservice", "livetranscriptionagent", "sandboxhelper", "statuskitagent", "betaenrollmentd", "contentlinkingd", "diagnosticextensionsd", "gamed", "heard", "homed", "itunescloudd", "lldb", "mds", "mediaanalysisd", "metrickitd", "mobiletimerd", "proactived", "ptpcamerad", "studentd", "talagent", "watchlistd", "apptranslocation", "xcrun", "ds_store", "caches", "crashreporter", "trash", "pearcleaner", "amsdatamigratortool", "arfilecache", "assistant", "chromium", "cloudkit", "webkit", "databases", "diagnostic", "cache", "gamekit", "homebrew", "logi", "microsoft", "mozilla", "sync", "google", "sentinel", "hexnode", "sentry", "tvappservices"]
|
||||
}
|
||||
|
||||
|
||||
|
||||
func reversePathsSearch(completion: @escaping () -> Void = {}) {
|
||||
Task(priority: .high) {
|
||||
self.processLocations()
|
||||
@@ -34,17 +35,19 @@ class ReversePathsSearcher {
|
||||
}
|
||||
|
||||
private func processLocations() {
|
||||
let allPaths = appState.appInfoStore.flatMap { $0.files.map { $0.path.pearFormat() } }
|
||||
let allPaths = appState.appInfoStore.flatMap { $0.fileSize.keys.map { $0.path.pearFormat() } }
|
||||
let allNames = appState.appInfoStore.map { $0.appName.pearFormat() }
|
||||
|
||||
for location in locations.reverse.paths where fileManager.fileExists(atPath: location) {
|
||||
dispatchGroup.enter()
|
||||
processLocation(location, allPaths: allPaths, allNames: allNames)
|
||||
dispatchGroup.leave()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private func processLocation(_ location: String, allPaths: [String], allNames: [String]) {
|
||||
|
||||
do {
|
||||
let contents = try fileManager.contentsOfDirectory(atPath: location)
|
||||
contents.forEach { itemName in
|
||||
@@ -61,8 +64,7 @@ class ReversePathsSearcher {
|
||||
let itemPath = itemURL.path.pearFormat()
|
||||
let itemLastPathComponent = itemURL.lastPathComponent.pearFormat()
|
||||
|
||||
guard !skipped.contains(where: { formattedItemName.contains($0) }),
|
||||
// !allPaths.contains(itemPath),
|
||||
guard !skipReverse.contains(where: { formattedItemName.contains($0) }),
|
||||
!allPaths.contains(where: { $0 == itemPath || $0.hasSuffix("/\(itemLastPathComponent)") }),
|
||||
!allNames.contains(formattedItemName),
|
||||
isSupportedFileType(at: itemURL.path) else {
|
||||
@@ -74,7 +76,9 @@ class ReversePathsSearcher {
|
||||
|
||||
private func calculateFileDetails() {
|
||||
collection.forEach { path in
|
||||
fileSize[path] = totalSizeOnDisk(for: path)
|
||||
let size = totalSizeOnDisk(for: path)
|
||||
fileSize[path] = size.real
|
||||
fileSizeLogical[path] = size.logical
|
||||
fileIcon[path] = getIconForFileOrFolderNS(atPath: path)
|
||||
}
|
||||
}
|
||||
@@ -83,6 +87,7 @@ class ReversePathsSearcher {
|
||||
dispatchGroup.notify(queue: .main) {
|
||||
var updatedZombieFile = ZombieFile.empty
|
||||
updatedZombieFile.fileSize = self.fileSize
|
||||
updatedZombieFile.fileSizeLogical = self.fileSizeLogical
|
||||
updatedZombieFile.fileIcon = self.fileIcon
|
||||
self.appState.zombieFile = updatedZombieFile
|
||||
self.appState.showProgress = false
|
||||
|
||||
@@ -441,56 +441,11 @@ struct SimpleSearchStyle: TextFieldStyle {
|
||||
}
|
||||
|
||||
}
|
||||
.disabled(appState.instantProgress != 0 && appState.instantProgress != appState.instantTotal)
|
||||
.padding(6)
|
||||
.overlay(
|
||||
Group {
|
||||
GeometryReader { geometry in
|
||||
ZStack(alignment: .leading) {
|
||||
|
||||
|
||||
if appState.instantProgress != 0 && appState.instantProgress != appState.instantTotal {
|
||||
let totalWidth = geometry.size.width
|
||||
let progressFraction = CGFloat(appState.instantProgress / appState.instantTotal)
|
||||
let progressWidth = totalWidth * progressFraction
|
||||
|
||||
RoundedRectangle(cornerRadius: 6)
|
||||
.fill(Color.accentColor)
|
||||
.padding(4)
|
||||
.frame(width: progressWidth)
|
||||
.animation(.linear, value: progressWidth)
|
||||
.allowsHitTesting(false)
|
||||
|
||||
}
|
||||
|
||||
RoundedRectangle(cornerRadius: 6)
|
||||
.strokeBorder(Color("mode").opacity(0.2), lineWidth: 1)
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
}
|
||||
// ZStack {
|
||||
// RoundedRectangle(cornerRadius: 6)
|
||||
// .strokeBorder(Color("mode").opacity(0.2), lineWidth: 1)
|
||||
// .allowsHitTesting(false)
|
||||
//
|
||||
// if appState.instantTotal > 0 {
|
||||
// let progressWidth = CGFloat(appState.instantProgress / appState.instantTotal)
|
||||
// * NSScreen.main.bounds.width // Assuming full width for simplicity; adjust as needed
|
||||
// RoundedRectangle(cornerRadius: 6)
|
||||
// .fill(Color.accentColor.opacity(0.5)) // Adjust color and opacity as needed
|
||||
// .frame(width: progressWidth)
|
||||
// .animation(.linear, value: progressWidth)
|
||||
// .allowsHitTesting(false)
|
||||
// }
|
||||
//// if appState.instantProgress != 0 && appState.instantProgress != appState.instantTotal {
|
||||
//// ProgressView("", value: appState.instantProgress, total: appState.instantTotal)
|
||||
//// .progressViewStyle(.linear)
|
||||
//// .padding()
|
||||
//// }
|
||||
// }
|
||||
|
||||
|
||||
}
|
||||
)
|
||||
.onHover { hovering in
|
||||
withAnimation(Animation.easeInOut(duration: 0.15)) {
|
||||
|
||||
@@ -28,10 +28,12 @@ class ThemeSettings: ObservableObject {
|
||||
if let components = UserDefaults.standard.object(forKey: colorKey) as? [CGFloat], components.count >= 4 {
|
||||
themeColor = Color(.sRGB, red: components[0], green: components[1], blue: components[2], opacity: components[3])
|
||||
} else {
|
||||
|
||||
themeColor = Color.clear
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func saveThemeColor() {
|
||||
let nsColor = NSColor(themeColor)
|
||||
if let components = nsColor.cgColor.components {
|
||||
@@ -43,17 +45,61 @@ class ThemeSettings: ObservableObject {
|
||||
themeColor = dark ? Color(.sRGB, red: 0.149, green: 0.149, blue: 0.149, opacity: 1) : Color(.sRGB, red: 1.0, green: 1.0, blue: 1.0, opacity: 1)
|
||||
saveThemeColor()
|
||||
}
|
||||
|
||||
func setPreset(preset: String, colorScheme: DisplayMode) {
|
||||
themeColor = getColorForPreset(preset: preset, colorScheme: colorScheme)
|
||||
saveThemeColor()
|
||||
}
|
||||
|
||||
func getColorForPreset(preset: String, colorScheme: DisplayMode) -> Color {
|
||||
let slate = colorScheme.colorScheme == .light ? Color(.sRGB, red: 0.499549, green: 0.545169, blue: 0.682028, opacity: 1) : Color(.sRGB, red: 0.188143, green: 0.208556, blue: 0.262679, opacity: 1)
|
||||
let solarized = colorScheme.colorScheme == .light ? Color(.sRGB, red: 0.554372, green: 0.6557, blue: 0.734336, opacity: 1) : Color(.sRGB, red: 0.117257, green: 0.22506, blue: 0.249171, opacity: 1)
|
||||
let dracula = colorScheme.colorScheme == .light ? Color(.sRGB, red: 0.567094, green: 0.562125, blue: 0.81285, opacity: 1) : Color(.sRGB, red: 0.268614, green: 0.264737, blue: 0.383503, opacity: 1)
|
||||
|
||||
switch preset {
|
||||
case "slate":
|
||||
return slate
|
||||
case "solarized":
|
||||
return solarized
|
||||
case "dracula":
|
||||
return dracula
|
||||
default:
|
||||
return themeColor
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Color {
|
||||
/// Darkens the color by a percentage.
|
||||
func darker(by percentage: CGFloat = 30.0) -> Color {
|
||||
var hsb = (hue: CGFloat(0), saturation: CGFloat(0), brightness: CGFloat(0), alpha: CGFloat(0))
|
||||
NSColor(self).getHue(&hsb.hue, saturation: &hsb.saturation, brightness: &hsb.brightness, alpha: &hsb.alpha)
|
||||
return Color(hue: hsb.hue, saturation: hsb.saturation, brightness: max(hsb.brightness - percentage/100, 0), opacity: hsb.alpha)
|
||||
}
|
||||
|
||||
/// Lightens the color by a percentage.
|
||||
func lighter(by percentage: CGFloat = 30.0) -> Color {
|
||||
var hsb = (hue: CGFloat(0), saturation: CGFloat(0), brightness: CGFloat(0), alpha: CGFloat(0))
|
||||
NSColor(self).getHue(&hsb.hue, saturation: &hsb.saturation, brightness: &hsb.brightness, alpha: &hsb.alpha)
|
||||
return Color(hue: hsb.hue, saturation: hsb.saturation, brightness: min(hsb.brightness + percentage / 100, 1.0), opacity: hsb.alpha)
|
||||
}
|
||||
}
|
||||
|
||||
extension UserDefaults {
|
||||
func color(forKey key: String) -> Color? {
|
||||
guard let components = array(forKey: key) as? [CGFloat], components.count == 4 else {
|
||||
return nil
|
||||
}
|
||||
return Color(.sRGB, red: components[0], green: components[1], blue: components[2], opacity: components[3])
|
||||
}
|
||||
|
||||
func setColor(_ color: Color, forKey key: String) {
|
||||
let nsColor = NSColor(color)
|
||||
if let components = nsColor.cgColor.components, components.count == 4 {
|
||||
set(components, forKey: key)
|
||||
} else {
|
||||
removeObject(forKey: key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,20 +174,13 @@ func isCurrentUserAdmin(completion: @escaping (Bool) -> Void) {
|
||||
}
|
||||
|
||||
// Check if appearance is dark mode
|
||||
func isDarkModeEnabled() -> Bool {
|
||||
let process = Process()
|
||||
process.executableURL = URL(fileURLWithPath: "/bin/zsh")
|
||||
process.arguments = ["-c", "defaults read -g AppleInterfaceStyle"]
|
||||
let pipe = Pipe()
|
||||
process.standardOutput = pipe
|
||||
process.standardError = pipe
|
||||
try? process.run()
|
||||
let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
||||
if !data.isEmpty{
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
func isDarkMode() -> Bool {
|
||||
return NSApp.effectiveAppearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua
|
||||
}
|
||||
|
||||
// Set app color mode
|
||||
func setAppearance(mode: NSAppearance.Name) {
|
||||
NSApp.appearance = NSAppearance(named: mode)
|
||||
}
|
||||
|
||||
// Check if Pearcleaner has any windows open
|
||||
@@ -216,6 +209,13 @@ func findAndShowWindows(named titles: [String]) {
|
||||
}
|
||||
}
|
||||
|
||||
// Copy to clipboard
|
||||
func copyToClipboard(text: String) {
|
||||
let pasteboard = NSPasteboard.general
|
||||
pasteboard.clearContents()
|
||||
pasteboard.setString(text, forType: .string)
|
||||
}
|
||||
|
||||
|
||||
// Check if appearance is dark mode
|
||||
//func getCasks() -> [String] {
|
||||
@@ -458,86 +458,73 @@ func printOS(_ items: Any..., separator: String = " ", terminator: String = "\n"
|
||||
}
|
||||
|
||||
|
||||
|
||||
//func totalSizeOnDisk2(for paths: [URL]) -> Int64 {
|
||||
// var totalSize = 0
|
||||
//
|
||||
// let process = Process()
|
||||
// process.launchPath = "/usr/bin/du"
|
||||
// process.arguments = ["-sk"] + paths.map { (url: URL) -> String in
|
||||
// return url.path
|
||||
// }
|
||||
// let pipe = Pipe()
|
||||
// process.standardOutput = pipe
|
||||
// process.standardError = pipe//FileHandle.nullDevice
|
||||
//
|
||||
// try? process.run()
|
||||
// process.waitUntilExit()
|
||||
//
|
||||
// let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
||||
// if let output = String(data: data, encoding: .utf8) {
|
||||
// let lines = output.components(separatedBy: .newlines)
|
||||
// for line in lines {
|
||||
// let components = line.components(separatedBy: "\t")
|
||||
// if let sizeString = components.first, let size = Int(sizeString) {
|
||||
// totalSize += size
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return Int64(totalSize) * 1024 // Convert the size from kilobytes to bytes
|
||||
//}
|
||||
|
||||
|
||||
// Get total size of folders and files using DU cli command
|
||||
func totalSizeOnDisk(for paths: [URL]) -> Int64 {
|
||||
// Get size of files
|
||||
func totalSizeOnDisk(for paths: [URL]) -> (real: Int64, logical: Int64) {
|
||||
let fileManager = FileManager.default
|
||||
var totalSize: Int64 = 0
|
||||
var totalAllocatedSize: Int64 = 0
|
||||
var totalFileSize: Int64 = 0
|
||||
|
||||
for url in paths {
|
||||
var isDirectory: ObjCBool = false
|
||||
if fileManager.fileExists(atPath: url.path, isDirectory: &isDirectory) {
|
||||
let keys: [URLResourceKey] = [.totalFileAllocatedSizeKey, .fileSizeKey]
|
||||
if isDirectory.boolValue {
|
||||
// It's a directory, recurse into it
|
||||
if let enumerator = fileManager.enumerator(at: url, includingPropertiesForKeys: [.totalFileAllocatedSizeKey], errorHandler: nil) {
|
||||
if let enumerator = fileManager.enumerator(at: url, includingPropertiesForKeys: keys, errorHandler: nil) {
|
||||
for case let fileURL as URL in enumerator {
|
||||
do {
|
||||
let fileAttributes = try fileURL.resourceValues(forKeys: [.totalFileAllocatedSizeKey])
|
||||
if let fileSize = fileAttributes.totalFileAllocatedSize {
|
||||
totalSize += Int64(fileSize)
|
||||
let fileAttributes = try fileURL.resourceValues(forKeys: Set(keys))
|
||||
if let allocatedSize = fileAttributes.totalFileAllocatedSize {
|
||||
totalAllocatedSize += Int64(allocatedSize)
|
||||
}
|
||||
if let fileSize = fileAttributes.fileSize {
|
||||
totalFileSize += Int64(fileSize)
|
||||
}
|
||||
} catch {
|
||||
printOS("Error getting file size for \(fileURL): \(error)")
|
||||
print("Error getting file attributes for \(fileURL): \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// It's a file
|
||||
do {
|
||||
let fileAttributes = try url.resourceValues(forKeys: [.totalFileAllocatedSizeKey])
|
||||
if let fileSize = fileAttributes.totalFileAllocatedSize {
|
||||
totalSize += Int64(fileSize)
|
||||
let fileAttributes = try url.resourceValues(forKeys: Set(keys))
|
||||
if let allocatedSize = fileAttributes.totalFileAllocatedSize {
|
||||
totalAllocatedSize += Int64(allocatedSize)
|
||||
}
|
||||
if let fileSize = fileAttributes.fileSize {
|
||||
totalFileSize += Int64(fileSize)
|
||||
}
|
||||
} catch {
|
||||
printOS("Error getting file size for \(url): \(error)")
|
||||
print("Error getting file attributes for \(url): \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return totalSize
|
||||
return (real: totalAllocatedSize, logical: totalFileSize)
|
||||
}
|
||||
|
||||
func totalSizeOnDisk(for path: URL) -> Int64 {
|
||||
|
||||
|
||||
func totalSizeOnDisk(for path: URL) -> (real: Int64, logical: Int64) {
|
||||
return totalSizeOnDisk(for: [path])
|
||||
}
|
||||
|
||||
// ByteFormatter
|
||||
func formatByte(size: Int64) -> String {
|
||||
func formatByte(size: Int64) -> (human: String, byte: String) {
|
||||
let byteCountFormatter = ByteCountFormatter()
|
||||
byteCountFormatter.countStyle = .file
|
||||
byteCountFormatter.allowedUnits = [.useAll]
|
||||
return byteCountFormatter.string(fromByteCount: size)
|
||||
let human = byteCountFormatter.string(fromByteCount: size)
|
||||
|
||||
let numberformatter = NumberFormatter()
|
||||
numberformatter.numberStyle = .decimal
|
||||
let formattedNumber = numberformatter.string(from: NSNumber(value: size)) ?? "\(size)"
|
||||
let byte = "\(formattedNumber)"
|
||||
|
||||
return (human: human, byte: byte)
|
||||
|
||||
}
|
||||
|
||||
// Only process supported files
|
||||
@@ -603,12 +590,9 @@ func uninstallPearcleaner(appState: AppState, locations: Locations) {
|
||||
|
||||
// Get app info for Pearcleaner
|
||||
let appInfo = AppInfoFetcher.getAppInfo(atPath: Bundle.main.bundleURL)
|
||||
// appState.appInfo = appInfo!
|
||||
|
||||
// Find application files for Pearcleaner
|
||||
let pathFinder = AppPathFinder(appInfo: appInfo!, appState: appState, locations: locations)
|
||||
pathFinder.findPaths()
|
||||
// findPathsForApp(appInfo: appInfo!,appState: appState, locations: locations)
|
||||
AppPathFinder(appInfo: appInfo!, appState: appState, locations: locations).findPaths()
|
||||
|
||||
// Kill Pearcleaner and tell Finder to trash the files
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
|
||||
|
||||
@@ -23,7 +23,6 @@ struct PearcleanerApp: App {
|
||||
@AppStorage("displayMode") var displayMode: DisplayMode = .system
|
||||
@AppStorage("settings.general.mini") private var mini: Bool = false
|
||||
@AppStorage("settings.general.miniview") private var miniView: Bool = true
|
||||
@AppStorage("settings.general.instant") private var instantSearch: Bool = true
|
||||
@AppStorage("settings.general.features") private var features: String = ""
|
||||
@AppStorage("settings.general.brew") private var brew: Bool = false
|
||||
@AppStorage("settings.menubar.enabled") private var menubarEnabled: Bool = false
|
||||
@@ -102,13 +101,7 @@ struct PearcleanerApp: App {
|
||||
let sortedApps = getSortedApps(paths: fsm.folderPaths, appState: appState)
|
||||
appState.sortedApps = sortedApps
|
||||
|
||||
|
||||
// Find all app paths/information on load if instantSearch is enabled
|
||||
if instantSearch {
|
||||
loadAllPaths(allApps: sortedApps, appState: appState, locations: locations)
|
||||
}
|
||||
|
||||
|
||||
// Enable menubar item
|
||||
if menubarEnabled {
|
||||
MenuBarExtraManager.shared.addMenuBarExtra(withView: {
|
||||
MenuBarMiniAppView(search: $search, showPopover: $showPopover)
|
||||
@@ -140,6 +133,9 @@ struct PearcleanerApp: App {
|
||||
hasLaunched = true
|
||||
}
|
||||
|
||||
// Load extra conditions from GitHub
|
||||
loadConditionsFromGitHub()
|
||||
|
||||
|
||||
// TIMERS ////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -183,6 +179,7 @@ struct PearcleanerApp: App {
|
||||
|
||||
|
||||
class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
|
||||
var observer: NSObjectProtocol?
|
||||
|
||||
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||
let menubarEnabled = UserDefaults.standard.bool(forKey: "settings.menubar.enabled")
|
||||
@@ -191,11 +188,49 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
|
||||
|
||||
func applicationDidFinishLaunching(_ notification: Notification) {
|
||||
let menubarEnabled = UserDefaults.standard.bool(forKey: "settings.menubar.enabled")
|
||||
|
||||
if UserDefaults.standard.object(forKey: "themeColor") == nil {
|
||||
self.appearanceChanged()
|
||||
}
|
||||
|
||||
if menubarEnabled {
|
||||
findAndHideWindows(named: ["Pearcleaner"])
|
||||
NSApplication.shared.setActivationPolicy(.accessory)
|
||||
}
|
||||
|
||||
// Start observing the appearance change
|
||||
observer = DistributedNotificationCenter.default().addObserver(forName: NSNotification.Name(rawValue: "AppleInterfaceThemeChangedNotification"), object: nil, queue: OperationQueue.main) { [weak self] _ in
|
||||
let themeMode = UserDefaults.standard.string(forKey: "settings.general.selectedTheme")
|
||||
if themeMode == "Auto" {
|
||||
self?.appearanceChanged()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
func appearanceChanged() {
|
||||
let dm = UserDefaults.standard.integer(forKey: "displayMode")
|
||||
var displayMode = DisplayMode(rawValue: dm)
|
||||
let dark = isDarkMode()
|
||||
if dark {
|
||||
displayMode?.colorScheme = .dark
|
||||
} else {
|
||||
displayMode?.colorScheme = .light
|
||||
}
|
||||
|
||||
UserDefaults.standard.set(displayMode?.rawValue, forKey: "displayMode")
|
||||
|
||||
ThemeSettings.shared.themeColor = isDarkMode() ? Color(.sRGB, red: 0.149, green: 0.149, blue: 0.149, opacity: 1) : Color(.sRGB, red: 1.0, green: 1.0, blue: 1.0, opacity: 1)
|
||||
ThemeSettings.shared.saveThemeColor()
|
||||
|
||||
}
|
||||
|
||||
func applicationWillTerminate(_ notification: Notification) {
|
||||
// Stop observing the appearance change
|
||||
if let observer = observer {
|
||||
DistributedNotificationCenter.default().removeObserver(observer)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -220,4 +255,3 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -23,16 +23,15 @@ struct GeneralSettingsTab: View {
|
||||
@AppStorage("displayMode") var displayMode: DisplayMode = .system
|
||||
@AppStorage("settings.sentinel.enable") private var sentinel: Bool = false
|
||||
@AppStorage("settings.general.brew") private var brew: Bool = false
|
||||
@AppStorage("settings.general.instant") private var instantSearch: Bool = true
|
||||
@AppStorage("settings.general.selectedTheme") var selectedTheme: String = "Auto"
|
||||
@AppStorage("settings.general.selectedSort") var selectedSortAlpha: Bool = true
|
||||
// @State private var finderExtensionEnabled: Bool = false
|
||||
@AppStorage("settings.general.sizeType") var sizeType: String = "Real"
|
||||
@State private var diskStatus: Bool = false
|
||||
@State private var accessStatus: Bool = false
|
||||
private let themes = ["Auto", "Dark", "Light"]
|
||||
@Binding var showPopover: Bool
|
||||
@Binding var search: String
|
||||
|
||||
@State var selectedIndex: Int?
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
VStack {
|
||||
@@ -44,30 +43,6 @@ struct GeneralSettingsTab: View {
|
||||
.padding(.leading)
|
||||
|
||||
|
||||
HStack(spacing: 0) {
|
||||
Image(systemName: instantSearch ? "bolt" : "bolt.slash")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 20, height: 20)
|
||||
.padding(.trailing)
|
||||
.foregroundStyle(Color("mode").opacity(0.5))
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
Text("\(instantSearch ? "Instant search is enabled" : "Instant search is disabled")")
|
||||
.font(.callout)
|
||||
.foregroundStyle(Color("mode").opacity(0.5))
|
||||
}
|
||||
|
||||
InfoButton(text: "When instant search is enabled, all application files are gathered and cached for later use on startup instead of on each app click. There might be a slight delay of a few seconds when launching Pearcleaner depending on the amount of apps you have installed.", color: nil, label: "")
|
||||
|
||||
Spacer()
|
||||
Toggle(isOn: $instantSearch, label: {
|
||||
})
|
||||
.toggleStyle(.switch)
|
||||
}
|
||||
.padding(5)
|
||||
.padding(.leading)
|
||||
|
||||
|
||||
HStack(spacing: 0) {
|
||||
Image(systemName: brew ? "mug.fill" : "mug")
|
||||
.resizable()
|
||||
@@ -104,14 +79,88 @@ struct GeneralSettingsTab: View {
|
||||
.font(.callout)
|
||||
.foregroundStyle(Color("mode").opacity(0.5))
|
||||
}
|
||||
InfoButton(text: "When searching for app files or leftover files, the list will be sorted based on this choice", color: nil, label: "")
|
||||
InfoButton(text: "When searching for app files or leftover files, the list will be sorted either alphabetically or by size(large to small)", color: nil, label: "")
|
||||
Spacer()
|
||||
Picker("", selection: $selectedSortAlpha) {
|
||||
Text("Alphabetical").tag(true)
|
||||
Text("Size").tag(false)
|
||||
SegmentedPicker(
|
||||
["Alpha", "Size"],
|
||||
selectedIndex: Binding(
|
||||
get: { selectedSortAlpha ? 0 : 1 },
|
||||
set: { newIndex in
|
||||
withAnimation(.easeInOut(duration: 0.3)) {
|
||||
selectedSortAlpha = (newIndex == 0)
|
||||
}
|
||||
.pickerStyle(SegmentedPickerStyle())
|
||||
.frame(width: 200)
|
||||
}),
|
||||
selectionAlignment: .bottom,
|
||||
content: { item, isSelected in
|
||||
Text(item)
|
||||
.font(.callout)
|
||||
.foregroundColor(isSelected ? Color("mode") : Color("mode").opacity(0.5))
|
||||
.padding(.horizontal)
|
||||
.padding(.bottom, 5)
|
||||
.frame(width: 75)
|
||||
|
||||
},
|
||||
selection: {
|
||||
VStack(spacing: 0) {
|
||||
Spacer()
|
||||
Color("pear").frame(height: 1)
|
||||
}
|
||||
})
|
||||
}
|
||||
.padding(5)
|
||||
.padding(.leading)
|
||||
|
||||
|
||||
HStack(spacing: 0) {
|
||||
Image(systemName: "plus.forwardslash.minus")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 20, height: 20)
|
||||
.padding(.trailing)
|
||||
.foregroundStyle(Color("mode").opacity(0.5))
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
Text("File size display")
|
||||
.font(.callout)
|
||||
.foregroundStyle(Color("mode").opacity(0.5))
|
||||
}
|
||||
InfoButton(text: "Real size type will show how much actual allocated space the file has on disk. Logical type shows the binary size. The filesystem can compress and deduplicate sectors on disk, so real size is sometimes smaller(or bigger) than logical size. Finder size is similar to if you right click > Get Info on a file in Finder, which will show both the logical and real sizes together.", color: nil, label: "")
|
||||
Spacer()
|
||||
SegmentedPicker(
|
||||
["Real", "Logical", "Finder"],
|
||||
selectedIndex: Binding(
|
||||
get: {
|
||||
switch sizeType {
|
||||
case "Real": return 0
|
||||
case "Logical": return 1
|
||||
case "Finder": return 2
|
||||
default: return 0
|
||||
}
|
||||
},
|
||||
set: { newIndex in
|
||||
withAnimation(.easeInOut(duration: 0.3)) {
|
||||
switch newIndex {
|
||||
case 0: sizeType = "Real"
|
||||
case 1: sizeType = "Logical"
|
||||
case 2: sizeType = "Finder"
|
||||
default: sizeType = "Real"
|
||||
}
|
||||
}
|
||||
}),
|
||||
selectionAlignment: .bottom,
|
||||
content: { item, isSelected in
|
||||
Text(item)
|
||||
.font(.callout)
|
||||
.foregroundColor(isSelected ? Color("mode") : Color("mode").opacity(0.5) )
|
||||
.padding(.horizontal)
|
||||
.padding(.bottom, 5)
|
||||
.frame(width: 75)
|
||||
},
|
||||
selection: {
|
||||
VStack(spacing: 0) {
|
||||
Spacer()
|
||||
Color("pear").frame(height: 1)
|
||||
}
|
||||
})
|
||||
}
|
||||
.padding(5)
|
||||
.padding(.leading)
|
||||
@@ -275,7 +324,7 @@ struct GeneralSettingsTab: View {
|
||||
|
||||
}
|
||||
.padding(20)
|
||||
.frame(width: 500, height: 560)
|
||||
.frame(width: 500, height: 600)
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -31,18 +31,9 @@ struct InterfaceSettingsTab: View {
|
||||
@Binding var showPopover: Bool
|
||||
@Binding var search: String
|
||||
let icons = ["externaldrive", "trash", "folder", "pear-1", "pear-1.5", "pear-2", "pear-3", "pear-4"]
|
||||
private let themes = ["Auto", "Dark", "Light"]
|
||||
|
||||
// @State private var slate = Color(.sRGB, red: 0.188143, green: 0.208556, blue: 0.262679, opacity: 1)
|
||||
// @State private var solarized = Color(.sRGB, red: 0.117257, green: 0.22506, blue: 0.249171, opacity: 1)
|
||||
// @State private var dracula = Color(.sRGB, red: 0.268614, green: 0.264737, blue: 0.383503, opacity: 1)
|
||||
|
||||
var body: some View {
|
||||
|
||||
let slate = displayMode.colorScheme == .light ? Color(.sRGB, red: 0.499549, green: 0.545169, blue: 0.682028, opacity: 1) : Color(.sRGB, red: 0.188143, green: 0.208556, blue: 0.262679, opacity: 1)
|
||||
let solarized = displayMode.colorScheme == .light ? Color(.sRGB, red: 0.554372, green: 0.6557, blue: 0.734336, opacity: 1) : Color(.sRGB, red: 0.117257, green: 0.22506, blue: 0.249171, opacity: 1)
|
||||
let dracula = displayMode.colorScheme == .light ? Color(.sRGB, red: 0.567094, green: 0.562125, blue: 0.81285, opacity: 1) : Color(.sRGB, red: 0.268614, green: 0.264737, blue: 0.383503, opacity: 1)
|
||||
|
||||
Form {
|
||||
VStack {
|
||||
|
||||
@@ -92,23 +83,21 @@ struct InterfaceSettingsTab: View {
|
||||
Spacer()
|
||||
|
||||
Button("") {
|
||||
themeSettings.themeColor = slate
|
||||
themeSettings.saveThemeColor()
|
||||
themeSettings.setPreset(preset: "slate", colorScheme: displayMode)
|
||||
}
|
||||
.buttonStyle(PresetColor(fillColor: slate, label: "Slate"))
|
||||
.buttonStyle(PresetColor(fillColor: themeSettings.getColorForPreset(preset: "slate", colorScheme: displayMode), label: "Slate"))
|
||||
|
||||
Button("") {
|
||||
themeSettings.themeColor = dracula
|
||||
themeSettings.saveThemeColor()
|
||||
themeSettings.setPreset(preset: "dracula", colorScheme: displayMode)
|
||||
}
|
||||
.buttonStyle(PresetColor(fillColor: dracula, label: "Dracula"))
|
||||
.buttonStyle(PresetColor(fillColor: themeSettings.getColorForPreset(preset: "dracula", colorScheme: displayMode), label: "Dracula"))
|
||||
.padding(.horizontal)
|
||||
|
||||
Button("") {
|
||||
themeSettings.themeColor = solarized
|
||||
themeSettings.setPreset(preset: "solarized", colorScheme: displayMode)
|
||||
themeSettings.saveThemeColor()
|
||||
}
|
||||
.buttonStyle(PresetColor(fillColor: solarized, label: "Solarized"))
|
||||
.buttonStyle(PresetColor(fillColor: themeSettings.getColorForPreset(preset: "solarized", colorScheme: displayMode), label: "Solarized"))
|
||||
|
||||
Spacer()
|
||||
|
||||
@@ -151,18 +140,50 @@ struct InterfaceSettingsTab: View {
|
||||
}
|
||||
InfoButton(text: "Changing the color mode will reset the base color to defaults", color: nil, label: "")
|
||||
Spacer()
|
||||
Picker("", selection: $selectedTheme) {
|
||||
ForEach(themes, id: \.self) { theme in
|
||||
Text(theme)
|
||||
SegmentedPicker(
|
||||
["Auto", "Dark", "Light"],
|
||||
selectedIndex: Binding(
|
||||
get: {
|
||||
switch selectedTheme {
|
||||
case "Dark": return 1
|
||||
case "Light": return 2
|
||||
default: return 0
|
||||
}
|
||||
},
|
||||
set: { newIndex in
|
||||
withAnimation(.easeInOut(duration: 0.3)) {
|
||||
switch newIndex {
|
||||
case 1: selectedTheme = "Dark"
|
||||
case 2: selectedTheme = "Light"
|
||||
default: selectedTheme = "Auto"
|
||||
}
|
||||
}
|
||||
.pickerStyle(SegmentedPickerStyle())
|
||||
.frame(width: 200)
|
||||
}),
|
||||
selectionAlignment: .bottom,
|
||||
content: { item, isSelected in
|
||||
Text(item)
|
||||
.font(.callout)
|
||||
.foregroundColor(isSelected ? Color("mode") : Color("mode").opacity(0.5) )
|
||||
.padding(.horizontal)
|
||||
.padding(.bottom, 5)
|
||||
.frame(width: 65)
|
||||
},
|
||||
selection: {
|
||||
VStack(spacing: 0) {
|
||||
Spacer()
|
||||
Color("pear").frame(height: 1)
|
||||
}
|
||||
})
|
||||
.onChange(of: selectedTheme) { newTheme in
|
||||
switch newTheme {
|
||||
case "Auto":
|
||||
displayMode.colorScheme = nil
|
||||
themeSettings.resetToDefault(dark: isDarkModeEnabled())
|
||||
if isDarkMode() {
|
||||
displayMode.colorScheme = .dark
|
||||
} else {
|
||||
displayMode.colorScheme = .light
|
||||
}
|
||||
// displayMode.colorScheme = nil
|
||||
themeSettings.resetToDefault(dark: isDarkMode())
|
||||
// Refresh foreground colors
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||
self.selectedTab = .interface
|
||||
@@ -172,7 +193,7 @@ struct InterfaceSettingsTab: View {
|
||||
}
|
||||
case "Dark":
|
||||
displayMode.colorScheme = .dark
|
||||
themeSettings.resetToDefault(dark: true)
|
||||
// themeSettings.resetToDefault(dark: true)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||
self.selectedTab = .interface
|
||||
}
|
||||
@@ -181,7 +202,7 @@ struct InterfaceSettingsTab: View {
|
||||
}
|
||||
case "Light":
|
||||
displayMode.colorScheme = .light
|
||||
themeSettings.resetToDefault(dark: false)
|
||||
// themeSettings.resetToDefault(dark: false)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||
self.selectedTab = .interface
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ struct AppListItems: View {
|
||||
|
||||
if (isHovered || isSelected) && !mini {
|
||||
Triangle()
|
||||
.fill(isSelected ? themeSettings.themeColor : Color("mode").opacity(0.1))
|
||||
.fill(themeSettings.themeColor)
|
||||
.frame(width: isSelected ? 16 : 8, height: 30)
|
||||
.padding(.leading, 5)
|
||||
.offset(x: 22)
|
||||
@@ -115,7 +115,6 @@ struct AppListItems: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
// .grayscale(opacityForItem(appInfo.path))
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -14,24 +14,36 @@ struct FilesView: View {
|
||||
@State private var showPop: Bool = false
|
||||
@AppStorage("settings.general.mini") private var mini: Bool = false
|
||||
@AppStorage("settings.sentinel.enable") private var sentinel: Bool = false
|
||||
@AppStorage("settings.general.instant") var instantSearch: Bool = true
|
||||
@AppStorage("settings.general.brew") private var brew: Bool = false
|
||||
@AppStorage("settings.menubar.enabled") private var menubarEnabled: Bool = false
|
||||
@AppStorage("settings.general.selectedSort") var selectedSortAlpha: Bool = true
|
||||
@AppStorage("settings.general.sizeType") var sizeType: String = "Real"
|
||||
@State private var localKey = UUID()
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@Binding var showPopover: Bool
|
||||
@Binding var search: String
|
||||
var regularWin: Bool
|
||||
// @State private var selectedOption = "Alpha"
|
||||
@State private var elapsedTime = 0
|
||||
@State private var timer: Timer? = nil
|
||||
|
||||
var body: some View {
|
||||
|
||||
|
||||
let totalSelectedSize = appState.selectedItems.reduce(Int64(0)) { (result, url) in
|
||||
result + (appState.appInfo.fileSize[url] ?? 0)
|
||||
var totalSelectedSize: (real: String, logical: String, finder: String) {
|
||||
var totalReal: Int64 = 0
|
||||
var totalLogical: Int64 = 0
|
||||
for url in appState.selectedItems {
|
||||
let realSize = appState.appInfo.fileSize[url] ?? 0
|
||||
let logicalSize = appState.appInfo.fileSizeLogical[url] ?? 0
|
||||
totalReal += realSize
|
||||
totalLogical += logicalSize
|
||||
}
|
||||
return (real: formatByte(size: totalReal).human, logical: formatByte(size: totalLogical).human, finder: "\(formatByte(size: totalLogical).byte) (\(formatByte(size: totalReal).human))")
|
||||
}
|
||||
|
||||
let displaySizeTotal = sizeType == "Real" ? formatByte(size: appState.appInfo.totalSize).human :
|
||||
sizeType == "Logical" ? formatByte(size: appState.appInfo.totalSizeLogical).human :
|
||||
"\(formatByte(size: appState.appInfo.totalSizeLogical).byte) (\(formatByte(size: appState.appInfo.totalSize).human))"
|
||||
|
||||
VStack(alignment: .center) {
|
||||
if appState.showProgress {
|
||||
@@ -73,22 +85,6 @@ struct FilesView: View {
|
||||
|
||||
Spacer()
|
||||
|
||||
if instantSearch {
|
||||
Button("Rescan") {
|
||||
updateOnMain {
|
||||
appState.showProgress.toggle()
|
||||
let pathFinder = AppPathFinder(appInfo: appState.appInfo, appState: appState, locations: locations) {
|
||||
updateOnMain {
|
||||
appState.showProgress = false
|
||||
}
|
||||
}
|
||||
pathFinder.findPaths()
|
||||
}
|
||||
}
|
||||
.buttonStyle(SimpleButtonStyle(icon: "arrow.counterclockwise.circle.fill", help: "Rescan files"))
|
||||
}
|
||||
|
||||
|
||||
Button("Close") {
|
||||
updateOnMain {
|
||||
appState.appInfo = AppInfo.empty
|
||||
@@ -144,7 +140,8 @@ struct FilesView: View {
|
||||
Spacer()
|
||||
|
||||
VStack(alignment: .trailing, spacing: 5) {
|
||||
Text("\(formatByte(size: appState.appInfo.totalSize))").font(.title).fontWeight(.bold)
|
||||
|
||||
Text("\(displaySizeTotal)").font(.title).fontWeight(.bold).help("Total size on disk")
|
||||
Text("\(appState.appInfo.fileSize.count == 1 ? "\(appState.selectedItems.count) / \(appState.appInfo.fileSize.count) item" : "\(appState.selectedItems.count) / \(appState.appInfo.fileSize.count) items")")
|
||||
.font(.callout).foregroundStyle((Color("mode").opacity(0.5)))
|
||||
}
|
||||
@@ -161,10 +158,10 @@ struct FilesView: View {
|
||||
// Item selection and sorting toolbar
|
||||
HStack() {
|
||||
Toggle("", isOn: Binding(
|
||||
get: { self.appState.selectedItems.count == self.appState.appInfo.files.count },
|
||||
get: { self.appState.selectedItems.count == self.appState.appInfo.fileSize.keys.count },
|
||||
set: { newValue in
|
||||
updateOnMain {
|
||||
self.appState.selectedItems = newValue ? Set(self.appState.appInfo.files) : []
|
||||
self.appState.selectedItems = newValue ? Set(self.appState.appInfo.fileSize.keys) : []
|
||||
}
|
||||
}
|
||||
))
|
||||
@@ -212,7 +209,6 @@ struct FilesView: View {
|
||||
|
||||
Button("") {
|
||||
selectedSortAlpha.toggle()
|
||||
// selectedOption = selectedOption == "Alpha" ? "Size" : "Alpha"
|
||||
}
|
||||
.buttonStyle(SimpleButtonStyle(icon: selectedSortAlpha ? "textformat.abc" : "textformat.123", help: selectedSortAlpha ? "Sorted alphabetically" : "Sorted by size"))
|
||||
|
||||
@@ -228,10 +224,9 @@ struct FilesView: View {
|
||||
|
||||
ScrollView() {
|
||||
LazyVStack {
|
||||
let sortedFilesSize = appState.appInfo.files.sorted(by: { appState.appInfo.fileSize[$0, default: 0] > appState.appInfo.fileSize[$1, default: 0] })
|
||||
let sortedFilesSize = appState.appInfo.fileSize.keys.sorted(by: { appState.appInfo.fileSize[$0, default: 0] > appState.appInfo.fileSize[$1, default: 0] })
|
||||
|
||||
// let sortedFilesAlpha = appState.appInfo.files
|
||||
let sortedFilesAlpha = appState.appInfo.files.sorted { firstURL, secondURL in
|
||||
let sortedFilesAlpha = appState.appInfo.fileSize.keys.sorted { firstURL, secondURL in
|
||||
let isFirstPathApp = firstURL.pathExtension == "app"
|
||||
let isSecondPathApp = secondURL.pathExtension == "app"
|
||||
if isFirstPathApp, !isSecondPathApp {
|
||||
@@ -247,10 +242,10 @@ struct FilesView: View {
|
||||
let sort = selectedSortAlpha ? sortedFilesAlpha : sortedFilesSize
|
||||
|
||||
ForEach(Array(sort.enumerated()), id: \.element) { index, path in
|
||||
if let fileSize = appState.appInfo.fileSize[path], let fileIcon = appState.appInfo.fileIcon[path] {
|
||||
if let fileSize = appState.appInfo.fileSize[path], let fileSizeL = appState.appInfo.fileSizeLogical[path], let fileIcon = appState.appInfo.fileIcon[path] {
|
||||
let iconImage = fileIcon.map(Image.init(nsImage:))
|
||||
VStack {
|
||||
FileDetailsItem(size: fileSize, icon: iconImage, path: path)
|
||||
FileDetailsItem(size: fileSize, sizeL: fileSizeL, icon: iconImage, path: path)
|
||||
if index < sort.count - 1 {
|
||||
Divider().padding(.leading, 40).opacity(0.5)
|
||||
}
|
||||
@@ -260,7 +255,12 @@ struct FilesView: View {
|
||||
|
||||
}
|
||||
.padding()
|
||||
.onChange(of: sizeType) { _ in
|
||||
localKey = UUID()
|
||||
}
|
||||
.id(localKey)
|
||||
}
|
||||
|
||||
|
||||
|
||||
Spacer()
|
||||
@@ -269,7 +269,7 @@ struct FilesView: View {
|
||||
|
||||
Spacer()
|
||||
|
||||
Button("\(formatByte(size: totalSelectedSize))") {
|
||||
Button("\(sizeType == "Logical" ? totalSelectedSize.logical : sizeType == "Finder" ? totalSelectedSize.finder : totalSelectedSize.real)") {
|
||||
Task {
|
||||
updateOnMain {
|
||||
search = ""
|
||||
@@ -305,7 +305,6 @@ struct FilesView: View {
|
||||
removeApp(appState: appState, withId: appState.appInfo.id)
|
||||
} else {
|
||||
// Add deleted appInfo object to trashed array
|
||||
appState.appInfo.files = []
|
||||
appState.appInfo.fileSize = [:]
|
||||
appState.trashedFiles.append(appState.appInfo)
|
||||
|
||||
@@ -346,7 +345,10 @@ struct FilesView: View {
|
||||
|
||||
struct FileDetailsItem: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
@State private var isHovered = false
|
||||
@AppStorage("settings.general.sizeType") var sizeType: String = "Real"
|
||||
let size: Int64?
|
||||
let sizeL: Int64?
|
||||
let icon: Image?
|
||||
let path: URL
|
||||
|
||||
@@ -372,6 +374,8 @@ struct FileDetailsItem: View {
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 30, height: 30)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||
.brightness(isHovered ? 0.2 : 0)
|
||||
.shadow(color: Color("mode"), radius: isHovered ? 2 : 0)
|
||||
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
@@ -396,19 +400,35 @@ struct FileDetailsItem: View {
|
||||
|
||||
Spacer()
|
||||
|
||||
Text(formatByte(size:size!))
|
||||
let displaySize = sizeType == "Real" ? formatByte(size: size!).human :
|
||||
sizeType == "Logical" ? formatByte(size: sizeL!).human :
|
||||
"\(formatByte(size: sizeL!).byte) (\(formatByte(size: size!).human))"
|
||||
|
||||
Text("\(displaySize)")
|
||||
|
||||
|
||||
Button("") {
|
||||
}
|
||||
.onHover { hovering in
|
||||
withAnimation(Animation.easeIn(duration: 0.2)) {
|
||||
self.isHovered = hovering
|
||||
}
|
||||
}
|
||||
.onTapGesture {
|
||||
NSWorkspace.shared.selectFile(path.path, inFileViewerRootedAtPath: path.deletingLastPathComponent().path)
|
||||
}
|
||||
.buttonStyle(SimpleButtonStyle(icon: "folder.fill", help: "Show in Finder"))
|
||||
.contextMenu {
|
||||
if path.pathExtension == "app" {
|
||||
Button("Open \(path.deletingPathExtension().lastPathComponent)") {
|
||||
NSWorkspace.shared.open(path)
|
||||
}
|
||||
Divider()
|
||||
}
|
||||
Button("Copy Path") {
|
||||
copyToClipboard(text: path.path)
|
||||
}
|
||||
Button("View in Finder") {
|
||||
NSWorkspace.shared.selectFile(path.path, inFileViewerRootedAtPath: path.deletingLastPathComponent().path)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.fill(Color.white.opacity(0))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ struct MenuBarMiniAppView: View {
|
||||
@State private var showSys: Bool = true
|
||||
@State private var showUsr: Bool = true
|
||||
@AppStorage("settings.general.mini") private var mini: Bool = false
|
||||
@AppStorage("settings.general.instant") private var instantSearch: Bool = true
|
||||
@AppStorage("settings.general.glass") private var glass: Bool = true
|
||||
@AppStorage("settings.general.popover") private var popoverStay: Bool = true
|
||||
@Binding var showPopover: Bool
|
||||
@@ -40,15 +39,7 @@ struct MenuBarMiniAppView: View {
|
||||
if appState.reload {
|
||||
VStack {
|
||||
Spacer()
|
||||
ProgressView(instantSearch ? "Refreshing app list and caching files" : "Refreshing app list")
|
||||
if instantSearch {
|
||||
Image(systemName: "bolt.fill")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.foregroundStyle(Color("mode").opacity(0.5))
|
||||
.frame(width: 30, height: 30)
|
||||
.padding()
|
||||
}
|
||||
ProgressView("Refreshing app list")
|
||||
Spacer()
|
||||
}
|
||||
.padding(.vertical)
|
||||
@@ -70,13 +61,7 @@ struct MenuBarMiniAppView: View {
|
||||
appState.currentView = .zombie
|
||||
appState.showProgress.toggle()
|
||||
showPopover.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)
|
||||
}
|
||||
reversePreloader(allApps: appState.sortedApps, appState: appState, locations: locations, reverseAddon: true)
|
||||
} else {
|
||||
appState.currentView = .zombie
|
||||
showPopover.toggle()
|
||||
|
||||
@@ -155,7 +155,6 @@ struct MiniAppView: View {
|
||||
@State private var showSys: Bool = true
|
||||
@State private var showUsr: Bool = true
|
||||
@AppStorage("settings.general.mini") private var mini: Bool = false
|
||||
@AppStorage("settings.general.instant") private var instantSearch: Bool = true
|
||||
@AppStorage("settings.general.glass") private var glass: Bool = true
|
||||
@Binding var showPopover: Bool
|
||||
|
||||
@@ -175,15 +174,7 @@ struct MiniAppView: View {
|
||||
if appState.reload {
|
||||
VStack {
|
||||
Spacer()
|
||||
ProgressView(instantSearch ? "Refreshing app list and caching files" : "Refreshing app list")
|
||||
if instantSearch {
|
||||
Image(systemName: "bolt.fill")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.foregroundStyle(Color("mode").opacity(0.5))
|
||||
.frame(width: 30, height: 30)
|
||||
.padding()
|
||||
}
|
||||
ProgressView("Refreshing app list")
|
||||
Spacer()
|
||||
}
|
||||
.padding(.vertical)
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
//
|
||||
// SegmentedPicker.swift
|
||||
// Pearcleaner
|
||||
//
|
||||
// Created by Alin Lupascu on 4/17/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
public struct SegmentedPicker<Element, Content, Selection>: View
|
||||
where
|
||||
Content: View,
|
||||
Selection: View {
|
||||
|
||||
public typealias Data = [Element]
|
||||
|
||||
@State private var frames: [CGRect]
|
||||
@Binding private var selectedIndex: Data.Index?
|
||||
|
||||
private let data: Data
|
||||
private let selection: () -> Selection
|
||||
private let content: (Data.Element, Bool) -> Content
|
||||
private let selectionAlignment: VerticalAlignment
|
||||
|
||||
public init(_ data: Data,
|
||||
selectedIndex: Binding<Data.Index?>,
|
||||
selectionAlignment: VerticalAlignment = .center,
|
||||
@ViewBuilder content: @escaping (Data.Element, Bool) -> Content,
|
||||
@ViewBuilder selection: @escaping () -> Selection) {
|
||||
|
||||
self.data = data
|
||||
self.content = content
|
||||
self.selection = selection
|
||||
self._selectedIndex = selectedIndex
|
||||
self._frames = State(wrappedValue: Array(repeating: .zero,
|
||||
count: data.count))
|
||||
self.selectionAlignment = selectionAlignment
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
ZStack(alignment: Alignment(horizontal: .horizontalCenterAlignment,
|
||||
vertical: selectionAlignment)) {
|
||||
|
||||
if let selectedIndex = selectedIndex {
|
||||
selection()
|
||||
.frame(width: frames[selectedIndex].width,
|
||||
height: frames[selectedIndex].height)
|
||||
.alignmentGuide(.horizontalCenterAlignment) { dimensions in
|
||||
dimensions[HorizontalAlignment.center]
|
||||
}
|
||||
}
|
||||
|
||||
HStack(spacing: 0) {
|
||||
ForEach(data.indices, id: \.self) { index in
|
||||
Button(action: { selectedIndex = index },
|
||||
label: { content(data[index], selectedIndex == index) }
|
||||
)
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
.background(GeometryReader { proxy in
|
||||
Color.clear.onAppear { frames[index] = proxy.frame(in: .global) }
|
||||
})
|
||||
.alignmentGuide(.horizontalCenterAlignment,
|
||||
isActive: selectedIndex == index) { dimensions in
|
||||
dimensions[HorizontalAlignment.center]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension HorizontalAlignment {
|
||||
private enum CenterAlignmentID: AlignmentID {
|
||||
static func defaultValue(in dimension: ViewDimensions) -> CGFloat {
|
||||
return dimension[HorizontalAlignment.center]
|
||||
}
|
||||
}
|
||||
|
||||
static var horizontalCenterAlignment: HorizontalAlignment {
|
||||
HorizontalAlignment(CenterAlignmentID.self)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension View {
|
||||
@ViewBuilder
|
||||
@inlinable func alignmentGuide(_ alignment: HorizontalAlignment,
|
||||
isActive: Bool,
|
||||
computeValue: @escaping (ViewDimensions) -> CGFloat) -> some View {
|
||||
if isActive {
|
||||
alignmentGuide(alignment, computeValue: computeValue)
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
@inlinable func alignmentGuide(_ alignment: VerticalAlignment,
|
||||
isActive: Bool,
|
||||
computeValue: @escaping (ViewDimensions) -> CGFloat) -> some View {
|
||||
|
||||
if isActive {
|
||||
alignmentGuide(alignment, computeValue: computeValue)
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,13 +13,10 @@ struct RegularMode: View {
|
||||
@EnvironmentObject var themeSettings: ThemeSettings
|
||||
@AppStorage("settings.general.glass") private var glass: Bool = false
|
||||
@AppStorage("settings.general.sidebarWidth") private var sidebarWidth: Double = 280
|
||||
@AppStorage("settings.general.instant") private var instantSearch: Bool = true
|
||||
@Binding var search: String
|
||||
@State private var showSys: Bool = true
|
||||
@State private var showUsr: Bool = true
|
||||
@Binding var showPopover: Bool
|
||||
// @State private var isHovering = false
|
||||
// @State private var isHovering2 = false
|
||||
|
||||
|
||||
var filteredApps: [AppInfo] {
|
||||
@@ -30,7 +27,6 @@ struct RegularMode: View {
|
||||
}
|
||||
}
|
||||
|
||||
// let slate = Color(.sRGB, red: 0.188143, green: 0.208556, blue: 0.262679, opacity: 1)
|
||||
|
||||
var body: some View {
|
||||
|
||||
@@ -41,13 +37,7 @@ struct RegularMode: View {
|
||||
if appState.reload {
|
||||
VStack {
|
||||
Spacer()
|
||||
ProgressView(instantSearch ? "Refreshing app list and caching files" : "Refreshing app list")
|
||||
if instantSearch {
|
||||
Image(systemName: "bolt")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 30, height: 30)
|
||||
}
|
||||
ProgressView("Refreshing app list")
|
||||
Spacer()
|
||||
}
|
||||
.frame(width: sidebarWidth)
|
||||
@@ -159,7 +149,6 @@ struct Header: View {
|
||||
@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
|
||||
|
||||
|
||||
@@ -198,15 +187,8 @@ struct Header: View {
|
||||
let sortedApps = getSortedApps(paths: fsm.folderPaths, appState: appState)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
appState.sortedApps = sortedApps
|
||||
if instantSearch {
|
||||
loadAllPaths(allApps: sortedApps, appState: appState, locations: locations) {
|
||||
appState.reload.toggle()
|
||||
}
|
||||
} else {
|
||||
appState.reload.toggle()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(minHeight: 20)
|
||||
|
||||
@@ -13,7 +13,6 @@ struct TopBar: View {
|
||||
@AppStorage("settings.general.glass") private var glass: Bool = false
|
||||
@AppStorage("settings.sentinel.enable") private var sentinel: Bool = false
|
||||
@AppStorage("settings.general.mini") private var mini: Bool = false
|
||||
@AppStorage("settings.general.instant") private var instantSearch: Bool = true
|
||||
@EnvironmentObject var appState: AppState
|
||||
@Binding var showPopover: Bool
|
||||
@EnvironmentObject var locations: Locations
|
||||
@@ -47,12 +46,7 @@ struct TopBar: View {
|
||||
updateOnMain {
|
||||
appState.zombieFile = .empty
|
||||
appState.showProgress.toggle()
|
||||
if instantSearch {
|
||||
let reverse = ReversePathsSearcher(appState: appState, locations: locations)
|
||||
reverse.reversePathsSearch()
|
||||
} else {
|
||||
loadAllPaths(allApps: appState.sortedApps, appState: appState, locations: locations, reverseAddon: true)
|
||||
}
|
||||
reversePreloader(allApps: appState.sortedApps, appState: appState, locations: locations, reverseAddon: true)
|
||||
}
|
||||
}
|
||||
.buttonStyle(SimpleButtonStyle(icon: "arrow.counterclockwise.circle.fill", help: "Rescan files"))
|
||||
@@ -68,13 +62,7 @@ struct TopBar: View {
|
||||
appState.currentView = .zombie
|
||||
appState.showProgress.toggle()
|
||||
showPopover.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)
|
||||
}
|
||||
reversePreloader(allApps: appState.sortedApps, appState: appState, locations: locations, reverseAddon: true)
|
||||
} else {
|
||||
appState.currentView = .zombie
|
||||
showPopover.toggle()
|
||||
|
||||
@@ -12,7 +12,6 @@ struct TopBarMini: View {
|
||||
@AppStorage("displayMode") var displayMode: DisplayMode = .system
|
||||
@AppStorage("settings.sentinel.enable") private var sentinel: Bool = false
|
||||
@AppStorage("settings.general.mini") private var mini: Bool = false
|
||||
@AppStorage("settings.general.instant") private var instantSearch: Bool = true
|
||||
@Binding var search: String
|
||||
@Binding var showPopover: Bool
|
||||
@EnvironmentObject var appState: AppState
|
||||
@@ -36,13 +35,7 @@ struct TopBarMini: View {
|
||||
appState.currentView = .zombie
|
||||
appState.showProgress.toggle()
|
||||
showPopover.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)
|
||||
}
|
||||
reversePreloader(allApps: appState.sortedApps, appState: appState, locations: locations, reverseAddon: true)
|
||||
} else {
|
||||
appState.currentView = .zombie
|
||||
showPopover.toggle()
|
||||
@@ -79,12 +72,7 @@ struct TopBarMini: View {
|
||||
appState.currentView = .zombie
|
||||
appState.showProgress.toggle()
|
||||
showPopover.toggle()
|
||||
if instantSearch {
|
||||
let reverse = ReversePathsSearcher(appState: appState, locations: locations)
|
||||
reverse.reversePathsSearch()
|
||||
} else {
|
||||
loadAllPaths(allApps: appState.sortedApps, appState: appState, locations: locations, reverseAddon: true)
|
||||
}
|
||||
reversePreloader(allApps: appState.sortedApps, appState: appState, locations: locations, reverseAddon: true)
|
||||
} else {
|
||||
appState.currentView = .zombie
|
||||
showPopover.toggle()
|
||||
@@ -133,7 +121,7 @@ struct SearchBarMiniBottom: View {
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
TextField("\(appState.instantTotal > appState.instantProgress ? " Searching the file system" : " Search")", text: $search)
|
||||
TextField(" Search", text: $search)
|
||||
.textFieldStyle(SimpleSearchStyle(trash: true, text: $search))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,9 +14,10 @@ struct ZombieView: View {
|
||||
@State private var showPop: Bool = false
|
||||
@AppStorage("settings.general.mini") private var mini: Bool = false
|
||||
@AppStorage("settings.sentinel.enable") private var sentinel: Bool = false
|
||||
@AppStorage("settings.general.instant") private var instantSearch: Bool = true
|
||||
@AppStorage("settings.menubar.enabled") private var menubarEnabled: Bool = false
|
||||
@AppStorage("settings.general.selectedSort") var selectedSortAlpha: Bool = true
|
||||
@AppStorage("settings.general.sizeType") var sizeType: String = "Real"
|
||||
@State private var localKey = UUID()
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@Binding var showPopover: Bool
|
||||
@Binding var search: String
|
||||
@@ -27,28 +28,45 @@ struct ZombieView: View {
|
||||
|
||||
var body: some View {
|
||||
|
||||
let totalSelectedZombieSize = appState.selectedZombieItems.reduce(Int64(0)) { (result, url) in
|
||||
result + (appState.zombieFile.fileSize[url] ?? 0)
|
||||
}
|
||||
let totalSelectedZombieSize: (real: String, logical: String, finder: String) = {
|
||||
var totalReal: Int64 = 0
|
||||
var totalLogical: Int64 = 0
|
||||
|
||||
let filteredAndSortedFiles: ([URL], Int64) = {
|
||||
let filteredFiles = appState.zombieFile.fileSize.filter { (url, _) in
|
||||
for url in appState.selectedZombieItems {
|
||||
let realSize = appState.zombieFile.fileSize[url] ?? 0
|
||||
let logicalSize = appState.zombieFile.fileSizeLogical[url] ?? 0
|
||||
totalReal += realSize
|
||||
totalLogical += logicalSize
|
||||
}
|
||||
return (formatByte(size: totalReal).human, formatByte(size:totalLogical).human, "\(formatByte(size: totalLogical).byte) (\(formatByte(size: totalReal).human))")
|
||||
}()
|
||||
|
||||
let filteredAndSortedFiles: ([URL], Int64, Int64) = {
|
||||
let fileSizeReal = appState.zombieFile.fileSize
|
||||
let fileSizeLogical = appState.zombieFile.fileSizeLogical
|
||||
let filteredFilesReal = fileSizeReal.filter { (url, _) in
|
||||
searchZ.isEmpty || url.lastPathComponent.localizedCaseInsensitiveContains(searchZ)
|
||||
}
|
||||
|
||||
let sortedFilteredFiles = filteredFiles.sorted(by: {
|
||||
let filteredFilesLogical = fileSizeLogical.filter { (url, _) in
|
||||
searchZ.isEmpty || url.lastPathComponent.localizedCaseInsensitiveContains(searchZ)
|
||||
}
|
||||
let filesToSort = (sizeType == "Real" || sizeType == "Finder" ? filteredFilesReal : filteredFilesLogical)
|
||||
let sortedFilteredFiles = filesToSort.sorted(by: {
|
||||
if selectedSortAlpha {
|
||||
return $0.key.lastPathComponent.pearFormat() < $1.key.lastPathComponent.pearFormat()
|
||||
} else {
|
||||
return $0.value > $1.value
|
||||
}
|
||||
}).map { $0.key }
|
||||
|
||||
let totalSize = filteredFiles.values.reduce(0, +)
|
||||
|
||||
return (sortedFilteredFiles, totalSize)
|
||||
let totalSize = filteredFilesReal.values.reduce(0, +)
|
||||
let totalSizeL = filteredFilesLogical.values.reduce(0, +)
|
||||
return (sortedFilteredFiles, totalSize, totalSizeL)
|
||||
}()
|
||||
|
||||
let displaySizeTotal = sizeType == "Real" ? formatByte(size: filteredAndSortedFiles.1).human :
|
||||
sizeType == "Logical" ? formatByte(size: filteredAndSortedFiles.2).human :
|
||||
"\(formatByte(size: filteredAndSortedFiles.2).byte) (\(formatByte(size: filteredAndSortedFiles.1).human))"
|
||||
|
||||
VStack(alignment: .center) {
|
||||
if appState.showProgress {
|
||||
VStack {
|
||||
@@ -62,6 +80,7 @@ struct ZombieView: View {
|
||||
.progressViewStyle(.linear)
|
||||
.frame(width: 400, height: 10)
|
||||
|
||||
|
||||
Text("\(elapsedTime)")
|
||||
.font(.title).monospacedDigit()
|
||||
.foregroundStyle(Color("mode").opacity(0.5))
|
||||
@@ -97,13 +116,7 @@ struct ZombieView: View {
|
||||
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)
|
||||
}
|
||||
reversePreloader(allApps: appState.sortedApps, appState: appState, locations: locations, reverseAddon: true)
|
||||
}
|
||||
}
|
||||
.buttonStyle(SimpleButtonStyle(icon: "arrow.counterclockwise.circle.fill", help: "Rescan files"))
|
||||
@@ -142,14 +155,14 @@ struct ZombieView: View {
|
||||
Spacer()
|
||||
|
||||
}
|
||||
Text("Files and folders remaining from previously installed applications")
|
||||
Text("Remaining files and folders from previous installs")
|
||||
.font(.callout).foregroundStyle(Color("mode").opacity(0.5))
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
VStack(alignment: .trailing, spacing: 5) {
|
||||
Text("\(formatByte(size: filteredAndSortedFiles.1))").font(.title).fontWeight(.bold)
|
||||
Text("\(displaySizeTotal)").font(.title).fontWeight(.bold).help("Total size on disk")
|
||||
|
||||
Text("\(appState.zombieFile.fileSize.count == 1 ? "\(appState.selectedZombieItems.count) / \(appState.zombieFile.fileSize.count) item" : "\(appState.selectedZombieItems.count) / \(appState.zombieFile.fileSize.count) items")")
|
||||
.font(.callout).foregroundStyle(Color("mode").opacity(0.5))
|
||||
@@ -195,10 +208,10 @@ struct ZombieView: View {
|
||||
ScrollView() {
|
||||
LazyVStack {
|
||||
ForEach(Array(filteredAndSortedFiles.0.enumerated()), id: \.element) { index, file in
|
||||
if let fileSize = appState.zombieFile.fileSize[file], let fileIcon = appState.zombieFile.fileIcon[file] {
|
||||
if let fileSize = appState.zombieFile.fileSize[file], let fileSizeL = appState.zombieFile.fileSizeLogical[file], let fileIcon = appState.zombieFile.fileIcon[file] {
|
||||
let iconImage = fileIcon.map(Image.init(nsImage:))
|
||||
VStack {
|
||||
ZombieFileDetailsItem(size: fileSize, icon: iconImage, path: file)
|
||||
ZombieFileDetailsItem(size: fileSize, sizeL: fileSizeL, icon: iconImage, path: file)
|
||||
.padding(.trailing)
|
||||
|
||||
if index < filteredAndSortedFiles.0.count - 1 {
|
||||
@@ -210,7 +223,12 @@ struct ZombieView: View {
|
||||
|
||||
}
|
||||
.padding()
|
||||
.onChange(of: sizeType) { _ in
|
||||
localKey = UUID()
|
||||
}
|
||||
.id(localKey)
|
||||
}
|
||||
|
||||
|
||||
Spacer()
|
||||
|
||||
@@ -218,7 +236,7 @@ struct ZombieView: View {
|
||||
|
||||
Spacer()
|
||||
|
||||
Button("\(formatByte(size: totalSelectedZombieSize))") {
|
||||
Button("\(sizeType == "Logical" ? totalSelectedZombieSize.logical : sizeType == "Finder" ? totalSelectedZombieSize.finder : totalSelectedZombieSize.real)") {
|
||||
Task {
|
||||
updateOnMain {
|
||||
appState.zombieFile = .empty
|
||||
@@ -271,7 +289,10 @@ struct ZombieView: View {
|
||||
|
||||
struct ZombieFileDetailsItem: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
@State private var isHovered = false
|
||||
@AppStorage("settings.general.sizeType") var sizeType: String = "Real"
|
||||
let size: Int64?
|
||||
let sizeL: Int64?
|
||||
let icon: Image?
|
||||
let path: URL
|
||||
|
||||
@@ -295,6 +316,8 @@ struct ZombieFileDetailsItem: View {
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 30, height: 30)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||
.brightness(isHovered ? 0.2 : 0)
|
||||
.shadow(color: Color("mode"), radius: isHovered ? 2 : 0)
|
||||
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
@@ -313,20 +336,35 @@ struct ZombieFileDetailsItem: View {
|
||||
|
||||
Spacer()
|
||||
|
||||
Text(formatByte(size:size!))
|
||||
let displaySize = sizeType == "Real" ? formatByte(size: size!).human :
|
||||
sizeType == "Logical" ? formatByte(size: sizeL!).human :
|
||||
"\(formatByte(size: sizeL!).byte) (\(formatByte(size: size!).human))"
|
||||
|
||||
Text("\(displaySize)")
|
||||
|
||||
|
||||
Button("") {
|
||||
}
|
||||
.onHover { hovering in
|
||||
withAnimation(Animation.easeIn(duration: 0.2)) {
|
||||
self.isHovered = hovering
|
||||
}
|
||||
}
|
||||
.onTapGesture {
|
||||
NSWorkspace.shared.selectFile(path.path, inFileViewerRootedAtPath: path.deletingLastPathComponent().path)
|
||||
}
|
||||
.contextMenu {
|
||||
if path.pathExtension == "app" {
|
||||
Button("Open \(path.deletingPathExtension().lastPathComponent)") {
|
||||
NSWorkspace.shared.open(path)
|
||||
}
|
||||
Divider()
|
||||
}
|
||||
Button("Copy Path") {
|
||||
copyToClipboard(text: path.path)
|
||||
}
|
||||
Button("View in Finder") {
|
||||
NSWorkspace.shared.selectFile(path.path, inFileViewerRootedAtPath: path.deletingLastPathComponent().path)
|
||||
}
|
||||
.buttonStyle(SimpleButtonStyle(icon: "folder.fill", help: "Show in Finder"))
|
||||
|
||||
}
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.fill(Color.white.opacity(0))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user