mirror of
https://github.com/exituser/Pearcleaner.git
synced 2026-09-17 06:09:01 +00:00
Road to v3
This commit is contained in:
@@ -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 */,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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()
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||||
appState.sortedApps.userApps = []
|
appState.sortedApps.userApps = []
|
||||||
appState.sortedApps.systemApps = []
|
appState.sortedApps.systemApps = []
|
||||||
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
Label("Undo Removal", systemImage: "clear")
|
Label("Undo Removal", systemImage: "clear")
|
||||||
|
|||||||
@@ -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]
|
||||||
static let empty = AppInfo(id: UUID(), path: URL(fileURLWithPath: ""), bundleIdentifier: "", appName: "", appVersion: "", appIcon: nil, webApp: false, wrapped: false)
|
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: [:])
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
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] = [
|
||||||
|
|||||||
@@ -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!)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
// if let storedAppInfo = appState.appInfoStore.first(where: { $0.path == appInfo?.path }) {
|
||||||
|
// 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 {
|
||||||
appState.currentView = .files
|
printOS("No path query parameter found in the URL")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
print("No path query parameter found in the URL")
|
printOS("URL does not match the expected scheme and host")
|
||||||
}
|
|
||||||
} else {
|
|
||||||
print("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
|
||||||
|
// }
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
+372
-74
@@ -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
|
||||||
|
|
||||||
|
|
||||||
@@ -387,10 +358,17 @@ func findPathsForApp(appState: AppState, locations: Locations) {
|
|||||||
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
|
||||||
|
updatedAppInfo.fileSize = fileSize
|
||||||
|
updatedAppInfo.fileIcon = fileIcon
|
||||||
|
if !backgroundRun {
|
||||||
|
appState.appInfo = updatedAppInfo
|
||||||
appState.selectedItems = Set(sortedCollection)
|
appState.selectedItems = Set(sortedCollection)
|
||||||
// writeLog(string: "\n\nappStatePaths: \(appState.paths)")
|
}
|
||||||
// writeLog(string: "\n\nappStateSelected: \(appState.selectedItems)")
|
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
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -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")
|
|
||||||
//
|
func getCurrentTimestamp() -> String {
|
||||||
// // Replace the placeholder with the actual executable path
|
let dateFormatter = DateFormatter()
|
||||||
// plistContent = plistContent.replacingOccurrences(of: "__EXECUTABLE_PATH__", with: executableURL.path)
|
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS"
|
||||||
//
|
return dateFormatter.string(from: Date())
|
||||||
// // Create a temporary plist file with the updated content
|
}
|
||||||
// let temporaryPlistURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("com.alienator88.PearcleanerMonitor.plist")
|
|
||||||
//
|
|
||||||
// do {
|
// Execute sudo command
|
||||||
// try plistContent.write(to: temporaryPlistURL, atomically: false, encoding: .utf8)
|
//func executePrivilegedCommand(launchPath: String, arguments: [String]) {
|
||||||
// } catch {
|
|
||||||
// print("Error writing the temporary plist file: \(error)")
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
// let task = Process()
|
// let task = Process()
|
||||||
// task.launchPath = "/bin/launchctl"
|
// task.launchPath = launchPath
|
||||||
// task.arguments = ["unload", "-w", temporaryPlistURL.path]
|
// task.arguments = arguments
|
||||||
//
|
//
|
||||||
// let pipe = Pipe()
|
// var authorization: AuthorizationRef? = nil
|
||||||
// task.standardOutput = pipe
|
// let status = AuthorizationCreate(nil, nil, AuthorizationFlags(), &authorization)
|
||||||
// task.standardError = pipe
|
|
||||||
//
|
//
|
||||||
|
// if status == errAuthorizationSuccess {
|
||||||
|
// if let rightName = (kAuthorizationRightExecute as NSString).utf8String {
|
||||||
|
// let item = AuthorizationItem(name: rightName, valueLength: 0, value: nil, flags: 0)
|
||||||
|
// var items = [item]
|
||||||
|
// items.withUnsafeMutableBufferPointer { bufferPointer in
|
||||||
|
// var rights = AuthorizationRights(count: UInt32(bufferPointer.count), items: bufferPointer.baseAddress)
|
||||||
|
//
|
||||||
|
// let status = AuthorizationCopyRights(authorization!, &rights, nil, AuthorizationFlags([.extendRights, .interactionAllowed]), nil)
|
||||||
|
//
|
||||||
|
// if status == errAuthorizationSuccess {
|
||||||
// task.launch()
|
// task.launch()
|
||||||
//
|
|
||||||
//// let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
|
||||||
//// if let output = String(data: data, encoding: .utf8) {
|
|
||||||
//// print("Output: \(output)")
|
|
||||||
//// }
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//import FileWatcher
|
|
||||||
//
|
|
||||||
//// MARK: https://github.com/eonist/FileWatcher
|
|
||||||
//
|
|
||||||
//func fileW() {
|
|
||||||
// let filewatcher = FileWatcher([NSString(string: "~/.Trash").expandingTildeInPath])
|
|
||||||
// filewatcher.queue = DispatchQueue.global()
|
|
||||||
// filewatcher.callback = { event in
|
|
||||||
// print("Something happened here: " + event.path)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// filewatcher.start()
|
|
||||||
//}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//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
|
|
||||||
//}
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ struct GeneralSettingsTab: View {
|
|||||||
.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)
|
||||||
|
|||||||
@@ -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)")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -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}
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
appState.appInfo = appInfo!
|
showAppInFiles(appInfo: appInfo!, mini: mini, appState: appState, locations: locations, showPopover: $showPopover)
|
||||||
findPathsForApp(appState: appState, locations: locations)
|
|
||||||
if mini {
|
// DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
||||||
showPopover = true
|
// updateOnMain {
|
||||||
|
// appState.appInfo = .empty
|
||||||
|
// if let storedAppInfo = appState.appInfoStore.first(where: { $0.path == appInfo!.path }) {
|
||||||
|
// 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 {
|
||||||
appState.currentView = .files
|
printOS("Error: Dropped file is not an application bundle")
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
print("Error: Dropped file is not an application bundle")
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.shouldFlash = true
|
self.shouldFlash = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
if mini {
|
|
||||||
search = ""
|
search = ""
|
||||||
|
if mini {
|
||||||
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,16 +319,28 @@ 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"))
|
||||||
|
|
||||||
@@ -343,11 +368,9 @@ struct FileDetailsItem: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
if size.isEmpty {
|
|
||||||
ProgressView().controlSize(.small)
|
Text(formatByte(size:size!))
|
||||||
} else {
|
|
||||||
Text(size)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Button("") {
|
Button("") {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
if appState.currentView == .files {
|
||||||
FilesView(showPopover: $showPopover, search: $search)
|
FilesView(showPopover: $showPopover, search: $search)
|
||||||
.id(appState.appInfo.id)
|
.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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,9 +15,11 @@ 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()
|
||||||
|
|
||||||
@@ -37,7 +39,7 @@ 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()
|
||||||
|
|||||||
@@ -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,6 +131,7 @@ 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 {
|
||||||
|
|||||||
@@ -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))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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)
|
||||||
// }
|
// }
|
||||||
//}
|
//}
|
||||||
|
|||||||
Reference in New Issue
Block a user