mirror of
https://github.com/exituser/Pearcleaner.git
synced 2026-09-17 09:38:51 +00:00
WIP
This commit is contained in:
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user