mirror of
https://github.com/exituser/Pearcleaner.git
synced 2026-09-17 06:09:01 +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 */; };
|
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;
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 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 {
|
||||||
@@ -23,63 +23,80 @@ 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 {
|
||||||
|
|
||||||
HStack(alignment: .center, spacing: 0) {
|
if isDrillView {
|
||||||
|
VStack {
|
||||||
// App List
|
Toggle("Drill", isOn: $isDrillView).padding()
|
||||||
HStack(spacing: 0){
|
PearDiskView()
|
||||||
|
.environmentObject(appState)
|
||||||
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()
|
Spacer()
|
||||||
}
|
}
|
||||||
.zIndex(2)
|
|
||||||
.background(backgroundView(themeManager: themeManager))
|
.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 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,109 +170,88 @@ struct ZombieView: View {
|
|||||||
.padding(.top, 0)
|
.padding(.top, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
Toggle("Tree Map View", isOn: $isTreeMap)
|
|
||||||
|
|
||||||
|
// Item selection and sorting toolbar
|
||||||
if isTreeMap {
|
HStack {
|
||||||
TreeMapChart(items: appState.zombieFile.toItems(), onItemSelected: { selectedItem in
|
Toggle("", isOn: Binding(
|
||||||
// if selectedItem.isDirectory {
|
get: {
|
||||||
// drillDown(to: selectedItem)
|
if searchZ.isEmpty {
|
||||||
// } else {
|
// All items are selected if no filter is applied and all items are selected
|
||||||
// NSWorkspace.shared.selectFile(selectedItem.url.path, inFileViewerRootedAtPath: selectedItem.url.deletingLastPathComponent().path)
|
return selectedZombieItemsLocal.count == appState.zombieFile.fileSize.count
|
||||||
// }
|
} else {
|
||||||
}, hoveredItem: $hoveredItem)
|
// All currently filtered files are selected when a filter is applied
|
||||||
} else {
|
return Set(memoizedFiles).isSubset(of: selectedZombieItemsLocal) && selectedZombieItemsLocal.count == memoizedFiles.count
|
||||||
// Item selection and sorting toolbar
|
}
|
||||||
HStack {
|
},
|
||||||
Toggle("", isOn: Binding(
|
set: { newValue in
|
||||||
get: {
|
if newValue {
|
||||||
if searchZ.isEmpty {
|
if searchZ.isEmpty {
|
||||||
// All items are selected if no filter is applied and all items are selected
|
// Select all files if no filter is applied
|
||||||
return selectedZombieItemsLocal.count == appState.zombieFile.fileSize.count
|
selectedZombieItemsLocal = Set(appState.zombieFile.fileSize.keys)
|
||||||
} else {
|
} else {
|
||||||
// All currently filtered files are selected when a filter is applied
|
// Select only filtered files if a filter is applied
|
||||||
return Set(memoizedFiles).isSubset(of: selectedZombieItemsLocal) && selectedZombieItemsLocal.count == memoizedFiles.count
|
selectedZombieItemsLocal.formUnion(memoizedFiles)
|
||||||
}
|
}
|
||||||
},
|
} else {
|
||||||
set: { newValue in
|
if searchZ.isEmpty {
|
||||||
if newValue {
|
// Deselect all files if no filter is applied
|
||||||
if searchZ.isEmpty {
|
selectedZombieItemsLocal.removeAll()
|
||||||
// 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 {
|
} else {
|
||||||
if searchZ.isEmpty {
|
// Deselect only filtered files if a filter is applied
|
||||||
// Deselect all files if no filter is applied
|
selectedZombieItemsLocal.subtract(memoizedFiles)
|
||||||
selectedZombieItemsLocal.removeAll()
|
|
||||||
} else {
|
|
||||||
// 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateTotalSizes()
|
||||||
|
|
||||||
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"))
|
))
|
||||||
|
.toggleStyle(SimpleCheckboxToggleStyle())
|
||||||
|
.help("All checkboxes")
|
||||||
|
|
||||||
|
|
||||||
}
|
SearchBar(search: $searchZ, darker: true, glass: glass, sidebar: false)
|
||||||
.padding(.horizontal)
|
|
||||||
.padding(.vertical)
|
|
||||||
|
|
||||||
|
|
||||||
Divider()
|
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
|
.onChange(of: searchZ) { newValue in
|
||||||
|
updateMemoizedFiles(for: newValue, sizeType: sizeType, selectedSortAlpha: selectedSortAlpha)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
ScrollView() {
|
|
||||||
LazyVStack {
|
Button("") {
|
||||||
ForEach(memoizedFiles, id: \.self) { file in
|
selectedSortAlpha.toggle()
|
||||||
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:)) {
|
updateMemoizedFiles(for: searchZ, sizeType: sizeType, selectedSortAlpha: selectedSortAlpha, force: true)
|
||||||
VStack {
|
}
|
||||||
ZombieFileDetailsItem(size: fileSize, sizeL: fileSizeL, icon: iconImage, path: file, isSelected: self.binding(for: file))
|
.buttonStyle(SimpleButtonStyle(icon: selectedSortAlpha ? "textformat.abc" : "textformat.123", help: selectedSortAlpha ? "Sorted alphabetically" : "Sorted by size"))
|
||||||
.padding(.vertical, 5)
|
|
||||||
}
|
|
||||||
|
}
|
||||||
|
.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()
|
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)
|
||||||
|
|||||||
Reference in New Issue
Block a user