From af53a77c76fbc82ebcac11b3678c58c81e288ce6 Mon Sep 17 00:00:00 2001 From: Alin Date: Sat, 3 Aug 2024 17:50:09 -0600 Subject: [PATCH] Updates --- Pearcleaner.xcodeproj/project.pbxproj | 4 + .../xcshareddata/swiftpm/Package.resolved | 2 +- Pearcleaner/Logic/AppCommands.swift | 3 +- Pearcleaner/Logic/Logic.swift | 7 +- Pearcleaner/Logic/ReversePathsFetch.swift | 6 +- Pearcleaner/Logic/Styles.swift | 10 +- Pearcleaner/Logic/Utilities.swift | 78 ++++++++++- Pearcleaner/PearDisk/FolderDetail.swift | 1 + Pearcleaner/PearDisk/PearDiskView.swift | 3 +- Pearcleaner/PearDisk/SunburstChart.swift | 128 ++++++++++++++++++ Pearcleaner/PearDisk/TreeMap.swift | 6 +- Pearcleaner/Settings/Folders.swift | 5 +- Pearcleaner/Settings/General.swift | 27 +++- Pearcleaner/Settings/Interface.swift | 46 ++++++- Pearcleaner/Views/AppListItems.swift | 51 +++++-- Pearcleaner/Views/AppSearchView.swift | 12 +- Pearcleaner/Views/FilesView.swift | 119 ++++++++-------- Pearcleaner/Views/MiniMode.swift | 4 +- Pearcleaner/Views/ZombieView.swift | 23 ++-- 19 files changed, 428 insertions(+), 107 deletions(-) create mode 100644 Pearcleaner/PearDisk/SunburstChart.swift diff --git a/Pearcleaner.xcodeproj/project.pbxproj b/Pearcleaner.xcodeproj/project.pbxproj index 359ea8e..30a9c49 100644 --- a/Pearcleaner.xcodeproj/project.pbxproj +++ b/Pearcleaner.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ C71848442B8D2D600046CB13 /* ZombieView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C71848432B8D2D600046CB13 /* ZombieView.swift */; }; C72893052AFD42E600C8C1CD /* DeepLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = C72893042AFD42E600C8C1CD /* DeepLink.swift */; }; C72893122AFD51EA00C8C1CD /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = C72893112AFD51EA00C8C1CD /* main.swift */; }; + C72F4B6C2C5DF4ED00B04F60 /* SunburstChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = C72F4B6B2C5DF4ED00B04F60 /* SunburstChart.swift */; }; C736E7652C2DF1C9009EDB6A /* AppPathsFetch-Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = C736E7642C2DF1C9009EDB6A /* AppPathsFetch-Async.swift */; }; C74412C72BBF249000DDFCA8 /* AppPathsFetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C74412C62BBF249000DDFCA8 /* AppPathsFetch.swift */; }; C74D76B72C5C4ABC00748DD1 /* TreeMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = C74D76B62C5C4ABC00748DD1 /* TreeMap.swift */; }; @@ -102,6 +103,7 @@ C72893042AFD42E600C8C1CD /* DeepLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLink.swift; sourceTree = ""; }; C728930F2AFD51EA00C8C1CD /* PearcleanerSentinel */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = PearcleanerSentinel; sourceTree = BUILT_PRODUCTS_DIR; }; C72893112AFD51EA00C8C1CD /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; + C72F4B6B2C5DF4ED00B04F60 /* SunburstChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SunburstChart.swift; sourceTree = ""; }; C736E7642C2DF1C9009EDB6A /* AppPathsFetch-Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppPathsFetch-Async.swift"; sourceTree = ""; }; C74412C62BBF249000DDFCA8 /* AppPathsFetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPathsFetch.swift; sourceTree = ""; }; C74D76B62C5C4ABC00748DD1 /* TreeMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TreeMap.swift; sourceTree = ""; }; @@ -220,6 +222,7 @@ C74D76B92C5D2DF300748DD1 /* FileSearch.swift */, C74D76B62C5C4ABC00748DD1 /* TreeMap.swift */, C74D76BC2C5D2DF300748DD1 /* FolderDetail.swift */, + C72F4B6B2C5DF4ED00B04F60 /* SunburstChart.swift */, ); path = PearDisk; sourceTree = ""; @@ -456,6 +459,7 @@ C76D08482AF83C3F00D07867 /* Update.swift in Sources */, C7FB173B2B96321300B96F9A /* AppsListView.swift in Sources */, C77B90162AF19377009CC655 /* AppState.swift in Sources */, + C72F4B6C2C5DF4ED00B04F60 /* SunburstChart.swift in Sources */, C74D76B72C5C4ABC00748DD1 /* TreeMap.swift in Sources */, C74D76C02C5D2DF300748DD1 /* FolderDetail.swift in Sources */, C7DD49EE2BAB7F6000CCBA16 /* ReversePathsFetch.swift in Sources */, diff --git a/Pearcleaner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Pearcleaner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 4299a74..23d1012 100644 --- a/Pearcleaner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Pearcleaner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -7,7 +7,7 @@ "location" : "https://github.com/alienator88/AlinFoundation", "state" : { "branch" : "main", - "revision" : "986ac354d2a01f87ed4c37d19ce80a752a62fd4c" + "revision" : "705a7e5fe3c1374000d2fd29f1c58fa476b0453c" } }, { diff --git a/Pearcleaner/Logic/AppCommands.swift b/Pearcleaner/Logic/AppCommands.swift index 2e832d0..7338380 100644 --- a/Pearcleaner/Logic/AppCommands.swift +++ b/Pearcleaner/Logic/AppCommands.swift @@ -15,6 +15,7 @@ struct AppCommands: Commands { let fsm: FolderSettingsManager let updater: Updater let themeManager: ThemeManager + @AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true init(appState: AppState, locations: Locations, fsm: FolderSettingsManager, updater: Updater, themeManager: ThemeManager) { self.appState = appState @@ -37,7 +38,7 @@ struct AppCommands: Commands { .keyboardShortcut("u", modifiers: .command) Button { - withAnimation(.easeInOut(duration: 0.5)) { + withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) { reloadAppsList(appState: appState, fsm: fsm) } } label: { diff --git a/Pearcleaner/Logic/Logic.swift b/Pearcleaner/Logic/Logic.swift index 6bd62ca..8c8811f 100644 --- a/Pearcleaner/Logic/Logic.swift +++ b/Pearcleaner/Logic/Logic.swift @@ -110,6 +110,7 @@ func listAppSupportDirectories() -> [String] { // Load app paths on launch func reversePreloader(allApps: [AppInfo], appState: AppState, locations: Locations, fsm: FolderSettingsManager, completion: @escaping () -> Void = {}) { + @AppStorage("settings.interface.animationEnabled") var animationEnabled: Bool = true // appState.operationQueueLeftover.maxConcurrentOperationCount = 10 // Adjust this value as needed // appState.shouldCancelOperations = false // appState.appInfoStore.removeAll() @@ -122,7 +123,7 @@ func reversePreloader(allApps: [AppInfo], appState: AppState, locations: Locatio updateOnMain { printOS("Reverse search processed successfully") appState.showProgress = false - withAnimation { + withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) { appState.leftoverProgress.1 = 0.0 } appState.leftoverProgress.0 = "Reverse search completed successfully" @@ -218,6 +219,8 @@ func reversePreloader(allApps: [AppInfo], appState: AppState, locations: Locatio // Load item in Files view func showAppInFiles(appInfo: AppInfo, appState: AppState, locations: Locations, showPopover: Binding) { + @AppStorage("settings.interface.animationEnabled") var animationEnabled: Bool = true + showPopover.wrappedValue = false updateOnMain { @@ -238,7 +241,7 @@ func showAppInFiles(appInfo: AppInfo, appState: AppState, locations: Locations, appState.appInfo = appInfo // Animate the view change and popover display. - withAnimation(Animation.easeIn(duration: 0.4)) { + withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) { appState.currentView = .files showPopover.wrappedValue.toggle() } diff --git a/Pearcleaner/Logic/ReversePathsFetch.swift b/Pearcleaner/Logic/ReversePathsFetch.swift index 0ce4ac5..e7fe9ea 100644 --- a/Pearcleaner/Logic/ReversePathsFetch.swift +++ b/Pearcleaner/Logic/ReversePathsFetch.swift @@ -71,7 +71,7 @@ class ReversePathsSearcher { guard !skipReverse.contains(where: { itemName.pearFormat().contains($0) }), isSupportedFileType(at: itemURL.path), - !isRelatedToInstalledApp(itemPath: itemPath), + !isRelatedToInstalledApp(itemPath: itemPath, itemPathOG: itemURL), !isExcludedByConditions(itemPath: itemPath) else { return @@ -80,9 +80,9 @@ class ReversePathsSearcher { collection.append(itemURL) } - private func isRelatedToInstalledApp(itemPath: String) -> Bool { + private func isRelatedToInstalledApp(itemPath: String, itemPathOG: URL) -> Bool { for app in sortedApps { - if itemPath.contains(app.bundleIdentifier.pearFormat()) || itemPath.contains(app.appName.pearFormat()) { + if itemPath.contains(app.bundleIdentifier.pearFormat()) || itemPath.contains(app.appName.pearFormat()) || itemPathOG.containerNameByUUID().pearFormat().contains(app.bundleIdentifier.pearFormat()) { return true } } diff --git a/Pearcleaner/Logic/Styles.swift b/Pearcleaner/Logic/Styles.swift index 26dd34f..2407a6a 100644 --- a/Pearcleaner/Logic/Styles.swift +++ b/Pearcleaner/Logic/Styles.swift @@ -14,6 +14,8 @@ struct ResetSettingsButtonStyle: ButtonStyle { let label: String let help: String @State private var hovered = false + @AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true + func makeBody(configuration: Configuration) -> some View { HStack(alignment: .center) { @@ -35,7 +37,7 @@ struct ResetSettingsButtonStyle: ButtonStyle { .scaleEffect(configuration.isPressed ? 0.95 : 1) .animation(.easeOut(duration: 0.2), value: configuration.isPressed) .onHover { hovering in - withAnimation(Animation.easeIn(duration: 0.15)) { + withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) { hovered = hovering } } @@ -52,6 +54,7 @@ struct ResetSettingsButtonStyle: ButtonStyle { struct SimpleCheckboxToggleStyle: ToggleStyle { @EnvironmentObject private var themeManager: ThemeManager @State private var isHovered: Bool = false + @AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true func makeBody(configuration: Configuration) -> some View { HStack { @@ -66,7 +69,7 @@ struct SimpleCheckboxToggleStyle: ToggleStyle { } } .onTapGesture { - withAnimation(.smooth()) { + withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) { configuration.isOn.toggle() } } @@ -240,6 +243,7 @@ struct GlassEffect: NSViewRepresentable { struct SimpleButtonBrightStyle: ButtonStyle { + @AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true @State private var hovered = false let icon: String let label: String @@ -266,7 +270,7 @@ struct SimpleButtonBrightStyle: ButtonStyle { } .padding(5) .onHover { hovering in - withAnimation() { + withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) { hovered = hovering } } diff --git a/Pearcleaner/Logic/Utilities.swift b/Pearcleaner/Logic/Utilities.swift index 02d8a14..8049e15 100644 --- a/Pearcleaner/Logic/Utilities.swift +++ b/Pearcleaner/Logic/Utilities.swift @@ -124,7 +124,6 @@ func saveURLsToFile(urls: Set, appState: AppState) { } - // Open trash folder func openTrash() { if let trashURL = try? FileManager.default.url(for: .trashDirectory, in: .userDomainMask, appropriateFor: nil, create: false) { @@ -240,6 +239,83 @@ func showLocalized(url: URL) -> String { return url.lastPathComponent } +extension URL { + func localizedName() -> String? { + do { + let resourceValues = try self.resourceValues(forKeys: [.localizedNameKey]) + return resourceValues.localizedName + } catch { + print("Error getting localized name: \(error)") + return nil + } + } +} + +extension String { + func localizedName() -> String? { + let url = URL(fileURLWithPath: self) + do { + let resourceValues = try url.resourceValues(forKeys: [.localizedNameKey]) + return resourceValues.localizedName + } catch { + print("Error getting localized name: \(error)") + return nil + } + } +} + +extension URL { + /// Returns the bundle name of the container by its UUID if found. + func containerNameByUUID() -> String { + // Extract the last path component, which should be the UUID + let uuid = self.lastPathComponent + + // Ensure the UUID matches the expected pattern. + let uuidRegex = try! NSRegularExpression( + pattern: "^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$", + options: .caseInsensitive + ) + let range = NSRange(location: 0, length: uuid.utf16.count) + guard uuidRegex.firstMatch(in: uuid, options: [], range: range) != nil else { +// print("The URL does not point to a valid UUID container.") + return "" + } + + // Path to the Containers directory. + let containersPath = FileManager.default.homeDirectoryForCurrentUser + .appendingPathComponent("Library/Containers") + + do { + // List all directories in the Containers folder. + let containerDirectories = try FileManager.default.contentsOfDirectory( + at: containersPath, + includingPropertiesForKeys: nil, + options: .skipsHiddenFiles + ) + + // Iterate over each directory to find a match with the UUID. + for directory in containerDirectories { + let directoryName = directory.lastPathComponent + + if directoryName == uuid { + // Attempt to read the metadata plist file. + let metadataPlistURL = directory.appendingPathComponent(".com.apple.containermanagerd.metadata.plist") + + if let metadataDict = NSDictionary(contentsOf: metadataPlistURL), + let applicationBundleID = metadataDict["MCMMetadataIdentifier"] as? String { + return applicationBundleID + } + } + } + } catch { + print("Error accessing the Containers directory: \(error)") + } + + // Return nil if no matching UUID is found. + return "" + } +} + // Return image for different folders func folderImages(for path: String) -> AnyView? { if path.contains("/Library/Containers/") || path.contains("/Library/Group Containers/") { diff --git a/Pearcleaner/PearDisk/FolderDetail.swift b/Pearcleaner/PearDisk/FolderDetail.swift index f1cfb40..baca11e 100644 --- a/Pearcleaner/PearDisk/FolderDetail.swift +++ b/Pearcleaner/PearDisk/FolderDetail.swift @@ -182,6 +182,7 @@ struct FolderDetailView: View { self.childItems = items self.currentPath = url self.currentItem = Item(url: url, name: url.lastPathComponent, size: items.reduce(0) { $0 + $1.size }) + print("Items loaded") case .failure(let error): self.errorMessage = "Error loading contents: \(error.localizedDescription)" print("Error loading contents: \(error)") diff --git a/Pearcleaner/PearDisk/PearDiskView.swift b/Pearcleaner/PearDisk/PearDiskView.swift index 66103f8..64276b2 100644 --- a/Pearcleaner/PearDisk/PearDiskView.swift +++ b/Pearcleaner/PearDisk/PearDiskView.swift @@ -57,7 +57,8 @@ struct SidebarView: View { let basePaths = [ URL(fileURLWithPath: "/Volumes"), URL(fileURLWithPath: "/Users"), - URL(fileURLWithPath: "/Users/alin"), +// URL(fileURLWithPath: "/Users/alin"), + URL(fileURLWithPath: "/"), ] for basePath in basePaths { diff --git a/Pearcleaner/PearDisk/SunburstChart.swift b/Pearcleaner/PearDisk/SunburstChart.swift new file mode 100644 index 0000000..730eb3b --- /dev/null +++ b/Pearcleaner/PearDisk/SunburstChart.swift @@ -0,0 +1,128 @@ +//// +//// SunburstChart.swift +//// Pearcleaner +//// +//// Created by Alin Lupascu on 8/2/24. +//// +// +// +//import SwiftUI +//import Charts +// +//final class Item: Identifiable { +// let id = UUID() +// let url: URL +// let name: String +// var size: Int64 +// let isDirectory: Bool? +// +// init(url: URL, name: String, size: Int64, isDirectory: Bool = false) { +// self.url = url +// self.name = name +// self.size = size +// self.isDirectory = isDirectory +// } +//} +// +//struct SunburstChart: View { +// let chartWidth: CGFloat = 175 +// let items: [Item] +// +// @State private var selectedTotal1: Double? +// @State private var selectedSegment1: Item? +// +// @State private var selectedTotal2: Double? +// @State private var selectedSegment2: Item? +// +// init(items: [Item]) { +// self.items = items +// } +// +// var body: some View { +// VStack { +// Text(selectedText) +// .font(.title) +// Text(selectedSizeText) +// .font(.title3) +// +// ZStack { +// Chart(getFiles()) { item in +// SectorMark( +// angle: .value("Size", Double(item.size)), +// innerRadius: .ratio(0.6), +// outerRadius: .ratio(0.9), +// angularInset: 1.5 +// ) +// .foregroundStyle(Color.blue.opacity(selectOpacity(item: item, selectedItem: selectedSegment2, ring: 2))) +// } +// .frame(width: chartWidth * 2, height: chartWidth * 2) +// .chartAngleSelection(value: $selectedTotal2) +// .onChange(of: selectedTotal2) { newValue in +// updateSelection(newValue: newValue, items: getFiles(), updateSelected: { selectedSegment2 = $0 }) +// selectedSegment1 = nil +// } +// +// Chart(getDirectories()) { item in +// SectorMark( +// angle: .value("Size", Double(item.size)), +// innerRadius: .ratio(0.5), +// angularInset: 1.5 +// ) +// .foregroundStyle(Color.green.opacity(selectOpacity(item: item, selectedItem: selectedSegment1))) +// } +// .frame(width: chartWidth, height: chartWidth) +// .chartAngleSelection(value: $selectedTotal1) +// .onChange(of: selectedTotal1) { newValue in +// updateSelection(newValue: newValue, items: getDirectories(), updateSelected: { selectedSegment1 = $0 }) +// selectedSegment2 = nil +// } +// } +// } +// } +// +// private var selectedText: String { +// if let selectedItem = selectedSegment1 ?? selectedSegment2 { +// return selectedItem.name +// } else { +// return "File System Overview" +// } +// } +// +// private var selectedSizeText: String { +// if let selectedItem = selectedSegment1 ?? selectedSegment2 { +// return ByteCountFormatter.string(fromByteCount: selectedItem.size, countStyle: .file) +// } else { +// return "Select a segment..." +// } +// } +// +// private func getDirectories() -> [Item] { +// return items.filter { $0.isDirectory == true } +// } +// +// private func getFiles() -> [Item] { +// return items.filter { $0.isDirectory == false } +// } +// +// private func selectOpacity(item: Item, selectedItem: Item?, ring: Int = 1) -> Double { +// if let selectedItem = selectedItem, selectedItem.id == item.id { +// return 1.0 +// } +// return ring == 1 ? 0.8 : 0.55 +// } +// +// private func updateSelection(newValue: Double?, items: [Item], updateSelected: (Item?) -> Void) { +// guard let newValue = newValue else { +// updateSelected(nil) +// return +// } +// +// var accumulatedSize: Double = 0 +// let selectedItem = items.first { item in +// accumulatedSize += Double(item.size) +// return newValue <= accumulatedSize +// } +// +// updateSelected(selectedItem) +// } +//} diff --git a/Pearcleaner/PearDisk/TreeMap.swift b/Pearcleaner/PearDisk/TreeMap.swift index c778cce..eb07dc4 100644 --- a/Pearcleaner/PearDisk/TreeMap.swift +++ b/Pearcleaner/PearDisk/TreeMap.swift @@ -15,6 +15,7 @@ struct TreeMapChart: View { let items: [Item] var onItemSelected: (Item) -> Void @Binding var hoveredItem: Item? + @AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true var displayedItems: [Item] { items//.filter { $0.size >= 1_048_576 } // Filter to keep only items with size >= 1MB @@ -46,6 +47,7 @@ struct TreeMapView: View { var onItemSelected: (Item) -> Void @Binding var hoveredItem: Item? @State private var hoveredIndex: Int? + @AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true var body: some View { Group { @@ -109,7 +111,7 @@ struct TreeMapView: View { } } .onHover { isHovering in - withAnimation { + withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) { hoveredIndex = isHovering ? i : nil hoveredItem = isHovering ? file : nil } @@ -160,8 +162,6 @@ final class Item: Identifiable { let name: String var size: Int64 let isDirectory: Bool? -// let parentURL: URL? -// let timestamp: Date init(url: URL, name: String, size: Int64, isDirectory: Bool = false) { self.url = url diff --git a/Pearcleaner/Settings/Folders.swift b/Pearcleaner/Settings/Folders.swift index 6f77efc..93aebf0 100644 --- a/Pearcleaner/Settings/Folders.swift +++ b/Pearcleaner/Settings/Folders.swift @@ -18,6 +18,7 @@ struct FolderSettingsTab: View { @EnvironmentObject var themeManager: ThemeManager @State private var isHovered = false @AppStorage("settings.general.glass") private var glass: Bool = false + @AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true var body: some View { @@ -54,7 +55,7 @@ struct FolderSettingsTab: View { } .disabled(fsm.defaultPaths.contains(fsm.folderPaths[index])) .onHover { hovering in - withAnimation(Animation.easeInOut(duration: 0.4)) { + withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) { isHovered = hovering } if isHovered && !fsm.defaultPaths.contains(fsm.folderPaths[index]) { @@ -150,7 +151,7 @@ struct FolderSettingsTab: View { Spacer() } .onHover { hovering in - withAnimation(Animation.easeInOut(duration: 0.4)) { + withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) { isHovered = hovering } if isHovered { diff --git a/Pearcleaner/Settings/General.swift b/Pearcleaner/Settings/General.swift index fa3719e..c3cd6d5 100644 --- a/Pearcleaner/Settings/General.swift +++ b/Pearcleaner/Settings/General.swift @@ -17,6 +17,7 @@ struct GeneralSettingsTab: View { @AppStorage("settings.sentinel.enable") private var sentinel: Bool = false @AppStorage("settings.general.brew") private var brew: Bool = false @AppStorage("settings.general.oneshot") private var oneShotMode: Bool = false + @AppStorage("settings.general.confirmAlert") private var confirmAlert: Bool = false @AppStorage("settings.general.selectedSort") var selectedSortAlpha: Bool = true @AppStorage("settings.general.sizeType") var sizeType: String = "Real" @State private var isResetting = false @@ -55,6 +56,30 @@ struct GeneralSettingsTab: View { .padding(.leading) + HStack(spacing: 0) { + Image(systemName: confirmAlert ? "exclamationmark.triangle.fill" : "exclamationmark.triangle") + .resizable() + .scaledToFit() + .frame(width: 20, height: 20) + .padding(.trailing) + .foregroundStyle(.primary.opacity(0.5)) + VStack(alignment: .leading, spacing: 5) { + Text("\(confirmAlert ? "Confirmation alerts enabled" : "Confirmation alerts disabled")") + .font(.callout) + .foregroundStyle(.primary.opacity(0.5)) + } + + InfoButton(text: "When deleting files using the Trash button, you can prevent accidental deletions by showing an alert before proceeding with the action") + + Spacer() + Toggle(isOn: $confirmAlert, label: { + }) + .toggleStyle(.switch) + } + .padding(5) + .padding(.leading) + + HStack(spacing: 0) { Image(systemName: oneShotMode ? "scope" : "circlebadge") .resizable() @@ -234,7 +259,7 @@ struct GeneralSettingsTab: View { appState.updateExtensionStatus() } .padding(20) - .frame(width: 500, height: 500) + .frame(width: 500, height: 520) } diff --git a/Pearcleaner/Settings/Interface.swift b/Pearcleaner/Settings/Interface.swift index 22bf1fa..54aa5f7 100644 --- a/Pearcleaner/Settings/Interface.swift +++ b/Pearcleaner/Settings/Interface.swift @@ -24,6 +24,8 @@ struct InterfaceSettingsTab: View { @AppStorage("settings.general.glass") private var glass: Bool = false @AppStorage("settings.general.popover") private var popoverStay: Bool = true @AppStorage("settings.general.miniview") private var miniView: Bool = true + @AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true + @AppStorage("settings.interface.minimalist") private var minimalEnabled: Bool = true @AppStorage("settings.interface.selectedMenubarIcon") var selectedMenubarIcon: String = "bubbles.and.sparkles.fill" @State private var isLaunchAtLoginEnabled: Bool = false @Binding var showPopover: Bool @@ -76,6 +78,48 @@ struct InterfaceSettingsTab: View { .padding(.leading) + HStack(spacing: 0) { + Image(systemName: animationEnabled ? "play" : "play.slash") + .resizable() + .scaledToFit() + .frame(width: 20, height: 20) + .padding(.trailing) + .foregroundStyle(.primary.opacity(0.5)) + VStack(alignment: .leading, spacing: 5) { + Text("\(animationEnabled ? "Animations enabled" : "Animations disabled")") + .font(.callout) + .foregroundStyle(.primary.opacity(0.5)) + } + Spacer() + Toggle(isOn: $animationEnabled, label: { + }) + .toggleStyle(.switch) + } + .padding(5) + .padding(.leading) + + + HStack(spacing: 0) { + Image(systemName: minimalEnabled ? "list.dash.header.rectangle" : "list.bullet.rectangle") + .resizable() + .scaledToFit() + .frame(width: 20, height: 20) + .padding(.trailing) + .foregroundStyle(.primary.opacity(0.5)) + VStack(alignment: .leading, spacing: 5) { + Text("\(minimalEnabled ? "Minimalist app list rows enabled" : "Minimalist app list rows disabled")") + .font(.callout) + .foregroundStyle(.primary.opacity(0.5)) + } + Spacer() + Toggle(isOn: $minimalEnabled, label: { + }) + .toggleStyle(.switch) + } + .padding(5) + .padding(.leading) + + HStack(spacing: 0) { Image(systemName: "paintbrush") .resizable() @@ -380,7 +424,7 @@ struct InterfaceSettingsTab: View { } .padding(20) - .frame(width: 500, height: 540) + .frame(width: 500, height: 600) } diff --git a/Pearcleaner/Views/AppListItems.swift b/Pearcleaner/Views/AppListItems.swift index 6bea604..9b517cc 100644 --- a/Pearcleaner/Views/AppListItems.swift +++ b/Pearcleaner/Views/AppListItems.swift @@ -20,6 +20,8 @@ struct AppListItems: View { @AppStorage("settings.general.mini") private var mini: Bool = false @AppStorage("settings.general.glass") private var glass: Bool = true @AppStorage("settings.menubar.enabled") private var menubarEnabled: Bool = false + @AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true + @AppStorage("settings.interface.minimalist") private var minimalEnabled: Bool = true @Binding var showPopover: Bool @EnvironmentObject var locations: Locations let itemId = UUID() @@ -30,7 +32,7 @@ struct AppListItems: View { var body: some View { - ZStack() { + VStack() { HStack(alignment: .center) { @@ -45,14 +47,39 @@ struct AppListItems: View { } - VStack(alignment: .leading, spacing: 5) { + if minimalEnabled { Text(appInfo.appName) - .font(.system(size: (isHovered || isSelected) ? 14 : 12)) + .font(.system(size: (isSelected) ? 14 : 12)) .lineLimit(1) .truncationMode(.tail) - .padding(.horizontal, 5) - } + } else { + VStack(alignment: .center, spacing: 2) { + HStack { + Text(appInfo.appName) + .font(.system(size: (isSelected) ? 14 : 12)) + .lineLimit(1) + .truncationMode(.tail) + Spacer() + } + HStack(spacing: 5) { + Text("v\(appInfo.appVersion)") + .font(.footnote) + .lineLimit(1) + .truncationMode(.tail) + .opacity(0.5) + Text("•").font(.footnote).opacity(0.5) + + Text(bundleSize == 0 ? "calculating" : "\(formatByte(size: bundleSize).human)") + .font(.footnote) + .lineLimit(1) + .truncationMode(.tail) + .opacity(0.5) + Spacer() + } + + } + } Spacer() @@ -78,9 +105,13 @@ struct AppListItems: View { ) } - Text(bundleSize == 0 ? "v\(appInfo.appVersion)" : (isHovered ? "v\(appInfo.appVersion)" : formatByte(size: bundleSize).human)) - .font(.system(size: (isHovered || isSelected) ? 12 : 10)) - .foregroundStyle(.primary.opacity(0.5)) + if minimalEnabled { + Text(bundleSize == 0 ? "v\(appInfo.appVersion)" : (isHovered ? "v\(appInfo.appVersion)" : formatByte(size: bundleSize).human)) + .font(.system(size: (isHovered || isSelected) ? 12 : 10)) + .foregroundStyle(.primary.opacity(0.5)) + } + + } @@ -90,13 +121,13 @@ struct AppListItems: View { .padding(.vertical, 5) .help(appInfo.appName) .onHover { hovering in - withAnimation(Animation.easeIn(duration: 0.3)) { + withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) { self.isHovered = hovering self.hoveredItemPath = isHovered ? appInfo.path : nil } } .onTapGesture { - withAnimation(Animation.easeInOut(duration: 0.4)) { + withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) { if isSelected { appState.appInfo = .empty appState.selectedItems = [] diff --git a/Pearcleaner/Views/AppSearchView.swift b/Pearcleaner/Views/AppSearchView.swift index 6b672dc..b9ab895 100644 --- a/Pearcleaner/Views/AppSearchView.swift +++ b/Pearcleaner/Views/AppSearchView.swift @@ -23,6 +23,7 @@ struct AppSearchView: View { @State private var showMenu = false @Binding var isMenuBar: Bool @AppStorage("settings.general.selectedSortAppsList") var selectedSortAlpha: Bool = true + @AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true var body: some View { @@ -58,7 +59,7 @@ struct AppSearchView: View { if search.isEmpty { Button("Refresh") { - withAnimation { + withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) { showPopover = false reloadAppsList(appState: appState, fsm: fsm) } @@ -78,7 +79,7 @@ struct AppSearchView: View { VStack(alignment: .leading) { Button("") { - withAnimation(.easeInOut(duration: 0.5)) { + withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) { selectedSortAlpha.toggle() } } @@ -86,7 +87,7 @@ struct AppSearchView: View { if mini && !menubarEnabled { Button("") { - withAnimation(.easeInOut(duration: 0.5)) { + withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) { appState.currentView = .empty appState.appInfo = AppInfo.empty showPopover = false @@ -98,7 +99,7 @@ struct AppSearchView: View { Button("Leftover Files") { showMenu = false - withAnimation(.easeInOut(duration: 0.5)) { + withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) { showPopover = false appState.appInfo = .empty if appState.zombieFile.fileSize.keys.isEmpty { @@ -188,6 +189,7 @@ struct SimpleSearchStyle: TextFieldStyle { @EnvironmentObject var themeManager: ThemeManager @AppStorage("settings.general.mini") private var mini: Bool = false @AppStorage("settings.menubar.enabled") private var menubarEnabled: Bool = false + @AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true func _body(configuration: TextField) -> some View { @@ -229,7 +231,7 @@ struct SimpleSearchStyle: TextFieldStyle { } .onHover { hovering in - withAnimation(Animation.easeInOut(duration: 0.15)) { + withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) { self.isHovered = hovering self.isFocused = true } diff --git a/Pearcleaner/Views/FilesView.swift b/Pearcleaner/Views/FilesView.swift index e14d69e..c1bf6e4 100644 --- a/Pearcleaner/Views/FilesView.swift +++ b/Pearcleaner/Views/FilesView.swift @@ -20,6 +20,8 @@ struct FilesView: View { @AppStorage("settings.general.selectedSort") var selectedSortAlpha: Bool = true @AppStorage("settings.general.sizeType") var sizeType: String = "Real" @AppStorage("settings.general.filesWarning") private var warning: Bool = false + @AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true + @AppStorage("settings.general.confirmAlert") private var confirmAlert: Bool = false @State private var showAlert = false @Environment(\.colorScheme) var colorScheme @Binding var showPopover: Bool @@ -299,86 +301,88 @@ struct FilesView: View { Spacer() - InfoButton(text: "Always double-check the files/folders marked for removal. In some rare cases, Pearcleaner may find some unrelated files when app names are too similar.", color: .primary.opacity(0.5), warning: true, edge: .top) .padding(.top) Button("\(sizeType == "Logical" ? totalSelectedSize.logical : sizeType == "Finder" ? totalSelectedSize.finder : totalSelectedSize.real)") { - Task { + showCustomAlert(enabled: confirmAlert, title: "Warning", message: "Are you sure you want to remove these files?", style: .warning, onOk: { + Task { - let selectedItemsArray = Array(appState.selectedItems) + let selectedItemsArray = Array(appState.selectedItems) - killApp(appId: appState.appInfo.bundleIdentifier) { - - moveFilesToTrash(appState: appState, at: selectedItemsArray) { success in + killApp(appId: appState.appInfo.bundleIdentifier) { - // Send Sentinel FileWatcher start notification - sendStartNotificationFW() + moveFilesToTrash(appState: appState, at: selectedItemsArray) { success in - guard success else { - return - } + // Send Sentinel FileWatcher start notification + sendStartNotificationFW() - if appState.selectedItems.count == appState.appInfo.fileSize.keys.count { - updateOnMain { - search = "" - withAnimation { - if mini || menubarEnabled { - appState.currentView = .apps - showPopover = false - } else { - appState.currentView = .empty + guard success else { + return + } + + if appState.selectedItems.count == appState.appInfo.fileSize.keys.count { + updateOnMain { + search = "" + withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) { + if mini || menubarEnabled { + appState.currentView = .apps + showPopover = false + } else { + appState.currentView = .empty + } } } } - } - // Remove app from app list if main app bundle is removed for regular and wrapped apps - if (appState.appInfo.wrapped && selectedItemsArray.contains(where: { $0.absoluteString == appState.appInfo.path.deletingLastPathComponent().deletingLastPathComponent().absoluteString })) || - (!appState.appInfo.wrapped && selectedItemsArray.contains(where: { $0.absoluteString == appState.appInfo.path.absoluteString })) { - // Change view back to empty/apps if main app bundle is removed - updateOnMain { - search = "" - withAnimation { - if mini || menubarEnabled { - appState.currentView = .apps - showPopover = false - } else { - appState.currentView = .empty + // Remove app from app list if main app bundle is removed for regular and wrapped apps + if (appState.appInfo.wrapped && selectedItemsArray.contains(where: { $0.absoluteString == appState.appInfo.path.deletingLastPathComponent().deletingLastPathComponent().absoluteString })) || + (!appState.appInfo.wrapped && selectedItemsArray.contains(where: { $0.absoluteString == appState.appInfo.path.absoluteString })) { + // Change view back to empty/apps if main app bundle is removed + updateOnMain { + search = "" + withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) { + if mini || menubarEnabled { + appState.currentView = .apps + showPopover = false + } else { + appState.currentView = .empty + } } } - } - // Match found, remove the app - removeApp(appState: appState, withPath: appState.appInfo.path) + // Match found, remove the app + removeApp(appState: appState, withPath: appState.appInfo.path) - if appState.oneShotMode { - NSApp.terminate(nil) - } - - } else { - // Add deleted appInfo object to trashed array - appState.trashedFiles.append(appState.appInfo) - - // Clear out appInfoStore object (Used for leftover file search) -// if let index = appState.appInfoStore.firstIndex(where: { $0.path == appState.appInfo.path }) { -// appState.appInfoStore[index] = .empty -// } - - updateOnMain { - // Remove items from the list - appState.appInfo.fileSize = appState.appInfo.fileSize.filter { !appState.selectedItems.contains($0.key) } - // Update the selectedFiles to remove references that are no longer present - appState.selectedItems.removeAll() if appState.oneShotMode { NSApp.terminate(nil) } + + } else { + // Add deleted appInfo object to trashed array + appState.trashedFiles.append(appState.appInfo) + + // Clear out appInfoStore object (Used for leftover file search) + // if let index = appState.appInfoStore.firstIndex(where: { $0.path == appState.appInfo.path }) { + // appState.appInfoStore[index] = .empty + // } + + updateOnMain { + // Remove items from the list + appState.appInfo.fileSize = appState.appInfo.fileSize.filter { !appState.selectedItems.contains($0.key) } + // Update the selectedFiles to remove references that are no longer present + appState.selectedItems.removeAll() + if appState.oneShotMode { + NSApp.terminate(nil) + } + } } } } - } - } + } + }) + } .buttonStyle(UninstallButton(isEnabled: !appState.selectedItems.isEmpty)) @@ -434,6 +438,7 @@ struct FileDetailsItem: View { @EnvironmentObject var appState: AppState @State private var isHovered = false @AppStorage("settings.general.sizeType") var sizeType: String = "Real" + @AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true let size: Int64? let sizeL: Int64? let icon: Image? @@ -505,7 +510,7 @@ struct FileDetailsItem: View { NSWorkspace.shared.selectFile(path.path, inFileViewerRootedAtPath: path.deletingLastPathComponent().path) } .onHover { hovering in - withAnimation(Animation.easeIn(duration: 0.2)) { + withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) { self.isHovered = hovering } } diff --git a/Pearcleaner/Views/MiniMode.swift b/Pearcleaner/Views/MiniMode.swift index 35c11ac..b85999c 100644 --- a/Pearcleaner/Views/MiniMode.swift +++ b/Pearcleaner/Views/MiniMode.swift @@ -18,6 +18,7 @@ struct MiniMode: View { @State private var showUsr: Bool = true @AppStorage("settings.general.glass") private var glass: Bool = false @AppStorage("settings.general.popover") private var popoverStay: Bool = true + @AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true @Binding var showPopover: Bool @@ -54,6 +55,7 @@ struct MiniEmptyView: View { @EnvironmentObject var appState: AppState @EnvironmentObject var locations: Locations @AppStorage("settings.general.mini") private var mini: Bool = false + @AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true @Binding var showPopover: Bool var body: some View { @@ -86,7 +88,7 @@ struct MiniEmptyView: View { } .onTapGesture { - withAnimation(.easeInOut(duration: 0.5)) { + withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) { appState.currentView = .apps } } diff --git a/Pearcleaner/Views/ZombieView.swift b/Pearcleaner/Views/ZombieView.swift index af1bdf6..a30a3c0 100644 --- a/Pearcleaner/Views/ZombieView.swift +++ b/Pearcleaner/Views/ZombieView.swift @@ -20,8 +20,10 @@ struct ZombieView: View { @AppStorage("settings.general.glass") private var glass: Bool = false @AppStorage("settings.sentinel.enable") private var sentinel: Bool = false @AppStorage("settings.menubar.enabled") private var menubarEnabled: Bool = false + @AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true @AppStorage("settings.general.selectedSort") var selectedSortAlpha: Bool = true @AppStorage("settings.general.sizeType") var sizeType: String = "Real" + @AppStorage("settings.general.confirmAlert") private var confirmAlert: Bool = false @Environment(\.colorScheme) var colorScheme @Binding var showPopover: Bool @Binding var search: String @@ -266,10 +268,9 @@ struct ZombieView: View { .buttonStyle(RescanButton()) Button("\(sizeType == "Logical" ? totalLogicalSizeUninstallBtn : sizeType == "Finder" ? totalFinderSizeUninstallBtn : totalRealSizeUninstallBtn)") { + showCustomAlert(enabled: confirmAlert, title: "Warning", message: "Are you sure you want to remove these files?", style: .warning, onOk: { Task { - - let selectedItemsArray = Array(selectedZombieItemsLocal) moveFilesToTrash(appState: appState, at: selectedItemsArray) { success in @@ -283,7 +284,7 @@ struct ZombieView: View { appState.zombieFile = .empty search = "" searchZ = "" - withAnimation { + withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) { if mini || menubarEnabled { appState.currentView = .apps showPopover = false @@ -306,21 +307,12 @@ struct ZombieView: View { updateMemoizedFiles(for: searchZ, sizeType: sizeType, selectedSortAlpha: selectedSortAlpha, force: true) } -// updateOnMain { -// // Remove items from the list -// appState.zombieFile.fileSize = appState.zombieFile.fileSize.filter { !selectedZombieItemsLocal.contains($0.key) } -// // Update the selectedZombieFiles to remove references that are no longer present -// selectedZombieItemsLocal.removeAll() -// updateTotalSizes() -// -// } - } -// updateMemoizedFiles(for: searchZ, sizeType: sizeType, selectedSortAlpha: selectedSortAlpha, force: true) - } + }) + } .buttonStyle(UninstallButton(isEnabled: !selectedZombieItemsLocal.isEmpty)) .disabled(selectedZombieItemsLocal.isEmpty) @@ -472,6 +464,7 @@ struct ZombieFileDetailsItem: View { @EnvironmentObject var appState: AppState @State private var isHovered = false @AppStorage("settings.general.sizeType") var sizeType: String = "Real" + @AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true let size: Int64? let sizeL: Int64? let icon: Image? @@ -527,7 +520,7 @@ struct ZombieFileDetailsItem: View { .help(path.path) } .onHover { hovering in - withAnimation(Animation.easeIn(duration: 0.2)) { + withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) { self.isHovered = hovering } }