diff --git a/Gemfile.lock b/Gemfile.lock index 9078571..48a2d59 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -10,17 +10,17 @@ GEM artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.3.0) - aws-partitions (1.935.0) - aws-sdk-core (3.196.1) + aws-partitions (1.943.0) + aws-sdk-core (3.197.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.651.0) aws-sigv4 (~> 1.8) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.82.0) - aws-sdk-core (~> 3, >= 3.193.0) + aws-sdk-kms (1.83.0) + aws-sdk-core (~> 3, >= 3.197.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.151.0) - aws-sdk-core (~> 3, >= 3.194.0) + aws-sdk-s3 (1.152.1) + aws-sdk-core (~> 3, >= 3.197.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.8) aws-sigv4 (1.8.0) @@ -147,7 +147,7 @@ GEM os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) highline (2.0.3) - http-cookie (1.0.5) + http-cookie (1.0.6) domain_name (~> 0.5) httpclient (2.8.3) jmespath (1.6.2) @@ -171,8 +171,8 @@ GEM trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) retriable (3.1.2) - rexml (3.2.8) - strscan (>= 3.0.9) + rexml (3.2.9) + strscan rouge (2.0.7) ruby2_keywords (0.0.5) rubyzip (2.3.2) diff --git a/Pearcleaner.xcodeproj/project.pbxproj b/Pearcleaner.xcodeproj/project.pbxproj index 5939bc4..d8b6c05 100644 --- a/Pearcleaner.xcodeproj/project.pbxproj +++ b/Pearcleaner.xcodeproj/project.pbxproj @@ -305,10 +305,10 @@ C77B90222AF2D616009CC655 /* FilesView.swift */, C71848432B8D2D600046CB13 /* ZombieView.swift */, C76D08542AF89CDE00D07867 /* RegularMode.swift */, - C7D31D492AFEB26700C7ED9E /* AppListItems.swift */, C7045A272B03E71D00376976 /* MiniMode.swift */, C7FB173A2B96321300B96F9A /* AppsListView.swift */, C7FEBA102BDC422200AE195F /* AppSearchView.swift */, + C7D31D492AFEB26700C7ED9E /* AppListItems.swift */, ); path = Views; sourceTree = ""; @@ -568,8 +568,8 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - APP_BUILD = 44; - APP_VERSION = 3.6.5; + APP_BUILD = 45; + APP_VERSION = 3.7.0; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -638,8 +638,8 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - APP_BUILD = 44; - APP_VERSION = 3.6.5; + APP_BUILD = 45; + APP_VERSION = 3.7.0; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; diff --git a/Pearcleaner/Logic/AppCommands.swift b/Pearcleaner/Logic/AppCommands.swift index 3189709..5021b48 100644 --- a/Pearcleaner/Logic/AppCommands.swift +++ b/Pearcleaner/Logic/AppCommands.swift @@ -33,16 +33,7 @@ struct AppCommands: Commands { Button { withAnimation(.easeInOut(duration: 0.5)) { - // Refresh Apps list - updateOnMain { - appState.reload.toggle() - } - let sortedApps = getSortedApps(paths: fsm.folderPaths, appState: appState) - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - appState.sortedApps = [] - appState.sortedApps = sortedApps - appState.reload.toggle() - } + reloadAppsList(appState: appState, fsm: fsm) } } label: { Text("Refresh Apps") @@ -65,12 +56,9 @@ struct AppCommands: Commands { Button { undoTrash(appState: appState) { - let sortedApps = getSortedApps(paths: fsm.folderPaths, appState: appState) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - appState.sortedApps = sortedApps - for app in appState.trashedFiles { - AppPathFinder(appInfo: app, appState: appState, locations: locations, undo: true).findPaths() - } + reloadAppsList(appState: appState, fsm: fsm) + for app in appState.trashedFiles { + AppPathFinder(appInfo: app, appState: appState, locations: locations, undo: true).findPaths() } } } label: { diff --git a/Pearcleaner/Logic/AppInfoFetch.swift b/Pearcleaner/Logic/AppInfoFetch.swift index bf82c5c..96d605d 100644 --- a/Pearcleaner/Logic/AppInfoFetch.swift +++ b/Pearcleaner/Logic/AppInfoFetch.swift @@ -46,22 +46,19 @@ class AppInfoFetcher { return nil } -// let appName = bundle.infoDictionary?["CFBundleDisplayName"] as? String ?? bundle.infoDictionary?["CFBundleName"] as? String -// ?? bundle.localizedInfoDictionary?[kCFBundleNameKey as String] as? String ?? path.deletingPathExtension().lastPathComponent - let appName = wrapped ? path.deletingLastPathComponent().deletingLastPathComponent().deletingPathExtension().lastPathComponent.capitalizingFirstLetter() : path.deletingPathExtension().lastPathComponent.capitalizingFirstLetter() - let appVersion = bundle.infoDictionary?["CFBundleShortVersionString"] as? String - ?? bundle.infoDictionary?["CFBundleVersion"] as? String - ?? "" + let appVersion = (bundle.infoDictionary?["CFBundleShortVersionString"] as? String)?.isEmpty ?? true + ? bundle.infoDictionary?["CFBundleVersion"] as? String ?? "" + : bundle.infoDictionary?["CFBundleShortVersionString"] as? String ?? "" let appIcon = fetchAppIcon(for: path, wrapped: wrapped) + let webApp = (bundle.infoDictionary?["LSTemplateApplication"] as? Bool ?? false || bundle.infoDictionary?["CFBundleExecutable"] as? String == "app_mode_loader") - let webApp = bundle.infoDictionary?["LSTemplateApplication"] as? Bool ?? false let system = !path.path.contains(NSHomeDirectory()) return AppInfo(id: UUID(), path: path, bundleIdentifier: bundleIdentifier, appName: appName, appVersion: appVersion, appIcon: appIcon, - webApp: webApp, wrapped: wrapped, system: system, files: [], fileSize: [:], fileSizeLogical: [:], fileIcon: [:]) + webApp: webApp, wrapped: wrapped, system: system, bundleSize: 0, files: [], fileSize: [:], fileSizeLogical: [:], fileIcon: [:]) } private static func fetchAppIcon(for path: URL, wrapped: Bool) -> NSImage? { diff --git a/Pearcleaner/Logic/AppState.swift b/Pearcleaner/Logic/AppState.swift index 6de4655..2fab8d2 100644 --- a/Pearcleaner/Logic/AppState.swift +++ b/Pearcleaner/Logic/AppState.swift @@ -47,6 +47,7 @@ class AppState: ObservableObject { webApp: false, wrapped: false, system: false, + bundleSize: 0, files: [], fileSize: [:], fileSizeLogical: [:], @@ -98,6 +99,7 @@ struct AppInfo: Identifiable, Equatable, Hashable { let webApp: Bool let wrapped: Bool let system: Bool + var bundleSize: Int64 var files: [URL] var fileSize: [URL:Int64] var fileSizeLogical: [URL:Int64] @@ -112,7 +114,7 @@ struct AppInfo: Identifiable, Equatable, Hashable { } - static let empty = AppInfo(id: UUID(), path: URL(fileURLWithPath: ""), bundleIdentifier: "", appName: "", appVersion: "", appIcon: nil, webApp: false, wrapped: false, system: false, files: [], fileSize: [:], fileSizeLogical: [:], fileIcon: [:]) + static let empty = AppInfo(id: UUID(), path: URL(fileURLWithPath: ""), bundleIdentifier: "", appName: "", appVersion: "", appIcon: nil, webApp: false, wrapped: false, system: false, bundleSize: 0, files: [], fileSize: [:], fileSizeLogical: [:], fileIcon: [:]) } diff --git a/Pearcleaner/Logic/Logic.swift b/Pearcleaner/Logic/Logic.swift index 2ba9743..d49ce65 100644 --- a/Pearcleaner/Logic/Logic.swift +++ b/Pearcleaner/Logic/Logic.swift @@ -39,8 +39,6 @@ func getSortedApps(paths: [String], appState: AppState) -> [AppInfo] { // Get app info and sort let sortedApps = apps .compactMap { AppInfoFetcher.getAppInfo(atPath: $0) } - .sorted { $0.appName.replacingOccurrences(of: ".", with: "").lowercased() < $1.appName.replacingOccurrences(of: ".", with: "").lowercased() } - return sortedApps } diff --git a/Pearcleaner/Logic/Styles.swift b/Pearcleaner/Logic/Styles.swift index 9c4401a..fa3c0f4 100644 --- a/Pearcleaner/Logic/Styles.swift +++ b/Pearcleaner/Logic/Styles.swift @@ -570,13 +570,7 @@ struct AnimatedSearchStyle: TextFieldStyle { .foregroundStyle(isHovered ? Color("mode").opacity(0.8) : Color("mode").opacity(0.5)) .onTapGesture { withAnimation { - // Refresh Apps list - appState.reload.toggle() - let sortedApps = getSortedApps(paths: fsm.folderPaths, appState: appState) - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - appState.sortedApps = sortedApps - appState.reload.toggle() - } + reloadAppsList(appState: appState, fsm: fsm) } } diff --git a/Pearcleaner/Logic/Utilities.swift b/Pearcleaner/Logic/Utilities.swift index c7ec78d..564ee98 100644 --- a/Pearcleaner/Logic/Utilities.swift +++ b/Pearcleaner/Logic/Utilities.swift @@ -27,19 +27,25 @@ func updateOnMain(after delay: Double? = nil, _ updates: @escaping () -> Void) { // Execute functions on background thread func updateOnBackground(_ updates: @escaping () -> Void) { - DispatchQueue.global(qos: .background).async { + DispatchQueue.global(qos: .userInitiated).async { updates() } } +// Reload apps list +func reloadAppsList(appState: AppState, fsm: FolderSettingsManager) { + appState.reload = true + updateOnBackground { + let sortedApps = getSortedApps(paths: fsm.folderPaths, appState: appState) + // Update UI on the main thread + updateOnMain { + appState.sortedApps = sortedApps + appState.reload = false + } + } +} + -// Resize window -//func resizeWindow(width: CGFloat, height: CGFloat) { -// if let window = NSApplication.shared.windows.first { -// let newSize = NSSize(width: width, height: height) -// window.setContentSize(newSize) -// } -//} func resizeWindowAuto(windowSettings: WindowSettings, title: String) { if let window = NSApplication.shared.windows.first(where: { $0.title == title }) { @@ -160,16 +166,26 @@ func caskCleanup(app: String) { #elseif arch(arm64) let cmd = "/opt/homebrew/bin/brew" #endif + let formattedApp = app.lowercased().replacingOccurrences(of: " ", with: "-") process.executableURL = URL(fileURLWithPath: "/bin/zsh") - process.arguments = ["-c", "\(cmd) uninstall --cask \(app) --force; \(cmd) cleanup"] -// let pipe = Pipe() - process.standardOutput = FileHandle.nullDevice//pipe - process.standardError = FileHandle.nullDevice//pipe +// process.arguments = ["-c", "\(cmd) uninstall --cask \"\(formattedApp)\" --force; \(cmd) cleanup"] + process.arguments = ["-c", """ + found_cask=$(\(cmd) list --cask | grep "\(formattedApp)") + if [ -n "$found_cask" ]; then + \(cmd) uninstall --cask "$found_cask" --force; + \(cmd) cleanup; + else + echo "Cask not found for \(formattedApp)"; + fi + """] + let pipe = Pipe() + process.standardOutput = pipe + process.standardError = pipe try? process.run() process.waitUntilExit() // Ensure the process completes -// let data = pipe.fileHandleForReading.readDataToEndOfFile() -// let output = (String(data: data, encoding: .utf8) ?? "") as String -// print(output) + let data = pipe.fileHandleForReading.readDataToEndOfFile() + let output = (String(data: data, encoding: .utf8) ?? "") as String + print(output) } } @@ -499,6 +515,7 @@ func formatByte(size: Int64) -> (human: String, byte: String) { } + // Only process supported files func isSupportedFileType(at path: String) -> Bool { let fileManager = FileManager.default diff --git a/Pearcleaner/PearcleanerApp.swift b/Pearcleaner/PearcleanerApp.swift index 27cb510..38eb3fe 100644 --- a/Pearcleaner/PearcleanerApp.swift +++ b/Pearcleaner/PearcleanerApp.swift @@ -88,9 +88,9 @@ struct PearcleanerApp: App { // Disable tabbing NSWindow.allowsAutomaticWindowTabbing = false - // Get Apps - let sortedApps = getSortedApps(paths: fsm.folderPaths, appState: appState) - appState.sortedApps = sortedApps + // Load apps list on startup + reloadAppsList(appState: appState, fsm: fsm) + // Enable menubar item if menubarEnabled { diff --git a/Pearcleaner/Views/AppListItems.swift b/Pearcleaner/Views/AppListItems.swift index 96baf76..deed71a 100644 --- a/Pearcleaner/Views/AppListItems.swift +++ b/Pearcleaner/Views/AppListItems.swift @@ -24,10 +24,10 @@ struct AppListItems: View { @EnvironmentObject var locations: Locations let itemId = UUID() let appInfo: AppInfo - var isSelected: Bool { - appState.appInfo.path == appInfo.path - } + var isSelected: Bool { appState.appInfo.path == appInfo.path } @State private var hoveredItemPath: URL? = nil + @State private var bundleSize: Int64 = 0 + var body: some View { ZStack() { @@ -40,9 +40,7 @@ struct AppListItems: View { .resizable() .aspectRatio(contentMode: .fit) .frame(width: 30) -// .frame(width: (isHovered || isSelected) ? 32 : 30, height: (isHovered || isSelected) ? 32 : 30) .clipShape(RoundedRectangle(cornerRadius: 8)) -// .shadow(color: Color("pop").opacity(0.8), radius: isSelected ? 2 : 0) } } @@ -80,17 +78,13 @@ struct AppListItems: View { ) } - Text(appInfo.appVersion) - .font(.system(size: (isHovered || isSelected) ? 12 : 10)) - .foregroundStyle(Color("mode").opacity(0.5)) - -// if (isHovered || isSelected) { -// RoundedRectangle(cornerRadius: 50) -// .fill(isSelected ? Color("pear") : Color("mode").opacity(0.5)) -// .frame(width: isSelected ? 4 : 2, height: 25) -// .padding(.trailing, 5) -// } - + if bundleSize == 0 { + ProgressView().controlSize(.mini) + } else { + Text("\(isHovered ? "v\(appInfo.appVersion)" : formatByte(size: bundleSize).human)") + .font(.system(size: (isHovered || isSelected) ? 12 : 10)) + .foregroundStyle(Color("mode").opacity(0.5)) + } } } @@ -99,7 +93,7 @@ struct AppListItems: View { .padding(.vertical, 5) .help(appInfo.appName) .onHover { hovering in - withAnimation(Animation.easeIn(duration: 0.2)) { + withAnimation(Animation.easeIn(duration: 0.3)) { self.isHovered = hovering self.hoveredItemPath = isHovered ? appInfo.path : nil } @@ -132,6 +126,19 @@ struct AppListItems: View { } } + .onAppear { + DispatchQueue.global(qos: .userInitiated).async { + let size = totalSizeOnDisk(for: appInfo.path).logical + DispatchQueue.main.async { + self.bundleSize = size + + // Optionally, update the appInfo in the appState array + if let index = appState.sortedApps.firstIndex(where: { $0.path == appInfo.path }) { + appState.sortedApps[index].bundleSize = size + } + } + } + } } diff --git a/Pearcleaner/Views/AppSearchView.swift b/Pearcleaner/Views/AppSearchView.swift index 09a8cdd..c6eb954 100644 --- a/Pearcleaner/Views/AppSearchView.swift +++ b/Pearcleaner/Views/AppSearchView.swift @@ -22,6 +22,9 @@ struct AppSearchView: View { @State private var showSys: Bool = true @State private var showUsr: Bool = true @Binding var isMenuBar: Bool + @AppStorage("settings.general.selectedSortAppsList") var selectedSortAlpha: Bool = true + @State private var progress: Double = 0.0 + var body: some View { VStack(alignment: .center, spacing: 0) { @@ -53,17 +56,11 @@ struct AppSearchView: View { if search.isEmpty { Button("Refresh") { withAnimation { - // Refresh Apps list - appState.reload.toggle() showPopover = false - let sortedApps = getSortedApps(paths: fsm.folderPaths, appState: appState) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - appState.sortedApps = sortedApps - appState.reload.toggle() - } + reloadAppsList(appState: appState, fsm: fsm) } } - .buttonStyle(SimpleButtonStyle(icon: "arrow.uturn.backward.circle", help: "Refresh apps (⌘+R)", size: 16)) + .buttonStyle(SimpleButtonStyle(icon: "arrow.counterclockwise.circle", help: "Refresh apps (⌘+R)", size: 16)) } SearchBar(search: $search, darker: (mini || menubarEnabled) ? false : true, glass: glass) @@ -77,6 +74,13 @@ struct AppSearchView: View { .popover(isPresented: $showMenu) { VStack(alignment: .leading) { + Button("") { + withAnimation(.easeInOut(duration: 0.5)) { + selectedSortAlpha.toggle() + } + } + .buttonStyle(SimpleButtonStyle(icon: "circle.fill", label: "App List Sorting", help: "Can also click on User/System headers to toggle this", size: 5)) + if mini && !menubarEnabled { Button("") { withAnimation(.easeInOut(duration: 0.5)) { @@ -142,12 +146,21 @@ struct AppSearchView: View { } private var filteredApps: [AppInfo] { + let apps: [AppInfo] if search.isEmpty { - return appState.sortedApps + apps = appState.sortedApps } else { - return appState.sortedApps.filter { $0.appName.localizedCaseInsensitiveContains(search) } + apps = appState.sortedApps.filter { $0.appName.localizedCaseInsensitiveContains(search) } + } + + switch selectedSortAlpha { + case true: + return apps.sorted { $0.appName.replacingOccurrences(of: ".", with: "").lowercased() < $1.appName.replacingOccurrences(of: ".", with: "").lowercased() } + case false: + return apps.sorted { $0.bundleSize > $1.bundleSize } } } + } diff --git a/Pearcleaner/Views/AppsListView.swift b/Pearcleaner/Views/AppsListView.swift index 4de21ff..62f8a47 100644 --- a/Pearcleaner/Views/AppsListView.swift +++ b/Pearcleaner/Views/AppsListView.swift @@ -12,6 +12,7 @@ struct AppsListView: View { @Binding var search: String @Binding var showPopover: Bool @AppStorage("settings.general.mini") private var mini: Bool = false + @AppStorage("settings.general.selectedSort") var selectedSortAlpha: Bool = true var filteredApps: [AppInfo] @@ -62,12 +63,12 @@ struct SectionView: View { struct Header: View { let title: String let count: Int -// @State private var hovered = false @EnvironmentObject var appState: AppState @EnvironmentObject var locations: Locations @EnvironmentObject var fsm: FolderSettingsManager @Binding var showPopover: Bool @AppStorage("settings.general.glass") private var glass: Bool = true + @AppStorage("settings.general.selectedSortAppsList") var selectedSortAlpha: Bool = true var body: some View { @@ -87,6 +88,10 @@ struct Header: View { } .frame(minHeight: 20) .padding(5) + .help("Click header to change sorting order") + .onTapGesture { + selectedSortAlpha.toggle() + } } } diff --git a/Pearcleaner/Views/FilesView.swift b/Pearcleaner/Views/FilesView.swift index a702e99..b453e4e 100644 --- a/Pearcleaner/Views/FilesView.swift +++ b/Pearcleaner/Views/FilesView.swift @@ -120,7 +120,7 @@ struct FilesView: View { HStack { Text("\(appState.appInfo.appName)").font(.title).fontWeight(.bold).lineLimit(1) Text("•").foregroundStyle(Color("AccentColor")) - Text("\(appState.appInfo.appVersion)").font(.title3) + Text("v\(appState.appInfo.appVersion)").font(.title3) if appState.appInfo.appName.count < 5 { InfoButton(text: "Pearcleaner searches for files via a combination of bundle id and app name. \(appState.appInfo.appName) has a common or short app name so there might be unrelated files found. Please check the list thoroughly before uninstalling.") } diff --git a/Pearcleaner/Views/MiniMode.swift b/Pearcleaner/Views/MiniMode.swift index ea2f188..82315ca 100644 --- a/Pearcleaner/Views/MiniMode.swift +++ b/Pearcleaner/Views/MiniMode.swift @@ -173,7 +173,12 @@ struct MiniAppView: View { if appState.reload { VStack { Spacer() - ProgressView("Refreshing app list") + ProgressView() { + Text("Gathering app details") + .font(.callout) + .foregroundStyle(Color("mode").opacity(0.5)) + .padding(5) + } Spacer() } .padding(.vertical) diff --git a/Pearcleaner/Views/RegularMode.swift b/Pearcleaner/Views/RegularMode.swift index f80073a..7d6408d 100644 --- a/Pearcleaner/Views/RegularMode.swift +++ b/Pearcleaner/Views/RegularMode.swift @@ -33,7 +33,12 @@ struct RegularMode: View { if appState.reload { VStack { Spacer() - ProgressView("Refreshing app list") + ProgressView() { + Text("Gathering app details") + .font(.callout) + .foregroundStyle(Color("mode").opacity(0.5)) + .padding(5) + } Spacer() } .frame(width: sidebarWidth) diff --git a/Pearcleaner/Views/ZombieView.swift b/Pearcleaner/Views/ZombieView.swift index 45a1801..6b8c719 100644 --- a/Pearcleaner/Views/ZombieView.swift +++ b/Pearcleaner/Views/ZombieView.swift @@ -13,6 +13,8 @@ struct ZombieView: View { @EnvironmentObject var locations: Locations @EnvironmentObject var fsm: FolderSettingsManager @State private var showPop: Bool = false + @AppStorage("settings.general.leftoverWarning") private var warning: Bool = false + @State private var showAlert = false @AppStorage("settings.general.mini") private var mini: Bool = false @AppStorage("settings.general.glass") private var glass: Bool = false @AppStorage("settings.sentinel.enable") private var sentinel: Bool = false @@ -36,45 +38,6 @@ struct ZombieView: View { var body: some View { -// let totalSelectedZombieSize: (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))") -// }() - -// let filteredAndSortedFiles: ([URL], Int64, Int64) = { -// let fileSizeReal = appState.zombieFile.fileSize -// let fileSizeLogical = appState.zombieFile.fileSizeLogical -// 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) { if appState.showProgress { VStack { @@ -150,7 +113,7 @@ struct ZombieView: View { VStack(alignment: .leading, spacing: 10){ HStack { Text("Leftover Files").font(.title).fontWeight(.bold) - InfoButton(text: "Leftover file search is not and can never be 100% accurate as it doesn't have any old/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 old/uninstalled applications.", color: .orange, warning: true) + 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) Spacer() } @@ -326,9 +289,34 @@ struct ZombieView: View { .onAppear { updateMemoizedFiles(for: searchZ, sizeType: sizeType, selectedSortAlpha: selectedSortAlpha, force: true) } + .sheet(isPresented: $showAlert, content: { + VStack(spacing: 10) { + Text("Important") + .font(.headline) + Spacer() + 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.") + .font(.subheadline) + Spacer() + Button("Close") { + warning = true + showAlert = false + } + .buttonStyle(SimpleButtonStyle(icon: "x.circle.fill", label: "Close", help: "Dismiss")) + Spacer() + } + .padding(15) + .frame(width: 400, height: 250) + .background(GlassEffect(material: .hudWindow, blendingMode: .behindWindow)) + }) } } + .onAppear { + if !warning { + showAlert = true + } + } + } private func binding(for file: URL) -> Binding { diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 8da6854..3ca0b61 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -11,7 +11,7 @@ # # Uncomment the line if you want fastlane to automatically update itself -# update_fastlane +update_fastlane opt_out_usage @@ -51,6 +51,11 @@ platform :mac do UI.user_error!("Failed to find .app file in #{output_path}") end + # Empty the Archives folder + archives_path = "#{Dir.home}/Library/Developer/Xcode/Archives" + FileUtils.rm_rf(Dir.glob("#{archives_path}/*")) + + # Open Finder at the Downloads folder system("open #{output_path}") diff --git a/features.json b/features.json index 35465d2..af6047a 100644 --- a/features.json +++ b/features.json @@ -1,4 +1,5 @@ { + "3.7.0": "- App List Size Sorting: You can now sort the sidebar app list alphabetically or by descending size. Click the User/System header to toggle or from the searchbar menu|- Leftover Files: On first use, a warning/explanation sheet will be presented to the user", "3.5.0": "- Finder Extension: When enabled, you can right click an app in finder and uninstall with Pearcleaner directly|- Theme System: You can now select a base theme color for the application window", "3.3.0": "- Folder Settings: Add more directories where Pearcleaner should search for app files|- Progress: Show progress bar on startup when loading all app files with instant search enabled|- App Icon: Show background color behind app icons in FilesView based on icon's average color mapping|- Progress: Show time counter on regular file search views", "3.2.0": "- Menubar Item: You can choose to access Pearcleaner from the menubar. This will show a slightly modified Mini Mode view.",