mirror of
https://github.com/exituser/Pearcleaner.git
synced 2026-09-17 07:19:04 +00:00
Road to v3
This commit is contained in:
@@ -34,11 +34,24 @@ struct AppCommands: Commands {
|
||||
// Refresh Apps list
|
||||
appState.reload.toggle()
|
||||
let sortedApps = getSortedApps()
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
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)
|
||||
}
|
||||
updateOnMain {
|
||||
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: {
|
||||
Text("Refresh Apps")
|
||||
@@ -50,10 +63,6 @@ struct AppCommands: Commands {
|
||||
} label: {
|
||||
Text("Uninstall Pearcleaner")
|
||||
}
|
||||
// .keyboardShortcut("C", modifiers: .command)
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -65,10 +74,13 @@ struct AppCommands: Commands {
|
||||
{
|
||||
undoTrash(appState: appState) {
|
||||
let sortedApps = getSortedApps()
|
||||
appState.sortedApps.userApps = []
|
||||
appState.sortedApps.systemApps = []
|
||||
appState.sortedApps.userApps = sortedApps.userApps
|
||||
appState.sortedApps.systemApps = sortedApps.systemApps
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
appState.sortedApps.userApps = []
|
||||
appState.sortedApps.systemApps = []
|
||||
appState.sortedApps.userApps = sortedApps.userApps
|
||||
appState.sortedApps.systemApps = sortedApps.systemApps
|
||||
loadAllPaths(allApps: sortedApps.userApps + sortedApps.systemApps, appState: appState, locations: locations)
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Label("Undo Removal", systemImage: "clear")
|
||||
|
||||
@@ -13,9 +13,12 @@ let home = FileManager.default.homeDirectoryForCurrentUser.path
|
||||
class AppState: ObservableObject
|
||||
{
|
||||
@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 selectedItems = Set<URL>()
|
||||
@Published var selectedZombieItems = Set<URL>()
|
||||
@Published var trashedFiles: [URL] = []
|
||||
@Published var alertType = AlertType.off
|
||||
@Published var currentView = CurrentDetailsView.empty
|
||||
@Published var showAlert: Bool = false
|
||||
@@ -23,12 +26,11 @@ class AppState: ObservableObject
|
||||
@Published var isReminderVisible: Bool = false
|
||||
@Published var releases = [Release]()
|
||||
@Published var progressBar: (String, Double) = ("Ready", 0.0)
|
||||
// @Published var progressManager = ProgressManager()
|
||||
@Published var reload: Bool = false
|
||||
@Published var showProgress: Bool = false
|
||||
@Published var popCount: Int = 0
|
||||
|
||||
|
||||
|
||||
//Window
|
||||
// @Published var winWidth: CGFloat = 1020
|
||||
|
||||
init() {
|
||||
self.appInfo = AppInfo(
|
||||
@@ -39,45 +41,27 @@ class AppState: ObservableObject
|
||||
appVersion: "",
|
||||
appIcon: nil,
|
||||
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 path: URL
|
||||
let bundleIdentifier: String
|
||||
@@ -86,11 +70,40 @@ struct AppInfo: Identifiable, Hashable {
|
||||
let appIcon: NSImage?
|
||||
let webApp: Bool
|
||||
let wrapped: Bool
|
||||
var files: [URL]
|
||||
var fileSize: [URL:Int64]
|
||||
var fileIcon: [URL:NSImage?]
|
||||
var totalSize: Int64
|
||||
{
|
||||
return fileSize.values.reduce(0, +)
|
||||
}
|
||||
|
||||
|
||||
static let empty = AppInfo(id: UUID(), path: URL(fileURLWithPath: ""), bundleIdentifier: "", appName: "", appVersion: "", appIcon: nil, webApp: false, wrapped: false, files: [], fileSize: [:], fileIcon: [:])
|
||||
|
||||
static let empty = AppInfo(id: UUID(), path: URL(fileURLWithPath: ""), bundleIdentifier: "", appName: "", appVersion: "", appIcon: nil, webApp: false, wrapped: false)
|
||||
}
|
||||
|
||||
|
||||
|
||||
struct ZombieFile: Identifiable, Equatable, Hashable {
|
||||
let id: UUID
|
||||
// var files: [URL]
|
||||
var fileSize: [URL:Int64]
|
||||
var fileIcon: [URL:NSImage?]
|
||||
var totalSize: Int64
|
||||
{
|
||||
return fileSize.values.reduce(0, +)
|
||||
}
|
||||
|
||||
|
||||
static let empty = ZombieFile(id: UUID(), fileSize: [:], fileIcon: [:])
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
enum CurrentTabView:Int
|
||||
{
|
||||
case general
|
||||
@@ -115,6 +128,7 @@ enum CurrentDetailsView:Int
|
||||
case empty
|
||||
case files
|
||||
case apps
|
||||
case zombie
|
||||
}
|
||||
|
||||
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 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) {
|
||||
// This handles dropping an app onto Pearcleaner
|
||||
if url.pathExtension == "app" {
|
||||
handleAppBundle(url: url, appState: appState, locations: locations)
|
||||
// This handles sentinel monitor launch
|
||||
} else if url.scheme == DeepLinkConstants.scheme,
|
||||
url.host == DeepLinkConstants.host,
|
||||
let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
|
||||
@@ -32,57 +34,88 @@ class DeeplinkManager {
|
||||
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, locations: locations)
|
||||
if self.mini {
|
||||
self.showPopover = true
|
||||
} else {
|
||||
appState.currentView = .files
|
||||
}
|
||||
}
|
||||
showAppInFiles(appInfo: appInfo!, mini: mini, appState: appState, locations: locations, showPopover: $showPopover)
|
||||
|
||||
// showPopover = false
|
||||
// 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 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 {
|
||||
print("No path query parameter found in the URL")
|
||||
printOS("No path query parameter found in the URL")
|
||||
}
|
||||
} else {
|
||||
print("URL does not match the expected scheme and host")
|
||||
printOS("URL does not match the expected scheme and host")
|
||||
}
|
||||
}
|
||||
|
||||
// func manage(url: URL, appState: AppState) {
|
||||
// guard url.scheme == DeepLinkConstants.scheme,
|
||||
// url.host == DeepLinkConstants.host,
|
||||
// let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
|
||||
// let queryItems = components.queryItems
|
||||
// else {
|
||||
// print("URL does not match the expected scheme and host")
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// if let path = queryItems.first(where: { $0.name == DeepLinkConstants.query })?.value {
|
||||
// let pathURL = URL(fileURLWithPath: path)
|
||||
// let appInfo = getAppInfo(atPath: pathURL)
|
||||
// updateOnMain {
|
||||
// appState.appInfo = appInfo!
|
||||
// findPathsForApp(appState: appState, appInfo: appState.appInfo)
|
||||
// appState.currentView = .files
|
||||
// }
|
||||
// } else {
|
||||
// print("No path query parameter found in the URL")
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
func handleAppBundle(url: URL, appState: AppState, locations: Locations) {
|
||||
let appInfo = getAppInfo(atPath: url)
|
||||
updateOnMain {
|
||||
appState.appInfo = appInfo!
|
||||
findPathsForApp(appState: appState, locations: locations)
|
||||
if self.mini {
|
||||
self.showPopover = true
|
||||
} else {
|
||||
appState.currentView = .files
|
||||
}
|
||||
}
|
||||
showAppInFiles(appInfo: appInfo!, mini: mini, appState: appState, locations: locations, showPopover: $showPopover)
|
||||
|
||||
// showPopover = false
|
||||
// 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 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
|
||||
|
||||
var apps: Category
|
||||
var reverse: Category
|
||||
// var widgets: Category
|
||||
// var plugins: Category
|
||||
|
||||
@@ -32,10 +33,8 @@ class Locations: ObservableObject {
|
||||
"\(home)/Library",
|
||||
"\(home)/Library/Application Scripts",
|
||||
"\(home)/Library/Application Support",
|
||||
// "\(home)/Library/Application Support/CrashReporter",
|
||||
"\(home)/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.ApplicationRecentDocuments",
|
||||
"\(home)/Library/Containers",
|
||||
// "\(home)/Library/Group Containers", // This is now handled by the function getGroupContainers()
|
||||
"\(home)/Library/Caches",
|
||||
"\(home)/Library/HTTPStorages",
|
||||
"\(home)/Library/Internet Plug-Ins",
|
||||
@@ -75,9 +74,36 @@ class Locations: ObservableObject {
|
||||
let subfolders = listAppSupportDirectories()
|
||||
for folder in subfolders {
|
||||
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: [
|
||||
// // User
|
||||
// "~/Library/Widgets",
|
||||
|
||||
+375
-77
@@ -7,6 +7,7 @@
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
//import AudioToolbox
|
||||
|
||||
|
||||
// Get list of apps and sort it
|
||||
@@ -53,7 +54,7 @@ func getApplications() -> (systemApps: [URL], userApps: [URL]) {
|
||||
}
|
||||
} catch {
|
||||
// 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 {
|
||||
appVersion = bundleVersion
|
||||
} 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 {
|
||||
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 {
|
||||
@@ -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 {
|
||||
let wrapperURL = path.appendingPathComponent("Wrapper")
|
||||
@@ -150,18 +151,18 @@ func getAppInfo(atPath path: URL) -> AppInfo? {
|
||||
return wrappedAppInfo
|
||||
}
|
||||
} else {
|
||||
print("No .app files found in the 'Wrapper' directory.")
|
||||
printOS("No .app files found in the 'Wrapper' directory.")
|
||||
}
|
||||
} catch {
|
||||
print("Error reading contents of 'Wrapper' directory: \(error.localizedDescription)")
|
||||
printOS("Error reading contents of 'Wrapper' directory: \(error.localizedDescription)")
|
||||
}
|
||||
} else {
|
||||
print("Error: 'Wrapper' directory not found at path: \(wrapperURL.path)")
|
||||
printOS("Error: 'Wrapper' directory not found at path: \(wrapperURL.path)")
|
||||
}
|
||||
|
||||
}
|
||||
} else {
|
||||
print("Bundle not found at path: \(path)")
|
||||
printOS("Bundle not found at path: \(path)")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -201,10 +202,10 @@ func getWrappedAppInfo(atPath path: URL) -> AppInfo? {
|
||||
|
||||
|
||||
} else {
|
||||
print("No matching image found for \(primaryIconFile).")
|
||||
printOS("No matching image found for \(primaryIconFile).")
|
||||
}
|
||||
} 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 {
|
||||
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 {
|
||||
@@ -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 {
|
||||
print("One or more variables missing for app at path: \(path)")
|
||||
printOS("One or more variables missing for app at path: \(path)")
|
||||
}
|
||||
} else {
|
||||
print("Bundle not found at path: \(path)")
|
||||
printOS("Bundle not found at path: \(path)")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -263,28 +264,11 @@ func darwinCT() -> (String, String) {
|
||||
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 ("", "")
|
||||
}
|
||||
|
||||
|
||||
// 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] {
|
||||
let home = FileManager.default.homeDirectoryForCurrentUser
|
||||
let appSupportLocation = home.appendingPathComponent("Library/Application Support/")
|
||||
@@ -312,7 +296,7 @@ func listAppSupportDirectories() -> [String] {
|
||||
|
||||
return filteredDirectories
|
||||
} catch {
|
||||
print("Error listing AppSupport directories: \(error.localizedDescription)")
|
||||
printOS("Error listing AppSupport directories: \(error.localizedDescription)")
|
||||
return []
|
||||
}
|
||||
}
|
||||
@@ -334,19 +318,16 @@ func killApp(appId: String, completion: @escaping () -> Void = {}) {
|
||||
|
||||
|
||||
// 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) {
|
||||
updateOnMain {
|
||||
appState.paths = []
|
||||
}
|
||||
let appInfo = appState.appInfo
|
||||
|
||||
var collection: [URL] = []
|
||||
if let url = URL(string: appInfo.path.absoluteString) {
|
||||
collection.insert(url, at: 0)
|
||||
}
|
||||
|
||||
|
||||
|
||||
let fileManager = FileManager.default
|
||||
// let progressManager = appState.progressManager
|
||||
let dispatchGroup = DispatchGroup()
|
||||
var bundleComponents = appInfo.bundleIdentifier.components(separatedBy: ".")
|
||||
if let lastComponent = bundleComponents.last, let rangeOfDash = lastComponent.range(of: "-") {
|
||||
@@ -368,16 +349,6 @@ func findPathsForApp(appState: AppState, locations: Locations) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
// DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||
// withAnimation {
|
||||
// progressManager.updateStatus(status: location)
|
||||
// progressManager.updateProgress()
|
||||
// progressManager.objectWillChange.send()
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
dispatchGroup.enter() // Enter the dispatch group
|
||||
|
||||
|
||||
@@ -385,12 +356,19 @@ func findPathsForApp(appState: AppState, locations: Locations) {
|
||||
do {
|
||||
|
||||
let contents = try fileManager.contentsOfDirectory(atPath: location)
|
||||
|
||||
|
||||
for item in contents {
|
||||
|
||||
let itemURL = URL(fileURLWithPath: location).appendingPathComponent(item)
|
||||
let itemL = ("\(item)").replacingOccurrences(of: ".", with: "").replacingOccurrences(of: " ", with: "").lowercased()
|
||||
let filterItem = "\(location)/\(itemL)"
|
||||
|
||||
// Skip directories that start with com.apple. except for a few
|
||||
if itemL.hasPrefix("comapple") && !itemL.hasPrefix("comappleconfigurator") && !itemL.hasPrefix("comappledt") && !itemL.hasPrefix("comappleiwork") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip adding item if the collection already has it to prevent duplicates
|
||||
if collection.contains(itemURL) {
|
||||
continue
|
||||
}
|
||||
@@ -403,18 +381,23 @@ func findPathsForApp(appState: AppState, locations: Locations) {
|
||||
collection.append(itemURL)
|
||||
}
|
||||
} else {
|
||||
// Catch all the com.apple folders in the OS since there's a ton and probably unrelated
|
||||
if itemL.contains("comapple") {
|
||||
// Catch Xcode files for Xcodes app
|
||||
if itemL.contains("xcode") && bundleIdentifierL.contains("comappledt") {
|
||||
if filterItem.contains("comrobotsandpencilsxcodesapp") || filterItem.contains("comoneminutegamesxcodecleaner") || filterItem.contains("iohyperappxcodecleaner") || filterItem.contains("xcodesjson") {
|
||||
continue
|
||||
}
|
||||
if itemL.contains(bundle) || itemL.contains(bundleIdentifierL) || (nameL.count < 6 && itemL.contains(nameL)) {
|
||||
collection.append(itemURL)
|
||||
}
|
||||
} else if itemL.contains("xcodes") && bundleIdentifierL.contains("comrobotsandpencilsxcodesapp") {
|
||||
if filterItem.contains("comappledt") {
|
||||
continue
|
||||
}
|
||||
if itemL.contains(bundle) || itemL.contains(bundleIdentifierL) || (nameL.count > 4 && itemL.contains(nameL)) {
|
||||
collection.append(itemURL)
|
||||
}
|
||||
}
|
||||
// Catch Xcode files
|
||||
else if itemL.contains("xcode") {
|
||||
if itemL.contains(bundle) || itemL.contains(bundleIdentifierL) || (nameL.count > 4 && itemL.contains(nameL)) {
|
||||
collection.append(itemURL)
|
||||
}
|
||||
} else {
|
||||
else {
|
||||
if itemL.contains(bundleIdentifierL) || itemL.contains(bundle) || (nameL.count > 3 && itemL.contains(nameL) || (nameP.count > 3 && itemL.contains(nameP))) {
|
||||
collection.append(itemURL)
|
||||
}
|
||||
@@ -423,13 +406,10 @@ func findPathsForApp(appState: AppState, locations: Locations) {
|
||||
|
||||
}
|
||||
} catch {
|
||||
// writeLog(string: "Error processing location: \(location)\n\(error)")
|
||||
print("Error processing location:", location, error)
|
||||
printOS("Error processing location:", location, error)
|
||||
continue
|
||||
}
|
||||
|
||||
// try await Task.sleep(nanoseconds: 50_000_000)
|
||||
|
||||
dispatchGroup.leave() // Leave the dispatch group
|
||||
|
||||
}
|
||||
@@ -439,15 +419,95 @@ func findPathsForApp(appState: AppState, locations: Locations) {
|
||||
collection.append(contentsOf: groupContainers)
|
||||
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
|
||||
dispatchGroup.notify(queue: .main) {
|
||||
|
||||
updateOnMain {
|
||||
appState.paths = sortedCollection
|
||||
appState.selectedItems = Set(sortedCollection)
|
||||
// writeLog(string: "\n\nappStatePaths: \(appState.paths)")
|
||||
// writeLog(string: "\n\nappStateSelected: \(appState.selectedItems)")
|
||||
updatedAppInfo.files = sortedCollection
|
||||
updatedAppInfo.fileSize = fileSize
|
||||
updatedAppInfo.fileIcon = fileIcon
|
||||
if !backgroundRun {
|
||||
appState.appInfo = updatedAppInfo
|
||||
appState.selectedItems = Set(sortedCollection)
|
||||
}
|
||||
appState.appInfoStore.append(updatedAppInfo)
|
||||
}
|
||||
}
|
||||
|
||||
completion()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Load app paths on launch
|
||||
func loadAllPaths(allApps: [AppInfo], appState: AppState, locations: Locations) {
|
||||
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] {
|
||||
let fileManager = FileManager.default
|
||||
|
||||
@@ -470,14 +531,14 @@ func getGroupContainers(bundleURL: URL) -> [URL] {
|
||||
let status = SecCodeCopySigningInformation(staticCode!, SecCSFlags(), &signingInformation)
|
||||
|
||||
if status != errSecSuccess {
|
||||
print("Failed to copy signing information. Status: \(status)")
|
||||
printOS("Failed to copy signing information. Status: \(status)")
|
||||
return []
|
||||
}
|
||||
|
||||
guard let topDict = signingInformation as? [String: Any],
|
||||
let entitlementsDict = topDict["entitlements-dict"] as? [String: Any],
|
||||
let appGroups = entitlementsDict["com.apple.security.application-groups"] as? [String] else {
|
||||
// print("No application groups to extract from entitlements for this app.")
|
||||
// printOS("No application groups to extract from entitlements for this app.")
|
||||
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
|
||||
func moveFilesToTrash(at fileURLs: [URL], completion: @escaping () -> Void = {}) {
|
||||
@AppStorage("settings.sentinel.enable") var sentinel: Bool = false
|
||||
if sentinel {
|
||||
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 {
|
||||
let posixFiles = fileURLs.map { "POSIX file \"\($0.path)\", " }.joined().dropLast(3)
|
||||
let posixFiles = filesFinder.map { "POSIX file \"\($0.path)\", " }.joined().dropLast(3)
|
||||
let scriptSource = """
|
||||
tell application \"Finder\" to delete { \(posixFiles)" }
|
||||
"""
|
||||
@@ -503,9 +685,9 @@ func moveFilesToTrash(at fileURLs: [URL], completion: @escaping () -> Void = {})
|
||||
if let scriptObject = NSAppleScript(source: scriptSource) {
|
||||
let output: NSAppleEventDescriptor = scriptObject.executeAndReturnError(&error)
|
||||
if let error = error {
|
||||
print("Error: \(error)")
|
||||
printOS("Error: \(error)")
|
||||
} else if let outputString = output.stringValue {
|
||||
print(outputString)
|
||||
printOS(outputString)
|
||||
}
|
||||
}
|
||||
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
|
||||
func undoTrash(appState: AppState, completion: @escaping () -> Void = {}) {
|
||||
@@ -541,9 +839,9 @@ func undoTrash(appState: AppState, completion: @escaping () -> Void = {}) {
|
||||
_ = checkAndRequestAccessibilityAccess(appState: appState)
|
||||
}
|
||||
}
|
||||
print("Error: \(error)")
|
||||
printOS("Error: \(error)")
|
||||
} else if let outputString = output.stringValue {
|
||||
print(outputString)
|
||||
printOS(outputString)
|
||||
}
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
@@ -574,7 +872,7 @@ func undoTrash(appState: AppState, completion: @escaping () -> Void = {}) {
|
||||
// do {
|
||||
// try task.run()
|
||||
// } catch {
|
||||
// print("Failed to run task: \(error)")
|
||||
// printOS("Failed to run task: \(error)")
|
||||
// return false
|
||||
// }
|
||||
//
|
||||
@@ -598,7 +896,7 @@ func undoTrash(appState: AppState, completion: @escaping () -> Void = {}) {
|
||||
// do {
|
||||
// try task.run()
|
||||
// } catch {
|
||||
// print("Failed to run task: \(error)")
|
||||
// printOS("Failed to run task: \(error)")
|
||||
// return
|
||||
// }
|
||||
//
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import OSLog
|
||||
|
||||
// Make updates on main thread
|
||||
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 accessStatus = FileManager.default.isReadableFile(atPath: fileURL.path)
|
||||
// print(accessStatus)
|
||||
// printOS(accessStatus)
|
||||
// if accessStatus {
|
||||
// diskP = true
|
||||
// _ = checkAndRequestAccessibilityAccess(appState: appState)
|
||||
@@ -225,6 +226,12 @@ func getIconForFileOrFolder(atPath path: URL) -> Image? {
|
||||
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
|
||||
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 ---
|
||||
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
|
||||
func totalSizeOnDisk(for paths: [URL]) -> String? {
|
||||
func totalSizeOnDisk(for paths: [URL]) -> Int64 {
|
||||
var totalSize = 0
|
||||
|
||||
let process = Process()
|
||||
@@ -290,7 +306,7 @@ func totalSizeOnDisk(for paths: [URL]) -> String? {
|
||||
}
|
||||
let pipe = Pipe()
|
||||
process.standardOutput = pipe
|
||||
process.standardError = FileHandle.nullDevice
|
||||
process.standardError = pipe//FileHandle.nullDevice
|
||||
|
||||
try? process.run()
|
||||
process.waitUntilExit()
|
||||
@@ -301,19 +317,24 @@ func totalSizeOnDisk(for paths: [URL]) -> String? {
|
||||
for line in lines {
|
||||
let components = line.components(separatedBy: "\t")
|
||||
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()
|
||||
byteCountFormatter.countStyle = .file
|
||||
byteCountFormatter.allowedUnits = [.useAll]
|
||||
return byteCountFormatter.string(fromByteCount: Int64(totalSize))
|
||||
}
|
||||
|
||||
func totalSizeOnDisk(for path: URL) -> String? {
|
||||
return totalSizeOnDisk(for: [path])
|
||||
return byteCountFormatter.string(fromByteCount: size)
|
||||
}
|
||||
|
||||
|
||||
@@ -410,10 +431,10 @@ func uninstallPearcleaner(appState: AppState, locations: Locations) {
|
||||
|
||||
// Get app info for Pearcleaner
|
||||
let appInfo = getAppInfo(atPath: Bundle.main.bundleURL)
|
||||
appState.appInfo = appInfo!
|
||||
// appState.appInfo = appInfo!
|
||||
|
||||
// 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
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
|
||||
@@ -443,7 +464,7 @@ func ensureApplicationSupportFolderExists(appState: AppState) {
|
||||
// Check to make sure Application Support/Support Admin folder exists
|
||||
if !fileManager.fileExists(atPath: supportURL.path) {
|
||||
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
|
||||
if !fileManager.fileExists(atPath: logFilePath) {
|
||||
if !fileManager.createFile(atPath: logFilePath, contents: nil, attributes: nil) {
|
||||
print("Failed to create the log file.")
|
||||
printOS("Failed to create the log file.")
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -469,7 +490,7 @@ func writeLog(string: String) {
|
||||
fileHandle.write(ns.data(using: .utf8)!)
|
||||
fileHandle.closeFile()
|
||||
} else {
|
||||
print("Error opening file for appending")
|
||||
printOS("Error opening file for appending")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -491,7 +512,7 @@ func launchctl(load: Bool) {
|
||||
do {
|
||||
try plistContent.write(to: temporaryPlistURL, atomically: false, encoding: .utf8)
|
||||
} catch {
|
||||
print("Error writing the temporary plist file: \(error)")
|
||||
printOS("Error writing the temporary plist file: \(error)")
|
||||
return
|
||||
}
|
||||
let task = Process()
|
||||
@@ -506,163 +527,56 @@ func launchctl(load: Bool) {
|
||||
|
||||
// let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
||||
// if let output = String(data: data, encoding: .utf8) {
|
||||
// print("Output: \(output)")
|
||||
// printOS("Output: \(output)")
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
//func unloadAgent() {
|
||||
// if let plistPath = Bundle.main.path(forResource: "com.alienator88.PearcleanerMonitor", ofType: "plist") {
|
||||
// var plistContent = try! String(contentsOfFile: plistPath)
|
||||
// let executableURL = Bundle.main.bundleURL.appendingPathComponent("Contents/MacOS/PearcleanerMonitor")
|
||||
//
|
||||
// // Replace the placeholder with the actual executable path
|
||||
// plistContent = plistContent.replacingOccurrences(of: "__EXECUTABLE_PATH__", with: executableURL.path)
|
||||
//
|
||||
// // Create a temporary plist file with the updated content
|
||||
// let temporaryPlistURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("com.alienator88.PearcleanerMonitor.plist")
|
||||
//
|
||||
// do {
|
||||
// try plistContent.write(to: temporaryPlistURL, atomically: false, encoding: .utf8)
|
||||
// } catch {
|
||||
// print("Error writing the temporary plist file: \(error)")
|
||||
// return
|
||||
// }
|
||||
// let task = Process()
|
||||
// task.launchPath = "/bin/launchctl"
|
||||
// task.arguments = ["unload", "-w", temporaryPlistURL.path]
|
||||
//
|
||||
// let pipe = Pipe()
|
||||
// task.standardOutput = pipe
|
||||
// task.standardError = pipe
|
||||
//
|
||||
// task.launch()
|
||||
//
|
||||
//// let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
||||
//// if let output = String(data: data, encoding: .utf8) {
|
||||
//// print("Output: \(output)")
|
||||
//// }
|
||||
// }
|
||||
//}
|
||||
|
||||
|
||||
|
||||
|
||||
func getCurrentTimestamp() -> String {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS"
|
||||
return dateFormatter.string(from: Date())
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
//import FileWatcher
|
||||
// Execute sudo command
|
||||
//func executePrivilegedCommand(launchPath: String, arguments: [String]) {
|
||||
// let task = Process()
|
||||
// task.launchPath = launchPath
|
||||
// task.arguments = arguments
|
||||
//
|
||||
//// MARK: https://github.com/eonist/FileWatcher
|
||||
// var authorization: AuthorizationRef? = nil
|
||||
// let status = AuthorizationCreate(nil, nil, AuthorizationFlags(), &authorization)
|
||||
//
|
||||
//func fileW() {
|
||||
// let filewatcher = FileWatcher([NSString(string: "~/.Trash").expandingTildeInPath])
|
||||
// filewatcher.queue = DispatchQueue.global()
|
||||
// filewatcher.callback = { event in
|
||||
// print("Something happened here: " + event.path)
|
||||
// }
|
||||
// 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)
|
||||
//
|
||||
// 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
|
||||
// let status = AuthorizationCopyRights(authorization!, &rights, nil, AuthorizationFlags([.extendRights, .interactionAllowed]), nil)
|
||||
//
|
||||
// if status == errAuthorizationSuccess {
|
||||
// task.launch()
|
||||
// } else {
|
||||
// let pathSize = try FileManager.default.contentsOfDirectory(at: path, includingPropertiesForKeys: nil).lazy.reduce(0) {
|
||||
// (try $1.resourceValues(forKeys: [.totalFileAllocatedSizeKey])
|
||||
// .totalFileAllocatedSize ?? 0) + $0
|
||||
// }
|
||||
// totalSize += pathSize
|
||||
// printOS("Authorization failed.")
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return totalSize
|
||||
// } else {
|
||||
// printOS("Failed to convert rightName to C-string.")
|
||||
// }
|
||||
//
|
||||
// func totalSizeOnDisk(includingSubfolders: Bool = false) throws -> String? {
|
||||
// if let totalSize = try self.totalAllocatedSize(includingSubfolders: includingSubfolders) {
|
||||
// let byteCountFormatter = ByteCountFormatter()
|
||||
// byteCountFormatter.countStyle = .file
|
||||
// byteCountFormatter.allowedUnits = [.useBytes, .useKB, .useMB, .useTB]
|
||||
// return byteCountFormatter.string(fromByteCount: Int64(totalSize))
|
||||
// }
|
||||
// return nil
|
||||
// } else {
|
||||
// printOS("Authorization creation failed.")
|
||||
// }
|
||||
//}
|
||||
|
||||
// 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
|
||||
//}
|
||||
|
||||
@@ -72,7 +72,7 @@ struct PearcleanerApp: App {
|
||||
} else {
|
||||
appState.currentView = .empty
|
||||
}
|
||||
|
||||
|
||||
// Disable tabbing
|
||||
NSWindow.allowsAutomaticWindowTabbing = false
|
||||
|
||||
@@ -85,13 +85,11 @@ struct PearcleanerApp: App {
|
||||
appState.sortedApps.userApps = sortedApps.userApps
|
||||
appState.sortedApps.systemApps = sortedApps.systemApps
|
||||
|
||||
// Load progressbar total
|
||||
// appState.progressManager.total = Double(locations.apps.paths.count)
|
||||
|
||||
|
||||
|
||||
Task {
|
||||
|
||||
// Find all app paths on load
|
||||
loadAllPaths(allApps: sortedApps.userApps + sortedApps.systemApps, appState: appState, locations: locations)
|
||||
|
||||
#if !DEBUG
|
||||
|
||||
// Make sure App Support folder exists in the future if needed for storage
|
||||
|
||||
@@ -102,7 +102,8 @@ struct GeneralSettingsTab: View {
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.fill(Color("mode").opacity(0.05))
|
||||
)
|
||||
|
||||
|
||||
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
Text("Mini").font(.title2)
|
||||
@@ -117,11 +118,10 @@ struct GeneralSettingsTab: View {
|
||||
.onChange(of: mini) { newVal in
|
||||
if mini {
|
||||
resizeWindowAuto(windowSettings: windowSettings)
|
||||
// resizeWindow(width: 300, height: 300)
|
||||
appState.currentView = miniView ? .apps : .empty
|
||||
// showPopover = false
|
||||
// appState.currentView = miniView ? .apps : .empty
|
||||
} else {
|
||||
resizeWindowAuto(windowSettings: windowSettings)
|
||||
// resizeWindow(width: 700, height: 500)
|
||||
if appState.appInfo.appName.isEmpty {
|
||||
appState.currentView = .empty
|
||||
} else {
|
||||
@@ -139,7 +139,7 @@ struct GeneralSettingsTab: View {
|
||||
|
||||
HStack {
|
||||
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")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.gray)
|
||||
@@ -160,7 +160,7 @@ struct GeneralSettingsTab: View {
|
||||
|
||||
HStack {
|
||||
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")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.gray)
|
||||
|
||||
@@ -9,7 +9,7 @@ import SwiftUI
|
||||
|
||||
struct SettingsView: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
|
||||
|
||||
var body: some View {
|
||||
|
||||
TabView() {
|
||||
|
||||
@@ -22,6 +22,12 @@ struct Asset: Codable {
|
||||
let browser_download_url: String
|
||||
}
|
||||
|
||||
extension Release {
|
||||
var modifiedBody: String {
|
||||
return body.replacingOccurrences(of: "- [x]", with: "").replacingOccurrences(of: "###", with: "")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct UpdateSettingsTab: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
@@ -46,7 +52,7 @@ struct UpdateSettingsTab: View {
|
||||
ForEach(appState.releases, id: \.id) { release in
|
||||
VStack(alignment: .leading) {
|
||||
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 decodedResponse = try? JSONDecoder().decode([Release].self, from: data) {
|
||||
DispatchQueue.main.async {
|
||||
let lastFiveReleases = Array(decodedResponse.prefix(3)) // Get only the last 3 recent releases
|
||||
appState.releases = lastFiveReleases
|
||||
let lastFewReleases = Array(decodedResponse.prefix(3)) // Get only the last 3 recent releases
|
||||
appState.releases = lastFewReleases
|
||||
|
||||
checkForUpdate(appState: appState, manual: manual)
|
||||
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
print("Fetch failed: \(error?.localizedDescription ?? "Unknown error")")
|
||||
printOS("Fetch failed: \(error?.localizedDescription ?? "Unknown error")")
|
||||
}.resume()
|
||||
}
|
||||
|
||||
@@ -172,7 +179,7 @@ func downloadUpdate(appState: AppState) {
|
||||
|
||||
|
||||
} 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 {
|
||||
print("Error updating the app: \(error)")
|
||||
printOS("Error updating the app: \(error)")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ struct AppListItems: View {
|
||||
let itemId = UUID()
|
||||
let appInfo: AppInfo
|
||||
var isSelected: Bool {
|
||||
appState.appInfo == appInfo
|
||||
appState.appInfo.path == appInfo.path
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -71,7 +71,7 @@ struct AppListItems: View {
|
||||
// .background(Color("mode").opacity(0.1))
|
||||
// .clipShape(.capsule)
|
||||
}
|
||||
|
||||
|
||||
Text(appInfo.appVersion)
|
||||
.font(.footnote)
|
||||
.foregroundStyle(Color("mode").opacity(0.5))
|
||||
@@ -95,25 +95,9 @@ struct AppListItems: View {
|
||||
}
|
||||
}
|
||||
.onTapGesture {
|
||||
showPopover = false
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
showAppInFiles(appInfo: appInfo, mini: mini, appState: appState, locations: locations, showPopover: $showPopover)
|
||||
}
|
||||
|
||||
// .popover(isPresented: Binding(
|
||||
// get: { showPopover && appState.appInfo.id == appInfo.id},
|
||||
// set: { _ in showPopover = false}
|
||||
|
||||
@@ -45,7 +45,7 @@ struct AppListView: View {
|
||||
if appState.reload {
|
||||
VStack {
|
||||
Spacer()
|
||||
ProgressView("Refreshing applications")
|
||||
ProgressView("Loading apps and files")
|
||||
Spacer()
|
||||
}
|
||||
.frame(width: sidebarWidth)
|
||||
@@ -84,7 +84,7 @@ struct AppListView: View {
|
||||
|
||||
if filteredUserApps.count > 0 {
|
||||
VStack {
|
||||
Header(title: "User", count: filteredUserApps.count)
|
||||
Header(title: "User", count: filteredUserApps.count, showPopover: $showPopover)
|
||||
ForEach(filteredUserApps, id: \.self) { appInfo in
|
||||
AppListItems(search: $search, showPopover: $showPopover, appInfo: appInfo)
|
||||
if appInfo != filteredUserApps.last {
|
||||
@@ -98,7 +98,7 @@ struct AppListView: View {
|
||||
|
||||
if filteredSystemApps.count > 0 {
|
||||
VStack {
|
||||
Header(title: "System", count: filteredSystemApps.count)
|
||||
Header(title: "System", count: filteredSystemApps.count, showPopover: $showPopover)
|
||||
ForEach(filteredSystemApps, id: \.self) { appInfo in
|
||||
AppListItems(search: $search, showPopover: $showPopover, appInfo: appInfo)
|
||||
if appInfo != filteredSystemApps.last {
|
||||
@@ -133,12 +133,16 @@ struct AppListView: View {
|
||||
// Details View
|
||||
VStack(spacing: 0) {
|
||||
if appState.currentView == .empty || appState.currentView == .apps {
|
||||
TopBar()
|
||||
TopBar(showPopover: $showPopover)
|
||||
AppDetailsEmptyView(showPopover: $showPopover)
|
||||
} else if appState.currentView == .files {
|
||||
TopBar()
|
||||
TopBar(showPopover: $showPopover)
|
||||
FilesView(showPopover: $showPopover, search: $search)
|
||||
.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)
|
||||
@@ -161,10 +165,10 @@ struct AppListView: View {
|
||||
// // // Check if the file URL has a ".app" extension
|
||||
// // if url.pathExtension.lowercased() == "app" {
|
||||
// // // Handle the dropped file URL here
|
||||
// // print("Dropped .app file URL: \(url)")
|
||||
// // printOS("Dropped .app file URL: \(url)")
|
||||
// // } else {
|
||||
// // // 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
|
||||
@State private var hovered = false
|
||||
@EnvironmentObject var appState: AppState
|
||||
|
||||
@EnvironmentObject var locations: Locations
|
||||
@Binding var showPopover: Bool
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
@@ -259,10 +264,12 @@ struct Header: View {
|
||||
withAnimation {
|
||||
// Refresh Apps list
|
||||
appState.reload.toggle()
|
||||
showPopover = false
|
||||
let sortedApps = getSortedApps()
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
appState.sortedApps.userApps = sortedApps.userApps
|
||||
appState.sortedApps.systemApps = sortedApps.systemApps
|
||||
loadAllPaths(allApps: sortedApps.userApps + sortedApps.systemApps, appState: appState, locations: locations)
|
||||
appState.reload.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,24 +125,58 @@ struct DropTarget: View, DropDelegate {
|
||||
return
|
||||
}
|
||||
guard let url = URL(dataRepresentation: data, relativeTo: nil) else {
|
||||
print("Error: Not a valid URL.")
|
||||
printOS("Error: Not a valid URL.")
|
||||
return
|
||||
}
|
||||
if url.pathExtension == "app" {
|
||||
let appInfo = getAppInfo(atPath: url)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.ants = false
|
||||
appState.appInfo = appInfo!
|
||||
findPathsForApp(appState: appState, locations: locations)
|
||||
if mini {
|
||||
showPopover = true
|
||||
} else {
|
||||
appState.currentView = .files
|
||||
}
|
||||
}
|
||||
// showPopover = false
|
||||
self.ants = false
|
||||
showAppInFiles(appInfo: appInfo!, mini: mini, appState: appState, locations: locations, showPopover: $showPopover)
|
||||
|
||||
// DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
||||
// 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 {
|
||||
print("Error: Dropped file is not an application bundle")
|
||||
printOS("Error: Dropped file is not an application bundle")
|
||||
DispatchQueue.main.async {
|
||||
self.shouldFlash = true
|
||||
}
|
||||
|
||||
@@ -11,25 +11,24 @@ import SwiftUI
|
||||
struct FilesView: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
@EnvironmentObject var locations: Locations
|
||||
@State private var appSize: String = ""
|
||||
@State private var showDetails: 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.sentinel.enable") private var sentinel: Bool = false
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@Binding var showPopover: Bool
|
||||
@Binding var search: String
|
||||
@State private var selectedOption = "Default"
|
||||
@State private var toggles: Bool = true
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .center) {
|
||||
if !self.showDetails {
|
||||
if appState.showProgress { //!self.showDetails {
|
||||
VStack {
|
||||
Spacer()
|
||||
// ProgressView("Finding application files..")
|
||||
// .progressViewStyle(.linear)
|
||||
// Spacer()
|
||||
Text("Finding application files..").font(.title3)
|
||||
Text("Almost there, still gathering files..").font(.title3)
|
||||
.foregroundStyle((.gray.opacity(0.8)))
|
||||
ProgressView()
|
||||
.progressViewStyle(.linear)
|
||||
@@ -71,8 +70,9 @@ struct FilesView: View {
|
||||
|
||||
Spacer()
|
||||
|
||||
Text("\(appSize)").font(.title).fontWeight(.bold)
|
||||
// .foregroundStyle(Color("AccentColor"))
|
||||
Text("\(formatByte(size: appState.appInfo.totalSize))").font(.title).fontWeight(.bold)
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ struct FilesView: View {
|
||||
// .foregroundStyle(Color("AccentColor"))
|
||||
.opacity(0.8)
|
||||
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))
|
||||
.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() {
|
||||
VStack {
|
||||
ForEach(Array(zip(appState.paths, itemDetails)), id: \.0) { path, details in
|
||||
if let firstPath = appState.paths.first, path == firstPath {
|
||||
FileDetailsItem(size: details.size, icon: details.icon, path: path)
|
||||
.padding(.trailing)
|
||||
Divider().padding(.leading, 40).padding(.trailing)
|
||||
} else {
|
||||
let sortedFilesSize = appState.appInfo.files.sorted(by: { appState.appInfo.fileSize[$0, default: 0] > appState.appInfo.fileSize[$1, default: 0] })
|
||||
|
||||
// let sortedFilesAlpha = appState.appInfo.files.sorted(by: { $0.lastPathComponent < $1.lastPathComponent })
|
||||
|
||||
let sort = selectedOption == "Default" ? appState.appInfo.files : sortedFilesSize
|
||||
|
||||
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 {
|
||||
FileDetailsItem(size: details.size, icon: details.icon, path: path)
|
||||
if path != appState.paths.last {
|
||||
FileDetailsItem(size: fileSize, icon: iconImage, path: path)
|
||||
if path != appState.appInfo.files.last {
|
||||
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() {
|
||||
Picker(selection: $selectedOption, label: Text("Sort")) {
|
||||
Text("Default").tag("Default")
|
||||
Text("Size").tag("Size")
|
||||
}
|
||||
.pickerStyle(SegmentedPickerStyle())
|
||||
.frame(width: 150)
|
||||
|
||||
Spacer()
|
||||
|
||||
if mini {
|
||||
@@ -186,8 +225,8 @@ struct FilesView: View {
|
||||
Task {
|
||||
updateOnMain {
|
||||
appState.appInfo = AppInfo.empty
|
||||
search = ""
|
||||
if mini {
|
||||
search = ""
|
||||
appState.currentView = .apps
|
||||
showPopover = false
|
||||
} else {
|
||||
@@ -195,15 +234,32 @@ struct FilesView: View {
|
||||
}
|
||||
}
|
||||
|
||||
let selectedItemsArray = Array(appState.selectedItems)
|
||||
var selectedItemsArray = Array(appState.selectedItems)
|
||||
.filter { !$0.path.contains(".Trash") }
|
||||
.map { path in
|
||||
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) {
|
||||
moveFilesToTrash(at: selectedItemsArray) {
|
||||
withAnimation {
|
||||
showPopover = false
|
||||
updateOnMain {
|
||||
appState.isReminderVisible.toggle()
|
||||
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) {
|
||||
showPopover = false
|
||||
let sortedApps = getSortedApps()
|
||||
updateOnMain {
|
||||
appState.sortedApps.userApps = []
|
||||
@@ -280,7 +297,9 @@ struct FilesView: View {
|
||||
appState.sortedApps.userApps = sortedApps.userApps
|
||||
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 {
|
||||
@EnvironmentObject var appState: AppState
|
||||
let size: String
|
||||
let size: Int64?
|
||||
let icon: Image?
|
||||
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 {
|
||||
|
||||
HStack(alignment: .center, spacing: 20) {
|
||||
@@ -306,19 +319,31 @@ struct FileDetailsItem: View {
|
||||
set: { isChecked in
|
||||
if isChecked {
|
||||
self.appState.selectedItems.insert(self.path)
|
||||
if self.path == appState.appInfo.path {
|
||||
self.appState.paths.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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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"))
|
||||
|
||||
|
||||
if let appIcon = icon {
|
||||
appIcon
|
||||
.resizable()
|
||||
@@ -343,12 +368,10 @@ struct FileDetailsItem: View {
|
||||
}
|
||||
|
||||
Spacer()
|
||||
if size.isEmpty {
|
||||
ProgressView().controlSize(.small)
|
||||
} else {
|
||||
Text(size)
|
||||
}
|
||||
|
||||
|
||||
Text(formatByte(size:size!))
|
||||
|
||||
|
||||
|
||||
Button("") {
|
||||
NSWorkspace.shared.selectFile(path.path, inFileViewerRootedAtPath: path.deletingLastPathComponent().path)
|
||||
|
||||
@@ -30,19 +30,28 @@ struct MiniMode: View {
|
||||
MiniEmptyView(showPopover: $showPopover)
|
||||
} else if appState.currentView == .files {
|
||||
TopBarMini(search: $search, showPopover: $showPopover)
|
||||
FilesView(showPopover: $showPopover, search: $search)
|
||||
.id(appState.appInfo.id)
|
||||
MiniAppView(search: $search, showPopover: $showPopover)
|
||||
} else if appState.currentView == .zombie {
|
||||
TopBarMini(search: $search, showPopover: $showPopover)
|
||||
MiniAppView(search: $search, showPopover: $showPopover)
|
||||
} else if appState.currentView == .apps {
|
||||
TopBarMini(search: $search, showPopover: $showPopover)
|
||||
MiniAppView(search: $search, showPopover: $showPopover)
|
||||
}
|
||||
|
||||
}
|
||||
// .padding(.leading, appState.sidebar ? 0 : 10)
|
||||
.transition(.move(edge: .leading))
|
||||
.popover(isPresented: $showPopover, arrowEdge: .trailing) {
|
||||
VStack {
|
||||
FilesView(showPopover: $showPopover, search: $search)
|
||||
.id(appState.appInfo.id)
|
||||
if appState.currentView == .files {
|
||||
FilesView(showPopover: $showPopover, search: $search)
|
||||
.id(appState.appInfo.id)
|
||||
} else if appState.currentView == .zombie {
|
||||
ZombieView(showPopover: $showPopover, search: $search)
|
||||
.id(appState.appInfo.id)
|
||||
}
|
||||
|
||||
}
|
||||
.interactiveDismissDisabled(popoverStay)
|
||||
.background(
|
||||
@@ -50,7 +59,7 @@ struct MiniMode: View {
|
||||
.fill(Color("pop"))
|
||||
.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
|
||||
//// if url.pathExtension.lowercased() == "app" {
|
||||
//// // Handle the dropped file URL here
|
||||
//// print("Dropped .app file URL: \(url)")
|
||||
//// printOS("Dropped .app file URL: \(url)")
|
||||
//// } else {
|
||||
//// // 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 {
|
||||
VStack {
|
||||
Spacer()
|
||||
ProgressView("Refreshing applications")
|
||||
ProgressView("Loadings apps and files")
|
||||
Spacer()
|
||||
}
|
||||
.padding(.vertical)
|
||||
@@ -216,7 +225,7 @@ struct MiniAppView: View {
|
||||
if filteredUserApps.count > 0 {
|
||||
|
||||
VStack {
|
||||
Header(title: "User", count: filteredUserApps.count)
|
||||
Header(title: "User", count: filteredUserApps.count, showPopover: $showPopover)
|
||||
ForEach(filteredUserApps, id: \.self) { appInfo in
|
||||
AppListItems(search: $search, showPopover: $showPopover, appInfo: appInfo)
|
||||
if appInfo != filteredUserApps.last {
|
||||
@@ -230,7 +239,7 @@ struct MiniAppView: View {
|
||||
if filteredSystemApps.count > 0 {
|
||||
|
||||
VStack {
|
||||
Header(title: "System", count: filteredSystemApps.count)
|
||||
Header(title: "System", count: filteredSystemApps.count, showPopover: $showPopover)
|
||||
ForEach(filteredSystemApps, id: \.self) { appInfo in
|
||||
AppListItems(search: $search, showPopover: $showPopover, appInfo: appInfo)
|
||||
if appInfo != filteredSystemApps.last {
|
||||
@@ -246,7 +255,7 @@ struct MiniAppView: View {
|
||||
}
|
||||
.scrollIndicators(.never)
|
||||
|
||||
if appState.currentView == .apps {
|
||||
if appState.currentView != .empty {
|
||||
SearchBarMiniBottom(search: $search)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,12 +15,14 @@ struct TopBar: View {
|
||||
@AppStorage("settings.sentinel.enable") private var sentinel: Bool = false
|
||||
@AppStorage("settings.general.mini") private var mini: Bool = false
|
||||
@EnvironmentObject var appState: AppState
|
||||
|
||||
@Binding var showPopover: Bool
|
||||
@EnvironmentObject var locations: Locations
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .center, spacing: 20) {
|
||||
|
||||
HStack(alignment: .center, spacing: 10) {
|
||||
|
||||
Spacer()
|
||||
|
||||
|
||||
if appState.isReminderVisible {
|
||||
Text("CMD + Z to undo")
|
||||
.font(.title2)
|
||||
@@ -34,10 +36,10 @@ struct TopBar: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Spacer()
|
||||
|
||||
if appState.currentView != .empty || appState.currentView != .apps {
|
||||
|
||||
if appState.currentView != .empty {//|| appState.currentView != .apps {
|
||||
Button("") {
|
||||
withAnimation(.easeInOut(duration: 0.5)) {
|
||||
appState.currentView = .empty
|
||||
@@ -45,8 +47,30 @@ struct TopBar: View {
|
||||
}
|
||||
}
|
||||
.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()
|
||||
|
||||
@@ -15,6 +15,7 @@ struct TopBarMini: View {
|
||||
@Binding var search: String
|
||||
@Binding var showPopover: Bool
|
||||
@EnvironmentObject var appState: AppState
|
||||
@EnvironmentObject var locations: Locations
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .center, spacing: 5) {
|
||||
@@ -42,28 +43,29 @@ struct TopBarMini: View {
|
||||
}
|
||||
}
|
||||
.buttonStyle(SimpleButtonStyle(icon: "list.dash", help: "Apps List", color: Color("mode")))
|
||||
} else if appState.currentView == .files {
|
||||
Button("") {
|
||||
withAnimation(.easeInOut(duration: 0.5)) {
|
||||
// updateOnMain {
|
||||
appState.currentView = .empty
|
||||
appState.appInfo = AppInfo.empty
|
||||
// }
|
||||
}
|
||||
// else if appState.currentView == .files {
|
||||
// Button("") {
|
||||
// withAnimation(.easeInOut(duration: 0.5)) {
|
||||
// // updateOnMain {
|
||||
// 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")))
|
||||
// }
|
||||
|
||||
}
|
||||
}
|
||||
.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 {
|
||||
if appState.currentView != .empty {
|
||||
HStack {
|
||||
Spacer()
|
||||
|
||||
@@ -84,22 +86,24 @@ struct TopBarMini: View {
|
||||
}
|
||||
.buttonStyle(SimpleButtonStyle(icon: "plus.square.dashed", help: "Drop Target", color: Color("mode")))
|
||||
|
||||
// Spacer()
|
||||
Button("") {
|
||||
withAnimation(.easeInOut(duration: 0.5)) {
|
||||
updateOnMain {
|
||||
if appState.zombieFile.fileSize.keys.count == 0 {
|
||||
appState.currentView = .zombie
|
||||
appState.showProgress.toggle()
|
||||
showPopover.toggle()
|
||||
reversePathsSearch(appState: appState, locations: locations)
|
||||
} else {
|
||||
appState.currentView = .zombie
|
||||
showPopover.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
.buttonStyle(SimpleButtonStyle(icon: "clock.arrow.circlepath", help: "Leftover Files", color: Color("mode")))
|
||||
|
||||
// Button("") {
|
||||
// withAnimation {
|
||||
// // Refresh Apps list
|
||||
// appState.reload.toggle()
|
||||
// let sortedApps = getSortedApps()
|
||||
// DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
// appState.sortedApps.userApps = sortedApps.userApps
|
||||
// appState.sortedApps.systemApps = sortedApps.systemApps
|
||||
// appState.reload.toggle()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// .buttonStyle(SimpleButtonStyle(icon: "arrow.circlepath", help: "Refresh app list", color: Color("mode")))
|
||||
// .padding(.leading, 5)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,10 +131,11 @@ struct SearchBarMini: View {
|
||||
|
||||
struct SearchBarMiniBottom: View {
|
||||
@Binding var search: String
|
||||
@EnvironmentObject var appState: AppState
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
TextField("Search", text: $search)
|
||||
TextField(" Search", text: $search)
|
||||
.textFieldStyle(SimpleSearchStyle(trash: true, text: $search))
|
||||
}
|
||||
.padding(.horizontal)
|
||||
|
||||
@@ -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))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user