Road to v3

This commit is contained in:
Alin
2024-02-27 09:03:35 -07:00
parent 638aed7e1b
commit 80a128a619
21 changed files with 1374 additions and 525 deletions
+8
View File
@@ -12,6 +12,7 @@
C7045A322B068AD700376976 /* NewWin.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7045A312B068AD700376976 /* NewWin.swift */; }; C7045A322B068AD700376976 /* NewWin.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7045A312B068AD700376976 /* NewWin.swift */; };
C7045A342B068B2900376976 /* UpdateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7045A332B068B2900376976 /* UpdateView.swift */; }; C7045A342B068B2900376976 /* UpdateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7045A332B068B2900376976 /* UpdateView.swift */; };
C7045A362B068DD700376976 /* PermView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7045A352B068DD700376976 /* PermView.swift */; }; C7045A362B068DD700376976 /* PermView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7045A352B068DD700376976 /* PermView.swift */; };
C71848442B8D2D600046CB13 /* ZombieView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C71848432B8D2D600046CB13 /* ZombieView.swift */; };
C72893052AFD42E600C8C1CD /* DeepLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = C72893042AFD42E600C8C1CD /* DeepLink.swift */; }; C72893052AFD42E600C8C1CD /* DeepLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = C72893042AFD42E600C8C1CD /* DeepLink.swift */; };
C72893122AFD51EA00C8C1CD /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = C72893112AFD51EA00C8C1CD /* main.swift */; }; C72893122AFD51EA00C8C1CD /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = C72893112AFD51EA00C8C1CD /* main.swift */; };
C7575D382B0181FC006A600A /* FileWatcher in Frameworks */ = {isa = PBXBuildFile; productRef = C7575D372B0181FC006A600A /* FileWatcher */; }; C7575D382B0181FC006A600A /* FileWatcher in Frameworks */ = {isa = PBXBuildFile; productRef = C7575D372B0181FC006A600A /* FileWatcher */; };
@@ -30,6 +31,7 @@
C77B901E2AF1A9DB009CC655 /* SettingsWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C77B901D2AF1A9DB009CC655 /* SettingsWindow.swift */; }; C77B901E2AF1A9DB009CC655 /* SettingsWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C77B901D2AF1A9DB009CC655 /* SettingsWindow.swift */; };
C77B90232AF2D616009CC655 /* FilesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C77B90222AF2D616009CC655 /* FilesView.swift */; }; C77B90232AF2D616009CC655 /* FilesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C77B90222AF2D616009CC655 /* FilesView.swift */; };
C7A27E812AFD7C4600166168 /* com.alienator88.PearcleanerSentinel.plist in Resources */ = {isa = PBXBuildFile; fileRef = C7A27E802AFD7C4600166168 /* com.alienator88.PearcleanerSentinel.plist */; }; C7A27E812AFD7C4600166168 /* com.alienator88.PearcleanerSentinel.plist in Resources */ = {isa = PBXBuildFile; fileRef = C7A27E802AFD7C4600166168 /* com.alienator88.PearcleanerSentinel.plist */; };
C7A9CE472B89164700EB6E78 /* Authorization.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7A9CE462B89164700EB6E78 /* Authorization.swift */; };
C7CF47242B3B3F1700979C5F /* WindowSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7CF47232B3B3F1700979C5F /* WindowSettings.swift */; }; C7CF47242B3B3F1700979C5F /* WindowSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7CF47232B3B3F1700979C5F /* WindowSettings.swift */; };
C7D31D462AFDA90C00C7ED9E /* Sentinel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7D31D452AFDA90C00C7ED9E /* Sentinel.swift */; }; C7D31D462AFDA90C00C7ED9E /* Sentinel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7D31D452AFDA90C00C7ED9E /* Sentinel.swift */; };
C7D31D482AFEB23700C7ED9E /* TopBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7D31D472AFEB23700C7ED9E /* TopBar.swift */; }; C7D31D482AFEB23700C7ED9E /* TopBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7D31D472AFEB23700C7ED9E /* TopBar.swift */; };
@@ -68,6 +70,7 @@
C7045A312B068AD700376976 /* NewWin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewWin.swift; sourceTree = "<group>"; }; C7045A312B068AD700376976 /* NewWin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewWin.swift; sourceTree = "<group>"; };
C7045A332B068B2900376976 /* UpdateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateView.swift; sourceTree = "<group>"; }; C7045A332B068B2900376976 /* UpdateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateView.swift; sourceTree = "<group>"; };
C7045A352B068DD700376976 /* PermView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermView.swift; sourceTree = "<group>"; }; C7045A352B068DD700376976 /* PermView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermView.swift; sourceTree = "<group>"; };
C71848432B8D2D600046CB13 /* ZombieView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZombieView.swift; sourceTree = "<group>"; };
C72893042AFD42E600C8C1CD /* DeepLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLink.swift; sourceTree = "<group>"; }; C72893042AFD42E600C8C1CD /* DeepLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLink.swift; sourceTree = "<group>"; };
C728930F2AFD51EA00C8C1CD /* PearcleanerSentinel */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = PearcleanerSentinel; sourceTree = BUILT_PRODUCTS_DIR; }; 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>"; }; C72893112AFD51EA00C8C1CD /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
@@ -89,6 +92,7 @@
C77B90242AF2D796009CC655 /* Pearcleaner.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Pearcleaner.entitlements; sourceTree = "<group>"; }; C77B90242AF2D796009CC655 /* Pearcleaner.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Pearcleaner.entitlements; sourceTree = "<group>"; };
C7A27E7F2AFD70DD00166168 /* PearcleanerSentinel.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = PearcleanerSentinel.entitlements; sourceTree = "<group>"; }; C7A27E7F2AFD70DD00166168 /* PearcleanerSentinel.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = PearcleanerSentinel.entitlements; sourceTree = "<group>"; };
C7A27E802AFD7C4600166168 /* com.alienator88.PearcleanerSentinel.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = com.alienator88.PearcleanerSentinel.plist; sourceTree = "<group>"; }; C7A27E802AFD7C4600166168 /* com.alienator88.PearcleanerSentinel.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = com.alienator88.PearcleanerSentinel.plist; sourceTree = "<group>"; };
C7A9CE462B89164700EB6E78 /* Authorization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Authorization.swift; sourceTree = "<group>"; };
C7CF47232B3B3F1700979C5F /* WindowSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowSettings.swift; sourceTree = "<group>"; }; C7CF47232B3B3F1700979C5F /* WindowSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowSettings.swift; sourceTree = "<group>"; };
C7D31D452AFDA90C00C7ED9E /* Sentinel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sentinel.swift; sourceTree = "<group>"; }; C7D31D452AFDA90C00C7ED9E /* Sentinel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sentinel.swift; sourceTree = "<group>"; };
C7D31D472AFEB23700C7ED9E /* TopBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopBar.swift; sourceTree = "<group>"; }; C7D31D472AFEB23700C7ED9E /* TopBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopBar.swift; sourceTree = "<group>"; };
@@ -140,6 +144,7 @@
C7D31D502AFF00F300C7ED9E /* Locations.swift */, C7D31D502AFF00F300C7ED9E /* Locations.swift */,
C7D31D542B008CB900C7ED9E /* Shapes.swift */, C7D31D542B008CB900C7ED9E /* Shapes.swift */,
C7CF47232B3B3F1700979C5F /* WindowSettings.swift */, C7CF47232B3B3F1700979C5F /* WindowSettings.swift */,
C7A9CE462B89164700EB6E78 /* Authorization.swift */,
); );
path = Logic; path = Logic;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -227,6 +232,7 @@
C7D31D492AFEB26700C7ED9E /* AppListItems.swift */, C7D31D492AFEB26700C7ED9E /* AppListItems.swift */,
C7D31D582B00A49300C7ED9E /* DropTarget.swift */, C7D31D582B00A49300C7ED9E /* DropTarget.swift */,
C7045A272B03E71D00376976 /* MiniMode.swift */, C7045A272B03E71D00376976 /* MiniMode.swift */,
C71848432B8D2D600046CB13 /* ZombieView.swift */,
); );
path = Views; path = Views;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -358,6 +364,7 @@
C77B901C2AF193A3009CC655 /* Styles.swift in Sources */, C77B901C2AF193A3009CC655 /* Styles.swift in Sources */,
C7045A282B03E71D00376976 /* MiniMode.swift in Sources */, C7045A282B03E71D00376976 /* MiniMode.swift in Sources */,
C72893052AFD42E600C8C1CD /* DeepLink.swift in Sources */, C72893052AFD42E600C8C1CD /* DeepLink.swift in Sources */,
C71848442B8D2D600046CB13 /* ZombieView.swift in Sources */,
C76D084C2AF83C9D00D07867 /* Permissions.swift in Sources */, C76D084C2AF83C9D00D07867 /* Permissions.swift in Sources */,
C76D084A2AF83C6E00D07867 /* General.swift in Sources */, C76D084A2AF83C6E00D07867 /* General.swift in Sources */,
C77B90042AF18E2E009CC655 /* PearcleanerApp.swift in Sources */, C77B90042AF18E2E009CC655 /* PearcleanerApp.swift in Sources */,
@@ -370,6 +377,7 @@
C7D31D482AFEB23700C7ED9E /* TopBar.swift in Sources */, C7D31D482AFEB23700C7ED9E /* TopBar.swift in Sources */,
C7F539382AF60865007DF1B2 /* Utilities.swift in Sources */, C7F539382AF60865007DF1B2 /* Utilities.swift in Sources */,
C76D08552AF89CDE00D07867 /* AppListView.swift in Sources */, C76D08552AF89CDE00D07867 /* AppListView.swift in Sources */,
C7A9CE472B89164700EB6E78 /* Authorization.swift in Sources */,
C7CF47242B3B3F1700979C5F /* WindowSettings.swift in Sources */, C7CF47242B3B3F1700979C5F /* WindowSettings.swift in Sources */,
C77B90232AF2D616009CC655 /* FilesView.swift in Sources */, C77B90232AF2D616009CC655 /* FilesView.swift in Sources */,
); );
+21 -9
View File
@@ -34,11 +34,24 @@ struct AppCommands: Commands {
// Refresh Apps list // Refresh Apps list
appState.reload.toggle() appState.reload.toggle()
let sortedApps = getSortedApps() let sortedApps = getSortedApps()
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { updateOnMain {
appState.sortedApps.userApps = []
appState.sortedApps.systemApps = []
appState.sortedApps.userApps = sortedApps.userApps appState.sortedApps.userApps = sortedApps.userApps
appState.sortedApps.systemApps = sortedApps.systemApps appState.sortedApps.systemApps = sortedApps.systemApps
}
Task(priority: .high){
loadAllPaths(allApps: sortedApps.userApps + sortedApps.systemApps, appState: appState, locations: locations)
}
updateOnMain {
appState.reload.toggle() appState.reload.toggle()
} }
// let sortedApps = getSortedApps()
// DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
// appState.sortedApps.userApps = sortedApps.userApps
// appState.sortedApps.systemApps = sortedApps.systemApps
// appState.reload.toggle()
// }
} }
} label: { } label: {
Text("Refresh Apps") Text("Refresh Apps")
@@ -50,10 +63,6 @@ struct AppCommands: Commands {
} label: { } label: {
Text("Uninstall Pearcleaner") Text("Uninstall Pearcleaner")
} }
// .keyboardShortcut("C", modifiers: .command)
} }
@@ -65,10 +74,13 @@ struct AppCommands: Commands {
{ {
undoTrash(appState: appState) { undoTrash(appState: appState) {
let sortedApps = getSortedApps() let sortedApps = getSortedApps()
appState.sortedApps.userApps = [] DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
appState.sortedApps.systemApps = [] appState.sortedApps.userApps = []
appState.sortedApps.userApps = sortedApps.userApps appState.sortedApps.systemApps = []
appState.sortedApps.systemApps = sortedApps.systemApps appState.sortedApps.userApps = sortedApps.userApps
appState.sortedApps.systemApps = sortedApps.systemApps
loadAllPaths(allApps: sortedApps.userApps + sortedApps.systemApps, appState: appState, locations: locations)
}
} }
} label: { } label: {
Label("Undo Removal", systemImage: "clear") Label("Undo Removal", systemImage: "clear")
+81 -37
View File
@@ -13,9 +13,12 @@ let home = FileManager.default.homeDirectoryForCurrentUser.path
class AppState: ObservableObject class AppState: ObservableObject
{ {
@Published var appInfo: AppInfo @Published var appInfo: AppInfo
@Published var paths: [URL] = [] @Published var appInfoStore: [AppInfo] = []
@Published var zombieFile: ZombieFile
@Published var sortedApps: (userApps: [AppInfo], systemApps: [AppInfo]) = ([], []) @Published var sortedApps: (userApps: [AppInfo], systemApps: [AppInfo]) = ([], [])
@Published var selectedItems = Set<URL>() @Published var selectedItems = Set<URL>()
@Published var selectedZombieItems = Set<URL>()
@Published var trashedFiles: [URL] = []
@Published var alertType = AlertType.off @Published var alertType = AlertType.off
@Published var currentView = CurrentDetailsView.empty @Published var currentView = CurrentDetailsView.empty
@Published var showAlert: Bool = false @Published var showAlert: Bool = false
@@ -23,12 +26,11 @@ class AppState: ObservableObject
@Published var isReminderVisible: Bool = false @Published var isReminderVisible: Bool = false
@Published var releases = [Release]() @Published var releases = [Release]()
@Published var progressBar: (String, Double) = ("Ready", 0.0) @Published var progressBar: (String, Double) = ("Ready", 0.0)
// @Published var progressManager = ProgressManager()
@Published var reload: Bool = false @Published var reload: Bool = false
@Published var showProgress: Bool = false
@Published var popCount: Int = 0
//Window
// @Published var winWidth: CGFloat = 1020
init() { init() {
self.appInfo = AppInfo( self.appInfo = AppInfo(
@@ -39,45 +41,27 @@ class AppState: ObservableObject
appVersion: "", appVersion: "",
appIcon: nil, appIcon: nil,
webApp: false, webApp: false,
wrapped: false wrapped: false,
files: [],
fileSize: [:],
fileIcon: [:]
)
self.zombieFile = ZombieFile(
id: UUID(),
// files: [],
fileSize: [:],
fileIcon: [:]
) )
} }
} }
//class ProgressManager: ObservableObject {
// @Published var progress: Double = 0.0
// @Published var total: Double = 0.0
// @Published var status: String = "Ready"
//
// func setTotal(_ total: Double) {
// DispatchQueue.main.async {
// self.total = total
// }
// }
//
// func updateProgress() {
// DispatchQueue.main.async {
// self.progress = min(max(0.0, self.progress + 1.0), Double(self.total))
// }
// }
//
// func updateStatus(status: String) {
// DispatchQueue.main.async {
// self.status = status
// }
// }
//
// func resetProgress() {
// DispatchQueue.main.async {
// self.progress = 0.0
// }
// }
//}
struct AppInfo: Identifiable, Hashable {
struct AppInfo: Identifiable, Equatable, Hashable {
let id: UUID let id: UUID
let path: URL let path: URL
let bundleIdentifier: String let bundleIdentifier: String
@@ -86,11 +70,40 @@ struct AppInfo: Identifiable, Hashable {
let appIcon: NSImage? let appIcon: NSImage?
let webApp: Bool let webApp: Bool
let wrapped: Bool let wrapped: Bool
var files: [URL]
var fileSize: [URL:Int64]
var fileIcon: [URL:NSImage?]
var totalSize: Int64
{
return fileSize.values.reduce(0, +)
}
static let empty = AppInfo(id: UUID(), path: URL(fileURLWithPath: ""), bundleIdentifier: "", appName: "", appVersion: "", appIcon: nil, webApp: false, wrapped: false, files: [], fileSize: [:], fileIcon: [:])
static let empty = AppInfo(id: UUID(), path: URL(fileURLWithPath: ""), bundleIdentifier: "", appName: "", appVersion: "", appIcon: nil, webApp: false, wrapped: false)
} }
struct ZombieFile: Identifiable, Equatable, Hashable {
let id: UUID
// var files: [URL]
var fileSize: [URL:Int64]
var fileIcon: [URL:NSImage?]
var totalSize: Int64
{
return fileSize.values.reduce(0, +)
}
static let empty = ZombieFile(id: UUID(), fileSize: [:], fileIcon: [:])
}
enum CurrentTabView:Int enum CurrentTabView:Int
{ {
case general case general
@@ -115,6 +128,7 @@ enum CurrentDetailsView:Int
case empty case empty
case files case files
case apps case apps
case zombie
} }
enum NewWindow:Int enum NewWindow:Int
@@ -171,6 +185,36 @@ enum DisplayMode: Int, CaseIterable {
} }
//class ProgressManager: ObservableObject {
// @Published var progress: Double = 0.0
// @Published var total: Double = 0.0
// @Published var status: String = "Ready"
//
// func setTotal(_ total: Double) {
// DispatchQueue.main.async {
// self.total = total
// }
// }
//
// func updateProgress() {
// DispatchQueue.main.async {
// self.progress = min(max(0.0, self.progress + 1.0), Double(self.total))
// }
// }
//
// func updateStatus(status: String) {
// DispatchQueue.main.async {
// self.status = status
// }
// }
//
// func resetProgress() {
// DispatchQueue.main.async {
// self.progress = 0.0
// }
// }
//}
//let (cacheDir, tempDir) = darwinCT() //let (cacheDir, tempDir) = darwinCT()
//let locations: [String] = [ //let locations: [String] = [
+113
View File
@@ -0,0 +1,113 @@
//
// Authorization.swift
// Pearcleaner
//
// Created by Alin Lupascu on 2/23/24.
//
import Foundation
import Swift
// https://github.com/x13a/authorization-swift
public struct Authorization {
public enum Error: Swift.Error {
case create(OSStatus)
case copyRights(OSStatus)
case exec(OSStatus)
}
public static func executeWithPrivileges(
_ command: String
) -> Result<FileHandle, Error> {
let RTLD_DEFAULT = UnsafeMutableRawPointer(bitPattern: -2)
var fn: @convention(c) (
AuthorizationRef,
UnsafePointer<CChar>, // path
AuthorizationFlags,
UnsafePointer<UnsafePointer<CChar>?>, // args
UnsafeMutablePointer<UnsafeMutablePointer<FILE>>?
) -> OSStatus
fn = unsafeBitCast(
dlsym(RTLD_DEFAULT, "AuthorizationExecuteWithPrivileges"),
to: type(of: fn)
)
var authorizationRef: AuthorizationRef? = nil
var err = AuthorizationCreate(nil, nil, [], &authorizationRef)
guard err == errAuthorizationSuccess else {
return .failure(.create(err))
}
defer { AuthorizationFree(authorizationRef!, [.destroyRights]) }
var components = command.components(separatedBy: " ")
var path = components.remove(at: 0).cString(using: .utf8)!
let name = kAuthorizationRightExecute.cString(using: .utf8)!
var items: AuthorizationItem = name.withUnsafeBufferPointer { nameBuf in
path.withUnsafeBufferPointer { pathBuf in
let pathPtr =
UnsafeMutableRawPointer(mutating: pathBuf.baseAddress!)
return AuthorizationItem(
name: nameBuf.baseAddress!,
valueLength: path.count,
value: pathPtr,
flags: 0
)
}
}
var rights: AuthorizationRights =
withUnsafeMutablePointer(to: &items) { items in
return AuthorizationRights(count: 1, items: items)
}
let flags: AuthorizationFlags = [
.interactionAllowed,
.preAuthorize,
.extendRights,
]
err = AuthorizationCopyRights(
authorizationRef!,
&rights,
nil,
flags,
nil
)
guard err == errAuthorizationSuccess else {
return .failure(.copyRights(err))
}
let rest = components.map { $0.cString(using: .utf8)! }
var args = Array<UnsafePointer<CChar>?>(
repeating: nil,
count: rest.count + 1
)
for (idx, arg) in rest.enumerated() {
args[idx] = UnsafePointer<CChar>?(arg)
}
var file = FILE()
let fh: FileHandle?
(err, fh) = withUnsafeMutablePointer(to: &file) { file in
var pipe = file
let err = fn(authorizationRef!, &path, [], &args, &pipe)
guard err == errAuthorizationSuccess else {
return (err, nil)
}
let fh = FileHandle(
fileDescriptor: fileno(pipe),
closeOnDealloc: true
)
return (err, fh)
}
guard err == errAuthorizationSuccess else {
return .failure(.exec(err))
}
return .success(fh!)
}
}
+76 -43
View File
@@ -23,8 +23,10 @@ class DeeplinkManager {
} }
func manage(url: URL, appState: AppState, locations: Locations) { func manage(url: URL, appState: AppState, locations: Locations) {
// This handles dropping an app onto Pearcleaner
if url.pathExtension == "app" { if url.pathExtension == "app" {
handleAppBundle(url: url, appState: appState, locations: locations) handleAppBundle(url: url, appState: appState, locations: locations)
// This handles sentinel monitor launch
} else if url.scheme == DeepLinkConstants.scheme, } else if url.scheme == DeepLinkConstants.scheme,
url.host == DeepLinkConstants.host, url.host == DeepLinkConstants.host,
let components = URLComponents(url: url, resolvingAgainstBaseURL: true), let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
@@ -32,57 +34,88 @@ class DeeplinkManager {
if let path = queryItems.first(where: { $0.name == DeepLinkConstants.query })?.value { if let path = queryItems.first(where: { $0.name == DeepLinkConstants.query })?.value {
let pathURL = URL(fileURLWithPath: path) let pathURL = URL(fileURLWithPath: path)
let appInfo = getAppInfo(atPath: pathURL) let appInfo = getAppInfo(atPath: pathURL)
updateOnMain { showAppInFiles(appInfo: appInfo!, mini: mini, appState: appState, locations: locations, showPopover: $showPopover)
appState.appInfo = appInfo!
findPathsForApp(appState: appState, locations: locations) // showPopover = false
if self.mini { // updateOnMain {
self.showPopover = true // appState.appInfo = .empty
} else { // if let storedAppInfo = appState.appInfoStore.first(where: { $0.path == appInfo?.path }) {
appState.currentView = .files // appState.appInfo = storedAppInfo
} //// appState.paths = storedAppInfo.fileSize.keys.map { $0 }//storedAppInfo.files
} // appState.selectedItems = Set(storedAppInfo.files)
// withAnimation(Animation.easeIn(duration: 0.4)) {
// if self.mini {
// self.showPopover.toggle()
// } else {
// appState.currentView = .files
// }
// }
// } else {
// // Handle the case where the appInfo is not found in the store
// printOS("AppInfo not found in the cached store, searching again")
// appState.appInfo = .empty
// appState.appInfo = appInfo!
// findPathsForApp(appInfo: appInfo!, appState: appState, locations: locations)
// withAnimation(Animation.easeIn(duration: 0.4)) {
// if self.mini {
// self.showPopover.toggle()
// } else {
// appState.currentView = .files
// }
// }
// }
// }
} else { } else {
print("No path query parameter found in the URL") printOS("No path query parameter found in the URL")
} }
} else { } else {
print("URL does not match the expected scheme and host") printOS("URL does not match the expected scheme and host")
} }
} }
// func manage(url: URL, appState: AppState) {
// guard url.scheme == DeepLinkConstants.scheme,
// url.host == DeepLinkConstants.host,
// let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
// let queryItems = components.queryItems
// else {
// print("URL does not match the expected scheme and host")
// return
// }
//
// if let path = queryItems.first(where: { $0.name == DeepLinkConstants.query })?.value {
// let pathURL = URL(fileURLWithPath: path)
// let appInfo = getAppInfo(atPath: pathURL)
// updateOnMain {
// appState.appInfo = appInfo!
// findPathsForApp(appState: appState, appInfo: appState.appInfo)
// appState.currentView = .files
// }
// } else {
// print("No path query parameter found in the URL")
// }
// }
func handleAppBundle(url: URL, appState: AppState, locations: Locations) { func handleAppBundle(url: URL, appState: AppState, locations: Locations) {
let appInfo = getAppInfo(atPath: url) let appInfo = getAppInfo(atPath: url)
updateOnMain { showAppInFiles(appInfo: appInfo!, mini: mini, appState: appState, locations: locations, showPopover: $showPopover)
appState.appInfo = appInfo!
findPathsForApp(appState: appState, locations: locations) // showPopover = false
if self.mini { // updateOnMain {
self.showPopover = true // appState.appInfo = .empty
} else { // if let storedAppInfo = appState.appInfoStore.first(where: { $0.path == appInfo?.path }) {
appState.currentView = .files // appState.appInfo = storedAppInfo
} //// appState.paths = storedAppInfo.fileSize.keys.map { $0 }//storedAppInfo.files
} // appState.selectedItems = Set(storedAppInfo.files)
// withAnimation(Animation.easeIn(duration: 0.4)) {
// if self.mini {
// self.showPopover.toggle()
// } else {
// appState.currentView = .files
// }
// }
// } else {
// // Handle the case where the appInfo is not found in the store
// printOS("AppInfo not found in the cached store, searching again")
// appState.appInfo = .empty
// appState.appInfo = appInfo!
// findPathsForApp(appInfo: appInfo!, appState: appState, locations: locations)
// withAnimation(Animation.easeIn(duration: 0.4)) {
// if self.mini {
// self.showPopover.toggle()
// } else {
// appState.currentView = .files
// }
// }
// }
// }
// updateOnMain {
// appState.appInfo = appInfo!
// findPathsForApp(appState: appState, locations: locations)
// if self.mini {
// self.showPopover = true
// } else {
// appState.currentView = .files
// }
// }
} }
} }
+29 -3
View File
@@ -19,6 +19,7 @@ class Locations: ObservableObject {
let tempDir: String let tempDir: String
var apps: Category var apps: Category
var reverse: Category
// var widgets: Category // var widgets: Category
// var plugins: Category // var plugins: Category
@@ -32,10 +33,8 @@ class Locations: ObservableObject {
"\(home)/Library", "\(home)/Library",
"\(home)/Library/Application Scripts", "\(home)/Library/Application Scripts",
"\(home)/Library/Application Support", "\(home)/Library/Application Support",
// "\(home)/Library/Application Support/CrashReporter",
"\(home)/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.ApplicationRecentDocuments", "\(home)/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.ApplicationRecentDocuments",
"\(home)/Library/Containers", "\(home)/Library/Containers",
// "\(home)/Library/Group Containers", // This is now handled by the function getGroupContainers()
"\(home)/Library/Caches", "\(home)/Library/Caches",
"\(home)/Library/HTTPStorages", "\(home)/Library/HTTPStorages",
"\(home)/Library/Internet Plug-Ins", "\(home)/Library/Internet Plug-Ins",
@@ -75,9 +74,36 @@ class Locations: ObservableObject {
let subfolders = listAppSupportDirectories() let subfolders = listAppSupportDirectories()
for folder in subfolders { for folder in subfolders {
self.apps.paths.append("\(home)/Library/Application Support/\(folder)") self.apps.paths.append("\(home)/Library/Application Support/\(folder)")
// writeLog(string: "Adding subfolder: \(home)/Library/Application Support/\(folder)")
} }
self.reverse = Category(name: "Reverse", paths: [
"\(home)/Library/Application Scripts",
"\(home)/Library/Application Support",
"\(home)/Library/Application Support/Caches",
"\(home)/Library/Containers",
"\(home)/Library/Caches",
"\(home)/Library/HTTPStorages",
"\(home)/Library/Internet Plug-Ins",
"\(home)/Library/LaunchAgents",
"\(home)/Library/Logs",
"\(home)/Library/Preferences",
"\(home)/Library/Preferences/ByHost",
"\(home)/Library/Saved Application State",
"\(home)/Library/WebKit",
"/Library/Application Support",
"/Library/Application Support/CrashReporter",
"/Library/Internet Plug-Ins",
"/Library/LaunchAgents",
"/Library/LaunchDaemons",
"/Library/PrivilegedHelperTools",
"/private/var/db/receipts",
cacheDir,
tempDir
])
// self.widgets = Category(name: "Widgets", paths: [ // self.widgets = Category(name: "Widgets", paths: [
// // User // // User
// "~/Library/Widgets", // "~/Library/Widgets",
+375 -77
View File
@@ -7,6 +7,7 @@
import Foundation import Foundation
import SwiftUI import SwiftUI
//import AudioToolbox
// Get list of apps and sort it // Get list of apps and sort it
@@ -53,7 +54,7 @@ func getApplications() -> (systemApps: [URL], userApps: [URL]) {
} }
} catch { } catch {
// Handle any potential errors here // Handle any potential errors here
print("Error: \(error)") printOS("Error: \(error)")
} }
} }
@@ -86,7 +87,7 @@ func getAppInfo(atPath path: URL) -> AppInfo? {
if let bundleVersion = bundle.infoDictionary?["CFBundleVersion"] as? String { if let bundleVersion = bundle.infoDictionary?["CFBundleVersion"] as? String {
appVersion = bundleVersion appVersion = bundleVersion
} else { } else {
print("Failed to retrieve bundle version") printOS("Failed to retrieve bundle version")
} }
} }
@@ -123,7 +124,7 @@ func getAppInfo(atPath path: URL) -> AppInfo? {
} }
if appIcon == nil { if appIcon == nil {
print("App Icon not found for app at path: \(path)") printOS("App Icon not found for app at path: \(path)")
} }
if bundle.infoDictionary?["LSTemplateApplication"] is Bool { if bundle.infoDictionary?["LSTemplateApplication"] is Bool {
@@ -133,7 +134,7 @@ func getAppInfo(atPath path: URL) -> AppInfo? {
} }
return AppInfo(id: UUID(), path: path, bundleIdentifier: bundleIdentifier, appName: appName ?? "", appVersion: appVersion, appIcon: appIcon, webApp: webApp ?? false, wrapped: false) return AppInfo(id: UUID(), path: path, bundleIdentifier: bundleIdentifier, appName: appName ?? "", appVersion: appVersion, appIcon: appIcon, webApp: webApp ?? false, wrapped: false, files: [], fileSize: [:], fileIcon: [:])
} else { } else {
let wrapperURL = path.appendingPathComponent("Wrapper") let wrapperURL = path.appendingPathComponent("Wrapper")
@@ -150,18 +151,18 @@ func getAppInfo(atPath path: URL) -> AppInfo? {
return wrappedAppInfo return wrappedAppInfo
} }
} else { } else {
print("No .app files found in the 'Wrapper' directory.") printOS("No .app files found in the 'Wrapper' directory.")
} }
} catch { } catch {
print("Error reading contents of 'Wrapper' directory: \(error.localizedDescription)") printOS("Error reading contents of 'Wrapper' directory: \(error.localizedDescription)")
} }
} else { } else {
print("Error: 'Wrapper' directory not found at path: \(wrapperURL.path)") printOS("Error: 'Wrapper' directory not found at path: \(wrapperURL.path)")
} }
} }
} else { } else {
print("Bundle not found at path: \(path)") printOS("Bundle not found at path: \(path)")
} }
return nil return nil
} }
@@ -201,10 +202,10 @@ func getWrappedAppInfo(atPath path: URL) -> AppInfo? {
} else { } else {
print("No matching image found for \(primaryIconFile).") printOS("No matching image found for \(primaryIconFile).")
} }
} else { } else {
print("Unable to access the directory at \(primaryIconFile).") printOS("Unable to access the directory at \(primaryIconFile).")
} }
} }
@@ -217,7 +218,7 @@ func getWrappedAppInfo(atPath path: URL) -> AppInfo? {
} }
if appIcon == nil { if appIcon == nil {
print("App Icon not found for app at path: \(path)") printOS("App Icon not found for app at path: \(path)")
} }
if bundle.infoDictionary?["LSTemplateApplication"] is Bool { if bundle.infoDictionary?["LSTemplateApplication"] is Bool {
@@ -227,13 +228,13 @@ func getWrappedAppInfo(atPath path: URL) -> AppInfo? {
} }
return AppInfo(id: UUID(), path: path, bundleIdentifier: bundleIdentifier, appName: appName ?? "", appVersion: appVersion, appIcon: appIcon, webApp: webApp ?? false, wrapped: true) return AppInfo(id: UUID(), path: path, bundleIdentifier: bundleIdentifier, appName: appName ?? "", appVersion: appVersion, appIcon: appIcon, webApp: webApp ?? false, wrapped: true, files: [], fileSize: [:], fileIcon: [:])
} else { } else {
print("One or more variables missing for app at path: \(path)") printOS("One or more variables missing for app at path: \(path)")
} }
} else { } else {
print("Bundle not found at path: \(path)") printOS("Bundle not found at path: \(path)")
} }
return nil return nil
} }
@@ -263,28 +264,11 @@ func darwinCT() -> (String, String) {
return (cacheDir, tempDir) return (cacheDir, tempDir)
} }
} }
print("Could not get DARWIN_USER_CACHE_DIR or DARWIN_USER_TEMP_DIR") printOS("Could not get DARWIN_USER_CACHE_DIR or DARWIN_USER_TEMP_DIR")
return ("", "") return ("", "")
} }
// Add subfolders of ~/Library/Application Support/ to locations for deeper search
//func appSupSubfolders2() throws -> [String] {
// let fileManager = FileManager.default
// let appSup = "\(home)/Library/Application Support/"
// let subfolders = try fileManager.contentsOfDirectory(atPath: appSup)
// let exclusionRegex = try NSRegularExpression(pattern: "\\bcom\\.apple\\b", options: [])
// let exclusions = ["MobileSync", ".DS_Store", "Xcode", "SyncServices", "networkserviceproxy", "DiskImages", "CallHistoryTransactions", "App Store", "CloudDocs", "icdd", "iCloud", "Instruments", "AddressBook", "FaceTime", "AskPermission", "CallHistoryDB"]
//
// let allowedFolders = subfolders.filter { folder in
// let range = NSRange(location: 0, length: folder.utf16.count)
// return exclusionRegex.firstMatch(in: folder, options: [], range: range) == nil && !exclusions.contains(folder)
// }
//
// return allowedFolders
//}
func listAppSupportDirectories() -> [String] { func listAppSupportDirectories() -> [String] {
let home = FileManager.default.homeDirectoryForCurrentUser let home = FileManager.default.homeDirectoryForCurrentUser
let appSupportLocation = home.appendingPathComponent("Library/Application Support/") let appSupportLocation = home.appendingPathComponent("Library/Application Support/")
@@ -312,7 +296,7 @@ func listAppSupportDirectories() -> [String] {
return filteredDirectories return filteredDirectories
} catch { } catch {
print("Error listing AppSupport directories: \(error.localizedDescription)") printOS("Error listing AppSupport directories: \(error.localizedDescription)")
return [] return []
} }
} }
@@ -334,19 +318,16 @@ func killApp(appId: String, completion: @escaping () -> Void = {}) {
// Find all possible paths for an app based on name/bundle id // Find all possible paths for an app based on name/bundle id
func findPathsForApp(appState: AppState, locations: Locations) { func findPathsForApp(appInfo: AppInfo = .empty, appState: AppState, locations: Locations, backgroundRun: Bool = false, completion: @escaping () -> Void = {}) {
Task(priority: .high) { Task(priority: .high) {
updateOnMain {
appState.paths = []
}
let appInfo = appState.appInfo
var collection: [URL] = [] var collection: [URL] = []
if let url = URL(string: appInfo.path.absoluteString) { if let url = URL(string: appInfo.path.absoluteString) {
collection.insert(url, at: 0) collection.insert(url, at: 0)
} }
let fileManager = FileManager.default let fileManager = FileManager.default
// let progressManager = appState.progressManager
let dispatchGroup = DispatchGroup() let dispatchGroup = DispatchGroup()
var bundleComponents = appInfo.bundleIdentifier.components(separatedBy: ".") var bundleComponents = appInfo.bundleIdentifier.components(separatedBy: ".")
if let lastComponent = bundleComponents.last, let rangeOfDash = lastComponent.range(of: "-") { if let lastComponent = bundleComponents.last, let rangeOfDash = lastComponent.range(of: "-") {
@@ -368,16 +349,6 @@ func findPathsForApp(appState: AppState, locations: Locations) {
continue continue
} }
// DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
// withAnimation {
// progressManager.updateStatus(status: location)
// progressManager.updateProgress()
// progressManager.objectWillChange.send()
// }
// }
dispatchGroup.enter() // Enter the dispatch group dispatchGroup.enter() // Enter the dispatch group
@@ -385,12 +356,19 @@ func findPathsForApp(appState: AppState, locations: Locations) {
do { do {
let contents = try fileManager.contentsOfDirectory(atPath: location) let contents = try fileManager.contentsOfDirectory(atPath: location)
for item in contents { for item in contents {
let itemURL = URL(fileURLWithPath: location).appendingPathComponent(item) let itemURL = URL(fileURLWithPath: location).appendingPathComponent(item)
let itemL = ("\(item)").replacingOccurrences(of: ".", with: "").replacingOccurrences(of: " ", with: "").lowercased() let itemL = ("\(item)").replacingOccurrences(of: ".", with: "").replacingOccurrences(of: " ", with: "").lowercased()
let filterItem = "\(location)/\(itemL)"
// Skip directories that start with com.apple. except for a few
if itemL.hasPrefix("comapple") && !itemL.hasPrefix("comappleconfigurator") && !itemL.hasPrefix("comappledt") && !itemL.hasPrefix("comappleiwork") {
continue
}
// Skip adding item if the collection already has it to prevent duplicates
if collection.contains(itemURL) { if collection.contains(itemURL) {
continue continue
} }
@@ -403,18 +381,23 @@ func findPathsForApp(appState: AppState, locations: Locations) {
collection.append(itemURL) collection.append(itemURL)
} }
} else { } else {
// Catch all the com.apple folders in the OS since there's a ton and probably unrelated // Catch Xcode files for Xcodes app
if itemL.contains("comapple") { if itemL.contains("xcode") && bundleIdentifierL.contains("comappledt") {
if filterItem.contains("comrobotsandpencilsxcodesapp") || filterItem.contains("comoneminutegamesxcodecleaner") || filterItem.contains("iohyperappxcodecleaner") || filterItem.contains("xcodesjson") {
continue
}
if itemL.contains(bundle) || itemL.contains(bundleIdentifierL) || (nameL.count < 6 && itemL.contains(nameL)) {
collection.append(itemURL)
}
} else if itemL.contains("xcodes") && bundleIdentifierL.contains("comrobotsandpencilsxcodesapp") {
if filterItem.contains("comappledt") {
continue
}
if itemL.contains(bundle) || itemL.contains(bundleIdentifierL) || (nameL.count > 4 && itemL.contains(nameL)) { if itemL.contains(bundle) || itemL.contains(bundleIdentifierL) || (nameL.count > 4 && itemL.contains(nameL)) {
collection.append(itemURL) collection.append(itemURL)
} }
} }
// Catch Xcode files else {
else if itemL.contains("xcode") {
if itemL.contains(bundle) || itemL.contains(bundleIdentifierL) || (nameL.count > 4 && itemL.contains(nameL)) {
collection.append(itemURL)
}
} else {
if itemL.contains(bundleIdentifierL) || itemL.contains(bundle) || (nameL.count > 3 && itemL.contains(nameL) || (nameP.count > 3 && itemL.contains(nameP))) { if itemL.contains(bundleIdentifierL) || itemL.contains(bundle) || (nameL.count > 3 && itemL.contains(nameL) || (nameP.count > 3 && itemL.contains(nameP))) {
collection.append(itemURL) collection.append(itemURL)
} }
@@ -423,13 +406,10 @@ func findPathsForApp(appState: AppState, locations: Locations) {
} }
} catch { } catch {
// writeLog(string: "Error processing location: \(location)\n\(error)") printOS("Error processing location:", location, error)
print("Error processing location:", location, error)
continue continue
} }
// try await Task.sleep(nanoseconds: 50_000_000)
dispatchGroup.leave() // Leave the dispatch group dispatchGroup.leave() // Leave the dispatch group
} }
@@ -439,15 +419,95 @@ func findPathsForApp(appState: AppState, locations: Locations) {
collection.append(contentsOf: groupContainers) collection.append(contentsOf: groupContainers)
let sortedCollection = collection.sorted(by: { $0.absoluteString < $1.absoluteString }) let sortedCollection = collection.sorted(by: { $0.absoluteString < $1.absoluteString })
// writeLog(string: "\n\nsortedCollection: \(sortedCollection)")
// Calculate file details (sizes and icons)
var fileSize: [URL: Int64] = [:]
var fileIcon: [URL: NSImage?] = [:]
var updatedAppInfo = appInfo
for path in collection {
var size: Int64
var icon: NSImage? = nil
size = totalSizeOnDisk(for: path)
icon = getIconForFileOrFolderNS(atPath: path)
fileSize[path] = size
fileIcon[path] = icon
}
// Save to appState // Save to appState
dispatchGroup.notify(queue: .main) { dispatchGroup.notify(queue: .main) {
updateOnMain { updateOnMain {
appState.paths = sortedCollection updatedAppInfo.files = sortedCollection
appState.selectedItems = Set(sortedCollection) updatedAppInfo.fileSize = fileSize
// writeLog(string: "\n\nappStatePaths: \(appState.paths)") updatedAppInfo.fileIcon = fileIcon
// writeLog(string: "\n\nappStateSelected: \(appState.selectedItems)") if !backgroundRun {
appState.appInfo = updatedAppInfo
appState.selectedItems = Set(sortedCollection)
}
appState.appInfoStore.append(updatedAppInfo)
}
}
completion()
}
}
// Load app paths on launch
func loadAllPaths(allApps: [AppInfo], appState: AppState, locations: Locations) {
updateOnMain {
appState.appInfoStore.removeAll()
}
for app in allApps {
findPathsForApp(appInfo: app, appState: appState, locations: locations, backgroundRun: true) {
}
}
}
// Load item in Files view
func showAppInFiles(appInfo: AppInfo, mini: Bool, appState: AppState, locations: Locations, showPopover: Binding<Bool>) {
showPopover.wrappedValue = false
DispatchQueue.main.asyncAfter(deadline: .now() + 0.0) {
updateOnMain {
appState.appInfo = .empty
if let storedAppInfo = appState.appInfoStore.first(where: { $0.path == appInfo.path }) {
appState.appInfo = storedAppInfo
appState.selectedItems = Set(storedAppInfo.files)
// withAnimation(Animation.easeIn(duration: 0.3)) {
if mini {
appState.currentView = .files
showPopover.wrappedValue.toggle()
} else {
appState.currentView = .files
}
// }
} else {
updateOnMain {
appState.showProgress = true
}
// Handle the case where the appInfo is not found in the store
// withAnimation(Animation.easeIn(duration: 0.4)) {
if mini {
appState.currentView = .files
showPopover.wrappedValue.toggle()
} else {
appState.currentView = .files
}
// }
printOS("AppInfo not found in the cached store, searching again")
appState.appInfo = .empty
appState.appInfo = appInfo
findPathsForApp(appInfo: appInfo, appState: appState, locations: locations) {
updateOnMain {
appState.selectedItems = Set(appInfo.files)
appState.showProgress = false
}
}
} }
} }
@@ -456,6 +516,7 @@ func findPathsForApp(appState: AppState, locations: Locations) {
} }
// Get group containers
func getGroupContainers(bundleURL: URL) -> [URL] { func getGroupContainers(bundleURL: URL) -> [URL] {
let fileManager = FileManager.default let fileManager = FileManager.default
@@ -470,14 +531,14 @@ func getGroupContainers(bundleURL: URL) -> [URL] {
let status = SecCodeCopySigningInformation(staticCode!, SecCSFlags(), &signingInformation) let status = SecCodeCopySigningInformation(staticCode!, SecCSFlags(), &signingInformation)
if status != errSecSuccess { if status != errSecSuccess {
print("Failed to copy signing information. Status: \(status)") printOS("Failed to copy signing information. Status: \(status)")
return [] return []
} }
guard let topDict = signingInformation as? [String: Any], guard let topDict = signingInformation as? [String: Any],
let entitlementsDict = topDict["entitlements-dict"] as? [String: Any], let entitlementsDict = topDict["entitlements-dict"] as? [String: Any],
let appGroups = entitlementsDict["com.apple.security.application-groups"] as? [String] else { let appGroups = entitlementsDict["com.apple.security.application-groups"] as? [String] else {
// print("No application groups to extract from entitlements for this app.") // printOS("No application groups to extract from entitlements for this app.")
return [] return []
} }
@@ -488,14 +549,135 @@ func getGroupContainers(bundleURL: URL) -> [URL] {
} }
// Reverse search for leftover zombie files
func reversePathsSearch(appState: AppState, locations: Locations, completion: @escaping () -> Void = {}) {
Task(priority: .high) {
var collection: [URL] = []
let fileManager = FileManager.default
let dispatchGroup = DispatchGroup()
let allPaths = appState.appInfoStore.flatMap { $0.files.map { $0.path.pearFormat() } }
let allNames = appState.appInfoStore.map { $0.appName.pearFormat() }
let skipped = ["apple", "comapple", "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", "sandobxhelper", "statuskitagent", "betaenrollmentd", "contentlinkingd", "diagnosticextensionsd", "gamed", "heard", "homed", "itunescloudd", "lldb", "mds", "mediaanalysisd", "metrickitd", "mobiletimerd", "proactived", "ptpcamerad", "studentd", "talagent", "watchlistd", "apptranslocation", "xcrun", "ds_store", "caches", "crashreporter"] // Skip system folders
// Skip locations that might not exist
for location in locations.reverse.paths {
if !fileManager.fileExists(atPath: location) {
continue
}
dispatchGroup.enter()
do {
let contents = try fileManager.contentsOfDirectory(atPath: location)
for item in contents {
let itemURL = URL(fileURLWithPath: location).appendingPathComponent(item)
let itemName = ("\(item)").pearFormat()
let itemPath = "\(location)/\(itemName)".pearFormat()
if skipped.contains(where: { itemName.contains($0) }) {
continue
}
if allPaths.contains(where: { $0 == itemPath }) || allNames.contains(where: { $0 == itemName }) {
continue
}
collection.append(itemURL)
}
} catch {
printOS("Error processing location:", location, error)
continue
}
dispatchGroup.leave() // Leave the dispatch group
}
let sortedCollection = collection.sorted(by: { $0.absoluteString < $1.absoluteString })
// Calculate file details (sizes and icons)
var fileSize: [URL: Int64] = [:]
var fileIcon: [URL: NSImage?] = [:]
var updatedZombieFile = ZombieFile.empty
for path in collection {
var size: Int64
var icon: NSImage? = nil
// size = 0
size = totalSizeOnDisk(for: path)
icon = getIconForFileOrFolderNS(atPath: path)
// icon = nil
fileSize[path] = size
fileIcon[path] = icon
}
// Save to appState
dispatchGroup.notify(queue: .main) {
updateOnMain {
// updatedZombieFile.files = sortedCollection
updatedZombieFile.fileSize = fileSize
updatedZombieFile.fileIcon = fileIcon
appState.selectedZombieItems = Set(sortedCollection)
appState.zombieFile = updatedZombieFile
// print(updatedZombieFile.fileSize.keys.count)
// print(updatedZombieFile)
appState.showProgress = false
}
}
completion()
}
}
// Move files to trash using applescript/Finder so it asks for user password if needed // Move files to trash using applescript/Finder so it asks for user password if needed
func moveFilesToTrash(at fileURLs: [URL], completion: @escaping () -> Void = {}) { func moveFilesToTrash(at fileURLs: [URL], completion: @escaping () -> Void = {}) {
@AppStorage("settings.sentinel.enable") var sentinel: Bool = false @AppStorage("settings.sentinel.enable") var sentinel: Bool = false
if sentinel { if sentinel {
launchctl(load: false) launchctl(load: false)
} }
var filesFinder = fileURLs
var filesSudo: [URL] = []
for file in fileURLs {
if isSocketFile(at: file) {
if let index = filesFinder.firstIndex(of: file) {
filesFinder.remove(at: index)
filesSudo.insert(file, at: 0)
}
}
}
if !filesSudo.isEmpty {
// Remove socket files with rm
let filesSudoPaths = filesSudo.map { $0.path }
do {
let fileHandler = try Authorization.executeWithPrivileges("/bin/rm -f \(filesSudoPaths.joined(separator: " "))").get()
printOS(String(bytes: fileHandler.readDataToEndOfFile(), encoding: .utf8)!)
} catch {
printOS("Failed to remove socket file/s with privileges: \(error)")
}
}
updateOnBackground { updateOnBackground {
let posixFiles = fileURLs.map { "POSIX file \"\($0.path)\", " }.joined().dropLast(3) let posixFiles = filesFinder.map { "POSIX file \"\($0.path)\", " }.joined().dropLast(3)
let scriptSource = """ let scriptSource = """
tell application \"Finder\" to delete { \(posixFiles)" } tell application \"Finder\" to delete { \(posixFiles)" }
""" """
@@ -503,9 +685,9 @@ func moveFilesToTrash(at fileURLs: [URL], completion: @escaping () -> Void = {})
if let scriptObject = NSAppleScript(source: scriptSource) { if let scriptObject = NSAppleScript(source: scriptSource) {
let output: NSAppleEventDescriptor = scriptObject.executeAndReturnError(&error) let output: NSAppleEventDescriptor = scriptObject.executeAndReturnError(&error)
if let error = error { if let error = error {
print("Error: \(error)") printOS("Error: \(error)")
} else if let outputString = output.stringValue { } else if let outputString = output.stringValue {
print(outputString) printOS(outputString)
} }
} }
DispatchQueue.main.async { DispatchQueue.main.async {
@@ -514,6 +696,122 @@ func moveFilesToTrash(at fileURLs: [URL], completion: @escaping () -> Void = {})
} }
} }
//func moveFilesToTrash(at fileURLs: [URL] = [], appState: AppState, undo: Bool = false, completion: @escaping () -> Void = {}) {
// @AppStorage("settings.sentinel.enable") var sentinel: Bool = false
// if sentinel {
// launchctl(load: false)
// }
//
// let home = FileManager.default.homeDirectoryForCurrentUser.path
//
// if undo {
// for url in appState.trashedFiles {
// let file = url.lastPathComponent
// let destinationURL = url.deletingLastPathComponent()
//
// let destinationPath = destinationURL.path
// let trashPath = "\(home)/.Trash/\(file)"
//
// if !destinationPath.starts(with: home) {
// executePrivilegedCommand(launchPath: "/bin/cp", arguments: ["-R"] + [trashPath, destinationPath])
// } else {
// let task = Process()
// task.launchPath = "/bin/cp"
// task.arguments = ["-R"] + [trashPath, destinationPath]
//
// let pipe = Pipe()
// task.standardOutput = pipe
// task.launch()
//
// task.waitUntilExit()
// }
// }
//
// AudioServicesPlaySystemSound(0xf);
//
// updateOnMain {
// appState.trashedFiles.removeAll()
// }
//
// } else {
// let privilegedURLs = fileURLs.filter { !$0.path.starts(with: home) }
//
// if !privilegedURLs.isEmpty {
// let filePaths = fileURLs.map { $0.path }
// executePrivilegedCommand(launchPath: "/bin/mv", arguments: ["-f"] + filePaths + ["\(home)/.Trash"])
// } else {
// let filePaths = fileURLs.map { $0.path }
// let task = Process()
// task.launchPath = "/bin/mv"
// task.arguments = ["-f"] + filePaths + ["\(home)/.Trash"]
//
// let pipe = Pipe()
// task.standardOutput = pipe
// task.launch()
//
// task.waitUntilExit()
// }
//
// AudioServicesPlaySystemSound(0x10);
//
// }
//
// completion()
//}
// Audio
//drag to trash.aif
//AudioServicesPlaySystemSound(0x10);
//poof item of dock.aif
//AudioServicesPlaySystemSound(0xf);
//func moveFilesToTrash(at fileURLs: [URL], completion: @escaping () -> Void = {}) {
// @AppStorage("settings.sentinel.enable") var sentinel: Bool = false
// if sentinel {
// launchctl(load: false)
// }
//
// let privilegedURLs = fileURLs.filter { !$0.path.starts(with: home) }
//
// if !privilegedURLs.isEmpty {
// print("Found system files")
// // Use privileged sudo mv function
// let filePaths = privilegedURLs.map { $0.path }
// executePrivilegedCommand(launchPath: "/bin/mv", arguments: filePaths + ["\(home)/.Trash"])
// completion()
// } else {
// // Use regular shell process for files within the user's home directory
// let filePaths = fileURLs.map { $0.path }
// let task = Process()
// task.launchPath = "/bin/mv"
// task.arguments = filePaths + ["\(home)/.Trash"]
//
// let pipe = Pipe()
// task.standardOutput = pipe
// task.launch()
//
// task.waitUntilExit()
//
// completion()
// }
//}
func isSocketFile(at url: URL) -> Bool {
do {
let attributes = try FileManager.default.attributesOfItem(atPath: url.path)
if let fileType = attributes[FileAttributeKey.type] as? FileAttributeType {
return fileType == .typeSocket
}
} catch {
print("Error: \(error)")
}
return false
}
// Undo trash action // Undo trash action
func undoTrash(appState: AppState, completion: @escaping () -> Void = {}) { func undoTrash(appState: AppState, completion: @escaping () -> Void = {}) {
@@ -541,9 +839,9 @@ func undoTrash(appState: AppState, completion: @escaping () -> Void = {}) {
_ = checkAndRequestAccessibilityAccess(appState: appState) _ = checkAndRequestAccessibilityAccess(appState: appState)
} }
} }
print("Error: \(error)") printOS("Error: \(error)")
} else if let outputString = output.stringValue { } else if let outputString = output.stringValue {
print(outputString) printOS(outputString)
} }
} }
DispatchQueue.main.async { DispatchQueue.main.async {
@@ -574,7 +872,7 @@ func undoTrash(appState: AppState, completion: @escaping () -> Void = {}) {
// do { // do {
// try task.run() // try task.run()
// } catch { // } catch {
// print("Failed to run task: \(error)") // printOS("Failed to run task: \(error)")
// return false // return false
// } // }
// //
@@ -598,7 +896,7 @@ func undoTrash(appState: AppState, completion: @escaping () -> Void = {}) {
// do { // do {
// try task.run() // try task.run()
// } catch { // } catch {
// print("Failed to run task: \(error)") // printOS("Failed to run task: \(error)")
// return // return
// } // }
// //
+66 -152
View File
@@ -7,6 +7,7 @@
import Foundation import Foundation
import SwiftUI import SwiftUI
import OSLog
// Make updates on main thread // Make updates on main thread
func updateOnMain(_ updates: @escaping () -> Void) { func updateOnMain(_ updates: @escaping () -> Void) {
@@ -110,7 +111,7 @@ func checkAndRequestFullDiskAccess(appState: AppState, skipAlert: Bool = false)
// let fileURL = URL(fileURLWithPath: "/Library/Application Support/com.apple.TCC/TCC.db") // let fileURL = URL(fileURLWithPath: "/Library/Application Support/com.apple.TCC/TCC.db")
// //
// let accessStatus = FileManager.default.isReadableFile(atPath: fileURL.path) // let accessStatus = FileManager.default.isReadableFile(atPath: fileURL.path)
// print(accessStatus) // printOS(accessStatus)
// if accessStatus { // if accessStatus {
// diskP = true // diskP = true
// _ = checkAndRequestAccessibilityAccess(appState: appState) // _ = checkAndRequestAccessibilityAccess(appState: appState)
@@ -225,6 +226,12 @@ func getIconForFileOrFolder(atPath path: URL) -> Image? {
return Image(nsImage: nsImage) return Image(nsImage: nsImage)
} }
func getIconForFileOrFolderNS(atPath path: URL) -> NSImage? {
let icon = NSWorkspace.shared.icon(forFile: path.path)
let nsImage = icon
return nsImage
}
// Relaunch app // Relaunch app
func relaunchApp(afterDelay seconds: TimeInterval = 0.5) -> Never { func relaunchApp(afterDelay seconds: TimeInterval = 0.5) -> Never {
@@ -263,6 +270,14 @@ extension FileManager {
} }
} }
// --- Extend print command to also output to the Console ---
func printOS(_ items: Any..., separator: String = " ", terminator: String = "\n") {
let message = items.map { "\($0)" }.joined(separator: separator)
let log = OSLog(subsystem: "com.alienator88.Pearcleaner", category: "Application")
os_log("%@", log: log, type: .debug, message)
}
// --- Gradient --- // --- Gradient ---
func schemeGradient(for colorScheme: ColorScheme) -> LinearGradient { func schemeGradient(for colorScheme: ColorScheme) -> LinearGradient {
@@ -279,8 +294,9 @@ func schemeGradient(for colorScheme: ColorScheme) -> LinearGradient {
// Get total size of folders and files using DU cli command // Get total size of folders and files using DU cli command
func totalSizeOnDisk(for paths: [URL]) -> String? { func totalSizeOnDisk(for paths: [URL]) -> Int64 {
var totalSize = 0 var totalSize = 0
let process = Process() let process = Process()
@@ -290,7 +306,7 @@ func totalSizeOnDisk(for paths: [URL]) -> String? {
} }
let pipe = Pipe() let pipe = Pipe()
process.standardOutput = pipe process.standardOutput = pipe
process.standardError = FileHandle.nullDevice process.standardError = pipe//FileHandle.nullDevice
try? process.run() try? process.run()
process.waitUntilExit() process.waitUntilExit()
@@ -301,19 +317,24 @@ func totalSizeOnDisk(for paths: [URL]) -> String? {
for line in lines { for line in lines {
let components = line.components(separatedBy: "\t") let components = line.components(separatedBy: "\t")
if let sizeString = components.first, let size = Int(sizeString) { if let sizeString = components.first, let size = Int(sizeString) {
totalSize += size * 1024 // Convert the size from kilobytes to bytes totalSize += size
} }
} }
} }
return Int64(totalSize) * 1024 // Convert the size from kilobytes to bytes
}
func totalSizeOnDisk(for path: URL) -> Int64 {
return totalSizeOnDisk(for: [path])
}
// ByteFormatter
func formatByte(size: Int64) -> String {
let byteCountFormatter = ByteCountFormatter() let byteCountFormatter = ByteCountFormatter()
byteCountFormatter.countStyle = .file byteCountFormatter.countStyle = .file
byteCountFormatter.allowedUnits = [.useAll] byteCountFormatter.allowedUnits = [.useAll]
return byteCountFormatter.string(fromByteCount: Int64(totalSize)) return byteCountFormatter.string(fromByteCount: size)
}
func totalSizeOnDisk(for path: URL) -> String? {
return totalSizeOnDisk(for: [path])
} }
@@ -410,10 +431,10 @@ func uninstallPearcleaner(appState: AppState, locations: Locations) {
// Get app info for Pearcleaner // Get app info for Pearcleaner
let appInfo = getAppInfo(atPath: Bundle.main.bundleURL) let appInfo = getAppInfo(atPath: Bundle.main.bundleURL)
appState.appInfo = appInfo! // appState.appInfo = appInfo!
// Find application files for Pearcleaner // Find application files for Pearcleaner
findPathsForApp(appState: appState, locations: locations) findPathsForApp(appInfo: appInfo!,appState: appState, locations: locations)
// Kill Pearcleaner and tell Finder to trash the files // Kill Pearcleaner and tell Finder to trash the files
DispatchQueue.main.asyncAfter(deadline: .now() + 2) { DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
@@ -443,7 +464,7 @@ func ensureApplicationSupportFolderExists(appState: AppState) {
// Check to make sure Application Support/Support Admin folder exists // Check to make sure Application Support/Support Admin folder exists
if !fileManager.fileExists(atPath: supportURL.path) { if !fileManager.fileExists(atPath: supportURL.path) {
try! fileManager.createDirectory(at: supportURL, withIntermediateDirectories: true) try! fileManager.createDirectory(at: supportURL, withIntermediateDirectories: true)
print("Created Application Support/Pearcleaner folder") printOS("Created Application Support/Pearcleaner folder")
} }
} }
@@ -457,7 +478,7 @@ func writeLog(string: String) {
// Check if the log file exists, and create it if it doesn't // Check if the log file exists, and create it if it doesn't
if !fileManager.fileExists(atPath: logFilePath) { if !fileManager.fileExists(atPath: logFilePath) {
if !fileManager.createFile(atPath: logFilePath, contents: nil, attributes: nil) { if !fileManager.createFile(atPath: logFilePath, contents: nil, attributes: nil) {
print("Failed to create the log file.") printOS("Failed to create the log file.")
return return
} }
} }
@@ -469,7 +490,7 @@ func writeLog(string: String) {
fileHandle.write(ns.data(using: .utf8)!) fileHandle.write(ns.data(using: .utf8)!)
fileHandle.closeFile() fileHandle.closeFile()
} else { } else {
print("Error opening file for appending") printOS("Error opening file for appending")
} }
} }
} }
@@ -491,7 +512,7 @@ func launchctl(load: Bool) {
do { do {
try plistContent.write(to: temporaryPlistURL, atomically: false, encoding: .utf8) try plistContent.write(to: temporaryPlistURL, atomically: false, encoding: .utf8)
} catch { } catch {
print("Error writing the temporary plist file: \(error)") printOS("Error writing the temporary plist file: \(error)")
return return
} }
let task = Process() let task = Process()
@@ -506,163 +527,56 @@ func launchctl(load: Bool) {
// let data = pipe.fileHandleForReading.readDataToEndOfFile() // let data = pipe.fileHandleForReading.readDataToEndOfFile()
// if let output = String(data: data, encoding: .utf8) { // if let output = String(data: data, encoding: .utf8) {
// print("Output: \(output)") // printOS("Output: \(output)")
// } // }
} }
} }
//func unloadAgent() {
// if let plistPath = Bundle.main.path(forResource: "com.alienator88.PearcleanerMonitor", ofType: "plist") {
// var plistContent = try! String(contentsOfFile: plistPath)
// let executableURL = Bundle.main.bundleURL.appendingPathComponent("Contents/MacOS/PearcleanerMonitor")
//
// // Replace the placeholder with the actual executable path
// plistContent = plistContent.replacingOccurrences(of: "__EXECUTABLE_PATH__", with: executableURL.path)
//
// // Create a temporary plist file with the updated content
// let temporaryPlistURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("com.alienator88.PearcleanerMonitor.plist")
//
// do {
// try plistContent.write(to: temporaryPlistURL, atomically: false, encoding: .utf8)
// } catch {
// print("Error writing the temporary plist file: \(error)")
// return
// }
// let task = Process()
// task.launchPath = "/bin/launchctl"
// task.arguments = ["unload", "-w", temporaryPlistURL.path]
//
// let pipe = Pipe()
// task.standardOutput = pipe
// task.standardError = pipe
//
// task.launch()
//
//// let data = pipe.fileHandleForReading.readDataToEndOfFile()
//// if let output = String(data: data, encoding: .utf8) {
//// print("Output: \(output)")
//// }
// }
//}
func getCurrentTimestamp() -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS"
return dateFormatter.string(from: Date())
}
// Execute sudo command
//func executePrivilegedCommand(launchPath: String, arguments: [String]) {
// let task = Process()
// task.launchPath = launchPath
// task.arguments = arguments
//import FileWatcher
// //
//// MARK: https://github.com/eonist/FileWatcher // var authorization: AuthorizationRef? = nil
// let status = AuthorizationCreate(nil, nil, AuthorizationFlags(), &authorization)
// //
//func fileW() { // if status == errAuthorizationSuccess {
// let filewatcher = FileWatcher([NSString(string: "~/.Trash").expandingTildeInPath]) // if let rightName = (kAuthorizationRightExecute as NSString).utf8String {
// filewatcher.queue = DispatchQueue.global() // let item = AuthorizationItem(name: rightName, valueLength: 0, value: nil, flags: 0)
// filewatcher.callback = { event in // var items = [item]
// print("Something happened here: " + event.path) // items.withUnsafeMutableBufferPointer { bufferPointer in
// } // var rights = AuthorizationRights(count: UInt32(bufferPointer.count), items: bufferPointer.baseAddress)
// //
// filewatcher.start() // let status = AuthorizationCopyRights(authorization!, &rights, nil, AuthorizationFlags([.extendRights, .interactionAllowed]), nil)
//} //
// if status == errAuthorizationSuccess {
// task.launch()
//extension Array where Element == URL {
// // DU shell way of getting sizes
// func totalAllocatedSize2(includingSubfolders: Bool = false) throws -> Int? {
// var totalSize = 0
//
// for path in self {
// let process = Process()
// process.launchPath = "/usr/bin/du"
// process.arguments = ["-sk", path.path]
//
// let pipe = Pipe()
// process.standardOutput = pipe
//
// try process.run()
// process.waitUntilExit()
//
// let data = pipe.fileHandleForReading.readDataToEndOfFile()
// if let output = String(data: data, encoding: .utf8) {
// let sizeString = output.components(separatedBy: "\t").first ?? ""
// if let size = Int(sizeString) {
// totalSize += size * 1024
// }
// }
// }
// return totalSize
// }
//
// func totalAllocatedSize(includingSubfolders: Bool = false) throws -> Int? {
// var totalSize = 0
//
// for path in self {
// if includingSubfolders {
// guard
// let urls = FileManager.default.enumerator(at: path, includingPropertiesForKeys: nil)?.allObjects as? [URL] else { return nil }
// let pathSize = try urls.lazy.reduce(0) {
// (try $1.resourceValues(forKeys: [.totalFileAllocatedSizeKey]).totalFileAllocatedSize ?? 0) + $0
// }
// totalSize += pathSize
// } else { // } else {
// let pathSize = try FileManager.default.contentsOfDirectory(at: path, includingPropertiesForKeys: nil).lazy.reduce(0) { // printOS("Authorization failed.")
// (try $1.resourceValues(forKeys: [.totalFileAllocatedSizeKey])
// .totalFileAllocatedSize ?? 0) + $0
// }
// totalSize += pathSize
// } // }
// } // }
// // } else {
// return totalSize // printOS("Failed to convert rightName to C-string.")
// } // }
// // } else {
// func totalSizeOnDisk(includingSubfolders: Bool = false) throws -> String? { // printOS("Authorization creation failed.")
// if let totalSize = try self.totalAllocatedSize(includingSubfolders: includingSubfolders) {
// let byteCountFormatter = ByteCountFormatter()
// byteCountFormatter.countStyle = .file
// byteCountFormatter.allowedUnits = [.useBytes, .useKB, .useMB, .useTB]
// return byteCountFormatter.string(fromByteCount: Int64(totalSize))
// }
// return nil
// } // }
//} //}
// executePrivilegedCommand(launchPath: "/bin/mv", arguments: ["-f"] + filesSudoPaths + ["\(home)/.Trash"])
//func isSymbolicLink(atPath path: URL) -> Bool {
//// let fileManager = FileManager.default
//
// do {
//// let path = "/Applications/Safari.app"
//// let url = URL(fileURLWithPath: path)
// let destinationPath = path.resolvingSymlinksInPath().path
// print("The symlink at \(path) points to \(destinationPath)")
// return true
// } catch {
// print("An error occurred: \(error)")
// return false
// }
// return false
//// var isDirectory = false
//// let exists = FileManager.default.fileExists(atPath: path, isDirectory: isDirectory)
//// if exists && isDirectory {
//// do {
//// let _ = try FileManager.default.destinationOfSymbolicLink(atPath: path)
//// return true
//// } catch {
//// return false
//// }
//// }
//// return false
//}
+4 -6
View File
@@ -72,7 +72,7 @@ struct PearcleanerApp: App {
} else { } else {
appState.currentView = .empty appState.currentView = .empty
} }
// Disable tabbing // Disable tabbing
NSWindow.allowsAutomaticWindowTabbing = false NSWindow.allowsAutomaticWindowTabbing = false
@@ -85,13 +85,11 @@ struct PearcleanerApp: App {
appState.sortedApps.userApps = sortedApps.userApps appState.sortedApps.userApps = sortedApps.userApps
appState.sortedApps.systemApps = sortedApps.systemApps appState.sortedApps.systemApps = sortedApps.systemApps
// Load progressbar total
// appState.progressManager.total = Double(locations.apps.paths.count)
Task { Task {
// Find all app paths on load
loadAllPaths(allApps: sortedApps.userApps + sortedApps.systemApps, appState: appState, locations: locations)
#if !DEBUG #if !DEBUG
// Make sure App Support folder exists in the future if needed for storage // Make sure App Support folder exists in the future if needed for storage
+6 -6
View File
@@ -102,7 +102,8 @@ struct GeneralSettingsTab: View {
RoundedRectangle(cornerRadius: 8) RoundedRectangle(cornerRadius: 8)
.fill(Color("mode").opacity(0.05)) .fill(Color("mode").opacity(0.05))
) )
HStack { HStack {
VStack(alignment: .leading, spacing: 5) { VStack(alignment: .leading, spacing: 5) {
Text("Mini").font(.title2) Text("Mini").font(.title2)
@@ -117,11 +118,10 @@ struct GeneralSettingsTab: View {
.onChange(of: mini) { newVal in .onChange(of: mini) { newVal in
if mini { if mini {
resizeWindowAuto(windowSettings: windowSettings) resizeWindowAuto(windowSettings: windowSettings)
// resizeWindow(width: 300, height: 300) // showPopover = false
appState.currentView = miniView ? .apps : .empty // appState.currentView = miniView ? .apps : .empty
} else { } else {
resizeWindowAuto(windowSettings: windowSettings) resizeWindowAuto(windowSettings: windowSettings)
// resizeWindow(width: 700, height: 500)
if appState.appInfo.appName.isEmpty { if appState.appInfo.appName.isEmpty {
appState.currentView = .empty appState.currentView = .empty
} else { } else {
@@ -139,7 +139,7 @@ struct GeneralSettingsTab: View {
HStack { HStack {
VStack(alignment: .leading, spacing: 5) { VStack(alignment: .leading, spacing: 5) {
Text("Mini - Default View").font(.title2) Text("Mini - \(miniView ? "Apps List" : "Drop Target")").font(.title2)
Text("Toggles drop target or apps list view on launch") Text("Toggles drop target or apps list view on launch")
.font(.footnote) .font(.footnote)
.foregroundStyle(.gray) .foregroundStyle(.gray)
@@ -160,7 +160,7 @@ struct GeneralSettingsTab: View {
HStack { HStack {
VStack(alignment: .leading, spacing: 5) { VStack(alignment: .leading, spacing: 5) {
Text("Mini - Popover").font(.title2) Text("Mini - \(popoverStay ? "Popover on Top" : "Popover not on Top")").font(.title2)
Text("Keeps file search popover on top in mini mode") Text("Keeps file search popover on top in mini mode")
.font(.footnote) .font(.footnote)
.foregroundStyle(.gray) .foregroundStyle(.gray)
+1 -1
View File
@@ -9,7 +9,7 @@ import SwiftUI
struct SettingsView: View { struct SettingsView: View {
@EnvironmentObject var appState: AppState @EnvironmentObject var appState: AppState
var body: some View { var body: some View {
TabView() { TabView() {
+13 -6
View File
@@ -22,6 +22,12 @@ struct Asset: Codable {
let browser_download_url: String let browser_download_url: String
} }
extension Release {
var modifiedBody: String {
return body.replacingOccurrences(of: "- [x]", with: "􀆊").replacingOccurrences(of: "###", with: "")
}
}
struct UpdateSettingsTab: View { struct UpdateSettingsTab: View {
@EnvironmentObject var appState: AppState @EnvironmentObject var appState: AppState
@@ -46,7 +52,7 @@ struct UpdateSettingsTab: View {
ForEach(appState.releases, id: \.id) { release in ForEach(appState.releases, id: \.id) { release in
VStack(alignment: .leading) { VStack(alignment: .leading) {
LabeledDivider(label: "\(release.tag_name)") LabeledDivider(label: "\(release.tag_name)")
Text(release.body) Text(release.modifiedBody)
} }
} }
@@ -99,15 +105,16 @@ func loadGithubReleases(appState: AppState, manual: Bool = false) {
if let data = data { if let data = data {
if let decodedResponse = try? JSONDecoder().decode([Release].self, from: data) { if let decodedResponse = try? JSONDecoder().decode([Release].self, from: data) {
DispatchQueue.main.async { DispatchQueue.main.async {
let lastFiveReleases = Array(decodedResponse.prefix(3)) // Get only the last 3 recent releases let lastFewReleases = Array(decodedResponse.prefix(3)) // Get only the last 3 recent releases
appState.releases = lastFiveReleases appState.releases = lastFewReleases
checkForUpdate(appState: appState, manual: manual) checkForUpdate(appState: appState, manual: manual)
} }
return return
} }
} }
print("Fetch failed: \(error?.localizedDescription ?? "Unknown error")") printOS("Fetch failed: \(error?.localizedDescription ?? "Unknown error")")
}.resume() }.resume()
} }
@@ -172,7 +179,7 @@ func downloadUpdate(appState: AppState) {
} catch { } catch {
print("Error moving downloaded file: \(error.localizedDescription)") printOS("Error moving downloaded file: \(error.localizedDescription)")
} }
} }
@@ -233,7 +240,7 @@ func UnzipAndReplace(DownloadedFileURL fileURL: String, appState: AppState) {
} catch { } catch {
print("Error updating the app: \(error)") printOS("Error updating the app: \(error)")
} }
} }
+4 -20
View File
@@ -21,7 +21,7 @@ struct AppListItems: View {
let itemId = UUID() let itemId = UUID()
let appInfo: AppInfo let appInfo: AppInfo
var isSelected: Bool { var isSelected: Bool {
appState.appInfo == appInfo appState.appInfo.path == appInfo.path
} }
var body: some View { var body: some View {
@@ -71,7 +71,7 @@ struct AppListItems: View {
// .background(Color("mode").opacity(0.1)) // .background(Color("mode").opacity(0.1))
// .clipShape(.capsule) // .clipShape(.capsule)
} }
Text(appInfo.appVersion) Text(appInfo.appVersion)
.font(.footnote) .font(.footnote)
.foregroundStyle(Color("mode").opacity(0.5)) .foregroundStyle(Color("mode").opacity(0.5))
@@ -95,25 +95,9 @@ struct AppListItems: View {
} }
} }
.onTapGesture { .onTapGesture {
showPopover = false showAppInFiles(appInfo: appInfo, mini: mini, appState: appState, locations: locations, showPopover: $showPopover)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
updateOnMain {
appState.appInfo = .empty
appState.appInfo = appInfo
// appState.progressManager.resetProgress()
findPathsForApp(appState: appState, locations: locations)
withAnimation(Animation.easeIn(duration: 0.4)) {
if mini {
showPopover.toggle()
} else {
appState.currentView = .files
}
}
}
}
} }
// .popover(isPresented: Binding( // .popover(isPresented: Binding(
// get: { showPopover && appState.appInfo.id == appInfo.id}, // get: { showPopover && appState.appInfo.id == appInfo.id},
// set: { _ in showPopover = false} // set: { _ in showPopover = false}
+15 -8
View File
@@ -45,7 +45,7 @@ struct AppListView: View {
if appState.reload { if appState.reload {
VStack { VStack {
Spacer() Spacer()
ProgressView("Refreshing applications") ProgressView("Loading apps and files")
Spacer() Spacer()
} }
.frame(width: sidebarWidth) .frame(width: sidebarWidth)
@@ -84,7 +84,7 @@ struct AppListView: View {
if filteredUserApps.count > 0 { if filteredUserApps.count > 0 {
VStack { VStack {
Header(title: "User", count: filteredUserApps.count) Header(title: "User", count: filteredUserApps.count, showPopover: $showPopover)
ForEach(filteredUserApps, id: \.self) { appInfo in ForEach(filteredUserApps, id: \.self) { appInfo in
AppListItems(search: $search, showPopover: $showPopover, appInfo: appInfo) AppListItems(search: $search, showPopover: $showPopover, appInfo: appInfo)
if appInfo != filteredUserApps.last { if appInfo != filteredUserApps.last {
@@ -98,7 +98,7 @@ struct AppListView: View {
if filteredSystemApps.count > 0 { if filteredSystemApps.count > 0 {
VStack { VStack {
Header(title: "System", count: filteredSystemApps.count) Header(title: "System", count: filteredSystemApps.count, showPopover: $showPopover)
ForEach(filteredSystemApps, id: \.self) { appInfo in ForEach(filteredSystemApps, id: \.self) { appInfo in
AppListItems(search: $search, showPopover: $showPopover, appInfo: appInfo) AppListItems(search: $search, showPopover: $showPopover, appInfo: appInfo)
if appInfo != filteredSystemApps.last { if appInfo != filteredSystemApps.last {
@@ -133,12 +133,16 @@ struct AppListView: View {
// Details View // Details View
VStack(spacing: 0) { VStack(spacing: 0) {
if appState.currentView == .empty || appState.currentView == .apps { if appState.currentView == .empty || appState.currentView == .apps {
TopBar() TopBar(showPopover: $showPopover)
AppDetailsEmptyView(showPopover: $showPopover) AppDetailsEmptyView(showPopover: $showPopover)
} else if appState.currentView == .files { } else if appState.currentView == .files {
TopBar() TopBar(showPopover: $showPopover)
FilesView(showPopover: $showPopover, search: $search) FilesView(showPopover: $showPopover, search: $search)
.id(appState.appInfo.id) .id(appState.appInfo.id)
} else if appState.currentView == .zombie {
TopBar(showPopover: $showPopover)
ZombieView(showPopover: $showPopover, search: $search)
.id(appState.appInfo.id)
} }
} }
// .padding(.leading, appState.sidebar ? 0 : 10) // .padding(.leading, appState.sidebar ? 0 : 10)
@@ -161,10 +165,10 @@ struct AppListView: View {
// // // Check if the file URL has a ".app" extension // // // Check if the file URL has a ".app" extension
// // if url.pathExtension.lowercased() == "app" { // // if url.pathExtension.lowercased() == "app" {
// // // Handle the dropped file URL here // // // Handle the dropped file URL here
// // print("Dropped .app file URL: \(url)") // // printOS("Dropped .app file URL: \(url)")
// // } else { // // } else {
// // // Print a message for non-.app files // // // Print a message for non-.app files
// // print("Unsupported file type. Only .app files are accepted.") // // printOS("Unsupported file type. Only .app files are accepted.")
// // } // // }
// } // }
// } // }
@@ -240,7 +244,8 @@ struct Header: View {
let count: Int let count: Int
@State private var hovered = false @State private var hovered = false
@EnvironmentObject var appState: AppState @EnvironmentObject var appState: AppState
@EnvironmentObject var locations: Locations
@Binding var showPopover: Bool
var body: some View { var body: some View {
HStack { HStack {
@@ -259,10 +264,12 @@ struct Header: View {
withAnimation { withAnimation {
// Refresh Apps list // Refresh Apps list
appState.reload.toggle() appState.reload.toggle()
showPopover = false
let sortedApps = getSortedApps() let sortedApps = getSortedApps()
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
appState.sortedApps.userApps = sortedApps.userApps appState.sortedApps.userApps = sortedApps.userApps
appState.sortedApps.systemApps = sortedApps.systemApps appState.sortedApps.systemApps = sortedApps.systemApps
loadAllPaths(allApps: sortedApps.userApps + sortedApps.systemApps, appState: appState, locations: locations)
appState.reload.toggle() appState.reload.toggle()
} }
} }
+47 -13
View File
@@ -125,24 +125,58 @@ struct DropTarget: View, DropDelegate {
return return
} }
guard let url = URL(dataRepresentation: data, relativeTo: nil) else { guard let url = URL(dataRepresentation: data, relativeTo: nil) else {
print("Error: Not a valid URL.") printOS("Error: Not a valid URL.")
return return
} }
if url.pathExtension == "app" { if url.pathExtension == "app" {
let appInfo = getAppInfo(atPath: url) let appInfo = getAppInfo(atPath: url)
// showPopover = false
DispatchQueue.main.async { self.ants = false
self.ants = false showAppInFiles(appInfo: appInfo!, mini: mini, appState: appState, locations: locations, showPopover: $showPopover)
appState.appInfo = appInfo!
findPathsForApp(appState: appState, locations: locations) // DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
if mini { // updateOnMain {
showPopover = true // appState.appInfo = .empty
} else { // if let storedAppInfo = appState.appInfoStore.first(where: { $0.path == appInfo!.path }) {
appState.currentView = .files // appState.appInfo = storedAppInfo
} // // appState.paths = storedAppInfo.fileSize.keys.map { $0 }//storedAppInfo.files
} // appState.selectedItems = Set(storedAppInfo.files)
// withAnimation(Animation.easeIn(duration: 0.4)) {
// if mini {
// showPopover.toggle()
// } else {
// appState.currentView = .files
// }
// }
// } else {
// // Handle the case where the appInfo is not found in the store
// printOS("AppInfo not found in the cached store, searching again")
// appState.appInfo = .empty
// appState.appInfo = appInfo!
// findPathsForApp(appInfo: appInfo!, appState: appState, locations: locations)
// withAnimation(Animation.easeIn(duration: 0.4)) {
// if mini {
// showPopover.toggle()
// } else {
// appState.currentView = .files
// }
// }
// }
// }
//
// }
// DispatchQueue.main.async {
// self.ants = false
// appState.appInfo = appInfo!
// findPathsForApp(appState: appState, locations: locations)
// if mini {
// showPopover = true
// } else {
// appState.currentView = .files
// }
// }
} else { } else {
print("Error: Dropped file is not an application bundle") printOS("Error: Dropped file is not an application bundle")
DispatchQueue.main.async { DispatchQueue.main.async {
self.shouldFlash = true self.shouldFlash = true
} }
+107 -84
View File
@@ -11,25 +11,24 @@ import SwiftUI
struct FilesView: View { struct FilesView: View {
@EnvironmentObject var appState: AppState @EnvironmentObject var appState: AppState
@EnvironmentObject var locations: Locations @EnvironmentObject var locations: Locations
@State private var appSize: String = ""
@State private var showDetails: Bool = false
@State private var showPop: Bool = false @State private var showPop: Bool = false
@State private var itemDetails: [(size: String, icon: Image?)] = []
@AppStorage("settings.general.mini") private var mini: Bool = false @AppStorage("settings.general.mini") private var mini: Bool = false
@AppStorage("settings.sentinel.enable") private var sentinel: Bool = false @AppStorage("settings.sentinel.enable") private var sentinel: Bool = false
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme
@Binding var showPopover: Bool @Binding var showPopover: Bool
@Binding var search: String @Binding var search: String
@State private var selectedOption = "Default"
@State private var toggles: Bool = true
var body: some View { var body: some View {
VStack(alignment: .center) { VStack(alignment: .center) {
if !self.showDetails { if appState.showProgress { //!self.showDetails {
VStack { VStack {
Spacer() Spacer()
// ProgressView("Finding application files..") // ProgressView("Finding application files..")
// .progressViewStyle(.linear) // .progressViewStyle(.linear)
// Spacer() // Spacer()
Text("Finding application files..").font(.title3) Text("Almost there, still gathering files..").font(.title3)
.foregroundStyle((.gray.opacity(0.8))) .foregroundStyle((.gray.opacity(0.8)))
ProgressView() ProgressView()
.progressViewStyle(.linear) .progressViewStyle(.linear)
@@ -71,8 +70,9 @@ struct FilesView: View {
Spacer() Spacer()
Text("\(appSize)").font(.title).fontWeight(.bold) Text("\(formatByte(size: appState.appInfo.totalSize))").font(.title).fontWeight(.bold)
// .foregroundStyle(Color("AccentColor"))
} }
@@ -84,7 +84,7 @@ struct FilesView: View {
// .foregroundStyle(Color("AccentColor")) // .foregroundStyle(Color("AccentColor"))
.opacity(0.8) .opacity(0.8)
Spacer() Spacer()
Text("\(appState.paths.count > 1 ? "\(appState.paths.count) items" : "\(appState.paths.count) item")").font(.callout) Text("\(appState.appInfo.fileSize.count > 1 ? "\(appState.appInfo.fileSize.count) items" : "\(appState.appInfo.fileSize.count) item")").font(.callout)
// .foregroundStyle(Color("AccentColor").opacity(0.7)) // .foregroundStyle(Color("AccentColor").opacity(0.7))
.underline() .underline()
} }
@@ -142,31 +142,70 @@ struct FilesView: View {
) )
HStack(alignment: .center) {
Spacer()
Text("\(toggles ? "Selected: All" : "Selected: None")").font(.subheadline)
Toggle("", isOn: $toggles)
.onChange(of: toggles) { value in
if value {
updateOnMain {
appState.selectedItems = Set(appState.appInfo.files)
}
} else {
updateOnMain {
appState.selectedItems.removeAll()
}
}
}
}
.padding(.top)
ScrollView() { ScrollView() {
VStack { VStack {
ForEach(Array(zip(appState.paths, itemDetails)), id: \.0) { path, details in let sortedFilesSize = appState.appInfo.files.sorted(by: { appState.appInfo.fileSize[$0, default: 0] > appState.appInfo.fileSize[$1, default: 0] })
if let firstPath = appState.paths.first, path == firstPath {
FileDetailsItem(size: details.size, icon: details.icon, path: path) // let sortedFilesAlpha = appState.appInfo.files.sorted(by: { $0.lastPathComponent < $1.lastPathComponent })
.padding(.trailing)
Divider().padding(.leading, 40).padding(.trailing) let sort = selectedOption == "Default" ? appState.appInfo.files : sortedFilesSize
} else {
ForEach(sort, id: \.self) { path in
if let fileSize = appState.appInfo.fileSize[path], let fileIcon = appState.appInfo.fileIcon[path] {
let iconImage = fileIcon.map(Image.init(nsImage:))
VStack { VStack {
FileDetailsItem(size: details.size, icon: details.icon, path: path) FileDetailsItem(size: fileSize, icon: iconImage, path: path)
if path != appState.paths.last { if path != appState.appInfo.files.last {
Divider().padding(.leading, 40) Divider().padding(.leading, 40)
} }
} }
.padding(.leading, 47).padding(.trailing) // .padding(.leading, 47).padding(.trailing)
// if let firstPath = appState.appInfo.files.first, path == firstPath {
// FileDetailsItem(size: fileSize, icon: iconImage, path: path)
// .padding(.trailing)
// Divider().padding(.leading, 40).padding(.trailing)
// } else {
// VStack {
// FileDetailsItem(size: fileSize, icon: iconImage, path: path)
// if path != appState.appInfo.files.last {
// Divider().padding(.leading, 40)
// }
// }
// .padding(.leading, 47).padding(.trailing)
// }
} }
} }
} }
} }
.padding() .padding([.bottom])
HStack() { HStack() {
Picker(selection: $selectedOption, label: Text("Sort")) {
Text("Default").tag("Default")
Text("Size").tag("Size")
}
.pickerStyle(SegmentedPickerStyle())
.frame(width: 150)
Spacer() Spacer()
if mini { if mini {
@@ -186,8 +225,8 @@ struct FilesView: View {
Task { Task {
updateOnMain { updateOnMain {
appState.appInfo = AppInfo.empty appState.appInfo = AppInfo.empty
search = ""
if mini { if mini {
search = ""
appState.currentView = .apps appState.currentView = .apps
showPopover = false showPopover = false
} else { } else {
@@ -195,15 +234,32 @@ struct FilesView: View {
} }
} }
let selectedItemsArray = Array(appState.selectedItems) var selectedItemsArray = Array(appState.selectedItems)
.filter { !$0.path.contains(".Trash") } .filter { !$0.path.contains(".Trash") }
.map { path in .map { path in
return path.path.contains("Wrapper") ? path.deletingLastPathComponent().deletingLastPathComponent() : path return path.path.contains("Wrapper") ? path.deletingLastPathComponent().deletingLastPathComponent() : path
} }
if let url = URL(string: appState.appInfo.path.absoluteString) {
let appFolderURL = url.deletingLastPathComponent() // Get the immediate parent directory
if appFolderURL.path == "/Applications" || appFolderURL.path == "\(home)/Applications" {
// Do nothing, skip insertion
} else if appFolderURL.pathComponents.count > 2 {
// Insert into selectedItemsArray only if there is an intermediary folder
selectedItemsArray.insert(appFolderURL, at: 0)
}
}
// Save trashed files for undo operation
// updateOnMain {
// appState.trashedFiles = selectedItemsArray
// }
killApp(appId: appState.appInfo.bundleIdentifier) { killApp(appId: appState.appInfo.bundleIdentifier) {
moveFilesToTrash(at: selectedItemsArray) { moveFilesToTrash(at: selectedItemsArray) {
withAnimation { withAnimation {
showPopover = false
updateOnMain { updateOnMain {
appState.isReminderVisible.toggle() appState.isReminderVisible.toggle()
if sentinel { if sentinel {
@@ -230,49 +286,10 @@ struct FilesView: View {
} }
} }
// .frame(minWidth: 500, minHeight: 500)
.onAppear {
Task {
calculateFileDetails()
}
}
} }
func calculateFileDetails() {
if appState.paths.count != 0 {
itemDetails = Array(repeating: (size: "", icon: nil), count: appState.paths.count)
Task {
for (index, path) in appState.paths.enumerated() {
var size = ""
var icon: Image? = nil
if let appSize = totalSizeOnDisk(for: path) {
size = "\(appSize)"
} else {
print("Error calculating the total size on disk for item \(index).")
}
if let folderIcon = getIconForFileOrFolder(atPath: path) {
icon = folderIcon
}
itemDetails[index] = (size: size, icon: icon)
}
if let appSize = totalSizeOnDisk(for: appState.paths) {
self.appSize = "\(appSize)"
self.showDetails = true
} else {
print("Error calculating the total size on disk.")
}
}
} else {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
calculateFileDetails()
}
}
}
func refreshAppList(_ appInfo: AppInfo) { func refreshAppList(_ appInfo: AppInfo) {
showPopover = false
let sortedApps = getSortedApps() let sortedApps = getSortedApps()
updateOnMain { updateOnMain {
appState.sortedApps.userApps = [] appState.sortedApps.userApps = []
@@ -280,7 +297,9 @@ struct FilesView: View {
appState.sortedApps.userApps = sortedApps.userApps appState.sortedApps.userApps = sortedApps.userApps
appState.sortedApps.systemApps = sortedApps.systemApps appState.sortedApps.systemApps = sortedApps.systemApps
} }
Task(priority: .high){
loadAllPaths(allApps: sortedApps.userApps + sortedApps.systemApps, appState: appState, locations: locations)
}
} }
} }
@@ -288,16 +307,10 @@ struct FilesView: View {
struct FileDetailsItem: View { struct FileDetailsItem: View {
@EnvironmentObject var appState: AppState @EnvironmentObject var appState: AppState
let size: String let size: Int64?
let icon: Image? let icon: Image?
let path: URL let path: URL
// init(size: String, icon: Image?, path: URL) {
// self.size = size
// self.icon = icon
// self.path = path.path.contains("Wrapper") ? path.deletingLastPathComponent().deletingLastPathComponent() : path
// }
var body: some View { var body: some View {
HStack(alignment: .center, spacing: 20) { HStack(alignment: .center, spacing: 20) {
@@ -306,19 +319,31 @@ struct FileDetailsItem: View {
set: { isChecked in set: { isChecked in
if isChecked { if isChecked {
self.appState.selectedItems.insert(self.path) self.appState.selectedItems.insert(self.path)
if self.path == appState.appInfo.path {
self.appState.paths.forEach { self.appState.selectedItems.insert($0) }
}
} else { } else {
self.appState.selectedItems.remove(self.path) self.appState.selectedItems.remove(self.path)
if self.path == appState.appInfo.path {
self.appState.selectedItems.forEach { self.appState.selectedItems.remove($0) }
}
} }
} }
// get: { self.appState.selectedItems.contains(self.path) },
// set: { isChecked in
// if isChecked {
// self.appState.selectedItems.insert(self.path)
// if self.path == appState.appInfo.path {
// self.appState.appInfo.fileSize.keys.forEach {
// self.appState.selectedItems.insert($0)
// }
// }
// } else {
// self.appState.selectedItems.remove(self.path)
// if self.path == appState.appInfo.path {
// self.appState.selectedItems.forEach {
// self.appState.selectedItems.remove($0)
// }
// }
// }
// }
)) ))
.disabled(self.path.path.contains(".Trash")) .disabled(self.path.path.contains(".Trash"))
if let appIcon = icon { if let appIcon = icon {
appIcon appIcon
.resizable() .resizable()
@@ -343,12 +368,10 @@ struct FileDetailsItem: View {
} }
Spacer() Spacer()
if size.isEmpty {
ProgressView().controlSize(.small) Text(formatByte(size:size!))
} else {
Text(size)
}
Button("") { Button("") {
NSWorkspace.shared.selectFile(path.path, inFileViewerRootedAtPath: path.deletingLastPathComponent().path) NSWorkspace.shared.selectFile(path.path, inFileViewerRootedAtPath: path.deletingLastPathComponent().path)
+20 -11
View File
@@ -30,19 +30,28 @@ struct MiniMode: View {
MiniEmptyView(showPopover: $showPopover) MiniEmptyView(showPopover: $showPopover)
} else if appState.currentView == .files { } else if appState.currentView == .files {
TopBarMini(search: $search, showPopover: $showPopover) TopBarMini(search: $search, showPopover: $showPopover)
FilesView(showPopover: $showPopover, search: $search) MiniAppView(search: $search, showPopover: $showPopover)
.id(appState.appInfo.id) } else if appState.currentView == .zombie {
TopBarMini(search: $search, showPopover: $showPopover)
MiniAppView(search: $search, showPopover: $showPopover)
} else if appState.currentView == .apps { } else if appState.currentView == .apps {
TopBarMini(search: $search, showPopover: $showPopover) TopBarMini(search: $search, showPopover: $showPopover)
MiniAppView(search: $search, showPopover: $showPopover) MiniAppView(search: $search, showPopover: $showPopover)
} }
} }
// .padding(.leading, appState.sidebar ? 0 : 10) // .padding(.leading, appState.sidebar ? 0 : 10)
.transition(.move(edge: .leading)) .transition(.move(edge: .leading))
.popover(isPresented: $showPopover, arrowEdge: .trailing) { .popover(isPresented: $showPopover, arrowEdge: .trailing) {
VStack { VStack {
FilesView(showPopover: $showPopover, search: $search) if appState.currentView == .files {
.id(appState.appInfo.id) FilesView(showPopover: $showPopover, search: $search)
.id(appState.appInfo.id)
} else if appState.currentView == .zombie {
ZombieView(showPopover: $showPopover, search: $search)
.id(appState.appInfo.id)
}
} }
.interactiveDismissDisabled(popoverStay) .interactiveDismissDisabled(popoverStay)
.background( .background(
@@ -50,7 +59,7 @@ struct MiniMode: View {
.fill(Color("pop")) .fill(Color("pop"))
.padding(-80) .padding(-80)
) )
.frame(minWidth: 650, minHeight: 500) .frame(width: 650, height: 500)
} }
@@ -73,10 +82,10 @@ struct MiniMode: View {
//// // Check if the file URL has a ".app" extension //// // Check if the file URL has a ".app" extension
//// if url.pathExtension.lowercased() == "app" { //// if url.pathExtension.lowercased() == "app" {
//// // Handle the dropped file URL here //// // Handle the dropped file URL here
//// print("Dropped .app file URL: \(url)") //// printOS("Dropped .app file URL: \(url)")
//// } else { //// } else {
//// // Print a message for non-.app files //// // Print a message for non-.app files
//// print("Unsupported file type. Only .app files are accepted.") //// printOS("Unsupported file type. Only .app files are accepted.")
//// } //// }
// } // }
// } // }
@@ -178,7 +187,7 @@ struct MiniAppView: View {
if appState.reload { if appState.reload {
VStack { VStack {
Spacer() Spacer()
ProgressView("Refreshing applications") ProgressView("Loadings apps and files")
Spacer() Spacer()
} }
.padding(.vertical) .padding(.vertical)
@@ -216,7 +225,7 @@ struct MiniAppView: View {
if filteredUserApps.count > 0 { if filteredUserApps.count > 0 {
VStack { VStack {
Header(title: "User", count: filteredUserApps.count) Header(title: "User", count: filteredUserApps.count, showPopover: $showPopover)
ForEach(filteredUserApps, id: \.self) { appInfo in ForEach(filteredUserApps, id: \.self) { appInfo in
AppListItems(search: $search, showPopover: $showPopover, appInfo: appInfo) AppListItems(search: $search, showPopover: $showPopover, appInfo: appInfo)
if appInfo != filteredUserApps.last { if appInfo != filteredUserApps.last {
@@ -230,7 +239,7 @@ struct MiniAppView: View {
if filteredSystemApps.count > 0 { if filteredSystemApps.count > 0 {
VStack { VStack {
Header(title: "System", count: filteredSystemApps.count) Header(title: "System", count: filteredSystemApps.count, showPopover: $showPopover)
ForEach(filteredSystemApps, id: \.self) { appInfo in ForEach(filteredSystemApps, id: \.self) { appInfo in
AppListItems(search: $search, showPopover: $showPopover, appInfo: appInfo) AppListItems(search: $search, showPopover: $showPopover, appInfo: appInfo)
if appInfo != filteredSystemApps.last { if appInfo != filteredSystemApps.last {
@@ -246,7 +255,7 @@ struct MiniAppView: View {
} }
.scrollIndicators(.never) .scrollIndicators(.never)
if appState.currentView == .apps { if appState.currentView != .empty {
SearchBarMiniBottom(search: $search) SearchBarMiniBottom(search: $search)
} }
} }
+32 -8
View File
@@ -15,12 +15,14 @@ struct TopBar: View {
@AppStorage("settings.sentinel.enable") private var sentinel: Bool = false @AppStorage("settings.sentinel.enable") private var sentinel: Bool = false
@AppStorage("settings.general.mini") private var mini: Bool = false @AppStorage("settings.general.mini") private var mini: Bool = false
@EnvironmentObject var appState: AppState @EnvironmentObject var appState: AppState
@Binding var showPopover: Bool
@EnvironmentObject var locations: Locations
var body: some View { var body: some View {
HStack(alignment: .center, spacing: 20) { HStack(alignment: .center, spacing: 10) {
Spacer() Spacer()
if appState.isReminderVisible { if appState.isReminderVisible {
Text("CMD + Z to undo") Text("CMD + Z to undo")
.font(.title2) .font(.title2)
@@ -34,10 +36,10 @@ struct TopBar: View {
} }
} }
} }
Spacer() Spacer()
if appState.currentView != .empty || appState.currentView != .apps { if appState.currentView != .empty {//|| appState.currentView != .apps {
Button("") { Button("") {
withAnimation(.easeInOut(duration: 0.5)) { withAnimation(.easeInOut(duration: 0.5)) {
appState.currentView = .empty appState.currentView = .empty
@@ -45,8 +47,30 @@ struct TopBar: View {
} }
} }
.buttonStyle(SimpleButtonStyle(icon: "house", help: "Home", color: Color("mode"))) .buttonStyle(SimpleButtonStyle(icon: "house", help: "Home", color: Color("mode")))
} }
if appState.currentView != .zombie {
Button("") {
withAnimation(.easeInOut(duration: 0.5)) {
updateOnMain {
if appState.zombieFile.fileSize.keys.count == 0 {
appState.currentView = .zombie
appState.showProgress.toggle()
showPopover.toggle()
reversePathsSearch(appState: appState, locations: locations)
} else {
appState.currentView = .zombie
showPopover.toggle()
}
}
}
}
.buttonStyle(SimpleButtonStyle(icon: "clock.arrow.circlepath", help: "Leftover Files", color: Color("mode")))
}
// Spacer() // Spacer()
+42 -37
View File
@@ -15,6 +15,7 @@ struct TopBarMini: View {
@Binding var search: String @Binding var search: String
@Binding var showPopover: Bool @Binding var showPopover: Bool
@EnvironmentObject var appState: AppState @EnvironmentObject var appState: AppState
@EnvironmentObject var locations: Locations
var body: some View { var body: some View {
HStack(alignment: .center, spacing: 5) { HStack(alignment: .center, spacing: 5) {
@@ -42,28 +43,29 @@ struct TopBarMini: View {
} }
} }
.buttonStyle(SimpleButtonStyle(icon: "list.dash", help: "Apps List", color: Color("mode"))) .buttonStyle(SimpleButtonStyle(icon: "list.dash", help: "Apps List", color: Color("mode")))
} else if appState.currentView == .files { }
Button("") { // else if appState.currentView == .files {
withAnimation(.easeInOut(duration: 0.5)) { // Button("") {
// updateOnMain { // withAnimation(.easeInOut(duration: 0.5)) {
appState.currentView = .empty // // updateOnMain {
appState.appInfo = AppInfo.empty // appState.currentView = .empty
// } // appState.appInfo = AppInfo.empty
// // }
//
// }
// }
// .buttonStyle(SimpleButtonStyle(icon: "plus.square.dashed", help: "Drop Target", color: Color("mode")))
// Button("") {
// withAnimation(.easeInOut(duration: 0.5)) {
// // updateOnMain {
// appState.currentView = .apps
// // }
// }
// }
// .buttonStyle(SimpleButtonStyle(icon: "list.dash", help: "Apps List", color: Color("mode")))
// }
} if appState.currentView != .empty {
}
.buttonStyle(SimpleButtonStyle(icon: "plus.square.dashed", help: "Drop Target", color: Color("mode")))
Button("") {
withAnimation(.easeInOut(duration: 0.5)) {
// updateOnMain {
appState.currentView = .apps
// }
}
}
.buttonStyle(SimpleButtonStyle(icon: "list.dash", help: "Apps List", color: Color("mode")))
}
if appState.currentView == .apps {
HStack { HStack {
Spacer() Spacer()
@@ -84,22 +86,24 @@ struct TopBarMini: View {
} }
.buttonStyle(SimpleButtonStyle(icon: "plus.square.dashed", help: "Drop Target", color: Color("mode"))) .buttonStyle(SimpleButtonStyle(icon: "plus.square.dashed", help: "Drop Target", color: Color("mode")))
// Spacer() Button("") {
withAnimation(.easeInOut(duration: 0.5)) {
updateOnMain {
if appState.zombieFile.fileSize.keys.count == 0 {
appState.currentView = .zombie
appState.showProgress.toggle()
showPopover.toggle()
reversePathsSearch(appState: appState, locations: locations)
} else {
appState.currentView = .zombie
showPopover.toggle()
}
}
}
}
.buttonStyle(SimpleButtonStyle(icon: "clock.arrow.circlepath", help: "Leftover Files", color: Color("mode")))
// Button("") {
// withAnimation {
// // Refresh Apps list
// appState.reload.toggle()
// let sortedApps = getSortedApps()
// DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
// appState.sortedApps.userApps = sortedApps.userApps
// appState.sortedApps.systemApps = sortedApps.systemApps
// appState.reload.toggle()
// }
// }
// }
// .buttonStyle(SimpleButtonStyle(icon: "arrow.circlepath", help: "Refresh app list", color: Color("mode")))
// .padding(.leading, 5)
} }
} }
@@ -127,10 +131,11 @@ struct SearchBarMini: View {
struct SearchBarMiniBottom: View { struct SearchBarMiniBottom: View {
@Binding var search: String @Binding var search: String
@EnvironmentObject var appState: AppState
var body: some View { var body: some View {
HStack { HStack {
TextField("Search", text: $search) TextField(" Search", text: $search)
.textFieldStyle(SimpleSearchStyle(trash: true, text: $search)) .textFieldStyle(SimpleSearchStyle(trash: true, text: $search))
} }
.padding(.horizontal) .padding(.horizontal)
+310
View File
@@ -0,0 +1,310 @@
//
// ZombieView.swift
// Pearcleaner
//
// Created by Alin Lupascu on 2/26/24.
//
import Foundation
import SwiftUI
struct ZombieView: View {
@EnvironmentObject var appState: AppState
@EnvironmentObject var locations: Locations
@State private var showPop: Bool = false
@AppStorage("settings.general.mini") private var mini: Bool = false
@AppStorage("settings.sentinel.enable") private var sentinel: Bool = false
@Environment(\.colorScheme) var colorScheme
@Binding var showPopover: Bool
@Binding var search: String
@State private var toggles: Bool = true
var body: some View {
VStack(alignment: .center) {
if appState.showProgress { //!self.showDetails {
VStack {
Spacer()
// ProgressView("Finding application files..")
// .progressViewStyle(.linear)
// Spacer()
Text("Gathering leftover files, this might take a while..").font(.title3)
.foregroundStyle((.gray.opacity(0.8)))
ProgressView()
.progressViewStyle(.linear)
.frame(width: 400, height: 10)
Spacer()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.transition(.opacity)
} else {
VStack() {
// Main Group
HStack() {
//icon
// if let appIcon = appState.appInfo.appIcon {
// Image(nsImage: appIcon)
// .resizable()
// .scaledToFit()
// // .aspectRatio(contentMode: .fit)
// .frame(width: 50, height: 50)
// .clipShape(RoundedRectangle(cornerRadius: 8))
// .padding(.leading)
// }
//app title, size and items
VStack(alignment: .center) {
HStack(alignment: .center) {
VStack(alignment: .leading, spacing: 5){
HStack(alignment: .center) {
Text("Leftover Files").font(.title).fontWeight(.bold)
// .foregroundStyle(Color("AccentColor"))
// Text("").foregroundStyle(Color("AccentColor"))
// Text("\(appState.appInfo.appVersion)").font(.title3)
// .foregroundStyle(.gray.opacity(0.8))
}
// Text("\(appState.appInfo.bundleIdentifier)").font(.title3)
// .foregroundStyle((.gray.opacity(0.8)))
}
Spacer()
Text("\(formatByte(size: appState.zombieFile.totalSize))").font(.title).fontWeight(.bold)
}
Divider().padding(.bottom, 7)
HStack{
Text("Files and folders remaining from previously installed applications")
.font(.callout)
// .foregroundStyle(Color("AccentColor"))
.opacity(0.8)
Spacer()
Text("\(appState.zombieFile.fileSize.count > 1 ? "\(appState.zombieFile.fileSize.count) items" : "\(appState.zombieFile.fileSize.count) item")").font(.callout)
// .foregroundStyle(Color("AccentColor").opacity(0.7))
.underline()
}
HStack() {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundStyle(.red)
.popover(isPresented: $showPop, arrowEdge: .top) {
VStack() {
Text("Leftover file search is not 100% accurate as it doesn't have any app bundles to check against.\nThis searches for files/folders and excludes the ones that have overlap with your currently installed apps. \nMake sure to confirm files marked for removal are correct.")
.padding()
.font(.title2)
}
}
Text("Warning")
.foregroundStyle(Color.red)
Spacer()
Toggle("\(toggles ? "Selected: All" : "Selected: None")", isOn: $toggles)
.controlSize(.small)
// .toggleStyle(.switch)
.onChange(of: toggles) { value in
if value {
updateOnMain {
appState.selectedZombieItems = Set(appState.zombieFile.fileSize.keys)
}
} else {
updateOnMain {
appState.selectedZombieItems.removeAll()
}
}
}
}
.padding(.top)
.onTapGesture {
showPop = true
}
}
.padding(20)
}
// .padding(.horizontal)
.background(
RoundedRectangle(cornerRadius: 8)
// .strokeBorder(Color("AccentColor"), lineWidth: 0.5)
.fill(Color("mode").opacity(colorScheme == .dark ? 0.05 : 0.05))
// .background(
// RoundedRectangle(cornerRadius: 8)
// .strokeBorder(Color("AccentColor").opacity(colorScheme == .dark ? 0.1 : 0.1), lineWidth: 1)
// )
)
ScrollView() {
LazyVStack {
ForEach(appState.zombieFile.fileSize.keys.sorted(by: { $0.lastPathComponent < $1.lastPathComponent }), id: \.self) { file in
if let fileSize = appState.zombieFile.fileSize[file], let fileIcon = appState.zombieFile.fileIcon[file] {
let iconImage = fileIcon.map(Image.init(nsImage:))
ZombieFileDetailsItem(size: fileSize, icon: iconImage, path: file)
.padding(.trailing)
if file != appState.zombieFile.fileSize.keys.sorted(by: { $0.absoluteString < $1.absoluteString }).last {
Divider().padding(.leading, 40)
}
}
}
}
}
.padding()
HStack() {
Spacer()
if mini {
Button("Close") {
updateOnMain {
appState.appInfo = AppInfo.empty
search = ""
appState.currentView = .apps
showPopover = false
}
}
}
Button("Rescan") {
updateOnMain {
appState.zombieFile = .empty
appState.showProgress.toggle()
reversePathsSearch(appState: appState, locations: locations)
}
}
Button("Remove") {
Task {
updateOnMain {
appState.zombieFile = .empty
search = ""
if mini {
appState.currentView = .apps
showPopover = false
} else {
appState.currentView = .empty
}
}
let selectedItemsArray = Array(appState.selectedZombieItems)
.filter { !$0.path.contains(".Trash") }
killApp(appId: appState.appInfo.bundleIdentifier) {
moveFilesToTrash(at: selectedItemsArray) {
withAnimation {
showPopover = false
updateOnMain {
appState.isReminderVisible.toggle()
if sentinel {
launchctl(load: true)
}
}
}
}
}
}
}
.disabled(appState.selectedZombieItems.isEmpty)
}
}
.transition(.opacity)
.padding(20)
}
}
}
func refreshAppList(_ appInfo: AppInfo) {
showPopover = false
let sortedApps = getSortedApps()
updateOnMain {
appState.sortedApps.userApps = []
appState.sortedApps.systemApps = []
appState.sortedApps.userApps = sortedApps.userApps
appState.sortedApps.systemApps = sortedApps.systemApps
}
Task(priority: .high){
loadAllPaths(allApps: sortedApps.userApps + sortedApps.systemApps, appState: appState, locations: locations)
}
}
}
struct ZombieFileDetailsItem: View {
@EnvironmentObject var appState: AppState
let size: Int64?
let icon: Image?
let path: URL
var body: some View {
HStack(alignment: .center, spacing: 20) {
Toggle("", isOn: Binding(
get: { self.appState.selectedZombieItems.contains(self.path) },
set: { isChecked in
if isChecked {
self.appState.selectedZombieItems.insert(self.path)
} else {
self.appState.selectedZombieItems.remove(self.path)
}
}
))
if let appIcon = icon {
appIcon
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 30, height: 30)
.clipShape(RoundedRectangle(cornerRadius: 8))
}
VStack(alignment: .leading, spacing: 5) {
Text(path.lastPathComponent)
.font(.title3)
.lineLimit(1)
.truncationMode(.tail)
.help(path.lastPathComponent)
Text(path.path)
.font(.footnote)
.lineLimit(2)
.truncationMode(.tail)
.opacity(0.5)
// .foregroundStyle(Color("AccentColor"))
.help(path.path)
}
Spacer()
Text(formatByte(size:size!))
Button("") {
NSWorkspace.shared.selectFile(path.path, inFileViewerRootedAtPath: path.deletingLastPathComponent().path)
}
.buttonStyle(SimpleButtonStyle(icon: "folder.fill", help: "Show in Finder", color: Color("mode")))
}
.background(
RoundedRectangle(cornerRadius: 10)
.fill(Color.white.opacity(0))
)
}
}
+4 -4
View File
@@ -108,7 +108,7 @@ func writeLog(string: String) {
// //
// return appFiles.map { $0.path } // return appFiles.map { $0.path }
// } catch { // } catch {
// print("Failed to get contents of trash directory: \(error)") // printOS("Failed to get contents of trash directory: \(error)")
// return [] // return []
// } // }
//} //}
@@ -126,13 +126,13 @@ func writeLog(string: String) {
//if hasAccessToTrashFolder() { //if hasAccessToTrashFolder() {
// writeLog(string: "Your tool has access to the Trash folder.") // writeLog(string: "Your tool has access to the Trash folder.")
// print("Your tool has access to the Trash folder.") // printOS("Your tool has access to the Trash folder.")
//} else { //} else {
// if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles") { // if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles") {
// NSWorkspace.shared.open(url) // NSWorkspace.shared.open(url)
// } // }
// writeLog(string: "Your tool does not have access to the Trash folder.") // writeLog(string: "Your tool does not have access to the Trash folder.")
// print("Your tool does not have access to the Trash folder.") // printOS("Your tool does not have access to the Trash folder.")
//} //}
@@ -280,6 +280,6 @@ func writeLog(string: String) {
// let fileManager = FileManager.default // let fileManager = FileManager.default
// let trashURLs = fileManager.urls(for: .trashDirectory, in: .userDomainMask) // let trashURLs = fileManager.urls(for: .trashDirectory, in: .userDomainMask)
// let trashContents = try? fileManager.contentsOfDirectory(at: trashURLs.first!, includingPropertiesForKeys: nil, options: []) // let trashContents = try? fileManager.contentsOfDirectory(at: trashURLs.first!, includingPropertiesForKeys: nil, options: [])
// print(trashContents as Any) // printOS(trashContents as Any)
// } // }
//} //}