From 67ced2328d49992a73ac05cca16b4278dc7e18b1 Mon Sep 17 00:00:00 2001 From: Alin Date: Fri, 8 Dec 2023 13:17:46 -0700 Subject: [PATCH] v1.8 --- Pearcleaner.xcodeproj/project.pbxproj | 8 +-- Pearcleaner/Logic/AppCommands.swift | 12 +++- Pearcleaner/Logic/AppState.swift | 8 ++- Pearcleaner/Logic/Logic.swift | 86 +++++++++++++++++++++++++-- Pearcleaner/Logic/Utilities.swift | 32 ++++++++++ Pearcleaner/PearcleanerApp.swift | 4 +- Pearcleaner/Views/AppListItems.swift | 11 +++- Pearcleaner/Views/AppListView.swift | 7 ++- Pearcleaner/Views/DropTarget.swift | 12 +++- Pearcleaner/Views/FilesView.swift | 14 ++++- Pearcleaner/Views/MiniMode.swift | 7 ++- 11 files changed, 176 insertions(+), 25 deletions(-) diff --git a/Pearcleaner.xcodeproj/project.pbxproj b/Pearcleaner.xcodeproj/project.pbxproj index 74624bc..cbea38f 100644 --- a/Pearcleaner.xcodeproj/project.pbxproj +++ b/Pearcleaner.xcodeproj/project.pbxproj @@ -535,7 +535,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 8; + CURRENT_PROJECT_VERSION = 9; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = BK8443AXLU; ENABLE_HARDENED_RUNTIME = YES; @@ -552,7 +552,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 1.7; + MARKETING_VERSION = 1.8; PRODUCT_BUNDLE_IDENTIFIER = com.alienator88.Pearcleaner; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -569,7 +569,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 8; + CURRENT_PROJECT_VERSION = 9; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = BK8443AXLU; ENABLE_HARDENED_RUNTIME = YES; @@ -586,7 +586,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 1.7; + MARKETING_VERSION = 1.8; PRODUCT_BUNDLE_IDENTIFIER = com.alienator88.Pearcleaner; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; diff --git a/Pearcleaner/Logic/AppCommands.swift b/Pearcleaner/Logic/AppCommands.swift index 9d78b61..6d961a2 100644 --- a/Pearcleaner/Logic/AppCommands.swift +++ b/Pearcleaner/Logic/AppCommands.swift @@ -38,7 +38,17 @@ struct AppCommands: Commands { Text("Refresh Apps") } .keyboardShortcut("r", modifiers: .command) - + + Button { + uninstallPearcleaner(appState: appState) + } label: { + Text("Uninstall Pearcleaner") + } +// .keyboardShortcut("C", modifiers: .command) + + + + } diff --git a/Pearcleaner/Logic/AppState.swift b/Pearcleaner/Logic/AppState.swift index 4c74e12..964911b 100644 --- a/Pearcleaner/Logic/AppState.swift +++ b/Pearcleaner/Logic/AppState.swift @@ -39,7 +39,8 @@ class AppState: ObservableObject appName: "", appVersion: "", appIcon: nil, - webApp: false + webApp: false, + wrapped: false ) } } @@ -52,8 +53,9 @@ struct AppInfo: Identifiable, Hashable { let appVersion: String let appIcon: NSImage? let webApp: Bool - - static let empty = AppInfo(id: UUID(), path: URL(fileURLWithPath: ""), bundleIdentifier: "", appName: "", appVersion: "", appIcon: nil, webApp: false) + let wrapped: Bool + + static let empty = AppInfo(id: UUID(), path: URL(fileURLWithPath: ""), bundleIdentifier: "", appName: "", appVersion: "", appIcon: nil, webApp: false, wrapped: false) } diff --git a/Pearcleaner/Logic/Logic.swift b/Pearcleaner/Logic/Logic.swift index 6ffab60..bb990c1 100644 --- a/Pearcleaner/Logic/Logic.swift +++ b/Pearcleaner/Logic/Logic.swift @@ -18,7 +18,7 @@ func getSortedApps() -> (systemApps: [AppInfo], userApps: [AppInfo]) { let sortedUserApps = apps.userApps .compactMap { getAppInfo(atPath: $0) } .sorted { $0.appName.replacingOccurrences(of: ".", with: "").lowercased() < $1.appName.replacingOccurrences(of: ".", with: "").lowercased() } - + return (systemApps: sortedSystemApps, userApps: sortedUserApps) } @@ -78,6 +78,7 @@ func getAppInfo(atPath path: URL) -> AppInfo? { var appIcon: NSImage? var appName: String? var webApp: Bool? + if let localizedName = bundle.localizedInfoDictionary?[kCFBundleNameKey as String] as? String { appName = localizedName } else if let bundleName = bundle.infoDictionary?["CFBundleName"] as? String { @@ -119,9 +120,86 @@ func getAppInfo(atPath path: URL) -> AppInfo? { } else { webApp = false } - - return AppInfo(id: UUID(), path: path, bundleIdentifier: bundleIdentifier, appName: appName ?? "", appVersion: appVersion, appIcon: appIcon, webApp: webApp ?? false) - + + + return AppInfo(id: UUID(), path: path, bundleIdentifier: bundleIdentifier, appName: appName ?? "", appVersion: appVersion, appIcon: appIcon, webApp: webApp ?? false, wrapped: false) + + } else { +// print("One or more variables missing for app at path: \(path)") + let wrapperURL = path.appendingPathComponent("Wrapper").appendingPathComponent(path.lastPathComponent) + if let wrappedAppInfo = getWrappedAppInfo(atPath: wrapperURL) { + return wrappedAppInfo + } + } + } else { + print("Bundle not found at path: \(path)") + } + return nil +} + + +func getWrappedAppInfo(atPath path: URL) -> AppInfo? { + if let bundle = Bundle(url: path) { + if let bundleIdentifier = bundle.bundleIdentifier, + let appVersion = bundle.infoDictionary?["CFBundleShortVersionString"] as? String { + var appIconFileName = bundle.infoDictionary?["CFBundleIconFile"] as? String + var appIcon: NSImage? + var appName: String? + var webApp: Bool? + + if let localizedName = bundle.localizedInfoDictionary?[kCFBundleNameKey as String] as? String { + appName = localizedName + } else if let bundleName = bundle.infoDictionary?["CFBundleName"] as? String { + appName = bundleName + } else { + appName = path.deletingPathExtension().lastPathComponent + } + + if appIconFileName == nil { + if let iconsDict = bundle.infoDictionary?["CFBundleIcons"] as? [String: Any] { + if let primaryIconDict = iconsDict["CFBundlePrimaryIcon"] as? [String: Any], + let iconFiles = primaryIconDict["CFBundleIconFiles"] as? [String], + let primaryIconFile = iconFiles.first { + // Now, you can use the primaryIconFile to construct the path to the icon file + let iconPath = path.appendingPathComponent(primaryIconFile) + appIconFileName = iconPath.path + + if let contents = try? FileManager.default.contentsOfDirectory(at: path, includingPropertiesForKeys: nil) { + // Find the first file that matches the specified prefix + if let foundURL = contents.first(where: { $0.lastPathComponent.hasPrefix(primaryIconFile) }), + let image = NSImage(contentsOfFile: foundURL.path) { + appIcon = image + + + } else { + print("No matching image found for \(primaryIconFile).") + } + } else { + print("Unable to access the directory at \(primaryIconFile).") + } + + } + } + } + + // Convert the icon to a 100x100 PNG image + if let pngIcon = appIcon.flatMap({ convertICNSToPNG(icon: $0, size: NSSize(width: 100, height: 100)) }) { + appIcon = pngIcon + } + + if appIcon == nil { + print("App Icon not found for app at path: \(path)") + } + + if bundle.infoDictionary?["LSTemplateApplication"] is Bool { + webApp = true + } else { + webApp = false + } + + + return AppInfo(id: UUID(), path: path, bundleIdentifier: bundleIdentifier, appName: appName ?? "", appVersion: appVersion, appIcon: appIcon, webApp: webApp ?? false, wrapped: true) + } else { print("One or more variables missing for app at path: \(path)") } diff --git a/Pearcleaner/Logic/Utilities.swift b/Pearcleaner/Logic/Utilities.swift index 6a2bcda..7c1e39b 100644 --- a/Pearcleaner/Logic/Utilities.swift +++ b/Pearcleaner/Logic/Utilities.swift @@ -394,6 +394,38 @@ func presentAlert(appState: AppState) -> Alert { +// --- Pearcleaner Uninstall -- +func uninstallPearcleaner(appState: AppState) { + + // Unload Sentinel Monitor if running + launchctl(load: false) + + // Get app info for Pearcleaner + let appInfo = getAppInfo(atPath: Bundle.main.bundleURL) + + // Find application files for Pearcleaner + findPathsForApp(appState: appState, appInfo: appInfo!) + + // Kill Pearcleaner and tell Finder to trash the files + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + let selectedItemsArray = Array(appState.selectedItems).filter { !$0.path.contains(".Trash") } + let posixFiles = selectedItemsArray.map { "POSIX file \"\($0.path)\", " }.joined().dropLast(3) + let scriptSource = """ + tell application \"Finder\" to delete { \(posixFiles)" } + """ + let task = Process() + task.launchPath = "/bin/sh" + task.arguments = ["-c", "sleep 1; osascript -e '\(scriptSource)'"] + task.launch() + + NSApp.terminate(nil) + exit(0) + } + +} + + + // --- Create Application Support folder if it doesn't exist --- func ensureApplicationSupportFolderExists(appState: AppState) { let fileManager = FileManager.default diff --git a/Pearcleaner/PearcleanerApp.swift b/Pearcleaner/PearcleanerApp.swift index cce6132..1cdf79c 100644 --- a/Pearcleaner/PearcleanerApp.swift +++ b/Pearcleaner/PearcleanerApp.swift @@ -71,9 +71,9 @@ struct PearcleanerApp: App { appState.sortedApps.systemApps = sortedApps.systemApps Task { - + #if !DEBUG - + // Make sure App Support folder exists in the future if needed for storage ensureApplicationSupportFolderExists(appState: appState) diff --git a/Pearcleaner/Views/AppListItems.swift b/Pearcleaner/Views/AppListItems.swift index b7c2d71..2401870 100644 --- a/Pearcleaner/Views/AppListItems.swift +++ b/Pearcleaner/Views/AppListItems.swift @@ -48,7 +48,16 @@ struct AppListItems: View { .background(Color("mode").opacity(0.1)) .clipShape(.capsule) } - + if appInfo.wrapped { + Text("iOS") + .font(.footnote) + .foregroundStyle(Color("mode").opacity(0.5)) + .frame(minWidth: 30, minHeight: 15) + .padding(2) + .background(Color("mode").opacity(0.1)) + .clipShape(.capsule) + } + Spacer() Text(appInfo.appVersion) diff --git a/Pearcleaner/Views/AppListView.swift b/Pearcleaner/Views/AppListView.swift index f991385..758d1be 100644 --- a/Pearcleaner/Views/AppListView.swift +++ b/Pearcleaner/Views/AppListView.swift @@ -133,7 +133,7 @@ struct AppListView: View { VStack(spacing: 0) { if appState.currentView == .empty { TopBar(reload: $reload) - AppDetailsEmptyView() + AppDetailsEmptyView(showPopover: $showPopover) } else if appState.currentView == .files { TopBar(reload: $reload) FilesView(showPopover: $showPopover) @@ -184,14 +184,15 @@ struct AppDetailsEmptyView: View { @Environment(\.colorScheme) var colorScheme @EnvironmentObject var appState: AppState @State private var animateGradient: Bool = false + @Binding var showPopover: Bool var body: some View { VStack() { Spacer() - DropTarget(appState: appState) - + DropTarget(appState: appState, showPopover: $showPopover) + Spacer() // if appState.isReminderVisible { diff --git a/Pearcleaner/Views/DropTarget.swift b/Pearcleaner/Views/DropTarget.swift index 26ee37b..b6b6287 100644 --- a/Pearcleaner/Views/DropTarget.swift +++ b/Pearcleaner/Views/DropTarget.swift @@ -17,9 +17,11 @@ struct DropTarget: View, DropDelegate { private var types: [UTType] = [.fileURL] @Environment(\.colorScheme) var colorScheme @State private var phase: CGFloat = 4 - - public init(appState: AppState) { + @Binding var showPopover: Bool + + public init(appState: AppState, showPopover: Binding) { self.appState = appState + _showPopover = showPopover } var body: some View { @@ -126,7 +128,11 @@ struct DropTarget: View, DropDelegate { self.ants = false appState.appInfo = appInfo! findPathsForApp(appState: appState, appInfo: appState.appInfo) - appState.currentView = .files + if mini { + showPopover = true + } else { + appState.currentView = .files + } } } else { print("Error: Dropped file is not an application bundle") diff --git a/Pearcleaner/Views/FilesView.swift b/Pearcleaner/Views/FilesView.swift index 7203ba4..b0be130 100644 --- a/Pearcleaner/Views/FilesView.swift +++ b/Pearcleaner/Views/FilesView.swift @@ -170,7 +170,12 @@ struct FilesView: View { } } - let selectedItemsArray = Array(appState.selectedItems).filter { !$0.path.contains(".Trash") } + let selectedItemsArray = Array(appState.selectedItems) + .filter { !$0.path.contains(".Trash") } + .map { path in + return path.path.contains("Wrapper") ? path.deletingLastPathComponent().deletingLastPathComponent() : path + } + killApp(appId: appState.appInfo.bundleIdentifier) { moveFilesToTrash(at: selectedItemsArray) { withAnimation { @@ -259,7 +264,14 @@ struct FileDetailsItem: View { let icon: Image? let path: URL +// init(size: String, icon: Image?, path: URL) { +// self.size = size +// self.icon = icon +// self.path = path.path.contains("Wrapper") ? path.deletingLastPathComponent().deletingLastPathComponent() : path +// } + var body: some View { + HStack(alignment: .center, spacing: 20) { Toggle("", isOn: Binding( get: { self.appState.selectedItems.contains(self.path) }, diff --git a/Pearcleaner/Views/MiniMode.swift b/Pearcleaner/Views/MiniMode.swift index 184459b..b556591 100644 --- a/Pearcleaner/Views/MiniMode.swift +++ b/Pearcleaner/Views/MiniMode.swift @@ -27,7 +27,7 @@ struct MiniMode: View { VStack(spacing: 0) { if appState.currentView == .empty { TopBarMini(reload: $reload, search: $search) - MiniEmptyView() + MiniEmptyView(showPopover: $showPopover) } else if appState.currentView == .files { TopBarMini(reload: $reload, search: $search) FilesView(showPopover: $showPopover) @@ -98,13 +98,14 @@ struct MiniEmptyView: View { @EnvironmentObject var appState: AppState @State private var animateGradient: Bool = false @AppStorage("settings.general.mini") private var mini: Bool = false - + @Binding var showPopover: Bool + var body: some View { VStack() { Spacer() - DropTarget(appState: appState) + DropTarget(appState: appState, showPopover: $showPopover) .frame(maxWidth: mini ? 300 : 500) Text("Drop an app to begin")