This commit is contained in:
Alin
2024-05-02 19:19:55 -06:00
parent a54371fb14
commit 6d599f68d8
13 changed files with 438 additions and 162 deletions
+1 -1
View File
@@ -19,7 +19,7 @@ class AppState: ObservableObject
@Published var zombieFile: ZombieFile @Published var zombieFile: ZombieFile
@Published var sortedApps: [AppInfo] = [] @Published var sortedApps: [AppInfo] = []
@Published var selectedItems = Set<URL>() @Published var selectedItems = Set<URL>()
@Published var selectedZombieItems = Set<URL>() // @Published var selectedZombieItems = Set<URL>()
@Published var alertType = AlertType.off @Published var alertType = AlertType.off
@Published var currentView = CurrentDetailsView.empty @Published var currentView = CurrentDetailsView.empty
@Published var showAlert: Bool = false @Published var showAlert: Bool = false
+1 -1
View File
@@ -99,7 +99,7 @@ class Locations: ObservableObject {
"/Library/PrivilegedHelperTools", "/Library/PrivilegedHelperTools",
"/private/var/db/receipts", "/private/var/db/receipts",
cacheDir, cacheDir,
tempDir // tempDir // Stop listing these files as there's a ton of them and they get cleaned up by the OS anyways
]) ])
} }
} }
+2 -2
View File
@@ -110,7 +110,7 @@ func listAppSupportDirectories() -> [String] {
// Load app paths on launch // Load app paths on launch
func reversePreloader(allApps: [AppInfo], appState: AppState, locations: Locations, reverseAddon: Bool = false, completion: @escaping () -> Void = {}) { func reversePreloader(allApps: [AppInfo], appState: AppState, locations: Locations, fsm: FolderSettingsManager, reverseAddon: Bool = false, completion: @escaping () -> Void = {}) {
let dispatchGroup = DispatchGroup() let dispatchGroup = DispatchGroup()
appState.appInfoStore.removeAll() appState.appInfoStore.removeAll()
@@ -127,7 +127,7 @@ func reversePreloader(allApps: [AppInfo], appState: AppState, locations: Locatio
func checkAllAppsProcessed(retryCount: Int = 0, maxRetry: Int = 120) { func checkAllAppsProcessed(retryCount: Int = 0, maxRetry: Int = 120) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
if appState.appInfoStore.count == allApps.count { if appState.appInfoStore.count == allApps.count {
ReversePathsSearcher(appState: appState, locations: locations).reversePathsSearch() { ReversePathsSearcher(appState: appState, locations: locations, fsm: fsm).reversePathsSearch() {
updateOnMain { updateOnMain {
appState.showProgress = false appState.showProgress = false
} }
+8 -1
View File
@@ -11,6 +11,7 @@ import AppKit
class ReversePathsSearcher { class ReversePathsSearcher {
private let appState: AppState private let appState: AppState
private let locations: Locations private let locations: Locations
private let fsm: FolderSettingsManager
private let fileManager = FileManager.default private let fileManager = FileManager.default
private var collection: [URL] = [] private var collection: [URL] = []
private var fileSize: [URL: Int64] = [:] private var fileSize: [URL: Int64] = [:]
@@ -18,9 +19,10 @@ class ReversePathsSearcher {
private var fileIcon: [URL: NSImage?] = [:] private var fileIcon: [URL: NSImage?] = [:]
private let dispatchGroup = DispatchGroup() private let dispatchGroup = DispatchGroup()
init(appState: AppState, locations: Locations) { init(appState: AppState, locations: Locations, fsm: FolderSettingsManager) {
self.appState = appState self.appState = appState
self.locations = locations self.locations = locations
self.fsm = fsm
} }
@@ -64,6 +66,11 @@ class ReversePathsSearcher {
let formattedItemName = itemName.pearFormat() let formattedItemName = itemName.pearFormat()
let itemPath = itemURL.path.pearFormat() let itemPath = itemURL.path.pearFormat()
let itemLastPathComponent = itemURL.lastPathComponent.pearFormat() let itemLastPathComponent = itemURL.lastPathComponent.pearFormat()
let exclusionList = fsm.fileFolderPathsZ.map { $0.pearFormat() }
if exclusionList.contains(itemPath) || itemPath.contains("dsstore") || itemPath.contains("daemonnameoridentifierhere") {
return
}
guard !skipReverse.contains(where: { formattedItemName.contains($0) }), guard !skipReverse.contains(where: { formattedItemName.contains($0) }),
!allPaths.contains(where: { $0 == itemPath || $0.hasSuffix("/\(itemLastPathComponent)") }), !allPaths.contains(where: { $0 == itemPath || $0.hasSuffix("/\(itemLastPathComponent)") }),
+2 -1
View File
@@ -476,7 +476,8 @@ extension Int {
// --- Extend String to remove periods, spaces and lowercase the string // --- Extend String to remove periods, spaces and lowercase the string
extension String { extension String {
func pearFormat() -> String { func pearFormat() -> String {
return self.replacingOccurrences(of: ".", with: "").replacingOccurrences(of: " ", with: "").replacingOccurrences(of: "-", with: "").lowercased() // Remove all non-alphanumeric characters using regular expression and convert to lowercase
return self.replacingOccurrences(of: "[^a-zA-Z0-9]", with: "", options: .regularExpression).lowercased()
} }
} }
+27 -25
View File
@@ -28,6 +28,32 @@ struct AboutSettingsTab: View {
} }
HStack{
Image(systemName: "star")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
VStack(alignment: .leading){
Text("GitHub")
Text("Submit a bug or feature request via the repo")
.font(.callout)
.foregroundStyle(Color("mode").opacity(0.5))
}
Spacer()
Button(""){
NSWorkspace.shared.open(URL(string: "https://github.com/alienator88/Pearcleaner/issues/new/choose")!)
}
.buttonStyle(SimpleButtonStyle(icon: "link", help: "View"))
}
.padding(5)
.padding(.leading)
.padding(.top, 5)
Divider() Divider()
.padding() .padding()
@@ -67,30 +93,6 @@ struct AboutSettingsTab: View {
.padding(.leading) .padding(.leading)
HStack{
Image(systemName: "n.circle")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
VStack(alignment: .leading){
Text("Namelix")
Text("Logo and branding generation")
.font(.callout)
.foregroundStyle(Color("mode").opacity(0.5))
}
Spacer()
Button(""){
NSWorkspace.shared.open(URL(string: "https://namelix.com/")!)
}
.buttonStyle(SimpleButtonStyle(icon: "link", help: "View"))
}
.padding(5)
.padding(.leading)
HStack{ HStack{
Image(systemName: "applescript") Image(systemName: "applescript")
@@ -149,7 +151,7 @@ struct AboutSettingsTab: View {
Spacer() Spacer()
Text("Made with ❤️ by Alin Lupascu (dev@itsalin.com)").font(.footnote).padding(.bottom) Text("Made with ❤️ by Alin Lupascu").font(.footnote).padding(.bottom)
} }
.padding(20) .padding(20)
.frame(width: 500, height: 600) .frame(width: 500, height: 600)
+158 -36
View File
@@ -21,17 +21,31 @@ struct FolderSettingsTab: View {
@EnvironmentObject var locations: Locations @EnvironmentObject var locations: Locations
@EnvironmentObject var fsm: FolderSettingsManager @EnvironmentObject var fsm: FolderSettingsManager
@State private var isHovered = false @State private var isHovered = false
@State private var isHoveredPlus = false
var body: some View { var body: some View {
Form { Form {
VStack { VStack(spacing: 0) {
HStack(spacing: 0) { HStack(spacing: 0) {
Text("Apps").font(.title2) Text("Apps").font(.title2)
InfoButton(text: "Locations that will be searched for .app files. Click a non-default path to remove it. Add new folders below or drag/drop a folder over the list.", color: nil, label: "") InfoButton(text: "Locations that will be searched for .app files. Click a non-default path to remove it. Add new folders using the + button or drag/drop over the list. Non-default paths can't be removed.", color: nil, label: "")
Spacer() Spacer()
Button("") {
selectFolder()
} }
.buttonStyle(SimpleButtonStyle(icon: "plus", help: "Add folder"))
.onHover(perform: { hovering in
if hovering {
NSCursor.pointingHand.push()
} else {
NSCursor.pop()
}
})
}
.padding(.bottom, 5)
ScrollView { ScrollView {
@@ -92,38 +106,92 @@ struct FolderSettingsTab: View {
} }
Divider()
.padding(.vertical)
// === LEFTOVER FILES ================================================================================================
HStack(spacing: 0) {
Text("Leftover Files").font(.title2)
InfoButton(text: "Add files or folders that will be ignored when searching for leftover files. Click a path to remove it from the list. Add new files/folders using the + button or drag/drop over the list.", color: nil, label: "")
Spacer()
// === OTHER ================================================================================================ Button("") {
selectFilesFoldersZ()
ZStack {
RoundedRectangle(cornerRadius: 10)
.fill(Color("mode").opacity(0.1))
// .strokeBorder(Color("mode").opacity(0.1), lineWidth: 2)
.frame(width: 300, height: 100)
Image(systemName: "plus")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 30, height: 30)
.foregroundStyle(isHoveredPlus ? Color("mode") : Color("mode").opacity(0.5))
} }
.padding(.top) .buttonStyle(SimpleButtonStyle(icon: "plus", help: "Add file/folder"))
.onTapGesture { .onHover(perform: { hovering in
selectFolder() if hovering {
}
.onHover { hovering in
withAnimation(Animation.easeInOut(duration: 0.4)) {
isHoveredPlus = hovering
}
if isHoveredPlus {
NSCursor.pointingHand.push() NSCursor.pointingHand.push()
} else { } else {
NSCursor.pop() NSCursor.pop()
} }
})
}
.padding(.bottom, 5)
ScrollView {
VStack(spacing: 5) {
if fsm.fileFolderPathsZ.count == 0 {
HStack {
Text("No files or folders added")
.font(.callout)
.opacity(0.5)
.padding(5)
Spacer()
}
.disabled(true)
}
ForEach(fsm.fileFolderPathsZ.indices, id: \.self) { index in
HStack {
Text(fsm.fileFolderPathsZ[index])
.font(.callout)
.padding(5)
Spacer()
}
.onHover { hovering in
withAnimation(Animation.easeInOut(duration: 0.4)) {
isHovered = hovering
}
if isHovered {
NSCursor.disappearingItem.push()
} else {
NSCursor.pop()
}
}
.onTapGesture {
fsm.removePathZ(at: index)
} }
if index != fsm.fileFolderPathsZ.indices.last {
Divider().opacity(0.5)
}
}
}
}
.scrollIndicators(.automatic)
.padding()
.background(Color("mode").opacity(0.05))
.clipShape(RoundedRectangle(cornerRadius: 10))
.onDrop(of: ["public.file-url"], isTargeted: nil) { providers -> Bool in
providers.forEach { provider in
provider.loadDataRepresentation(forTypeIdentifier: "public.file-url") { (data, error) in
guard let data = data, error == nil,
let url = URL(dataRepresentation: data, relativeTo: nil) else {
printOS("FSM: Failed to load URL")
return
}
updateOnMain {
fsm.addPathZ(url.path)
}
}
}
return true
}
@@ -134,7 +202,7 @@ struct FolderSettingsTab: View {
} }
.padding(20) .padding(20)
.frame(width: 500, height: 420) .frame(width: 500, height: 600)
} }
@@ -157,46 +225,100 @@ struct FolderSettingsTab: View {
} }
} }
private func selectFilesFoldersZ() {
let dialog = NSOpenPanel()
dialog.title = "Choose files or folders"
dialog.showsResizeIndicator = false
dialog.showsHiddenFiles = true
dialog.canChooseDirectories = true
dialog.canCreateDirectories = false
dialog.canChooseFiles = true
if dialog.runModal() == NSApplication.ModalResponse.OK {
if let result = dialog.url {
fsm.addPathZ(result.path)
}
} else {
return
}
}
} }
class FolderSettingsManager: ObservableObject { class FolderSettingsManager: ObservableObject {
@Published var folderPaths: [String] = [] @Published var folderPaths: [String] = []
private let userDefaultsKey = "settings.folders.apps" @Published var fileFolderPathsZ: [String] = []
private let appsKey = "settings.folders.apps"
private let zombieKey = "settings.folders.zombie"
let defaultPaths = ["/Applications", "\(NSHomeDirectory())/Applications"] let defaultPaths = ["/Applications", "\(NSHomeDirectory())/Applications"]
init() { init() {
loadDefaultPathsIfNeeded() loadDefaultPathsIfNeeded()
} }
// Application folders //////////////////////////////////////////////////////////////////////////////////
private func loadDefaultPathsIfNeeded() { private func loadDefaultPathsIfNeeded() {
var paths = UserDefaults.standard.stringArray(forKey: userDefaultsKey) ?? defaultPaths var appsPaths = UserDefaults.standard.stringArray(forKey: appsKey) ?? defaultPaths
if paths.count < 2 { let zombiePaths = UserDefaults.standard.stringArray(forKey: zombieKey) ?? []
paths = defaultPaths if appsPaths.count < 2 {
appsPaths = defaultPaths
} }
UserDefaults.standard.set(paths, forKey: userDefaultsKey) UserDefaults.standard.set(appsPaths, forKey: appsKey)
self.folderPaths = paths self.folderPaths = appsPaths
self.fileFolderPathsZ = zombiePaths
} }
func addPath(_ path: String) { func addPath(_ path: String) {
if !self.folderPaths.contains(path) { if !self.folderPaths.contains(path) {
self.folderPaths.append(path) self.folderPaths.append(path)
UserDefaults.standard.set(self.folderPaths, forKey: userDefaultsKey) UserDefaults.standard.set(self.folderPaths, forKey: appsKey)
} }
} }
func removePath(at index: Int) { func removePath(at index: Int) {
guard self.folderPaths.indices.contains(index) else { return } guard self.folderPaths.indices.contains(index) else { return }
self.folderPaths.remove(at: index) // Update local state self.folderPaths.remove(at: index) // Update local state
UserDefaults.standard.set(self.folderPaths, forKey: userDefaultsKey) UserDefaults.standard.set(self.folderPaths, forKey: appsKey)
} }
func refreshPaths() { func refreshPaths() {
self.folderPaths = UserDefaults.standard.stringArray(forKey: userDefaultsKey) ?? defaultPaths self.folderPaths = UserDefaults.standard.stringArray(forKey: appsKey) ?? defaultPaths
} }
func getPaths() -> [String] { func getPaths() -> [String] {
return UserDefaults.standard.stringArray(forKey: userDefaultsKey) ?? defaultPaths return UserDefaults.standard.stringArray(forKey: appsKey) ?? defaultPaths
}
// Leftover files //////////////////////////////////////////////////////////////////////////////////
func addPathZ(_ path: String) {
let sanitizedPath = path.hasPrefix("/private") ? String(path.dropFirst(8)) : path
if !self.fileFolderPathsZ.contains(sanitizedPath) {
self.fileFolderPathsZ.append(sanitizedPath)
UserDefaults.standard.set(self.fileFolderPathsZ, forKey: zombieKey)
} }
} }
func removePathZ(at index: Int) {
guard self.fileFolderPathsZ.indices.contains(index) else { return }
self.fileFolderPathsZ.remove(at: index) // Update local state
UserDefaults.standard.set(self.fileFolderPathsZ, forKey: zombieKey)
}
func refreshPathsZ() {
self.fileFolderPathsZ = UserDefaults.standard.stringArray(forKey: zombieKey) ?? []
}
func getPathsZ() -> [String] {
return UserDefaults.standard.stringArray(forKey: zombieKey) ?? []
}
}
-10
View File
@@ -18,11 +18,9 @@ struct FilesView: View {
@AppStorage("settings.menubar.enabled") private var menubarEnabled: Bool = false @AppStorage("settings.menubar.enabled") private var menubarEnabled: Bool = false
@AppStorage("settings.general.selectedSort") var selectedSortAlpha: Bool = true @AppStorage("settings.general.selectedSort") var selectedSortAlpha: Bool = true
@AppStorage("settings.general.sizeType") var sizeType: String = "Real" @AppStorage("settings.general.sizeType") var sizeType: String = "Real"
@State private var localKey = UUID()
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme
@Binding var showPopover: Bool @Binding var showPopover: Bool
@Binding var search: String @Binding var search: String
var regularWin: Bool
@State private var elapsedTime = 0 @State private var elapsedTime = 0
@State private var timer: Timer? = nil @State private var timer: Timer? = nil
@@ -247,20 +245,12 @@ struct FilesView: View {
VStack { VStack {
FileDetailsItem(size: fileSize, sizeL: fileSizeL, icon: iconImage, path: path) FileDetailsItem(size: fileSize, sizeL: fileSizeL, icon: iconImage, path: path)
.padding(.vertical, 5) .padding(.vertical, 5)
// .padding(.leading, 40)
// if index < sort.count - 1 {
// Divider().padding(.leading, 40).opacity(0.5)
// }
} }
} }
} }
} }
.padding() .padding()
.onChange(of: sizeType) { _ in
localKey = UUID()
}
.id(localKey)
} }
+2 -2
View File
@@ -178,10 +178,10 @@ struct MiniAppView: View {
.popover(isPresented: $showPopover, arrowEdge: .trailing) { .popover(isPresented: $showPopover, arrowEdge: .trailing) {
VStack { VStack {
if appState.currentView == .files { if appState.currentView == .files {
FilesView(showPopover: $showPopover, search: $search, regularWin: false) FilesView(showPopover: $showPopover, search: $search)
.id(appState.appInfo.id) .id(appState.appInfo.id)
} else if appState.currentView == .zombie { } else if appState.currentView == .zombie {
ZombieView(showPopover: $showPopover, search: $search, regularWin: false) ZombieView(showPopover: $showPopover, search: $search)
.id(appState.appInfo.id) .id(appState.appInfo.id)
} }
+2 -2
View File
@@ -59,10 +59,10 @@ struct RegularMode: View {
if appState.currentView == .empty || appState.currentView == .apps { if appState.currentView == .empty || appState.currentView == .apps {
AppDetailsEmptyView(showPopover: $showPopover) AppDetailsEmptyView(showPopover: $showPopover)
} else if appState.currentView == .files { } else if appState.currentView == .files {
FilesView(showPopover: $showPopover, search: $search, regularWin: true) FilesView(showPopover: $showPopover, search: $search)
.id(appState.appInfo.id) .id(appState.appInfo.id)
} else if appState.currentView == .zombie { } else if appState.currentView == .zombie {
ZombieView(showPopover: $showPopover, search: $search, regularWin: true) ZombieView(showPopover: $showPopover, search: $search)
.id(appState.appInfo.id) .id(appState.appInfo.id)
} }
} }
+9 -6
View File
@@ -11,6 +11,7 @@ struct Searchbar: View {
@EnvironmentObject var appState: AppState @EnvironmentObject var appState: AppState
@EnvironmentObject var themeSettings: ThemeSettings @EnvironmentObject var themeSettings: ThemeSettings
@EnvironmentObject var locations: Locations @EnvironmentObject var locations: Locations
@EnvironmentObject var fsm: FolderSettingsManager
var glass: Bool var glass: Bool
var sidebarWidth: Double var sidebarWidth: Double
var menubarEnabled: Bool var menubarEnabled: Bool
@@ -53,7 +54,7 @@ struct Searchbar: View {
showPopover = false showPopover = false
} }
} }
.buttonStyle(SimpleButtonStyle(icon: "plus.square.dashed", label: "Drop Target", help: "Drop Target")) .buttonStyle(SimpleButtonStyle(icon: "circle.fill", label: "Drop Target", help: "Drop Target", size: 5))
} }
@@ -62,12 +63,11 @@ struct Searchbar: View {
withAnimation(.easeInOut(duration: 0.5)) { withAnimation(.easeInOut(duration: 0.5)) {
showPopover = false showPopover = false
appState.appInfo = .empty appState.appInfo = .empty
appState.selectedZombieItems = []
if appState.zombieFile.fileSize.keys.isEmpty { if appState.zombieFile.fileSize.keys.isEmpty {
appState.currentView = .zombie appState.currentView = .zombie
appState.showProgress.toggle() appState.showProgress.toggle()
showPopover.toggle() showPopover.toggle()
reversePreloader(allApps: appState.sortedApps, appState: appState, locations: locations, reverseAddon: true) reversePreloader(allApps: appState.sortedApps, appState: appState, locations: locations, fsm: fsm, reverseAddon: true)
} else { } else {
appState.currentView = .zombie appState.currentView = .zombie
showPopover.toggle() showPopover.toggle()
@@ -92,7 +92,7 @@ struct Searchbar: View {
Button("Quit") { Button("Quit") {
NSApp.terminate(nil) NSApp.terminate(nil)
} }
.buttonStyle(SimpleButtonStyle(icon: "x.circle.fill", label: "Quit Pearcleaner", help: "Quit Pearcleaner")) .buttonStyle(SimpleButtonStyle(icon: "circle.fill", label: "Quit Pearcleaner", help: "Quit Pearcleaner", size: 5))
} }
} }
@@ -130,9 +130,11 @@ struct SimpleSearchStyle: TextFieldStyle {
@State var darker: Bool = false @State var darker: Bool = false
@State var glass: Bool = false @State var glass: Bool = false
@State var padding: CGFloat = 5 @State var padding: CGFloat = 5
@State var sidebar: Bool = true
@EnvironmentObject var appState: AppState @EnvironmentObject var appState: AppState
@EnvironmentObject var themeSettings: ThemeSettings @EnvironmentObject var themeSettings: ThemeSettings
@AppStorage("settings.general.mini") private var mini: Bool = false @AppStorage("settings.general.mini") private var mini: Bool = false
@AppStorage("settings.menubar.enabled") private var menubarEnabled: Bool = false
func _body(configuration: TextField<Self._Label>) -> some View { func _body(configuration: TextField<Self._Label>) -> some View {
@@ -141,7 +143,7 @@ struct SimpleSearchStyle: TextFieldStyle {
.fill(darker ? themeSettings.themeColor.darker(by: 5) : themeSettings.themeColor) .fill(darker ? themeSettings.themeColor.darker(by: 5) : themeSettings.themeColor)
.allowsHitTesting(false) .allowsHitTesting(false)
.frame(height: 30) .frame(height: 30)
.opacity(glass ? 0.0 : 1.0) .opacity((glass && (sidebar || !mini && !menubarEnabled)) || mini || menubarEnabled ? 0.0 : 1.0)
ZStack { ZStack {
@@ -202,12 +204,13 @@ struct SearchBar: View {
@State var darker: Bool = false @State var darker: Bool = false
@State var glass: Bool = false @State var glass: Bool = false
@State var padding: CGFloat = 5 @State var padding: CGFloat = 5
@State var sidebar: Bool = true
@EnvironmentObject var appState: AppState @EnvironmentObject var appState: AppState
var body: some View { var body: some View {
HStack { HStack {
TextField("", text: $search) TextField("", text: $search)
.textFieldStyle(SimpleSearchStyle(trash: true, text: $search, darker: darker, glass: glass, padding: padding)) .textFieldStyle(SimpleSearchStyle(trash: true, text: $search, darker: darker, glass: glass, padding: padding, sidebar: sidebar))
} }
} }
} }
+210 -69
View File
@@ -11,6 +11,7 @@ import SwiftUI
struct ZombieView: View { struct ZombieView: View {
@EnvironmentObject var appState: AppState @EnvironmentObject var appState: AppState
@EnvironmentObject var locations: Locations @EnvironmentObject var locations: Locations
@EnvironmentObject var fsm: FolderSettingsManager
@State private var showPop: Bool = false @State private var showPop: Bool = false
@AppStorage("settings.general.mini") private var mini: Bool = false @AppStorage("settings.general.mini") private var mini: Bool = false
@AppStorage("settings.general.glass") private var glass: Bool = false @AppStorage("settings.general.glass") private var glass: Bool = false
@@ -18,55 +19,61 @@ struct ZombieView: View {
@AppStorage("settings.menubar.enabled") private var menubarEnabled: Bool = false @AppStorage("settings.menubar.enabled") private var menubarEnabled: Bool = false
@AppStorage("settings.general.selectedSort") var selectedSortAlpha: Bool = true @AppStorage("settings.general.selectedSort") var selectedSortAlpha: Bool = true
@AppStorage("settings.general.sizeType") var sizeType: String = "Real" @AppStorage("settings.general.sizeType") var sizeType: String = "Real"
@State private var localKey = UUID()
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme
@Binding var showPopover: Bool @Binding var showPopover: Bool
@Binding var search: String @Binding var search: String
@State private var searchZ: String = "" @State private var searchZ: String = ""
var regularWin: Bool
@State private var elapsedTime = 0 @State private var elapsedTime = 0
@State private var timer: Timer? = nil @State private var timer: Timer? = nil
@State private var selectedZombieItemsLocal: Set<URL> = []
@State private var memoizedFiles: [URL] = []
@State private var lastSearchTermUsed: String? = nil
@State private var totalRealSize: Int64 = 0
@State private var totalLogicalSize: Int64 = 0
@State private var totalRealSizeUninstallBtn: String = ""
@State private var totalLogicalSizeUninstallBtn: String = ""
@State private var totalFinderSizeUninstallBtn: String = ""
var body: some View { var body: some View {
let totalSelectedZombieSize: (real: String, logical: String, finder: String) = { // let totalSelectedZombieSize: (real: String, logical: String, finder: String) = {
var totalReal: Int64 = 0 // var totalReal: Int64 = 0
var totalLogical: Int64 = 0 // var totalLogical: Int64 = 0
//
// for url in selectedZombieItemsLocal {
// let realSize = appState.zombieFile.fileSize[url] ?? 0
// let logicalSize = appState.zombieFile.fileSizeLogical[url] ?? 0
// totalReal += realSize
// totalLogical += logicalSize
// }
// return (formatByte(size: totalReal).human, formatByte(size:totalLogical).human, "\(formatByte(size: totalLogical).byte) (\(formatByte(size: totalReal).human))")
// }()
for url in appState.selectedZombieItems { // let filteredAndSortedFiles: ([URL], Int64, Int64) = {
let realSize = appState.zombieFile.fileSize[url] ?? 0 // let fileSizeReal = appState.zombieFile.fileSize
let logicalSize = appState.zombieFile.fileSizeLogical[url] ?? 0 // let fileSizeLogical = appState.zombieFile.fileSizeLogical
totalReal += realSize // let filteredFilesReal = fileSizeReal.filter { (url, _) in
totalLogical += logicalSize // searchZ.isEmpty || url.lastPathComponent.localizedCaseInsensitiveContains(searchZ)
} // }
return (formatByte(size: totalReal).human, formatByte(size:totalLogical).human, "\(formatByte(size: totalLogical).byte) (\(formatByte(size: totalReal).human))") // let filteredFilesLogical = fileSizeLogical.filter { (url, _) in
}() // searchZ.isEmpty || url.lastPathComponent.localizedCaseInsensitiveContains(searchZ)
// }
// let filesToSort = (sizeType == "Real" || sizeType == "Finder" ? filteredFilesReal : filteredFilesLogical)
// let sortedFilteredFiles = filesToSort.sorted(by: {
// if selectedSortAlpha {
// return $0.key.lastPathComponent.pearFormat() < $1.key.lastPathComponent.pearFormat()
// } else {
// return $0.value > $1.value
// }
// }).map { $0.key }
// let totalSize = filteredFilesReal.values.reduce(0, +)
// let totalSizeL = filteredFilesLogical.values.reduce(0, +)
// return (sortedFilteredFiles, totalSize, totalSizeL)
// }()
let filteredAndSortedFiles: ([URL], Int64, Int64) = { // let displaySizeTotal = sizeType == "Real" ? formatByte(size: filteredAndSortedFiles.1).human :
let fileSizeReal = appState.zombieFile.fileSize // sizeType == "Logical" ? formatByte(size: filteredAndSortedFiles.2).human :
let fileSizeLogical = appState.zombieFile.fileSizeLogical // "\(formatByte(size: filteredAndSortedFiles.2).byte) (\(formatByte(size: filteredAndSortedFiles.1).human))"
let filteredFilesReal = fileSizeReal.filter { (url, _) in
searchZ.isEmpty || url.lastPathComponent.localizedCaseInsensitiveContains(searchZ)
}
let filteredFilesLogical = fileSizeLogical.filter { (url, _) in
searchZ.isEmpty || url.lastPathComponent.localizedCaseInsensitiveContains(searchZ)
}
let filesToSort = (sizeType == "Real" || sizeType == "Finder" ? filteredFilesReal : filteredFilesLogical)
let sortedFilteredFiles = filesToSort.sorted(by: {
if selectedSortAlpha {
return $0.key.lastPathComponent.pearFormat() < $1.key.lastPathComponent.pearFormat()
} else {
return $0.value > $1.value
}
}).map { $0.key }
let totalSize = filteredFilesReal.values.reduce(0, +)
let totalSizeL = filteredFilesLogical.values.reduce(0, +)
return (sortedFilteredFiles, totalSize, totalSizeL)
}()
let displaySizeTotal = sizeType == "Real" ? formatByte(size: filteredAndSortedFiles.1).human :
sizeType == "Logical" ? formatByte(size: filteredAndSortedFiles.2).human :
"\(formatByte(size: filteredAndSortedFiles.2).byte) (\(formatByte(size: filteredAndSortedFiles.1).human))"
VStack(alignment: .center) { VStack(alignment: .center) {
if appState.showProgress { if appState.showProgress {
@@ -156,7 +163,7 @@ struct ZombieView: View {
VStack(alignment: .trailing, spacing: 5) { VStack(alignment: .trailing, spacing: 5) {
Text("\(displaySizeTotal)").font(.title).fontWeight(.bold).help("Total size on disk") Text("\(displaySizeTotal)").font(.title).fontWeight(.bold).help("Total size on disk")
Text("\(appState.zombieFile.fileSize.count == 1 ? "\(appState.selectedZombieItems.count) / \(appState.zombieFile.fileSize.count) item" : "\(appState.selectedZombieItems.count) / \(appState.zombieFile.fileSize.count) items")") Text("\(selectedZombieItemsLocal.count) / \(searchZ.isEmpty ? appState.zombieFile.fileSize.count : memoizedFiles.count) \(appState.zombieFile.fileSize.count == 1 ? "item" : "items")")
.font(.callout).foregroundStyle(Color("mode").opacity(0.5)) .font(.callout).foregroundStyle(Color("mode").opacity(0.5))
} }
@@ -170,23 +177,51 @@ struct ZombieView: View {
// Item selection and sorting toolbar // Item selection and sorting toolbar
HStack { HStack {
Toggle("", isOn: Binding( Toggle("", isOn: Binding(
get: { appState.selectedZombieItems.count == appState.zombieFile.fileSize.count }, get: {
set: { newValue in if searchZ.isEmpty {
updateOnMain { // All items are selected if no filter is applied and all items are selected
appState.selectedZombieItems = newValue ? Set(appState.zombieFile.fileSize.keys) : [] 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 {
// 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 {
// Deselect only filtered files if a filter is applied
selectedZombieItemsLocal.subtract(memoizedFiles)
}
}
updateTotalSizes()
} }
)) ))
.toggleStyle(SimpleCheckboxToggleStyle()) .toggleStyle(SimpleCheckboxToggleStyle())
.help("All checkboxes") .help("All checkboxes")
SearchBar(search: $searchZ, darker: true, glass: glass) SearchBar(search: $searchZ, darker: true, glass: glass, sidebar: false)
.padding(.horizontal) .padding(.horizontal)
.onChange(of: searchZ) { newValue in
updateMemoizedFiles(for: newValue, sizeType: sizeType, selectedSortAlpha: selectedSortAlpha)
}
Button("") { Button("") {
selectedSortAlpha.toggle() 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")) .buttonStyle(SimpleButtonStyle(icon: selectedSortAlpha ? "textformat.abc" : "textformat.123", help: selectedSortAlpha ? "Sorted alphabetically" : "Sorted by size"))
@@ -201,11 +236,11 @@ struct ZombieView: View {
ScrollView() { ScrollView() {
LazyVStack { LazyVStack {
ForEach(Array(filteredAndSortedFiles.0.enumerated()), id: \.element) { index, file in 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] { 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:)) let iconImage = fileIcon.map(Image.init(nsImage:))
VStack { VStack {
ZombieFileDetailsItem(size: fileSize, sizeL: fileSizeL, icon: iconImage, path: file) ZombieFileDetailsItem(size: fileSize, sizeL: fileSizeL, icon: iconImage, path: file, isSelected: self.binding(for: file))
.padding(.vertical, 5) .padding(.vertical, 5)
} }
} }
@@ -213,11 +248,7 @@ struct ZombieView: View {
} }
.padding() .padding()
.onChange(of: sizeType) { _ in
localKey = UUID()
} }
}
.id(localKey)
@@ -231,14 +262,14 @@ struct ZombieView: View {
updateOnMain { updateOnMain {
appState.zombieFile = .empty appState.zombieFile = .empty
appState.showProgress.toggle() appState.showProgress.toggle()
reversePreloader(allApps: appState.sortedApps, appState: appState, locations: locations, reverseAddon: true) reversePreloader(allApps: appState.sortedApps, appState: appState, locations: locations, fsm: fsm, reverseAddon: true)
} }
} }
.buttonStyle(RescanButton()) .buttonStyle(RescanButton())
Button("\(sizeType == "Logical" ? totalSelectedZombieSize.logical : sizeType == "Finder" ? totalSelectedZombieSize.finder : totalSelectedZombieSize.real)") { Button("\(sizeType == "Logical" ? totalLogicalSizeUninstallBtn : sizeType == "Finder" ? totalFinderSizeUninstallBtn : totalRealSizeUninstallBtn)") {
Task { Task {
if appState.selectedZombieItems.count == appState.zombieFile.fileSize.keys.count { if selectedZombieItemsLocal.count == appState.zombieFile.fileSize.keys.count {
updateOnMain { updateOnMain {
appState.zombieFile = .empty appState.zombieFile = .empty
search = "" search = ""
@@ -253,7 +284,7 @@ struct ZombieView: View {
} }
let selectedItemsArray = Array(appState.selectedZombieItems) let selectedItemsArray = Array(selectedZombieItemsLocal)
moveFilesToTrash(at: selectedItemsArray) { moveFilesToTrash(at: selectedItemsArray) {
withAnimation { withAnimation {
@@ -261,18 +292,22 @@ struct ZombieView: View {
} }
updateOnMain { updateOnMain {
// Remove items from the list // Remove items from the list
appState.zombieFile.fileSize = appState.zombieFile.fileSize.filter { !appState.selectedZombieItems.contains($0.key) } appState.zombieFile.fileSize = appState.zombieFile.fileSize.filter { !selectedZombieItemsLocal.contains($0.key) }
// Update the selectedZombieFiles to remove references that are no longer present // Update the selectedZombieFiles to remove references that are no longer present
appState.selectedZombieItems.removeAll() selectedZombieItemsLocal.removeAll()
} updateTotalSizes()
} }
} }
// updateMemoizedFiles(for: searchZ, sizeType: sizeType, selectedSortAlpha: selectedSortAlpha, force: true)
} }
.buttonStyle(UninstallButton(isEnabled: !appState.selectedZombieItems.isEmpty))
.disabled(appState.selectedZombieItems.isEmpty) }
.buttonStyle(UninstallButton(isEnabled: !selectedZombieItemsLocal.isEmpty))
.disabled(selectedZombieItemsLocal.isEmpty)
} }
@@ -281,10 +316,112 @@ struct ZombieView: View {
.transition(.opacity) .transition(.opacity)
.padding([.horizontal, .bottom], 20) .padding([.horizontal, .bottom], 20)
.padding(.top, !mini ? 10 : 0) .padding(.top, !mini ? 10 : 0)
.onAppear {
updateMemoizedFiles(for: searchZ, sizeType: sizeType, selectedSortAlpha: selectedSortAlpha, force: true)
}
} }
} }
} }
private func binding(for file: URL) -> Binding<Bool> {
Binding<Bool>(
get: { self.selectedZombieItemsLocal.contains(file) },
set: { isSelected in
if isSelected {
self.selectedZombieItemsLocal.insert(file)
} else {
self.selectedZombieItemsLocal.remove(file)
}
updateTotalSizes()
}
)
}
private func updateMemoizedFiles(for searchTerm: String, sizeType: String, selectedSortAlpha: Bool, force: Bool = false) {
if !force && searchTerm == lastSearchTermUsed && self.sizeType == sizeType && self.selectedSortAlpha == selectedSortAlpha {
return
}
let results = filterAndSortFiles(for: searchTerm, sizeType: sizeType, selectedSortAlpha: selectedSortAlpha)
memoizedFiles = results.files
totalRealSize = results.totalRealSize
totalLogicalSize = results.totalLogicalSize
lastSearchTermUsed = searchTerm
self.sizeType = sizeType
self.selectedSortAlpha = selectedSortAlpha
updateTotalSizes()
}
private func filterAndSortFiles(for searchTerm: String, sizeType: String, selectedSortAlpha: Bool) -> (files: [URL], totalRealSize: Int64, totalLogicalSize: Int64) {
let fileSizeReal = appState.zombieFile.fileSize
let fileSizeLogical = appState.zombieFile.fileSizeLogical
let filteredFilesReal = fileSizeReal.filter { url, _ in searchTerm.isEmpty || url.lastPathComponent.localizedCaseInsensitiveContains(searchTerm) }
let filteredFilesLogical = fileSizeLogical.filter { url, _ in searchTerm.isEmpty || url.lastPathComponent.localizedCaseInsensitiveContains(searchTerm) }
let filesToSort = sizeType == "Real" || sizeType == "Finder" ? filteredFilesReal : filteredFilesLogical
let sortedFilteredFiles = filesToSort.sorted { (left, right) -> Bool in
if selectedSortAlpha {
return left.key.lastPathComponent.pearFormat() < right.key.lastPathComponent.pearFormat()
} else {
return left.value > right.value
}
}.map(\.key)
let totalRealSize = filteredFilesReal.values.reduce(0, +)
let totalLogicalSize = filteredFilesLogical.values.reduce(0, +)
return (sortedFilteredFiles, totalRealSize, totalLogicalSize)
}
func calculateTotalSelectedZombieSize() -> (real: String, logical: String, finder: String) {
var totalReal: Int64 = 0
var totalLogical: Int64 = 0
for url in selectedZombieItemsLocal {
let realSize = appState.zombieFile.fileSize[url] ?? 0
let logicalSize = appState.zombieFile.fileSizeLogical[url] ?? 0
totalReal += realSize
totalLogical += logicalSize
}
return (formatByte(size: totalReal).human,
formatByte(size: totalLogical).human,
"\(formatByte(size: totalLogical).byte) (\(formatByte(size: totalReal).human))")
}
private func updateTotalSizes() {
let sizes = calculateTotalSelectedZombieSize()
totalRealSizeUninstallBtn = sizes.real
totalLogicalSizeUninstallBtn = sizes.logical
totalFinderSizeUninstallBtn = "\(sizes.logical) (\(sizes.real))"
}
private var displaySizeText: String {
switch sizeType {
case "Logical":
return totalLogicalSizeUninstallBtn
case "Finder":
return totalFinderSizeUninstallBtn
default:
return totalRealSizeUninstallBtn
}
}
private var displaySizeTotal: String {
switch sizeType {
case "Real":
return formatByte(size: totalRealSize).human
case "Logical":
return formatByte(size: totalLogicalSize).human
default:
return "\(formatByte(size: totalLogicalSize).byte) (\(formatByte(size: totalRealSize).human))"
}
}
} }
@@ -297,22 +434,26 @@ struct ZombieFileDetailsItem: View {
let sizeL: Int64? let sizeL: Int64?
let icon: Image? let icon: Image?
let path: URL let path: URL
@Binding var isSelected: Bool
var body: some View { var body: some View {
HStack(alignment: .center, spacing: 20) { HStack(alignment: .center, spacing: 20) {
Toggle("", isOn: Binding( Toggle("", isOn: $isSelected)
get: { self.appState.selectedZombieItems.contains(self.path) },
set: { isChecked in
if isChecked {
self.appState.selectedZombieItems.insert(self.path)
} else {
self.appState.selectedZombieItems.remove(self.path)
}
}
))
.toggleStyle(SimpleCheckboxToggleStyle()) .toggleStyle(SimpleCheckboxToggleStyle())
// Toggle("", isOn: Binding(
// get: { self.selectedZombieItemsLocal.contains(self.path) },
// set: { isChecked in
// if isChecked {
// self.selectedZombieItemsLocal.insert(self.path)
// } else {
// self.selectedZombieItemsLocal.remove(self.path)
// }
// }
// ))
// .toggleStyle(SimpleCheckboxToggleStyle())
if let appIcon = icon { if let appIcon = icon {
appIcon appIcon
.resizable() .resizable()
+13 -3
View File
@@ -27,10 +27,21 @@ class WindowSettings {
func loadWindowSettings() -> NSRect { func loadWindowSettings() -> NSRect {
// Retrieve window size
let width = CGFloat(UserDefaults.standard.float(forKey: mini ? windowWidthKeyMini : windowWidthKey)) let width = CGFloat(UserDefaults.standard.float(forKey: mini ? windowWidthKeyMini : windowWidthKey))
let height = CGFloat(UserDefaults.standard.float(forKey: mini ? windowHeightKeyMini : windowHeightKey)) let height = CGFloat(UserDefaults.standard.float(forKey: mini ? windowHeightKeyMini : windowHeightKey))
let x = CGFloat(UserDefaults.standard.float(forKey: windowXKey))
let y = CGFloat(UserDefaults.standard.float(forKey: windowYKey)) // Set default middle position if not set in UserDefaults
var x = CGFloat(UserDefaults.standard.float(forKey: windowXKey))
var y = CGFloat(UserDefaults.standard.float(forKey: windowYKey))
if UserDefaults.standard.object(forKey: windowXKey) == nil || UserDefaults.standard.object(forKey: windowYKey) == nil {
// Set window to center of the screen if not set
let screenSize = NSScreen.main?.frame.size ?? NSSize(width: 800, height: 600) // Fallback screen size
x = (screenSize.width - width) / 2
y = (screenSize.height - height) / 2
}
return NSRect(x: x, y: y, width: width, height: height) return NSRect(x: x, y: y, width: width, height: height)
} }
@@ -49,7 +60,6 @@ class WindowSettings {
newWindow.titleVisibility = .hidden newWindow.titleVisibility = .hidden
newWindow.setFrameAutosaveName("Pearcleaner") newWindow.setFrameAutosaveName("Pearcleaner")
newWindow.contentView = NSHostingView(rootView: contentView()) newWindow.contentView = NSHostingView(rootView: contentView())
// self.window = newWindow
self.windows.append(newWindow) self.windows.append(newWindow)
newWindow.makeKeyAndOrderFront(nil) newWindow.makeKeyAndOrderFront(nil)
} }