mirror of
https://github.com/exituser/Pearcleaner.git
synced 2026-09-17 03:48:55 +00:00
v3.8.3
This commit is contained in:
@@ -14,6 +14,10 @@
|
||||
C736E7652C2DF1C9009EDB6A /* AppPathsFetch-Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = C736E7642C2DF1C9009EDB6A /* AppPathsFetch-Async.swift */; };
|
||||
C74412C72BBF249000DDFCA8 /* AppPathsFetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C74412C62BBF249000DDFCA8 /* AppPathsFetch.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 */; };
|
||||
C7575D392B0182DE006A600A /* PearcleanerSentinel in CopyFiles */ = {isa = PBXBuildFile; fileRef = C728930F2AFD51EA00C8C1CD /* PearcleanerSentinel */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@@ -204,6 +212,18 @@
|
||||
name = Frameworks;
|
||||
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 */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -257,6 +277,7 @@
|
||||
C786F1A72C29E599001B3800 /* Builds */,
|
||||
C76D084F2AF84E0100D07867 /* Resources */,
|
||||
C76D08462AF83C2F00D07867 /* Settings */,
|
||||
C74D76B82C5D2D7900748DD1 /* PearDisk */,
|
||||
C77B901F2AF1B390009CC655 /* Views */,
|
||||
C72893072AFD442600C8C1CD /* Logic */,
|
||||
);
|
||||
@@ -274,7 +295,6 @@
|
||||
C7FEBA102BDC422200AE195F /* AppSearchView.swift */,
|
||||
C7D31D492AFEB26700C7ED9E /* AppListItems.swift */,
|
||||
C7EA06322C1CBE2000F872FC /* ConditionBuilderView.swift */,
|
||||
C74D76B62C5C4ABC00748DD1 /* TreeMap.swift */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
@@ -431,17 +451,20 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
C7DD49EA2BAB4D8100CCBA16 /* AppInfoFetch.swift in Sources */,
|
||||
C74D76BD2C5D2DF300748DD1 /* FileSearch.swift in Sources */,
|
||||
C79947962C34A794007279CC /* Tips.swift in Sources */,
|
||||
C76D08482AF83C3F00D07867 /* Update.swift in Sources */,
|
||||
C7FB173B2B96321300B96F9A /* AppsListView.swift in Sources */,
|
||||
C77B90162AF19377009CC655 /* AppState.swift in Sources */,
|
||||
C74D76B72C5C4ABC00748DD1 /* TreeMap.swift in Sources */,
|
||||
C74D76C02C5D2DF300748DD1 /* FolderDetail.swift in Sources */,
|
||||
C7DD49EE2BAB7F6000CCBA16 /* ReversePathsFetch.swift in Sources */,
|
||||
C7EA06332C1CBE2000F872FC /* ConditionBuilderView.swift in Sources */,
|
||||
C7DD49F02BABA14400CCBA16 /* Folders.swift in Sources */,
|
||||
C7D31D512AFF00F300C7ED9E /* Locations.swift in Sources */,
|
||||
C77B901A2AF1938F009CC655 /* Logic.swift in Sources */,
|
||||
C77B901C2AF193A3009CC655 /* Styles.swift in Sources */,
|
||||
C74D76BF2C5D2DF300748DD1 /* Data.swift in Sources */,
|
||||
C7045A282B03E71D00376976 /* MiniMode.swift in Sources */,
|
||||
C7ED86B12BA8A35500F13DE4 /* Interface.swift in Sources */,
|
||||
C72893052AFD42E600C8C1CD /* DeepLink.swift in Sources */,
|
||||
@@ -452,6 +475,7 @@
|
||||
C77B90042AF18E2E009CC655 /* PearcleanerApp.swift in Sources */,
|
||||
C76D08512AF850F300D07867 /* About.swift in Sources */,
|
||||
C77B90182AF19382009CC655 /* AppCommands.swift in Sources */,
|
||||
C74D76BE2C5D2DF300748DD1 /* PearDiskView.swift in Sources */,
|
||||
C7D31D4A2AFEB26700C7ED9E /* AppListItems.swift in Sources */,
|
||||
C77B901E2AF1A9DB009CC655 /* SettingsWindow.swift in Sources */,
|
||||
C74412C72BBF249000DDFCA8 /* AppPathsFetch.swift in Sources */,
|
||||
@@ -533,8 +557,8 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
APP_BUILD = 56;
|
||||
APP_VERSION = 3.8.2;
|
||||
APP_BUILD = 57;
|
||||
APP_VERSION = 3.8.3;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
@@ -603,8 +627,8 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
APP_BUILD = 56;
|
||||
APP_VERSION = 3.8.2;
|
||||
APP_BUILD = 57;
|
||||
APP_VERSION = 3.8.3;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
|
||||
@@ -65,8 +65,7 @@ class AppState: ObservableObject {
|
||||
id: UUID(),
|
||||
fileSize: [:],
|
||||
fileSizeLogical: [:],
|
||||
fileIcon: [:],
|
||||
isDirectory: [:]
|
||||
fileIcon: [:]
|
||||
)
|
||||
|
||||
updateExtensionStatus()
|
||||
@@ -134,7 +133,6 @@ struct ZombieFile: Identifiable, Equatable, Hashable {
|
||||
var fileSize: [URL:Int64]
|
||||
var fileSizeLogical: [URL:Int64]
|
||||
var fileIcon: [URL:NSImage?]
|
||||
var isDirectory: [URL:Bool]
|
||||
var totalSize: Int64
|
||||
{
|
||||
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 {
|
||||
/// Converts the data in ZombieFile into a list of Item instances.
|
||||
func toItems() -> [Item] {
|
||||
var items = [Item]()
|
||||
|
||||
// Iterate over each URL in fileSize to get corresponding properties
|
||||
for (url, size) in fileSize {
|
||||
let name = url.lastPathComponent
|
||||
let isDir = isDirectory[url] ?? false
|
||||
let parent = url.deletingLastPathComponent()
|
||||
|
||||
// Create an Item instance for each URL
|
||||
let item = Item(url: url, name: name, size: size, isDirectory: isDir, parentURL: parent)
|
||||
items.append(item)
|
||||
}
|
||||
|
||||
return items//.sorted { $0.size > $1.size }
|
||||
}
|
||||
}
|
||||
//extension ZombieFile {
|
||||
// func toItems() -> [Item] {
|
||||
// var items: [Item] = []
|
||||
//
|
||||
// for (url, size) in self.fileSize {
|
||||
// let name = url.lastPathComponent
|
||||
// let isDirectory = url.hasDirectoryPath
|
||||
//
|
||||
// let item = Item(
|
||||
// url: url,
|
||||
// name: name,
|
||||
// size: size,
|
||||
// isDirectory: isDirectory
|
||||
// )
|
||||
//
|
||||
// items.append(item)
|
||||
// }
|
||||
//
|
||||
// return items
|
||||
// }
|
||||
//}
|
||||
|
||||
|
||||
enum Arch {
|
||||
|
||||
@@ -18,7 +18,6 @@ class ReversePathsSearcher {
|
||||
private var fileSize: [URL: Int64] = [:]
|
||||
private var fileSizeLogical: [URL: Int64] = [:]
|
||||
private var fileIcon: [URL: NSImage?] = [:]
|
||||
private var isDirectory: [URL: Bool] = [:]
|
||||
private let dispatchGroup = DispatchGroup()
|
||||
private let sortedApps: [AppInfo]
|
||||
|
||||
@@ -113,7 +112,6 @@ class ReversePathsSearcher {
|
||||
fileSize[path] = size.real
|
||||
fileSizeLogical[path] = size.logical
|
||||
fileIcon[path] = getIconForFileOrFolderNS(atPath: path)
|
||||
isDirectory[path] = path.hasDirectoryPath
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,7 +121,6 @@ class ReversePathsSearcher {
|
||||
updatedZombieFile.fileSize = self.fileSize
|
||||
updatedZombieFile.fileSizeLogical = self.fileSizeLogical
|
||||
updatedZombieFile.fileIcon = self.fileIcon
|
||||
updatedZombieFile.isDirectory = self.isDirectory
|
||||
self.appState.zombieFile = updatedZombieFile
|
||||
self.appState.showProgress = false
|
||||
}
|
||||
|
||||
@@ -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)")
|
||||
// }
|
||||
//}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)")
|
||||
}
|
||||
}
|
||||
@@ -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 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 {
|
||||
let items: [Item]
|
||||
@@ -33,7 +17,7 @@ struct TreeMapChart: View {
|
||||
@Binding var hoveredItem: 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
|
||||
}
|
||||
|
||||
@@ -91,7 +75,7 @@ struct TreeMapView: View {
|
||||
private var content: some View {
|
||||
ForEach(0..<ld.content.count, id: \.self) { i in
|
||||
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 {
|
||||
Rectangle()
|
||||
@@ -121,12 +105,6 @@ struct TreeMapView: View {
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||
.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 {
|
||||
item.isDirectory ? .blue : .gray
|
||||
}
|
||||
// private func colorForItem(_ item: Item) -> Color {
|
||||
// item.isDirectory ? .blue : .gray
|
||||
// }
|
||||
|
||||
private func gradientForItem(_ item: Item) -> LinearGradient {
|
||||
item.isDirectory ? LinearGradient(
|
||||
LinearGradient(
|
||||
gradient: Gradient(colors: [Color.cyan, Color.blue]),
|
||||
startPoint: .topLeading,
|
||||
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))
|
||||
}
|
||||
|
||||
//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
|
||||
enum Direction {
|
||||
@@ -23,63 +23,80 @@ struct RegularMode: View {
|
||||
@Binding var showPopover: Bool
|
||||
@State private var showMenu = false
|
||||
@State var isMenuBar: Bool = false
|
||||
@State private var isDrillView: Bool = false
|
||||
|
||||
var body: some View {
|
||||
|
||||
HStack(alignment: .center, spacing: 0) {
|
||||
|
||||
// App List
|
||||
HStack(spacing: 0){
|
||||
|
||||
if appState.reload {
|
||||
VStack {
|
||||
Spacer()
|
||||
ProgressView() {
|
||||
Text("Gathering app details")
|
||||
.font(.callout)
|
||||
.foregroundStyle(.primary.opacity(0.5))
|
||||
.padding(5)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.frame(width: sidebarWidth)
|
||||
.padding(.vertical)
|
||||
} else {
|
||||
AppSearchView(glass: glass, menubarEnabled: menubarEnabled, mini: mini, search: $search, showPopover: $showPopover, isMenuBar: $isMenuBar)
|
||||
.frame(width: sidebarWidth)
|
||||
}
|
||||
|
||||
}
|
||||
.background(backgroundView(themeManager: themeManager, darker: true, glass: glass))
|
||||
.transition(.opacity)
|
||||
|
||||
SlideableDivider(dimension: $sidebarWidth)
|
||||
.background(backgroundView(themeManager: themeManager))
|
||||
.zIndex(3)
|
||||
|
||||
|
||||
// Details View
|
||||
HStack(spacing: 0) {
|
||||
Spacer()
|
||||
Group {
|
||||
if appState.currentView == .empty || appState.currentView == .apps {
|
||||
AppDetailsEmptyView(showPopover: $showPopover)
|
||||
} else 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)
|
||||
}
|
||||
}
|
||||
.transition(.opacity)
|
||||
if isDrillView {
|
||||
VStack {
|
||||
Toggle("Drill", isOn: $isDrillView).padding()
|
||||
PearDiskView()
|
||||
.environmentObject(appState)
|
||||
Spacer()
|
||||
}
|
||||
.zIndex(2)
|
||||
.background(backgroundView(themeManager: themeManager))
|
||||
.frame(minWidth: 900, minHeight: 600)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
} else {
|
||||
HStack(alignment: .center, spacing: 0) {
|
||||
// Toggle("", isOn: $isDrillView)
|
||||
|
||||
// App List
|
||||
HStack(spacing: 0){
|
||||
|
||||
if appState.reload {
|
||||
VStack {
|
||||
Spacer()
|
||||
ProgressView() {
|
||||
Text("Gathering app details")
|
||||
.font(.callout)
|
||||
.foregroundStyle(.primary.opacity(0.5))
|
||||
.padding(5)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.frame(width: sidebarWidth)
|
||||
.padding(.vertical)
|
||||
} else {
|
||||
AppSearchView(glass: glass, menubarEnabled: menubarEnabled, mini: mini, search: $search, showPopover: $showPopover, isMenuBar: $isMenuBar)
|
||||
.frame(width: sidebarWidth)
|
||||
}
|
||||
|
||||
}
|
||||
.background(backgroundView(themeManager: themeManager, darker: true, glass: glass))
|
||||
.transition(.opacity)
|
||||
|
||||
SlideableDivider(dimension: $sidebarWidth)
|
||||
.background(backgroundView(themeManager: themeManager))
|
||||
.zIndex(3)
|
||||
|
||||
|
||||
// Details View
|
||||
HStack(spacing: 0) {
|
||||
Spacer()
|
||||
Group {
|
||||
if appState.currentView == .empty || appState.currentView == .apps {
|
||||
AppDetailsEmptyView(showPopover: $showPopover)
|
||||
} else 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)
|
||||
}
|
||||
}
|
||||
.transition(.opacity)
|
||||
Spacer()
|
||||
}
|
||||
.zIndex(2)
|
||||
.background(backgroundView(themeManager: themeManager))
|
||||
}
|
||||
.frame(minWidth: 900, minHeight: 600)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
}
|
||||
.frame(minWidth: 900, minHeight: 600)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -36,8 +36,6 @@ struct ZombieView: View {
|
||||
@State private var totalRealSizeUninstallBtn: String = ""
|
||||
@State private var totalLogicalSizeUninstallBtn: String = ""
|
||||
@State private var totalFinderSizeUninstallBtn: String = ""
|
||||
@State private var isTreeMap: Bool = false
|
||||
@State private var hoveredItem: Item?
|
||||
|
||||
var body: some View {
|
||||
|
||||
@@ -172,109 +170,88 @@ struct ZombieView: View {
|
||||
.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
|
||||
HStack {
|
||||
Toggle("", isOn: Binding(
|
||||
get: {
|
||||
// Item selection and sorting toolbar
|
||||
HStack {
|
||||
Toggle("", isOn: Binding(
|
||||
get: {
|
||||
if searchZ.isEmpty {
|
||||
// All items are selected if no filter is applied and all items are selected
|
||||
return selectedZombieItemsLocal.count == appState.zombieFile.fileSize.count
|
||||
} else {
|
||||
// All currently filtered files are selected when a filter is applied
|
||||
return Set(memoizedFiles).isSubset(of: selectedZombieItemsLocal) && selectedZombieItemsLocal.count == memoizedFiles.count
|
||||
}
|
||||
},
|
||||
set: { newValue in
|
||||
if newValue {
|
||||
if searchZ.isEmpty {
|
||||
// All items are selected if no filter is applied and all items are selected
|
||||
return selectedZombieItemsLocal.count == appState.zombieFile.fileSize.count
|
||||
// Select all files if no filter is applied
|
||||
selectedZombieItemsLocal = Set(appState.zombieFile.fileSize.keys)
|
||||
} else {
|
||||
// All currently filtered files are selected when a filter is applied
|
||||
return Set(memoizedFiles).isSubset(of: selectedZombieItemsLocal) && selectedZombieItemsLocal.count == memoizedFiles.count
|
||||
// Select only filtered files if a filter is applied
|
||||
selectedZombieItemsLocal.formUnion(memoizedFiles)
|
||||
}
|
||||
},
|
||||
set: { newValue in
|
||||
if newValue {
|
||||
if searchZ.isEmpty {
|
||||
// Select all files if no filter is applied
|
||||
selectedZombieItemsLocal = Set(appState.zombieFile.fileSize.keys)
|
||||
} else {
|
||||
// Select only filtered files if a filter is applied
|
||||
selectedZombieItemsLocal.formUnion(memoizedFiles)
|
||||
}
|
||||
} else {
|
||||
if searchZ.isEmpty {
|
||||
// Deselect all files if no filter is applied
|
||||
selectedZombieItemsLocal.removeAll()
|
||||
} else {
|
||||
if searchZ.isEmpty {
|
||||
// Deselect all files if no filter is applied
|
||||
selectedZombieItemsLocal.removeAll()
|
||||
} else {
|
||||
// Deselect only filtered files if a filter is applied
|
||||
selectedZombieItemsLocal.subtract(memoizedFiles)
|
||||
}
|
||||
// Deselect only filtered files if a filter is applied
|
||||
selectedZombieItemsLocal.subtract(memoizedFiles)
|
||||
}
|
||||
|
||||
updateTotalSizes()
|
||||
}
|
||||
))
|
||||
.toggleStyle(SimpleCheckboxToggleStyle())
|
||||
.help("All checkboxes")
|
||||
|
||||
|
||||
SearchBar(search: $searchZ, darker: true, glass: glass, sidebar: false)
|
||||
.padding(.horizontal)
|
||||
.onChange(of: searchZ) { newValue in
|
||||
updateMemoizedFiles(for: newValue, sizeType: sizeType, selectedSortAlpha: selectedSortAlpha)
|
||||
}
|
||||
|
||||
|
||||
|
||||
Button("") {
|
||||
selectedSortAlpha.toggle()
|
||||
updateMemoizedFiles(for: searchZ, sizeType: sizeType, selectedSortAlpha: selectedSortAlpha, force: true)
|
||||
updateTotalSizes()
|
||||
}
|
||||
.buttonStyle(SimpleButtonStyle(icon: selectedSortAlpha ? "textformat.abc" : "textformat.123", help: selectedSortAlpha ? "Sorted alphabetically" : "Sorted by size"))
|
||||
))
|
||||
.toggleStyle(SimpleCheckboxToggleStyle())
|
||||
.help("All checkboxes")
|
||||
|
||||
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.vertical)
|
||||
|
||||
|
||||
Divider()
|
||||
SearchBar(search: $searchZ, darker: true, glass: glass, sidebar: false)
|
||||
.padding(.horizontal)
|
||||
.onChange(of: searchZ) { newValue in
|
||||
updateMemoizedFiles(for: newValue, sizeType: sizeType, selectedSortAlpha: selectedSortAlpha)
|
||||
}
|
||||
|
||||
|
||||
ScrollView() {
|
||||
LazyVStack {
|
||||
ForEach(memoizedFiles, id: \.self) { file in
|
||||
if let fileSize = appState.zombieFile.fileSize[file], let fileSizeL = appState.zombieFile.fileSizeLogical[file], let fileIcon = appState.zombieFile.fileIcon[file], let iconImage = fileIcon.map(Image.init(nsImage:)) {
|
||||
VStack {
|
||||
ZombieFileDetailsItem(size: fileSize, sizeL: fileSizeL, icon: iconImage, path: file, isSelected: self.binding(for: file))
|
||||
.padding(.vertical, 5)
|
||||
}
|
||||
|
||||
Button("") {
|
||||
selectedSortAlpha.toggle()
|
||||
updateMemoizedFiles(for: searchZ, sizeType: sizeType, selectedSortAlpha: selectedSortAlpha, force: true)
|
||||
}
|
||||
.buttonStyle(SimpleButtonStyle(icon: selectedSortAlpha ? "textformat.abc" : "textformat.123", help: selectedSortAlpha ? "Sorted alphabetically" : "Sorted by size"))
|
||||
|
||||
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.vertical)
|
||||
|
||||
|
||||
Divider()
|
||||
.padding(.horizontal)
|
||||
|
||||
|
||||
ScrollView() {
|
||||
LazyVStack {
|
||||
ForEach(memoizedFiles, id: \.self) { file in
|
||||
if let fileSize = appState.zombieFile.fileSize[file], let fileSizeL = appState.zombieFile.fileSizeLogical[file], let fileIcon = appState.zombieFile.fileIcon[file], let iconImage = fileIcon.map(Image.init(nsImage:)) {
|
||||
VStack {
|
||||
ZombieFileDetailsItem(size: fileSize, sizeL: fileSizeL, icon: iconImage, path: file, isSelected: self.binding(for: file))
|
||||
.padding(.vertical, 5)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
.padding()
|
||||
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Spacer()
|
||||
|
||||
HStack() {
|
||||
|
||||
Text(hoveredItem?.name ?? "")
|
||||
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user