From 2dc3bee97315bbe7c666cbbb5b81b3f13ea945a7 Mon Sep 17 00:00:00 2001 From: Alin Date: Fri, 3 May 2024 18:04:07 -0600 Subject: [PATCH] WIP --- Pearcleaner.xcodeproj/project.pbxproj | 4 + Pearcleaner/Logic/AppState.swift | 3 + Pearcleaner/Logic/Logic.swift | 32 +++- Pearcleaner/Logic/PermissionChecker.swift | 196 +++++++++++++++++++++ Pearcleaner/Logic/Styles.swift | 95 +++++++++++ Pearcleaner/Logic/Updater.swift | 10 +- Pearcleaner/Logic/Utilities.swift | 121 ++----------- Pearcleaner/PearcleanerApp.swift | 27 ++- Pearcleaner/Settings/General.swift | 199 +++++++++++++++++----- Pearcleaner/Settings/SettingsWindow.swift | 1 - Pearcleaner/Views/AppSearchView.swift | 2 + Pearcleaner/Views/FilesView.swift | 41 ++--- Pearcleaner/Views/ZombieView.swift | 39 +++-- Pearcleaner/Windows/PermView.swift | 107 +++++++++--- Pearcleaner/Windows/WindowSettings.swift | 31 +++- 15 files changed, 669 insertions(+), 239 deletions(-) create mode 100644 Pearcleaner/Logic/PermissionChecker.swift diff --git a/Pearcleaner.xcodeproj/project.pbxproj b/Pearcleaner.xcodeproj/project.pbxproj index dee8b0a..01ac025 100644 --- a/Pearcleaner.xcodeproj/project.pbxproj +++ b/Pearcleaner.xcodeproj/project.pbxproj @@ -16,6 +16,7 @@ C72893122AFD51EA00C8C1CD /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = C72893112AFD51EA00C8C1CD /* main.swift */; }; C74412C72BBF249000DDFCA8 /* AppPathsFetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C74412C62BBF249000DDFCA8 /* AppPathsFetch.swift */; }; C74FDC632BCD833D00B8960F /* Conditions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C74FDC622BCD833D00B8960F /* Conditions.swift */; }; + C75347642BE599FA00AAFCC3 /* PermissionChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = C75347632BE599FA00AAFCC3 /* PermissionChecker.swift */; }; C7575D392B0182DE006A600A /* PearcleanerSentinel in CopyFiles */ = {isa = PBXBuildFile; fileRef = C728930F2AFD51EA00C8C1CD /* PearcleanerSentinel */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; C76D08482AF83C3F00D07867 /* Update.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76D08472AF83C3F00D07867 /* Update.swift */; }; C76D084A2AF83C6E00D07867 /* General.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76D08492AF83C6E00D07867 /* General.swift */; }; @@ -107,6 +108,7 @@ C72893112AFD51EA00C8C1CD /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; C74412C62BBF249000DDFCA8 /* AppPathsFetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPathsFetch.swift; sourceTree = ""; }; C74FDC622BCD833D00B8960F /* Conditions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Conditions.swift; sourceTree = ""; }; + C75347632BE599FA00AAFCC3 /* PermissionChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionChecker.swift; sourceTree = ""; }; C76D08472AF83C3F00D07867 /* Update.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Update.swift; sourceTree = ""; }; C76D08492AF83C6E00D07867 /* General.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = General.swift; sourceTree = ""; }; C76D08502AF850F300D07867 /* About.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = About.swift; sourceTree = ""; }; @@ -205,6 +207,7 @@ C7ED86B22BA8ED8800F13DE4 /* Trash.swift */, C79D051F2BC99EB40083F976 /* ThemeManager.swift */, C7DE17642BE54F4900197715 /* Updater.swift */, + C75347632BE599FA00AAFCC3 /* PermissionChecker.swift */, ); path = Logic; sourceTree = ""; @@ -461,6 +464,7 @@ C7FB173B2B96321300B96F9A /* AppsListView.swift in Sources */, C7045A342B068B2900376976 /* UpdateView.swift in Sources */, C77B90162AF19377009CC655 /* AppState.swift in Sources */, + C75347642BE599FA00AAFCC3 /* PermissionChecker.swift in Sources */, C7DE17652BE54F4900197715 /* Updater.swift in Sources */, C7DD49EE2BAB7F6000CCBA16 /* ReversePathsFetch.swift in Sources */, C7045A322B068AD700376976 /* NewWin.swift in Sources */, diff --git a/Pearcleaner/Logic/AppState.swift b/Pearcleaner/Logic/AppState.swift index 550d983..5c3003b 100644 --- a/Pearcleaner/Logic/AppState.swift +++ b/Pearcleaner/Logic/AppState.swift @@ -29,6 +29,9 @@ class AppState: ObservableObject @Published var showProgress: Bool = false @Published var finderExtensionEnabled: Bool = false @Published var updateAvailable: Bool = false + @Published var permissionsOkay: Bool = true + @Published var permissionResults: PermissionsCheckResults? + init() { diff --git a/Pearcleaner/Logic/Logic.swift b/Pearcleaner/Logic/Logic.swift index d57ad89..672f717 100644 --- a/Pearcleaner/Logic/Logic.swift +++ b/Pearcleaner/Logic/Logic.swift @@ -199,7 +199,7 @@ func showAppInFiles(appInfo: AppInfo, appState: AppState, locations: Locations, // Move files to trash using applescript/Finder so it asks for user password if needed -func moveFilesToTrash(at fileURLs: [URL], completion: @escaping () -> Void = {}) { +func moveFilesToTrash(appState: AppState, at fileURLs: [URL], completion: @escaping (Bool) -> Void = {_ in }) { @AppStorage("settings.sentinel.enable") var sentinel: Bool = false if sentinel { launchctl(load: false) @@ -214,13 +214,26 @@ func moveFilesToTrash(at fileURLs: [URL], completion: @escaping () -> Void = {}) if let scriptObject = NSAppleScript(source: scriptSource) { let output: NSAppleEventDescriptor = scriptObject.executeAndReturnError(&error) if let error = error { - printOS("Error: \(error)") - } else if let outputString = output.stringValue { + checkAllPermissions(appState: appState) { results in + appState.permissionResults = results + if !results.allPermissionsGranted { + updateOnMain { + appState.permissionsOkay = false + } + } + } + printOS("Trash Error: \(error)") + DispatchQueue.main.async { + completion(false) // Indicate failure + } + return + } + if let outputString = output.stringValue { printOS(outputString) } } DispatchQueue.main.async { - completion() + completion(true) // Indicate success } } @@ -250,12 +263,15 @@ func undoTrash(appState: AppState, completion: @escaping () -> Void = {}) { if let scriptObject = NSAppleScript(source: scriptSource) { let output: NSAppleEventDescriptor = scriptObject.executeAndReturnError(&error) if let error = error { - if let value = error["NSAppleScriptErrorNumber"] { - if value as! Int == 1002 { - _ = checkAndRequestAccessibilityAccess(appState: appState) + checkAllPermissions(appState: appState) { results in + appState.permissionResults = results + if !results.allPermissionsGranted { + updateOnMain { + appState.permissionsOkay = false + } } } - printOS("Error: \(error)") + printOS("Undo Trash Error: \(error)") } else if let outputString = output.stringValue { printOS(outputString) } diff --git a/Pearcleaner/Logic/PermissionChecker.swift b/Pearcleaner/Logic/PermissionChecker.swift new file mode 100644 index 0000000..4897efc --- /dev/null +++ b/Pearcleaner/Logic/PermissionChecker.swift @@ -0,0 +1,196 @@ +// +// PermissionChecker.swift +// Pearcleaner +// +// Created by Alin Lupascu on 5/3/24. +// + +import SwiftUI +import EventKit + +struct PermissionsCheckResults { + var fullDiskAccess: Bool + var accessibility: Bool + var automation: Bool + var reminders: Bool + + // Computed property to check if all permissions are granted + var allPermissionsGranted: Bool { + return fullDiskAccess && accessibility && automation && reminders + } +} + +func checkAllPermissions(appState: AppState, completion: @escaping (PermissionsCheckResults) -> Void) { + let dispatchGroup = DispatchGroup() + + // Check Full Disk Access + var fullDiskAccess = false + @AppStorage("settings.permissions.hasLaunched") var hasLaunched: Bool = false + + let process = Process() + process.launchPath = "/usr/bin/sqlite3" + process.arguments = ["/Library/Application Support/com.apple.TCC/TCC.db", "select client from access where auth_value and service = 'kTCCServiceSystemPolicyAllFiles' and client = 'com.alienator88.Pearcleaner'"] + let pipe = Pipe() + process.standardOutput = pipe + process.launch() + process.waitUntilExit() // Ensure process completes + + let data = pipe.fileHandleForReading.readDataToEndOfFile() + let output = String(data: data, encoding: .utf8) + fullDiskAccess = (output?.contains("com.alienator88.Pearcleaner") ?? false) + + // Check Accessibility + let options: NSDictionary = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: true] + let accessibilityEnabled = AXIsProcessTrustedWithOptions(options) + + // Check Automation Permission + var automationAccess = false + dispatchGroup.enter() + checkAutomationPermission(appState: appState) { success in + automationAccess = success + dispatchGroup.leave() + } + + // Check Reminders Permission + var remindersAccess = false + dispatchGroup.enter() + checkRemindersAccess(appState: appState) { granted in + remindersAccess = granted + dispatchGroup.leave() + } + + // Wait for all async checks to complete + dispatchGroup.notify(queue: .main) { + let results = PermissionsCheckResults( + fullDiskAccess: fullDiskAccess, + accessibility: accessibilityEnabled, + automation: automationAccess, + reminders: remindersAccess + ) + + // Check if any permission is denied and show window + if !(results.fullDiskAccess && results.accessibility && results.automation && results.reminders) { + updateOnMain { + appState.permissionsOkay = false + } + } + + completion(results) + } +} + + + +// Check Finder Automation permission +func checkAutomationPermission(appState: AppState, completion: @escaping (Bool) -> Void) { + DispatchQueue.global(qos: .background).async { + let scriptText = "tell application \"Finder\" to return name of home" + var error: NSDictionary? + if let script = NSAppleScript(source: scriptText) { + let _ = script.executeAndReturnError(&error) + DispatchQueue.main.async { + completion(error == nil) + } + } else { + DispatchQueue.main.async { + completion(false) + } + } + } +} + +// Check Reminders storage permission +func checkRemindersAccess(appState: AppState, completion: @escaping (Bool) -> Void) { + let eventStore = EKEventStore() + eventStore.requestAccess(to: .reminder) { granted, error in + DispatchQueue.main.async { + if error != nil { + completion(false) + } else { + completion(granted) + } + } + } +} + + + + + +struct PermissionsNotificationView: View { + let appState: AppState + @State private var hovered: Bool = false + + var body: some View { + HStack { + Text("Missing Permissions!") + .font(.callout) + .opacity(0.5) + .padding(.leading, 7) + + Spacer() + + settingsLinkButton + } + .frame(height: 30) + .padding(5) + .background(Color("mode").opacity(0.05)) + .clipShape(RoundedRectangle(cornerRadius: 6)) + .padding(.horizontal) + .padding(.bottom) + } + + @ViewBuilder + private var settingsLinkButton: some View { + if #available(macOS 14.0, *) { + SettingsLink { + labelContent + } + .buttonStyle(PlainButtonStyle()) + .padding(4) + .background(Color.red) + .clipShape(RoundedRectangle(cornerRadius: 6)) + .onHover { hover in + withAnimation { + hovered = hover + } + } + .help("Check all permissions") + } else { + Button(action: { + NSApp.sendAction(Selector(("showPreferencesWindow:")), to: nil, from: nil) + }) { + labelContent + } + .buttonStyle(PlainButtonStyle()) + .padding(4) + .background(Color.red) + .clipShape(RoundedRectangle(cornerRadius: 6)) + .onHover { hover in + withAnimation { + hovered = hover + } + } + .help("Check all permissions") + } + + } + + @ViewBuilder + private var labelContent: some View { + HStack(alignment: .center) { + Image(systemName: !hovered ? "lock" : "lock.fill") + .resizable() + .scaledToFit() + .frame(width: 14, height: 14) + .foregroundStyle(.white) + Text("Check") + .foregroundStyle(.white) + } + .padding(3) + } +} + + + + diff --git a/Pearcleaner/Logic/Styles.swift b/Pearcleaner/Logic/Styles.swift index 27c8eeb..b8fc55a 100644 --- a/Pearcleaner/Logic/Styles.swift +++ b/Pearcleaner/Logic/Styles.swift @@ -218,6 +218,101 @@ struct InfoButton: View { } + +struct InfoButtonPerms: View { + @State private var isPopoverPresented: Bool = false + let color: Color + let label: String + let warning: Bool + + init(color: Color = Color("mode"), label: String = "", warning: Bool = false) { + self.color = color + self.label = label + self.warning = warning + + } + + var body: some View { + Button(action: { + self.isPopoverPresented.toggle() + }) { + HStack(alignment: .center, spacing: 5) { + Image(systemName: !warning ? "info.circle.fill" : "exclamationmark.triangle.fill") + .resizable() + .scaledToFit() + .frame(width: 14, height: 14) + .foregroundColor(!warning ? color.opacity(0.7) : color) + .frame(height: 16) + if !label.isEmpty { + Text(label) + .font(.callout) + .foregroundColor(color.opacity(0.7)) + + } + } + } + .buttonStyle(PlainButtonStyle()) + .onHover { isHovered in + if isHovered { + NSCursor.pointingHand.push() + } else { + NSCursor.pop() + } + } + .popover(isPresented: $isPopoverPresented, arrowEdge: .bottom) { + VStack(alignment: .leading, spacing: 10) { + + HStack(alignment: .top, spacing: 20) { + Image(systemName: "externaldrive") + .resizable() + .scaledToFit() + .frame(width: 20, height: 20) + .foregroundStyle(Color("mode").opacity(0.5)) + Text("Full Disk Access permission to find and delete files in system paths") + .font(.callout) + .foregroundStyle(Color("mode").opacity(0.5)) + } + + HStack(alignment: .top, spacing: 20) { + Image(systemName: "accessibility") + .resizable() + .scaledToFit() + .frame(width: 20, height: 20) + .foregroundStyle(Color("mode").opacity(0.5)) + Text("Accessibility permission to delete files via Finder") + .font(.callout) + .foregroundStyle(Color("mode").opacity(0.5)) + } + + HStack(alignment: .top, spacing: 20) { + Image(systemName: "gearshape.2") + .resizable() + .scaledToFit() + .frame(width: 20, height: 20) + .foregroundStyle(Color("mode").opacity(0.5)) + Text("Automation permission to perform extension actions via Finder") + .font(.callout) + .foregroundStyle(Color("mode").opacity(0.5)) + } + + HStack(alignment: .top, spacing: 20) { + Image(systemName: "calendar") + .resizable() + .scaledToFit() + .frame(width: 20, height: 20) + .foregroundStyle(Color("mode").opacity(0.5)) + Text("Reminders permission to find and delete some reminder files that certain apps cache") + .font(.callout) + .foregroundStyle(Color("mode").opacity(0.5)) + } + } + .padding() + } + .padding(.horizontal, 5) + } +} + + struct UninstallButton: ButtonStyle { @State private var hovered: Bool = false var isEnabled: Bool diff --git a/Pearcleaner/Logic/Updater.swift b/Pearcleaner/Logic/Updater.swift index c591707..cfcbcfd 100644 --- a/Pearcleaner/Logic/Updater.swift +++ b/Pearcleaner/Logic/Updater.swift @@ -41,7 +41,6 @@ func loadGithubReleases(appState: AppState, manual: Bool = false) { let lastFewReleases = Array(decodedResponse.prefix(3)) // Get only the last 3 recent releases appState.releases = lastFewReleases checkForUpdate(appState: appState, manual: manual) - } return } @@ -173,7 +172,7 @@ struct UpdateNotificationView: View { var body: some View { HStack { - Text("Update Available") + Text("Update Available!") .font(.callout) .opacity(0.5) .padding(.leading, 7) @@ -199,14 +198,15 @@ struct UpdateNotificationView: View { hovered = hovering } } + .onTapGesture { + NewWin.show(appState: appState, width: 500, height: 440, newWin: .update) + } .help("Download latest update") .padding(.horizontal, 5) .padding(.vertical, 4) .background(Color("pear")) .clipShape(RoundedRectangle(cornerRadius: 6)) - .onTapGesture { - NewWin.show(appState: appState, width: 500, height: 440, newWin: .update) - } + } .frame(height: 30) .padding(5) diff --git a/Pearcleaner/Logic/Utilities.swift b/Pearcleaner/Logic/Utilities.swift index 09964e2..01018df 100644 --- a/Pearcleaner/Logic/Utilities.swift +++ b/Pearcleaner/Logic/Utilities.swift @@ -49,86 +49,6 @@ func resizeWindowAuto(windowSettings: WindowSettings, title: String) { } -// Check FDA -func checkFullDiskAccessForApp() -> Bool { - let process = Process() - process.launchPath = "/usr/bin/sqlite3" - process.arguments = ["/Library/Application Support/com.apple.TCC/TCC.db", "select client from access where auth_value and service = \"kTCCServiceSystemPolicyAllFiles\" and client = \"com.alienator88.Pearcleaner\""] - - let pipe = Pipe() - let pipeErr = Pipe() - process.standardOutput = pipe - process.standardError = pipeErr - process.launch() - - let dataErr = pipeErr.fileHandleForReading.readDataToEndOfFile() - - let output = String(data: dataErr, encoding: .utf8) - - // Check if the app is in the results - if let result = output, result.isEmpty { - return true - } else { - return false - } -} - - - - - - -// Check for access to Full Disk access -func checkAndRequestFullDiskAccess(appState: AppState, skipAlert: Bool = false) -> Bool { - @AppStorage("settings.permissions.disk") var diskP: Bool = false - @AppStorage("settings.permissions.hasLaunched") var hasLaunched: Bool = false - - let process = Process() - process.launchPath = "/usr/bin/sqlite3" - process.arguments = ["/Library/Application Support/com.apple.TCC/TCC.db", "select client from access where auth_value and service = \"kTCCServiceSystemPolicyAllFiles\" and client = \"com.alienator88.Pearcleaner\""] - - let pipe = Pipe() - let pipeErr = Pipe() - process.standardOutput = pipe - process.standardError = pipeErr - process.launch() - - let dataErr = pipeErr.fileHandleForReading.readDataToEndOfFile() - - let output = String(data: dataErr, encoding: .utf8) - - // Check if the app is in the results - if let result = output, result.isEmpty { - diskP = true - _ = checkAndRequestAccessibilityAccess(appState: appState) - return true - } else { - diskP = false - if !skipAlert { - NewWin.show(appState: appState, width: 500, height: 350, newWin: .perm) - } - - return false - } - -} - - -// Check for access to System Events -func checkAndRequestAccessibilityAccess(appState: AppState) -> Bool { - @AppStorage("settings.permissions.events") var diskE: Bool = false - - let options: NSDictionary = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: true] - let accessEnabled = AXIsProcessTrustedWithOptions(options) - if accessEnabled { - diskE = true - return accessEnabled - } else { - diskE = false - return false - - } -} // Check app directory based on user permission func checkAppDirectoryAndUserRole(completion: @escaping ((isInCorrectDirectory: Bool, isAdmin: Bool)) -> Void) { @@ -195,12 +115,22 @@ func hasWindowOpen() -> Bool { func findAndHideWindows(named titles: [String]) { for title in titles { if let window = NSApp.windows.first(where: { $0.title == title }) { -// window.orderOut(nil) window.close() } } } +func findAndSetWindowFrame(named titles: [String], windowSettings: WindowSettings) { + for title in titles { + if let window = NSApp.windows.first(where: { $0.title == title }) { +// window.isRestorable = false // Doing this via view + let frame = windowSettings.loadWindowSettings() + window.setFrame(frame, display: true) + } + } +} + + func findAndShowWindows(named titles: [String]) { for title in titles { if let window = NSApp.windows.first(where: { $0.title == title }) { @@ -217,28 +147,7 @@ func copyToClipboard(text: String) { } -// 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) { @@ -677,12 +586,12 @@ func uninstallPearcleaner(appState: AppState, locations: Locations) { // --- Create Application Support folder if it doesn't exist --- func ensureApplicationSupportFolderExists(appState: AppState) { let fileManager = FileManager.default - let supportURL = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!.appendingPathComponent("Pearcleaner") - + let supportURL = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!.appendingPathComponent("com.alienator88.Pearcleaner") + // Check to make sure Application Support/Pearcleaner folder exists if !fileManager.fileExists(atPath: supportURL.path) { try! fileManager.createDirectory(at: supportURL, withIntermediateDirectories: true) - printOS("Created Application Support/Pearcleaner folder") + printOS("Created Application Support/com.alienator88.Pearcleaner folder") } } diff --git a/Pearcleaner/PearcleanerApp.swift b/Pearcleaner/PearcleanerApp.swift index 822c005..44c5dc5 100644 --- a/Pearcleaner/PearcleanerApp.swift +++ b/Pearcleaner/PearcleanerApp.swift @@ -17,8 +17,6 @@ struct PearcleanerApp: App { @StateObject var fsm = FolderSettingsManager() @State private var windowSettings = WindowSettings() @AppStorage("settings.updater.updateTimeframe") private var updateTimeframe: Int = 1 - @AppStorage("settings.permissions.disk") private var diskP: Bool = false - @AppStorage("settings.permissions.events") private var diskE: Bool = false @AppStorage("settings.permissions.hasLaunched") private var hasLaunched: Bool = false @AppStorage("displayMode") var displayMode: DisplayMode = .system @AppStorage("settings.general.mini") private var mini: Bool = false @@ -93,9 +91,9 @@ struct PearcleanerApp: App { // Disable tabbing NSWindow.allowsAutomaticWindowTabbing = false + // Set window size on load - let frame = windowSettings.loadWindowSettings() - NSApplication.shared.windows.first?.setFrame(frame, display: true) +// findAndSetWindowFrame(named: ["Pearcleaner"], windowSettings: windowSettings) // Get Apps let sortedApps = getSortedApps(paths: fsm.folderPaths, appState: appState) @@ -114,7 +112,7 @@ struct PearcleanerApp: App { } -#if !DEBUG +#if DEBUG Task { @@ -122,15 +120,12 @@ struct PearcleanerApp: App { ensureApplicationSupportFolderExists(appState: appState) // Check for updates after app launch - if diskP { - loadGithubReleases(appState: appState) - getFeatures(appState: appState, show: $showFeature, features: $features) - } - - // Check for disk/accessibility permissions just once on initial app launch - if !hasLaunched { - _ = checkAndRequestFullDiskAccess(appState: appState) - hasLaunched = true + checkAllPermissions(appState: appState) { results in + appState.permissionResults = results + if results.allPermissionsGranted { + loadGithubReleases(appState: appState) + getFeatures(appState: appState, show: $showFeature, features: $features) + } } // Load extra conditions from GitHub @@ -171,6 +166,7 @@ struct PearcleanerApp: App { .environmentObject(ThemeSettings.shared) .toolbarBackground(.clear) .preferredColorScheme(displayMode.colorScheme) + .willRestore() } } } @@ -180,6 +176,7 @@ struct PearcleanerApp: App { class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate { var observer: NSObjectProtocol? + var windowSettings = WindowSettings() func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { let menubarEnabled = UserDefaults.standard.bool(forKey: "settings.menubar.enabled") @@ -188,6 +185,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate { func applicationDidFinishLaunching(_ notification: Notification) { let menubarEnabled = UserDefaults.standard.bool(forKey: "settings.menubar.enabled") +// UserDefaults.standard.register(defaults: ["NSQuitAlwaysKeepsWindows" : false]) + findAndSetWindowFrame(named: ["Pearcleaner"], windowSettings: windowSettings) if UserDefaults.standard.object(forKey: "themeColor") == nil { self.appearanceChanged() diff --git a/Pearcleaner/Settings/General.swift b/Pearcleaner/Settings/General.swift index 28cc273..3829cec 100644 --- a/Pearcleaner/Settings/General.swift +++ b/Pearcleaner/Settings/General.swift @@ -27,6 +27,8 @@ struct GeneralSettingsTab: View { @AppStorage("settings.general.sizeType") var sizeType: String = "Real" @State private var diskStatus: Bool = false @State private var accessStatus: Bool = false + @State private var autoStatus: Bool = false + @State private var remStatus: Bool = false @Binding var showPopover: Bool @Binding var search: String @State var selectedIndex: Int? @@ -177,58 +179,163 @@ struct GeneralSettingsTab: View { HStack() { Text("Permissions").font(.title2) - Spacer() - } - .padding(.leading) - HStack(spacing: 0) { - Image(systemName: diskStatus ? "externaldrive" : "externaldrive") - .resizable() - .scaledToFit() - .frame(width: 20, height: 20) - .padding(.trailing) - .foregroundStyle(diskStatus ? .green : .red) - .saturation(displayMode.colorScheme == .dark ? 0.5 : 1) - Text(diskStatus ? "Full Disk permission granted" : "Full Disk permission **NOT** granted") - .font(.callout) - .foregroundStyle(Color("mode").opacity(0.5)) - Spacer() + InfoButtonPerms() - Button("") { - if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles") { - NSWorkspace.shared.open(url) + Button("Refresh") { + checkAllPermissions(appState: appState) { results in + updateOnMain { + appState.permissionResults = results + } + diskStatus = results.fullDiskAccess + accessStatus = results.accessibility + autoStatus = results.automation + remStatus = results.reminders + if results.allPermissionsGranted { + updateOnMain { + appState.permissionsOkay = true + } + } } } - .buttonStyle(SimpleButtonStyle(icon: "folder", help: "View disk permissions pane")) + .buttonStyle(SimpleButtonStyle(icon: "arrow.triangle.2.circlepath", help: "Refresh permissions")) + + Spacer() } - .padding(5) .padding(.leading) - HStack(spacing: 0) { - Image(systemName: accessStatus ? "accessibility" : "accessibility") - .resizable() - .scaledToFit() - .frame(width: 20, height: 20) - .padding(.trailing) - .foregroundStyle(accessStatus ? .green : .red) - .saturation(displayMode.colorScheme == .dark ? 0.5 : 1) - Text(accessStatus ? "Accessibility permission granted" : "Accessibility permission **NOT** granted") - .font(.callout) - .foregroundStyle(Color("mode").opacity(0.5)) + HStack { + HStack(spacing: 0) { + Image(systemName: "externaldrive") + .resizable() + .scaledToFit() + .frame(width: 20, height: 20) + .padding(.trailing) + .foregroundStyle(diskStatus ? .green : .red) + .saturation(displayMode.colorScheme == .dark ? 0.5 : 1) + Text("Full Disk") + .font(.callout) + .foregroundStyle(Color("mode").opacity(0.5)) + .frame(width: 100) + + Spacer() + + Button("") { + if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles") { + NSWorkspace.shared.open(url) + } + } + .buttonStyle(SimpleButtonStyle(icon: "arrow.right.circle.fill", help: "View disk permissions pane", size: 14)) + + Spacer() + + } + .padding(5) + .padding(.leading) + .frame(width: 200) + Spacer() - Button("") { - if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility") { - NSWorkspace.shared.open(url) + + HStack(spacing: 0) { + Image(systemName: "accessibility") + .resizable() + .scaledToFit() + .frame(width: 20, height: 20) + .padding(.trailing) + .foregroundStyle(accessStatus ? .green : .red) + .saturation(displayMode.colorScheme == .dark ? 0.5 : 1) + Text("Accessibility") + .font(.callout) + .foregroundStyle(Color("mode").opacity(0.5)) + .frame(width: 100) + + Spacer() + + Button("") { + if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility") { + NSWorkspace.shared.open(url) + } } + .buttonStyle(SimpleButtonStyle(icon: "arrow.right.circle.fill", help: "View accessibility permissions pane", size: 14)) + + Spacer() + } - .buttonStyle(SimpleButtonStyle(icon: "folder", help: "View accessibility permissions pane")) + .padding(5) + .padding(.leading) + .frame(width: 200) } - .padding(5) - .padding(.leading) + + + HStack { + HStack(spacing: 0) { + Image(systemName: "gearshape.2") + .resizable() + .scaledToFit() + .frame(width: 20, height: 20) + .padding(.trailing) + .foregroundStyle(autoStatus ? .green : .red) + .saturation(displayMode.colorScheme == .dark ? 0.5 : 1) + Text("Automation") + .font(.callout) + .foregroundStyle(Color("mode").opacity(0.5)) + .frame(width: 100) + + Spacer() + + Button("") { + if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Automation") { + NSWorkspace.shared.open(url) + } + } + .buttonStyle(SimpleButtonStyle(icon: "arrow.right.circle.fill", help: "View automation permissions pane", size: 14)) + + Spacer() + + } + .padding(5) + .padding(.leading) + .frame(width: 200) + + + Spacer() + + HStack(spacing: 0) { + Image(systemName: "calendar") + .resizable() + .scaledToFit() + .frame(width: 20, height: 20) + .padding(.trailing) + .foregroundStyle(remStatus ? .green : .red) + .saturation(displayMode.colorScheme == .dark ? 0.5 : 1) + Text("Reminders") + .font(.callout) + .foregroundStyle(Color("mode").opacity(0.5)) + .frame(width: 100) + + Spacer() + + Button("") { + if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Reminders") { + NSWorkspace.shared.open(url) + } + } + .buttonStyle(SimpleButtonStyle(icon: "arrow.right.circle.fill", help: "View reminders permissions pane", size: 14)) + + Spacer() + + } + .padding(5) + .padding(.leading) + .frame(width: 200) + + } + + // === Sentinel ============================================================================================= @@ -331,14 +438,26 @@ struct GeneralSettingsTab: View { Spacer() } .onAppear { - diskStatus = checkAndRequestFullDiskAccess(appState: appState, skipAlert: true) - accessStatus = checkAndRequestAccessibilityAccess(appState: appState) + checkAllPermissions(appState: appState) { results in + updateOnMain { + appState.permissionResults = results + } + diskStatus = results.fullDiskAccess + accessStatus = results.accessibility + autoStatus = results.automation + remStatus = results.reminders + if results.allPermissionsGranted { + updateOnMain { + appState.permissionsOkay = true + } + } + } appState.updateExtensionStatus() } } .padding(20) - .frame(width: 500, height: 620) + .frame(width: 500, height: 650) } diff --git a/Pearcleaner/Settings/SettingsWindow.swift b/Pearcleaner/Settings/SettingsWindow.swift index add41a2..8d7080f 100644 --- a/Pearcleaner/Settings/SettingsWindow.swift +++ b/Pearcleaner/Settings/SettingsWindow.swift @@ -51,7 +51,6 @@ struct SettingsView: View { .tag(CurrentTabView.about) } .background(backgroundView(themeSettings: themeSettings, glass: glass)) -// .background(glass ? GlassEffect(material: .sidebar, blendingMode: .behindWindow).edgesIgnoringSafeArea(.all) : nil) } diff --git a/Pearcleaner/Views/AppSearchView.swift b/Pearcleaner/Views/AppSearchView.swift index 8fb055d..55ca11e 100644 --- a/Pearcleaner/Views/AppSearchView.swift +++ b/Pearcleaner/Views/AppSearchView.swift @@ -30,6 +30,8 @@ struct AppSearchView: View { if appState.updateAvailable { UpdateNotificationView(appState: appState) + } else if !appState.permissionsOkay { + PermissionsNotificationView(appState: appState) } AppsListView(search: $search, showPopover: $showPopover, filteredApps: filteredApps) diff --git a/Pearcleaner/Views/FilesView.swift b/Pearcleaner/Views/FilesView.swift index e2d1d4a..8ccebfa 100644 --- a/Pearcleaner/Views/FilesView.swift +++ b/Pearcleaner/Views/FilesView.swift @@ -263,29 +263,32 @@ struct FilesView: View { Button("\(sizeType == "Logical" ? totalSelectedSize.logical : sizeType == "Finder" ? totalSelectedSize.finder : totalSelectedSize.real)") { Task { - if appState.selectedItems.count == appState.appInfo.fileSize.keys.count { - updateOnMain { - search = "" - if mini || menubarEnabled { - appState.currentView = .apps - showPopover = false - } else { - appState.currentView = .empty - } - } - } + let selectedItemsArray = Array(appState.selectedItems) killApp(appId: appState.appInfo.bundleIdentifier) { - moveFilesToTrash(at: selectedItemsArray) { - withAnimation { - showPopover = false -// updateOnMain { -// appState.currentView = mini ? .apps : .empty -// } - if sentinel { - launchctl(load: true) + + moveFilesToTrash(appState: appState, at: selectedItemsArray) { success in + if sentinel { + launchctl(load: true) + } + + guard success else { + return + } + + if appState.selectedItems.count == appState.appInfo.fileSize.keys.count { + updateOnMain { + search = "" + withAnimation { + if mini || menubarEnabled { + appState.currentView = .apps + showPopover = false + } else { + appState.currentView = .empty + } + } } } diff --git a/Pearcleaner/Views/ZombieView.swift b/Pearcleaner/Views/ZombieView.swift index e8cf2eb..45a1801 100644 --- a/Pearcleaner/Views/ZombieView.swift +++ b/Pearcleaner/Views/ZombieView.swift @@ -269,27 +269,34 @@ struct ZombieView: View { Button("\(sizeType == "Logical" ? totalLogicalSizeUninstallBtn : sizeType == "Finder" ? totalFinderSizeUninstallBtn : totalRealSizeUninstallBtn)") { Task { - if selectedZombieItemsLocal.count == appState.zombieFile.fileSize.keys.count { - updateOnMain { - appState.zombieFile = .empty - search = "" - searchZ = "" - if mini || menubarEnabled { - appState.currentView = .apps - showPopover = false - } else { - appState.currentView = .empty - } - } - } + let selectedItemsArray = Array(selectedZombieItemsLocal) - moveFilesToTrash(at: selectedItemsArray) { - withAnimation { - showPopover = false + moveFilesToTrash(appState: appState, at: selectedItemsArray) { success in + + guard success else { + return } + + if selectedZombieItemsLocal.count == appState.zombieFile.fileSize.keys.count { + updateOnMain { + appState.zombieFile = .empty + search = "" + searchZ = "" + withAnimation { + if mini || menubarEnabled { + appState.currentView = .apps + showPopover = false + } else { + appState.currentView = .empty + } + } + + } + } + updateOnMain { // Remove items from the list appState.zombieFile.fileSize = appState.zombieFile.fileSize.filter { !selectedZombieItemsLocal.contains($0.key) } diff --git a/Pearcleaner/Windows/PermView.swift b/Pearcleaner/Windows/PermView.swift index 4de7343..b66e70c 100644 --- a/Pearcleaner/Windows/PermView.swift +++ b/Pearcleaner/Windows/PermView.swift @@ -10,7 +10,8 @@ import SwiftUI struct PermView: View { @EnvironmentObject var appState: AppState - + @AppStorage("settings.general.selectedTab") private var selectedTab: CurrentTabView = .general + var body: some View { VStack { HStack { @@ -55,6 +56,28 @@ struct PermView: View { .foregroundStyle(Color("mode").opacity(0.5)) } + HStack(alignment: .top, spacing: 20) { + Image(systemName: "gearshape.2") + .resizable() + .scaledToFit() + .frame(width: 20, height: 20) + .foregroundStyle(Color("mode").opacity(0.5)) + Text("Automation permission to perform extension actions via Finder") + .font(.callout) + .foregroundStyle(Color("mode").opacity(0.5)) + } + + HStack(alignment: .top, spacing: 20) { + Image(systemName: "calendar") + .resizable() + .scaledToFit() + .frame(width: 20, height: 20) + .foregroundStyle(Color("mode").opacity(0.5)) + Text("Reminders permission to find and delete some reminder files that certain apps cache") + .font(.callout) + .foregroundStyle(Color("mode").opacity(0.5)) + } + HStack(alignment: .top, spacing: 20) { Image(systemName: "info.circle") .resizable() @@ -72,38 +95,74 @@ struct PermView: View { HStack { - Button(action: { - relaunchApp(afterDelay: 1) - }) { - Text("Restart") - } - .buttonStyle(SimpleButtonBrightStyle(icon: "arrow.uturn.left.circle", label: "Restart", help: "Restart", color: .red)) - .padding() - - Button(action: { - if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles") { - NSWorkspace.shared.open(url) + if #available(macOS 14.0, *) { + SettingsLink {} + .buttonStyle(SimpleButtonBrightStyle(icon: "gear", label: "Settings", help: "Check permissions in Settings", color: .accentColor)) + } else { + Button("Settings") { + NSApp.sendAction(Selector(("showPreferencesWindow:")), to: NSApp.delegate, from: nil) } - }) { - Text("Full Disk Access") + .buttonStyle(SimpleButtonBrightStyle(icon: "gear", label: "Settings", help: "Check permissions in Settings", color: .red)) } - .buttonStyle(SimpleButtonBrightStyle(icon: "externaldrive", label: "Full Disk", help: "Full Disk", color: .accentColor)) - .padding() - + +// Button(action: { +// relaunchApp(afterDelay: 1) +// }) { +// Text("Restart") +// } +// .buttonStyle(SimpleButtonBrightStyle(icon: "arrow.uturn.left.circle", label: "Restart", help: "Restart", color: .red)) +// .padding() + Button(action: { - if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility") { - NSWorkspace.shared.open(url) - } + NewWin.close() }) { - Text("Accessibility") + Text("Close") } - .buttonStyle(SimpleButtonBrightStyle(icon: "accessibility", label: "Accessibility", help: "Accessibility", color: .accentColor)) + .buttonStyle(SimpleButtonBrightStyle(icon: "checkmark.circle", label: "Ok", help: "Ok", color: .blue)) .padding() - + +// Button(action: { +// if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles") { +// NSWorkspace.shared.open(url) +// } +// }) { +// Text("Full Disk Access") +// } +// .buttonStyle(SimpleButtonBrightStyle(icon: "externaldrive", label: "Full Disk", help: "Full Disk", color: .accentColor)) +// .padding() +// +// Button(action: { +// if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility") { +// NSWorkspace.shared.open(url) +// } +// }) { +// Text("Accessibility") +// } +// .buttonStyle(SimpleButtonBrightStyle(icon: "accessibility", label: "Accessibility", help: "Accessibility", color: .accentColor)) +// .padding() +// +// Button("") { +// if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Automation") { +// NSWorkspace.shared.open(url) +// } +// } +// .buttonStyle(SimpleButtonBrightStyle(icon: "gearshape.2", label: "Automation", help: "Automation", color: .accentColor)) +// .padding() +// +// Button("") { +// if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Reminders") { +// NSWorkspace.shared.open(url) +// } +// } +// .buttonStyle(SimpleButtonBrightStyle(icon: "calendar", label: "Reminders", help: "Reminders", color: .accentColor)) + } - + .onAppear { + selectedTab = .general + } + } .padding(EdgeInsets(top: -25, leading: 0, bottom: 25, trailing: 0)) diff --git a/Pearcleaner/Windows/WindowSettings.swift b/Pearcleaner/Windows/WindowSettings.swift index dcec40f..7302273 100644 --- a/Pearcleaner/Windows/WindowSettings.swift +++ b/Pearcleaner/Windows/WindowSettings.swift @@ -35,13 +35,12 @@ class WindowSettings { var x = CGFloat(UserDefaults.standard.float(forKey: windowXKey)) var y = CGFloat(UserDefaults.standard.float(forKey: windowYKey)) - if UserDefaults.standard.object(forKey: windowXKey) == nil || UserDefaults.standard.object(forKey: windowYKey) == nil { - // Set window to center of the screen if not set - let screenSize = NSScreen.main?.frame.size ?? NSSize(width: 800, height: 600) // Fallback screen size - x = (screenSize.width - width) / 2 - y = (screenSize.height - height) / 2 - } + if UserDefaults.standard.object(forKey: windowXKey) == nil || UserDefaults.standard.object(forKey: windowYKey) == nil { + let screenFrame = NSScreen.main?.visibleFrame ?? NSRect(x: 0, y: 0, width: 800, height: 600) + x = (screenFrame.width - width) / 2 + screenFrame.origin.x + y = (screenFrame.height - height) / 2 + screenFrame.origin.y + } return NSRect(x: x, y: y, width: width, height: height) } @@ -64,3 +63,23 @@ class WindowSettings { newWindow.makeKeyAndOrderFront(nil) } } + + + +struct WillRestore: ViewModifier { + let restore: Bool + + func body(content: Content) -> some View { + content + .onReceive(NotificationCenter.default.publisher(for: NSWindow.didBecomeKeyNotification), perform: { output in + let window = output.object as! NSWindow + window.isRestorable = false + }) + } +} + +extension View { + func willRestore(_ restoreState: Bool = true) -> some View { + modifier(WillRestore(restore: restoreState)) + } +}