mirror of
https://github.com/exituser/Pearcleaner.git
synced 2026-09-17 09:38:51 +00:00
updates
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)") }),
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,39 +106,93 @@ 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)
|
|
||||||
.onTapGesture {
|
|
||||||
selectFolder()
|
|
||||||
}
|
|
||||||
.onHover { hovering in
|
|
||||||
withAnimation(Animation.easeInOut(duration: 0.4)) {
|
|
||||||
isHoveredPlus = hovering
|
|
||||||
}
|
|
||||||
if isHoveredPlus {
|
|
||||||
NSCursor.pointingHand.push()
|
|
||||||
} else {
|
|
||||||
NSCursor.pop()
|
|
||||||
}
|
}
|
||||||
|
.buttonStyle(SimpleButtonStyle(icon: "plus", help: "Add file/folder"))
|
||||||
|
.onHover(perform: { hovering in
|
||||||
|
if hovering {
|
||||||
|
NSCursor.pointingHand.push()
|
||||||
|
} else {
|
||||||
|
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) ?? []
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
.buttonStyle(UninstallButton(isEnabled: !selectedZombieItemsLocal.isEmpty))
|
||||||
.disabled(appState.selectedZombieItems.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()
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user