This commit is contained in:
Alin
2024-08-02 22:13:01 -06:00
parent e425219c99
commit 26f8478274
10 changed files with 778 additions and 195 deletions
+29 -5
View File
@@ -14,6 +14,10 @@
C736E7652C2DF1C9009EDB6A /* AppPathsFetch-Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = C736E7642C2DF1C9009EDB6A /* AppPathsFetch-Async.swift */; }; C736E7652C2DF1C9009EDB6A /* AppPathsFetch-Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = C736E7642C2DF1C9009EDB6A /* AppPathsFetch-Async.swift */; };
C74412C72BBF249000DDFCA8 /* AppPathsFetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C74412C62BBF249000DDFCA8 /* AppPathsFetch.swift */; }; C74412C72BBF249000DDFCA8 /* AppPathsFetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C74412C62BBF249000DDFCA8 /* AppPathsFetch.swift */; };
C74D76B72C5C4ABC00748DD1 /* TreeMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = C74D76B62C5C4ABC00748DD1 /* TreeMap.swift */; }; C74D76B72C5C4ABC00748DD1 /* TreeMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = C74D76B62C5C4ABC00748DD1 /* TreeMap.swift */; };
C74D76BD2C5D2DF300748DD1 /* FileSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C74D76B92C5D2DF300748DD1 /* FileSearch.swift */; };
C74D76BE2C5D2DF300748DD1 /* PearDiskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C74D76BA2C5D2DF300748DD1 /* PearDiskView.swift */; };
C74D76BF2C5D2DF300748DD1 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = C74D76BB2C5D2DF300748DD1 /* Data.swift */; };
C74D76C02C5D2DF300748DD1 /* FolderDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = C74D76BC2C5D2DF300748DD1 /* FolderDetail.swift */; };
C74FDC632BCD833D00B8960F /* Conditions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C74FDC622BCD833D00B8960F /* Conditions.swift */; }; C74FDC632BCD833D00B8960F /* Conditions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C74FDC622BCD833D00B8960F /* Conditions.swift */; };
C7575D392B0182DE006A600A /* PearcleanerSentinel in CopyFiles */ = {isa = PBXBuildFile; fileRef = C728930F2AFD51EA00C8C1CD /* PearcleanerSentinel */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; C7575D392B0182DE006A600A /* PearcleanerSentinel in CopyFiles */ = {isa = PBXBuildFile; fileRef = C728930F2AFD51EA00C8C1CD /* PearcleanerSentinel */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
C76D08482AF83C3F00D07867 /* Update.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76D08472AF83C3F00D07867 /* Update.swift */; }; C76D08482AF83C3F00D07867 /* Update.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76D08472AF83C3F00D07867 /* Update.swift */; };
@@ -101,6 +105,10 @@
C736E7642C2DF1C9009EDB6A /* AppPathsFetch-Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppPathsFetch-Async.swift"; sourceTree = "<group>"; }; C736E7642C2DF1C9009EDB6A /* AppPathsFetch-Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppPathsFetch-Async.swift"; sourceTree = "<group>"; };
C74412C62BBF249000DDFCA8 /* AppPathsFetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPathsFetch.swift; sourceTree = "<group>"; }; C74412C62BBF249000DDFCA8 /* AppPathsFetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPathsFetch.swift; sourceTree = "<group>"; };
C74D76B62C5C4ABC00748DD1 /* TreeMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TreeMap.swift; sourceTree = "<group>"; }; C74D76B62C5C4ABC00748DD1 /* TreeMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TreeMap.swift; sourceTree = "<group>"; };
C74D76B92C5D2DF300748DD1 /* FileSearch.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileSearch.swift; sourceTree = "<group>"; };
C74D76BA2C5D2DF300748DD1 /* PearDiskView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PearDiskView.swift; sourceTree = "<group>"; };
C74D76BB2C5D2DF300748DD1 /* Data.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Data.swift; sourceTree = "<group>"; };
C74D76BC2C5D2DF300748DD1 /* FolderDetail.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FolderDetail.swift; sourceTree = "<group>"; };
C74FDC622BCD833D00B8960F /* Conditions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Conditions.swift; sourceTree = "<group>"; }; C74FDC622BCD833D00B8960F /* Conditions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Conditions.swift; sourceTree = "<group>"; };
C76D08472AF83C3F00D07867 /* Update.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Update.swift; sourceTree = "<group>"; }; C76D08472AF83C3F00D07867 /* Update.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Update.swift; sourceTree = "<group>"; };
C76D08492AF83C6E00D07867 /* General.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = General.swift; sourceTree = "<group>"; }; C76D08492AF83C6E00D07867 /* General.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = General.swift; sourceTree = "<group>"; };
@@ -204,6 +212,18 @@
name = Frameworks; name = Frameworks;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
C74D76B82C5D2D7900748DD1 /* PearDisk */ = {
isa = PBXGroup;
children = (
C74D76BA2C5D2DF300748DD1 /* PearDiskView.swift */,
C74D76BB2C5D2DF300748DD1 /* Data.swift */,
C74D76B92C5D2DF300748DD1 /* FileSearch.swift */,
C74D76B62C5C4ABC00748DD1 /* TreeMap.swift */,
C74D76BC2C5D2DF300748DD1 /* FolderDetail.swift */,
);
path = PearDisk;
sourceTree = "<group>";
};
C76D08462AF83C2F00D07867 /* Settings */ = { C76D08462AF83C2F00D07867 /* Settings */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -257,6 +277,7 @@
C786F1A72C29E599001B3800 /* Builds */, C786F1A72C29E599001B3800 /* Builds */,
C76D084F2AF84E0100D07867 /* Resources */, C76D084F2AF84E0100D07867 /* Resources */,
C76D08462AF83C2F00D07867 /* Settings */, C76D08462AF83C2F00D07867 /* Settings */,
C74D76B82C5D2D7900748DD1 /* PearDisk */,
C77B901F2AF1B390009CC655 /* Views */, C77B901F2AF1B390009CC655 /* Views */,
C72893072AFD442600C8C1CD /* Logic */, C72893072AFD442600C8C1CD /* Logic */,
); );
@@ -274,7 +295,6 @@
C7FEBA102BDC422200AE195F /* AppSearchView.swift */, C7FEBA102BDC422200AE195F /* AppSearchView.swift */,
C7D31D492AFEB26700C7ED9E /* AppListItems.swift */, C7D31D492AFEB26700C7ED9E /* AppListItems.swift */,
C7EA06322C1CBE2000F872FC /* ConditionBuilderView.swift */, C7EA06322C1CBE2000F872FC /* ConditionBuilderView.swift */,
C74D76B62C5C4ABC00748DD1 /* TreeMap.swift */,
); );
path = Views; path = Views;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -431,17 +451,20 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
C7DD49EA2BAB4D8100CCBA16 /* AppInfoFetch.swift in Sources */, C7DD49EA2BAB4D8100CCBA16 /* AppInfoFetch.swift in Sources */,
C74D76BD2C5D2DF300748DD1 /* FileSearch.swift in Sources */,
C79947962C34A794007279CC /* Tips.swift in Sources */, C79947962C34A794007279CC /* Tips.swift in Sources */,
C76D08482AF83C3F00D07867 /* Update.swift in Sources */, C76D08482AF83C3F00D07867 /* Update.swift in Sources */,
C7FB173B2B96321300B96F9A /* AppsListView.swift in Sources */, C7FB173B2B96321300B96F9A /* AppsListView.swift in Sources */,
C77B90162AF19377009CC655 /* AppState.swift in Sources */, C77B90162AF19377009CC655 /* AppState.swift in Sources */,
C74D76B72C5C4ABC00748DD1 /* TreeMap.swift in Sources */, C74D76B72C5C4ABC00748DD1 /* TreeMap.swift in Sources */,
C74D76C02C5D2DF300748DD1 /* FolderDetail.swift in Sources */,
C7DD49EE2BAB7F6000CCBA16 /* ReversePathsFetch.swift in Sources */, C7DD49EE2BAB7F6000CCBA16 /* ReversePathsFetch.swift in Sources */,
C7EA06332C1CBE2000F872FC /* ConditionBuilderView.swift in Sources */, C7EA06332C1CBE2000F872FC /* ConditionBuilderView.swift in Sources */,
C7DD49F02BABA14400CCBA16 /* Folders.swift in Sources */, C7DD49F02BABA14400CCBA16 /* Folders.swift in Sources */,
C7D31D512AFF00F300C7ED9E /* Locations.swift in Sources */, C7D31D512AFF00F300C7ED9E /* Locations.swift in Sources */,
C77B901A2AF1938F009CC655 /* Logic.swift in Sources */, C77B901A2AF1938F009CC655 /* Logic.swift in Sources */,
C77B901C2AF193A3009CC655 /* Styles.swift in Sources */, C77B901C2AF193A3009CC655 /* Styles.swift in Sources */,
C74D76BF2C5D2DF300748DD1 /* Data.swift in Sources */,
C7045A282B03E71D00376976 /* MiniMode.swift in Sources */, C7045A282B03E71D00376976 /* MiniMode.swift in Sources */,
C7ED86B12BA8A35500F13DE4 /* Interface.swift in Sources */, C7ED86B12BA8A35500F13DE4 /* Interface.swift in Sources */,
C72893052AFD42E600C8C1CD /* DeepLink.swift in Sources */, C72893052AFD42E600C8C1CD /* DeepLink.swift in Sources */,
@@ -452,6 +475,7 @@
C77B90042AF18E2E009CC655 /* PearcleanerApp.swift in Sources */, C77B90042AF18E2E009CC655 /* PearcleanerApp.swift in Sources */,
C76D08512AF850F300D07867 /* About.swift in Sources */, C76D08512AF850F300D07867 /* About.swift in Sources */,
C77B90182AF19382009CC655 /* AppCommands.swift in Sources */, C77B90182AF19382009CC655 /* AppCommands.swift in Sources */,
C74D76BE2C5D2DF300748DD1 /* PearDiskView.swift in Sources */,
C7D31D4A2AFEB26700C7ED9E /* AppListItems.swift in Sources */, C7D31D4A2AFEB26700C7ED9E /* AppListItems.swift in Sources */,
C77B901E2AF1A9DB009CC655 /* SettingsWindow.swift in Sources */, C77B901E2AF1A9DB009CC655 /* SettingsWindow.swift in Sources */,
C74412C72BBF249000DDFCA8 /* AppPathsFetch.swift in Sources */, C74412C72BBF249000DDFCA8 /* AppPathsFetch.swift in Sources */,
@@ -533,8 +557,8 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
APP_BUILD = 56; APP_BUILD = 57;
APP_VERSION = 3.8.2; APP_VERSION = 3.8.3;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
@@ -603,8 +627,8 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
APP_BUILD = 56; APP_BUILD = 57;
APP_VERSION = 3.8.2; APP_VERSION = 3.8.3;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+23 -23
View File
@@ -65,8 +65,7 @@ class AppState: ObservableObject {
id: UUID(), id: UUID(),
fileSize: [:], fileSize: [:],
fileSizeLogical: [:], fileSizeLogical: [:],
fileIcon: [:], fileIcon: [:]
isDirectory: [:]
) )
updateExtensionStatus() updateExtensionStatus()
@@ -134,7 +133,6 @@ struct ZombieFile: Identifiable, Equatable, Hashable {
var fileSize: [URL:Int64] var fileSize: [URL:Int64]
var fileSizeLogical: [URL:Int64] var fileSizeLogical: [URL:Int64]
var fileIcon: [URL:NSImage?] var fileIcon: [URL:NSImage?]
var isDirectory: [URL:Bool]
var totalSize: Int64 var totalSize: Int64
{ {
return fileSize.values.reduce(0, +) return fileSize.values.reduce(0, +)
@@ -145,29 +143,31 @@ struct ZombieFile: Identifiable, Equatable, Hashable {
} }
static let empty = ZombieFile(id: UUID(), fileSize: [:], fileSizeLogical: [:], fileIcon: [:], isDirectory: [:]) static let empty = ZombieFile(id: UUID(), fileSize: [:], fileSizeLogical: [:], fileIcon: [:])
} }
extension ZombieFile { //extension ZombieFile {
/// Converts the data in ZombieFile into a list of Item instances. // func toItems() -> [Item] {
func toItems() -> [Item] { // var items: [Item] = []
var items = [Item]() //
// for (url, size) in self.fileSize {
// Iterate over each URL in fileSize to get corresponding properties // let name = url.lastPathComponent
for (url, size) in fileSize { // let isDirectory = url.hasDirectoryPath
let name = url.lastPathComponent //
let isDir = isDirectory[url] ?? false // let item = Item(
let parent = url.deletingLastPathComponent() // url: url,
// name: name,
// Create an Item instance for each URL // size: size,
let item = Item(url: url, name: name, size: size, isDirectory: isDir, parentURL: parent) // isDirectory: isDirectory
items.append(item) // )
} //
// items.append(item)
return items//.sorted { $0.size > $1.size } // }
} //
} // return items
// }
//}
enum Arch { enum Arch {
@@ -18,7 +18,6 @@ class ReversePathsSearcher {
private var fileSize: [URL: Int64] = [:] private var fileSize: [URL: Int64] = [:]
private var fileSizeLogical: [URL: Int64] = [:] private var fileSizeLogical: [URL: Int64] = [:]
private var fileIcon: [URL: NSImage?] = [:] private var fileIcon: [URL: NSImage?] = [:]
private var isDirectory: [URL: Bool] = [:]
private let dispatchGroup = DispatchGroup() private let dispatchGroup = DispatchGroup()
private let sortedApps: [AppInfo] private let sortedApps: [AppInfo]
@@ -113,7 +112,6 @@ class ReversePathsSearcher {
fileSize[path] = size.real fileSize[path] = size.real
fileSizeLogical[path] = size.logical fileSizeLogical[path] = size.logical
fileIcon[path] = getIconForFileOrFolderNS(atPath: path) fileIcon[path] = getIconForFileOrFolderNS(atPath: path)
isDirectory[path] = path.hasDirectoryPath
} }
} }
@@ -123,7 +121,6 @@ class ReversePathsSearcher {
updatedZombieFile.fileSize = self.fileSize updatedZombieFile.fileSize = self.fileSize
updatedZombieFile.fileSizeLogical = self.fileSizeLogical updatedZombieFile.fileSizeLogical = self.fileSizeLogical
updatedZombieFile.fileIcon = self.fileIcon updatedZombieFile.fileIcon = self.fileIcon
updatedZombieFile.isDirectory = self.isDirectory
self.appState.zombieFile = updatedZombieFile self.appState.zombieFile = updatedZombieFile
self.appState.showProgress = false self.appState.showProgress = false
} }
+184
View File
@@ -0,0 +1,184 @@
////
//// Item.swift
//// PearDisk
////
//// Created by Alin Lupascu on 7/17/24.
////
//
//import Foundation
//import SwiftData
//
//// Data Model
//@Model
//final class Item: Identifiable {
// @Attribute(.unique) let url: URL
// let name: String
// var size: Int64
// let isDirectory: Bool
// let parentURL: URL?
// let timestamp: Date
//
// init(url: URL, name: String, size: Int64, isDirectory: Bool, parentURL: URL? = nil) {
// self.url = url
// self.name = name
// self.size = size
// self.isDirectory = isDirectory
// self.parentURL = parentURL
// self.timestamp = Date()
// }
//}
//
//
//// Wipe peardisk.sqlite file
//func wipeSwiftDataStorage() async {
// let fileManager = FileManager.default
// guard let urlApp = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first else {
// print("Could not find the Application Support directory.")
// return
// }
//
// let peardiskURL = urlApp.appendingPathComponent("PearDisk").appendingPathComponent("Database")
//
// do {
//
// if fileManager.fileExists(atPath: peardiskURL.path) {
// try fileManager.removeItem(at: peardiskURL)
// print("\(peardiskURL.lastPathComponent) has been deleted.")
// }
// } catch {
// print("Error deleting SwiftData storage: \(error)")
// }
//}
//
//func wipeSwiftDataStorage2() async {
// guard let container = try? ModelContainer(for: Item.self) else {
// print("Failed to create ModelContainer.")
// return
// }
//
// let urlApp = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first
//
// if let urlApp = urlApp {
// let url = urlApp.appendingPathComponent("PearDisk").appendingPathComponent("peardisk.sqlite")
// if FileManager.default.fileExists(atPath: url.path) {
// print("Peardisk.sqlite: \(url.path(percentEncoded: false))")
// }
// } else {
// fatalError("Could not find the Application Support directory.")
// }
//
//
// await MainActor.run {
// let context = container.mainContext
//
// do {
// try context.delete(model: Item.self)
// try context.save()
// print("Peardisk.sqlite has been successfully wiped.")
// } catch {
// print("Error wiping SwiftData storage: \(error)")
// }
// }
//}
//
//
//
//
//// Print size of peardisk.sqlite file
//func printSwiftDataCacheSize() async {
// // Get the Application Support directory URL for your app
// guard let applicationSupportDirectory = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first else {
// print("Could not find the Application Support directory.")
// return
// }
//
// // Create the custom directory URL
// let customDirectoryURL = applicationSupportDirectory.appendingPathComponent("PearDisk").appendingPathComponent("Database")
// let storeURL = customDirectoryURL.appendingPathComponent("peardisk").appendingPathExtension("sqlite")
//
// // Check if the store URL exists
// guard FileManager.default.fileExists(atPath: storeURL.path) else {
// print("Store file does not exist.")
// return
// }
//
// // Initialize the ModelContainer with the custom store URL
// let modelConfiguration = ModelConfiguration(url: storeURL)
//
// do {
// let container = try ModelContainer(for: Item.self, configurations: modelConfiguration)
//
// await MainActor.run {
// let context = container.mainContext
// let fetchDescriptor = FetchDescriptor<Item>()
//
// do {
// let count = try context.fetchCount(fetchDescriptor)
// print("Number of Item objects: \(count)")
//
// // Get the size of the store file directly
// let fileManager = FileManager.default
// do {
// let attributes = try fileManager.attributesOfItem(atPath: storeURL.path)
// if let size = attributes[.size] as? Int64 {
// let sizeMB = Double(size) / (1024 * 1024)
// print("Total size of SwiftData storage: \(String(format: "%.2f", sizeMB)) MB")
// } else {
// print("Could not retrieve file size.")
// }
// } catch {
// print("Could not access file at \(storeURL.path): \(error)")
// }
// } catch {
// print("Error fetching data: \(error)")
// }
// }
// } catch {
// print("Could not create ModelContainer: \(error)")
// }
//}
//
//
//
//// Setup Data Model
//func createSharedModelContainer() -> ModelContainer {
// do {
// // Get the Application Support directory URL for your app
// guard let applicationSupportDirectory = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first else {
// fatalError("Could not find the Application Support directory.")
// }
//
// // Create the custom directory URL
// let customDirectoryURL = applicationSupportDirectory.appendingPathComponent("PearDisk", isDirectory: true).appendingPathComponent("Database", isDirectory: true)
//
// // Create the directory if it doesn't exist
// do {
// try FileManager.default.createDirectory(at: customDirectoryURL, withIntermediateDirectories: true, attributes: nil)
// } catch {
// fatalError("Could not create directory: \(error)")
// }
//
// // Create the store URL with the desired filename inside the custom directory
// let storeURL = customDirectoryURL.appendingPathComponent("peardisk").appendingPathExtension("sqlite")
//
// // Ensure the directory is accessible for reading and writing
// let fileManager = FileManager.default
// var isDirectory: ObjCBool = true
// if !fileManager.fileExists(atPath: customDirectoryURL.path, isDirectory: &isDirectory) {
// fatalError("Directory does not exist.")
// }
//
// if !fileManager.isWritableFile(atPath: customDirectoryURL.path) {
// fatalError("Directory is not writable.")
// }
//
// if !fileManager.isReadableFile(atPath: customDirectoryURL.path) {
// fatalError("Directory is not readable.")
// }
//
// let modelConfiguration = ModelConfiguration(url: storeURL)
// return try ModelContainer(for: Item.self, configurations: modelConfiguration)
// } catch {
// fatalError("Could not create ModelContainer: \(error)")
// }
//}
+87
View File
@@ -0,0 +1,87 @@
//
// FileSearch.swift
// Pearcleaner
//
// Created by Alin Lupascu on 7/20/24.
//
import Foundation
import SwiftData
import AlinFoundation
enum FastFileSearchError: Error {
case invalidPath
case failedToReadDirectory
case failedToGetFileInfo
}
class FastFileSearch {
private let queue = DispatchQueue(label: "com.alienator88.Pearcleaner.disk", qos: .userInitiated, attributes: .concurrent)
func search(url: URL, completion: @escaping (Result<[Item], Error>) -> Void) {
queue.async {
do {
let items = try self.searchRecursive(url: url)
let sortedItems = items.sorted { $0.size > $1.size }
DispatchQueue.main.async {
completion(.success(sortedItems))
}
} catch {
DispatchQueue.main.async {
completion(.failure(error))
}
}
}
}
private func searchRecursive(url: URL) throws -> [Item] {
var items: [Item] = []
guard let path = url.path.cString(using: .utf8) else {
throw FastFileSearchError.invalidPath
}
guard let dir = opendir(path) else {
throw FastFileSearchError.failedToReadDirectory
}
defer { closedir(dir) }
while let entry = readdir(dir) {
let name = withUnsafePointer(to: entry.pointee.d_name) { ptr in
ptr.withMemoryRebound(to: CChar.self, capacity: Int(entry.pointee.d_namlen)) {
String(cString: $0)
}
}
let exclude = [".", "..", ".DS_Store", ".localized"]
if exclude.contains(name) { continue }
let fullURL = url.appendingPathComponent(name)
let fullPath = fullURL.path
var st = stat()
guard lstat(fullPath, &st) == 0 else {
print("Failed to get file info for \(fullPath)")
continue
}
let isDirectory = (st.st_mode & S_IFMT) == S_IFDIR
if isDirectory {
// Recursively search subdirectories
do {
let subItems = try searchRecursive(url: fullURL)
items.append(contentsOf: subItems)
} catch {
print("Error processing directory \(fullPath): \(error)")
}
} else {
// Add non-directory files to the items array
let size = Int64(st.st_size)
let item = Item(url: fullURL, name: name, size: size)
items.append(item)
}
}
return items
}
}
+202
View File
@@ -0,0 +1,202 @@
//
// FolderDetailView.swift
// Pearcleaner
//
// Created by Alin Lupascu on 7/20/24.
//
import Foundation
import SwiftUI
//MARK: Detail View
struct FolderDetailView: View {
let rootURL: URL
@Binding var currentPath: URL?
@State private var currentItem: Item?
@State private var childItems: [Item] = []
@State private var isLoading = false
@State private var errorMessage: String?
@State private var forwardStack: [URL] = []
@State private var hoveredItem: Item?
private var fastSearch: FastFileSearch
init(rootURL: URL, currentPath: Binding<URL?>) {
self.rootURL = rootURL
self._currentPath = currentPath
self.fastSearch = FastFileSearch()
}
var body: some View {
VStack {
Spacer()
if isLoading {
ProgressView("Loading...")
} else if let errorMessage = errorMessage {
Text(errorMessage)
.foregroundColor(.red)
.padding()
} else if let currentItem = currentItem {
if !childItems.isEmpty {
TreeMapChart(items: childItems, onItemSelected: { selectedItem in
NSWorkspace.shared.selectFile(selectedItem.url.path, inFileViewerRootedAtPath: selectedItem.url.deletingLastPathComponent().path)
}, hoveredItem: $hoveredItem)
HStack {
Text("Total size: \(ByteCountFormatter.string(fromByteCount: currentItem.size, countStyle: .file))")
Spacer()
if let hoveredItem = hoveredItem {
Text("\(hoveredItem.name) (\(formatSize(hoveredItem.size)))")
}
Spacer()
VStack(alignment: .trailing) {
HStack {
RoundedRectangle(cornerRadius: 2)
.fill(LinearGradient(
gradient: Gradient(colors: [Color.cyan, Color.blue]),
startPoint: .topLeading,
endPoint: .bottomTrailing
))
.frame(width: 10, height: 10)
Text("Directory")
.padding(.trailing)
RoundedRectangle(cornerRadius: 2)
.fill(LinearGradient(
gradient: Gradient(colors: [Color.pink, Color.orange]),
startPoint: .topLeading,
endPoint: .bottomTrailing
))
.frame(width: 10, height: 10)
Text("File")
}
}
}
.padding(5)
} else {
Text("This folder is empty or there was an error loading its contents.")
.padding(5)
}
} else {
Text("No data available")
.padding(5)
}
Spacer()
}
.padding()
.onChange(of: currentPath) { newValue in
if let newPath = newValue {
loadContents(url: newPath)
}
}
// Might need this in another app where I won't use a Navigation View
// .onAppear {
// loadContents(url: rootURL)
// }
.toolbar {
// ToolbarItem(placement: .navigation) {
// Button {
// goBack()
// } label: {
// Image(systemName: "arrow.left")
// }
// .disabled(currentPath == rootURL)
// }
// ToolbarItem(placement: .navigation) {
// Button {
// goForward()
// } label: {
// Image(systemName: "arrow.right")
// }
// .disabled(forwardStack.isEmpty)
// }
ToolbarItem(placement: .principal) {
if let currentPath = currentPath {
Text(currentPath.path)
.font(.headline)
.lineLimit(1)
.truncationMode(.middle)
.padding(5)
.padding(.horizontal, 2)
.background {
RoundedRectangle(cornerRadius: 8)
.fill(.primary)
.opacity(0.05)
}
}
}
ToolbarItemGroup(placement: .automatic) {
Spacer()
Button {
printItemsCount()
} label: {
Image(systemName: "number.square")
}
Button {
refreshCurrentFolder()
} label: {
Image(systemName: "arrow.counterclockwise")
}
// Button {
// Task {
// await wipeSwiftDataStorage()
// }
// } label: {
// Image(systemName: "xmark.bin")
// }
// Button {
// Task {
// await printSwiftDataCacheSize()
// }
// } label: {
// Image(systemName: "number.square")
// }
}
}
}
private func loadContents(url: URL, wipeCache: Bool = false) {
isLoading = true
errorMessage = nil
fastSearch.search(url: url) { result in
switch result {
case .success(let items):
self.childItems = items
self.currentPath = url
self.currentItem = Item(url: url, name: url.lastPathComponent, size: items.reduce(0) { $0 + $1.size })
case .failure(let error):
self.errorMessage = "Error loading contents: \(error.localizedDescription)"
print("Error loading contents: \(error)")
}
self.isLoading = false
}
}
private func refreshCurrentFolder() {
if let currentPath = currentPath {
loadContents(url: currentPath, wipeCache: true)
}
}
private func printItemsCount() {
print("Number of items: \(childItems.count)")
}
}
+96
View File
@@ -0,0 +1,96 @@
//
// PearDiskView.swift
// Pearcleaner
//
// Created by Alin Lupascu on 7/17/24.
//
import SwiftUI
import AlinFoundation
//MARK: Main View
struct PearDiskView: View {
@EnvironmentObject var themeManager: ThemeManager
@State private var selectedVolume: URL?
@State private var currentPath: URL?
var body: some View {
NavigationSplitView {
SidebarView(selectedVolume: $selectedVolume)
} detail: {
if let volume = selectedVolume {
FolderDetailView(rootURL: volume, currentPath: $currentPath)
} else {
Text("Select a volume")
}
}
.onChange(of: selectedVolume) { path in
currentPath = path
}
}
}
//MARK: Sidebar
struct SidebarView: View {
@EnvironmentObject var themeManager: ThemeManager
@Binding var selectedVolume: URL?
@State private var volumes: [URL] = []
var body: some View {
List(volumes, id: \.self) { volume in
Text(volume.lastPathComponent)
.onTapGesture {
selectedVolume = volume
}
}
.onAppear {
loadVolumes()
}
.background(backgroundView(themeManager: themeManager))
}
private func loadVolumes() {
let fileManager = FileManager.default
// Define the base paths for the directories you want to scan
let basePaths = [
URL(fileURLWithPath: "/Volumes"),
URL(fileURLWithPath: "/Users"),
URL(fileURLWithPath: "/Users/alin"),
]
for basePath in basePaths {
do {
let urls = try fileManager.contentsOfDirectory(at: basePath, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles])
// Iterate over each URL in the directory
for url in urls {
if url.isSymlink() {
// If the item is a symbolic link, resolve it
let resolvedUrl = url.resolvingSymlinksInPath()
volumes.append(resolvedUrl)
} else {
volumes.append(url)
}
}
} catch {
print("Error loading volumes: \(error)")
}
}
}
}
extension URL {
public func isSymlink() -> Bool {
do {
let _ = try self.checkResourceIsReachable()
let resourceValues = try self.resourceValues(forKeys: [.isSymbolicLinkKey])
return resourceValues.isSymbolicLink == true
} catch {
return false
}
}
}
@@ -9,23 +9,7 @@ import Foundation
import SwiftUI import SwiftUI
import AlinFoundation import AlinFoundation
final class Item: Identifiable {
let url: URL
let name: String
var size: Int64
let isDirectory: Bool
let parentURL: URL?
let timestamp: Date
init(url: URL, name: String, size: Int64, isDirectory: Bool, parentURL: URL? = nil) {
self.url = url
self.name = name
self.size = size
self.isDirectory = isDirectory
self.parentURL = parentURL
self.timestamp = Date()
}
}
struct TreeMapChart: View { struct TreeMapChart: View {
let items: [Item] let items: [Item]
@@ -33,7 +17,7 @@ struct TreeMapChart: View {
@Binding var hoveredItem: Item? @Binding var hoveredItem: Item?
var displayedItems: [Item] { var displayedItems: [Item] {
items.filter { $0.size >= 1_048_576 } // Filter to keep only items with size >= 1MB items//.filter { $0.size >= 1_048_576 } // Filter to keep only items with size >= 1MB
.sorted(by: { $0.size > $1.size }) // Then sort the remaining items by size .sorted(by: { $0.size > $1.size }) // Then sort the remaining items by size
} }
@@ -91,7 +75,7 @@ struct TreeMapView: View {
private var content: some View { private var content: some View {
ForEach(0..<ld.content.count, id: \.self) { i in ForEach(0..<ld.content.count, id: \.self) { i in
let file = items[ld.content[i].index] let file = items[ld.content[i].index]
let maxDimension = max(ld.content[i].w, ld.content[i].h) / 5 // let maxDimension = max(ld.content[i].w, ld.content[i].h) / 5
ZStack { ZStack {
Rectangle() Rectangle()
@@ -121,12 +105,6 @@ struct TreeMapView: View {
.clipShape(RoundedRectangle(cornerRadius: 8)) .clipShape(RoundedRectangle(cornerRadius: 8))
.zIndex(1) .zIndex(1)
} else {
// Image(systemName: file.isDirectory ? "folder.fill" : "doc.fill")
// .resizable()
// .aspectRatio(contentMode: .fit)
// .frame(width: maxDimension, height: maxDimension)
// .foregroundStyle(.primary.opacity(0.5))
} }
} }
} }
@@ -140,20 +118,25 @@ struct TreeMapView: View {
} }
} }
private func colorForItem(_ item: Item) -> Color { // private func colorForItem(_ item: Item) -> Color {
item.isDirectory ? .blue : .gray // item.isDirectory ? .blue : .gray
} // }
private func gradientForItem(_ item: Item) -> LinearGradient { private func gradientForItem(_ item: Item) -> LinearGradient {
item.isDirectory ? LinearGradient( LinearGradient(
gradient: Gradient(colors: [Color.cyan, Color.blue]), gradient: Gradient(colors: [Color.cyan, Color.blue]),
startPoint: .topLeading, startPoint: .topLeading,
endPoint: .bottomTrailing endPoint: .bottomTrailing
) : LinearGradient(
gradient: Gradient(colors: [Color.pink, Color.orange]),
startPoint: .topLeading,
endPoint: .bottomTrailing
) )
// item.isDirectory ? LinearGradient(
// gradient: Gradient(colors: [Color.cyan, Color.blue]),
// startPoint: .topLeading,
// endPoint: .bottomTrailing
// ) : LinearGradient(
// gradient: Gradient(colors: [Color.pink, Color.orange]),
// startPoint: .topLeading,
// endPoint: .bottomTrailing
// )
} }
} }
@@ -172,7 +155,23 @@ func randomColor() -> Color {
brightness: Double.random(in: 0.90...1.0)) brightness: Double.random(in: 0.90...1.0))
} }
//MARK: =================================================================================================================== final class Item: Identifiable {
let url: URL
let name: String
var size: Int64
let isDirectory: Bool?
// let parentURL: URL?
// let timestamp: Date
init(url: URL, name: String, size: Int64, isDirectory: Bool = false) {
self.url = url
self.name = name
self.size = size
self.isDirectory = isDirectory
}
}
//MARK: Layout Generator ============================================================================================================
// Layout // Layout
enum Direction { enum Direction {
+17
View File
@@ -23,10 +23,24 @@ struct RegularMode: View {
@Binding var showPopover: Bool @Binding var showPopover: Bool
@State private var showMenu = false @State private var showMenu = false
@State var isMenuBar: Bool = false @State var isMenuBar: Bool = false
@State private var isDrillView: Bool = false
var body: some View { var body: some View {
if isDrillView {
VStack {
Toggle("Drill", isOn: $isDrillView).padding()
PearDiskView()
.environmentObject(appState)
Spacer()
}
.background(backgroundView(themeManager: themeManager))
.frame(minWidth: 900, minHeight: 600)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.edgesIgnoringSafeArea(.all)
} else {
HStack(alignment: .center, spacing: 0) { HStack(alignment: .center, spacing: 0) {
// Toggle("", isOn: $isDrillView)
// App List // App List
HStack(spacing: 0){ HStack(spacing: 0){
@@ -81,6 +95,9 @@ struct RegularMode: View {
.frame(minWidth: 900, minHeight: 600) .frame(minWidth: 900, minHeight: 600)
.edgesIgnoringSafeArea(.all) .edgesIgnoringSafeArea(.all)
} }
}
} }
-23
View File
@@ -36,8 +36,6 @@ struct ZombieView: View {
@State private var totalRealSizeUninstallBtn: String = "" @State private var totalRealSizeUninstallBtn: String = ""
@State private var totalLogicalSizeUninstallBtn: String = "" @State private var totalLogicalSizeUninstallBtn: String = ""
@State private var totalFinderSizeUninstallBtn: String = "" @State private var totalFinderSizeUninstallBtn: String = ""
@State private var isTreeMap: Bool = false
@State private var hoveredItem: Item?
var body: some View { var body: some View {
@@ -172,18 +170,7 @@ struct ZombieView: View {
.padding(.top, 0) .padding(.top, 0)
} }
Toggle("Tree Map View", isOn: $isTreeMap)
if isTreeMap {
TreeMapChart(items: appState.zombieFile.toItems(), onItemSelected: { selectedItem in
// if selectedItem.isDirectory {
// drillDown(to: selectedItem)
// } else {
// NSWorkspace.shared.selectFile(selectedItem.url.path, inFileViewerRootedAtPath: selectedItem.url.deletingLastPathComponent().path)
// }
}, hoveredItem: $hoveredItem)
} else {
// Item selection and sorting toolbar // Item selection and sorting toolbar
HStack { HStack {
Toggle("", isOn: Binding( Toggle("", isOn: Binding(
@@ -260,21 +247,11 @@ struct ZombieView: View {
} }
.padding() .padding()
} }
}
Spacer() Spacer()
HStack() { HStack() {
Text(hoveredItem?.name ?? "")
Spacer() Spacer()
InfoButton(text: "Leftover file search is not 100% accurate as it doesn't have any uninstalled app bundles to check against for file exclusion. This does a best guess search for files/folders and excludes the ones that have overlap with your currently installed applications. Please confirm files marked for deletion really do belong to uninstalled applications.", color: .orange, warning: true, edge: .top) InfoButton(text: "Leftover file search is not 100% accurate as it doesn't have any uninstalled app bundles to check against for file exclusion. This does a best guess search for files/folders and excludes the ones that have overlap with your currently installed applications. Please confirm files marked for deletion really do belong to uninstalled applications.", color: .orange, warning: true, edge: .top)