diff --git a/Pearcleaner.xcodeproj/project.pbxproj b/Pearcleaner.xcodeproj/project.pbxproj index 2ec85eb..559ad0b 100644 --- a/Pearcleaner.xcodeproj/project.pbxproj +++ b/Pearcleaner.xcodeproj/project.pbxproj @@ -36,6 +36,7 @@ C7D31D482AFEB23700C7ED9E /* TopBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7D31D472AFEB23700C7ED9E /* TopBar.swift */; }; C7D31D4A2AFEB26700C7ED9E /* AppListItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7D31D492AFEB26700C7ED9E /* AppListItems.swift */; }; C7D31D512AFF00F300C7ED9E /* Locations.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7D31D502AFF00F300C7ED9E /* Locations.swift */; }; + C7DC1FEE2B9F7D3D009AC317 /* features.json in Resources */ = {isa = PBXBuildFile; fileRef = C7DC1FED2B9F7D3D009AC317 /* features.json */; }; C7F539382AF60865007DF1B2 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7F539372AF60865007DF1B2 /* Utilities.swift */; }; C7FB173B2B96321300B96F9A /* AppsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7FB173A2B96321300B96F9A /* AppsListView.swift */; }; /* End PBXBuildFile section */ @@ -95,6 +96,7 @@ C7D31D472AFEB23700C7ED9E /* TopBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopBar.swift; sourceTree = ""; }; C7D31D492AFEB26700C7ED9E /* AppListItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppListItems.swift; sourceTree = ""; }; C7D31D502AFF00F300C7ED9E /* Locations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Locations.swift; sourceTree = ""; }; + C7DC1FED2B9F7D3D009AC317 /* features.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = features.json; sourceTree = SOURCE_ROOT; }; C7F539372AF60865007DF1B2 /* Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utilities.swift; sourceTree = ""; }; C7FB173A2B96321300B96F9A /* AppsListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppsListView.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -175,6 +177,7 @@ C76D084F2AF84E0100D07867 /* Resources */ = { isa = PBXGroup; children = ( + C7DC1FED2B9F7D3D009AC317 /* features.json */, C7A27E802AFD7C4600166168 /* com.alienator88.PearcleanerSentinel.plist */, C77B90142AF18E7C009CC655 /* Info.plist */, C77B90072AF18E2F009CC655 /* Assets.xcassets */, @@ -320,6 +323,7 @@ files = ( C77B90082AF18E2F009CC655 /* Assets.xcassets in Resources */, C7A27E812AFD7C4600166168 /* com.alienator88.PearcleanerSentinel.plist in Resources */, + C7DC1FEE2B9F7D3D009AC317 /* features.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -539,7 +543,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 21; + CURRENT_PROJECT_VERSION = 22; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = BK8443AXLU; @@ -557,7 +561,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 3.0.2; + MARKETING_VERSION = 3.1.0; PRODUCT_BUNDLE_IDENTIFIER = com.alienator88.Pearcleaner; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -574,7 +578,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 21; + CURRENT_PROJECT_VERSION = 22; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = BK8443AXLU; @@ -592,7 +596,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 3.0.2; + MARKETING_VERSION = 3.1.0; PRODUCT_BUNDLE_IDENTIFIER = com.alienator88.Pearcleaner; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; diff --git a/Pearcleaner/Logic/AppState.swift b/Pearcleaner/Logic/AppState.swift index f125300..da6bb4b 100644 --- a/Pearcleaner/Logic/AppState.swift +++ b/Pearcleaner/Logic/AppState.swift @@ -29,7 +29,6 @@ class AppState: ObservableObject @Published var showProgress: Bool = false @Published var popCount: Int = 0 - init() { self.appInfo = AppInfo( diff --git a/Pearcleaner/Logic/Utilities.swift b/Pearcleaner/Logic/Utilities.swift index 6ae415d..33342eb 100644 --- a/Pearcleaner/Logic/Utilities.swift +++ b/Pearcleaner/Logic/Utilities.swift @@ -157,6 +157,52 @@ func isDarkModeEnabled() -> Bool { } +// Check if appearance is dark mode +//func getCasks() -> [String] { +// let process = Process() +//#if arch(x86_64) +// let cmd = "/usr/local/bin/brew" +//#elseif arch(arm64) +// let cmd = "/opt/homebrew/bin/brew" +//#endif +// process.executableURL = URL(fileURLWithPath: cmd) +// process.arguments = ["list", "--cask"] +// let pipe = Pipe() +// process.standardOutput = pipe +// process.standardError = pipe +// try? process.run() +// process.waitUntilExit() // Ensure the process completes +// let data = pipe.fileHandleForReading.readDataToEndOfFile() +// if let output = String(data: data, encoding: .utf8), !output.isEmpty { +// return output.components(separatedBy: "\n").filter { !$0.isEmpty }.map { $0.replacingOccurrences(of: "-", with: " ") } +// } else { +// return [] +// } +//} + +// Brew cleanup +func caskCleanup(app: String) { + Task(priority: .high) { + let process = Process() +#if arch(x86_64) + let cmd = "/usr/local/bin/brew" +#elseif arch(arm64) + let cmd = "/opt/homebrew/bin/brew" +#endif + 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 + 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) + } +} + + // Check if symlink func isSymlink(atPath path: URL) -> Bool { do { @@ -218,22 +264,6 @@ func getIconForFileOrFolderNS(atPath path: URL) -> NSImage? { } // Get average color from image -//extension NSImage { -// var averageColor2: NSColor? { -// guard let inputImage = CIImage(image: self) else { return nil } -// let extentVector = CIVector(x: inputImage.extent.origin.x, y: inputImage.extent.origin.y, z: inputImage.extent.size.width, w: inputImage.extent.size.height) -// -// guard let filter = CIFilter(name: "CIAreaAverage", parameters: [kCIInputImageKey: inputImage, kCIInputExtentKey: extentVector]) else { return nil } -// guard let outputImage = filter.outputImage else { return nil } -// -// var bitmap = [UInt8](repeating: 0, count: 4) -// let context = CIContext(options: [.workingColorSpace: kCFNull!]) -// context.render(outputImage, toBitmap: &bitmap, rowBytes: 4, bounds: CGRect(x: 0, y: 0, width: 1, height: 1), format: .RGBA8, colorSpace: nil) -// -// return NSColor(red: CGFloat(bitmap[0]) / 255, green: CGFloat(bitmap[1]) / 255, blue: CGFloat(bitmap[2]) / 255, alpha: CGFloat(bitmap[3]) / 255) -// } -//} - extension NSImage { var averageColor: NSColor? { guard let tiffData = self.tiffRepresentation, let bitmapImage = NSBitmapImageRep(data: tiffData), let inputImage = CIImage(bitmapImageRep: bitmapImage) else { return nil } @@ -279,13 +309,10 @@ func removeApp(appState: AppState, withId id: UUID) { appState.sortedApps.remove(at: index) return // Exit the function if the app was found and removed } - // Remove from appInfoStore if found if let index = appState.appInfoStore.firstIndex(where: { $0.id == id }) { appState.appInfoStore.remove(at: index) } - // Clear out AppInfo state - appState.appInfo = AppInfo.empty } } diff --git a/Pearcleaner/PearcleanerApp.swift b/Pearcleaner/PearcleanerApp.swift index 3db173f..110c366 100644 --- a/Pearcleaner/PearcleanerApp.swift +++ b/Pearcleaner/PearcleanerApp.swift @@ -23,6 +23,7 @@ struct PearcleanerApp: App { @AppStorage("settings.general.miniview") private var miniView: Bool = true @AppStorage("settings.general.instant") private var instantSearch: Bool = true @AppStorage("settings.general.features") private var features: String = "" + @AppStorage("settings.general.brew") private var brew: Bool = false @State private var search = "" @State private var showPopover: Bool = false @State private var showFeature: Bool = false @@ -94,14 +95,11 @@ struct PearcleanerApp: App { appState.sortedApps = sortedApps - // Find all app paths on load + // Find all app paths/information on load if instantSearch is enabled if instantSearch { -// startEnd { - loadAllPaths(allApps: sortedApps, appState: appState, locations: locations) -// } + loadAllPaths(allApps: sortedApps, appState: appState, locations: locations) } - - + #if !DEBUG Task { diff --git a/Pearcleaner/Settings/General.swift b/Pearcleaner/Settings/General.swift index e43d4a5..cbb7dc0 100644 --- a/Pearcleaner/Settings/General.swift +++ b/Pearcleaner/Settings/General.swift @@ -19,6 +19,7 @@ struct GeneralSettingsTab: View { @AppStorage("settings.general.sidebarWidth") private var sidebarWidth: Double = 280 @AppStorage("displayMode") var displayMode: DisplayMode = .system @AppStorage("settings.sentinel.enable") private var sentinel: Bool = false + @AppStorage("settings.general.brew") private var brew: Bool = false @AppStorage("settings.general.instant") private var instantSearch: Bool = true @AppStorage("settings.general.selectedTheme") var selectedTheme: String = "Auto" @State private var diskStatus: Bool = false @@ -82,6 +83,30 @@ struct GeneralSettingsTab: View { .padding(.leading) + HStack(spacing: 0) { + Image(systemName: brew ? "mug.fill" : "mug") + .resizable() + .scaledToFit() + .frame(width: 20, height: 20) + .padding(.trailing) + .foregroundStyle(.gray) + VStack(alignment: .leading, spacing: 5) { + Text("\(brew ? "Homebrew cleanup is enabled" : "Homebrew cleanup is disabled")") + .font(.callout) + .foregroundStyle(.gray) + } + + InfoButton(text: "When homebrew cleanup is enabled, Pearcleaner will check if the app you are removing was installed via homebrew and execute a brew uninstall and brew cleanup command as well to let homebrew know that the app is removed. This way your homebrew list will be synced up correctly and caching will be removed.\n\nNOTE: If you undo the file delete with CMD+Z, the files will be put back but homebrew will not be aware of it. To get the homebrew list back in sync you'd need to run:\n brew install APPNAME --force", color: nil) + + Spacer() + Toggle(isOn: $brew, label: { + }) + .toggleStyle(.switch) + } + .padding(5) + .padding(.leading) + + HStack(spacing: 0) { Image(systemName: displayMode.colorScheme == .dark ? "moon.fill" : "sun.max.fill") .resizable() @@ -346,7 +371,7 @@ struct GeneralSettingsTab: View { } .padding(20) - .frame(width: 500, height: 650) + .frame(width: 500, height: 690) } diff --git a/Pearcleaner/Settings/SettingsWindow.swift b/Pearcleaner/Settings/SettingsWindow.swift index 87e8376..c40b256 100644 --- a/Pearcleaner/Settings/SettingsWindow.swift +++ b/Pearcleaner/Settings/SettingsWindow.swift @@ -10,7 +10,8 @@ import SwiftUI struct SettingsView: View { @EnvironmentObject var appState: AppState @Binding var showPopover: Bool - + @AppStorage("settings.general.glass") private var glass: Bool = true + var body: some View { TabView() { @@ -33,6 +34,8 @@ struct SettingsView: View { .tag(CurrentTabView.about) } + .background(glass ? GlassEffect(material: .sidebar, blendingMode: .behindWindow).edgesIgnoringSafeArea(.all) : nil) + } diff --git a/Pearcleaner/Views/FilesView.swift b/Pearcleaner/Views/FilesView.swift index 3ab8811..2b0961b 100644 --- a/Pearcleaner/Views/FilesView.swift +++ b/Pearcleaner/Views/FilesView.swift @@ -15,6 +15,7 @@ struct FilesView: View { @AppStorage("settings.general.mini") private var mini: Bool = false @AppStorage("settings.sentinel.enable") private var sentinel: Bool = false @AppStorage("settings.general.instant") var instantSearch: Bool = true + @AppStorage("settings.general.brew") private var brew: Bool = false @Environment(\.colorScheme) var colorScheme @Binding var showPopover: Bool @Binding var search: String @@ -216,12 +217,18 @@ struct FilesView: View { withAnimation { showPopover = false updateOnMain { + appState.currentView = mini ? .apps : .empty appState.isReminderVisible.toggle() } } + // Remove app from app list removeApp(appState: appState, withId: appState.appInfo.id) - -// refreshAppList(appState.appInfo) + // Brew cleanup if enabled + if brew { + caskCleanup(app: appState.appInfo.appName) + } + // Clear out AppInfo state + appState.appInfo = AppInfo.empty } } diff --git a/Pearcleaner/Views/MiniMode.swift b/Pearcleaner/Views/MiniMode.swift index b36bc92..2ccc120 100644 --- a/Pearcleaner/Views/MiniMode.swift +++ b/Pearcleaner/Views/MiniMode.swift @@ -182,7 +182,7 @@ struct MiniAppView: View { AppsListView(search: $search, showPopover: $showPopover, filteredApps: filteredApps) if appState.currentView != .empty { - SearchBarMiniBottom(search: $search).background(.ultraThinMaterial) + SearchBarMiniBottom(search: $search) } } .padding(.bottom) diff --git a/features.json b/features.json index 60a80a8..41d62c4 100644 --- a/features.json +++ b/features.json @@ -1,3 +1,4 @@ { + "3.1.0": "- Brew Cleanup: If you install apps using Homebrew, you can now enable brew cleanup in Pearcleaner settings to have it uninstall the app via brew as well to keep Homebrew synced up.", "3.0.0": "- Instant Search: Enable in settings to load app files on startup|- Semantic Versioning: Going forward will use semver (Ex. v0.0.0)|-Feature Alert: For each version, a feature alert will popup once on startup to show details|- Leftover File Cleaning: Search your mac for files leftover by uninstalled apps|- Sidebar Drag Handle: Resize the sidebar in regular mode|- Redesign UI/Settings/Icon: Some new buttons, layout changes, new official app icon and pear color theme accents|- File Sort: Sort files alphabetically or by size|- Socket File Removal: Finds socket files that aren't even visible in Finder with show hidden files enabled|- OSLog Output: Will print errors to the Console app for easier troubleshooting" }