mirror of
https://github.com/exituser/Pearcleaner.git
synced 2026-09-17 06:09:01 +00:00
WIP
This commit is contained in:
@@ -16,6 +16,7 @@
|
|||||||
C72893122AFD51EA00C8C1CD /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = C72893112AFD51EA00C8C1CD /* main.swift */; };
|
C72893122AFD51EA00C8C1CD /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = C72893112AFD51EA00C8C1CD /* main.swift */; };
|
||||||
C74412C72BBF249000DDFCA8 /* AppPathsFetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C74412C62BBF249000DDFCA8 /* AppPathsFetch.swift */; };
|
C74412C72BBF249000DDFCA8 /* AppPathsFetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C74412C62BBF249000DDFCA8 /* AppPathsFetch.swift */; };
|
||||||
C74FDC632BCD833D00B8960F /* Conditions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C74FDC622BCD833D00B8960F /* Conditions.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, ); }; };
|
C7575D392B0182DE006A600A /* PearcleanerSentinel in CopyFiles */ = {isa = PBXBuildFile; fileRef = C728930F2AFD51EA00C8C1CD /* PearcleanerSentinel */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
|
||||||
C76D08482AF83C3F00D07867 /* Update.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76D08472AF83C3F00D07867 /* Update.swift */; };
|
C76D08482AF83C3F00D07867 /* Update.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76D08472AF83C3F00D07867 /* Update.swift */; };
|
||||||
C76D084A2AF83C6E00D07867 /* General.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76D08492AF83C6E00D07867 /* General.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 = "<group>"; };
|
C72893112AFD51EA00C8C1CD /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
|
||||||
C74412C62BBF249000DDFCA8 /* AppPathsFetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPathsFetch.swift; sourceTree = "<group>"; };
|
C74412C62BBF249000DDFCA8 /* AppPathsFetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPathsFetch.swift; sourceTree = "<group>"; };
|
||||||
C74FDC622BCD833D00B8960F /* Conditions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Conditions.swift; sourceTree = "<group>"; };
|
C74FDC622BCD833D00B8960F /* Conditions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Conditions.swift; sourceTree = "<group>"; };
|
||||||
|
C75347632BE599FA00AAFCC3 /* PermissionChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionChecker.swift; sourceTree = "<group>"; };
|
||||||
C76D08472AF83C3F00D07867 /* Update.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Update.swift; sourceTree = "<group>"; };
|
C76D08472AF83C3F00D07867 /* Update.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Update.swift; sourceTree = "<group>"; };
|
||||||
C76D08492AF83C6E00D07867 /* General.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = General.swift; sourceTree = "<group>"; };
|
C76D08492AF83C6E00D07867 /* General.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = General.swift; sourceTree = "<group>"; };
|
||||||
C76D08502AF850F300D07867 /* About.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = About.swift; sourceTree = "<group>"; };
|
C76D08502AF850F300D07867 /* About.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = About.swift; sourceTree = "<group>"; };
|
||||||
@@ -205,6 +207,7 @@
|
|||||||
C7ED86B22BA8ED8800F13DE4 /* Trash.swift */,
|
C7ED86B22BA8ED8800F13DE4 /* Trash.swift */,
|
||||||
C79D051F2BC99EB40083F976 /* ThemeManager.swift */,
|
C79D051F2BC99EB40083F976 /* ThemeManager.swift */,
|
||||||
C7DE17642BE54F4900197715 /* Updater.swift */,
|
C7DE17642BE54F4900197715 /* Updater.swift */,
|
||||||
|
C75347632BE599FA00AAFCC3 /* PermissionChecker.swift */,
|
||||||
);
|
);
|
||||||
path = Logic;
|
path = Logic;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -461,6 +464,7 @@
|
|||||||
C7FB173B2B96321300B96F9A /* AppsListView.swift in Sources */,
|
C7FB173B2B96321300B96F9A /* AppsListView.swift in Sources */,
|
||||||
C7045A342B068B2900376976 /* UpdateView.swift in Sources */,
|
C7045A342B068B2900376976 /* UpdateView.swift in Sources */,
|
||||||
C77B90162AF19377009CC655 /* AppState.swift in Sources */,
|
C77B90162AF19377009CC655 /* AppState.swift in Sources */,
|
||||||
|
C75347642BE599FA00AAFCC3 /* PermissionChecker.swift in Sources */,
|
||||||
C7DE17652BE54F4900197715 /* Updater.swift in Sources */,
|
C7DE17652BE54F4900197715 /* Updater.swift in Sources */,
|
||||||
C7DD49EE2BAB7F6000CCBA16 /* ReversePathsFetch.swift in Sources */,
|
C7DD49EE2BAB7F6000CCBA16 /* ReversePathsFetch.swift in Sources */,
|
||||||
C7045A322B068AD700376976 /* NewWin.swift in Sources */,
|
C7045A322B068AD700376976 /* NewWin.swift in Sources */,
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ class AppState: ObservableObject
|
|||||||
@Published var showProgress: Bool = false
|
@Published var showProgress: Bool = false
|
||||||
@Published var finderExtensionEnabled: Bool = false
|
@Published var finderExtensionEnabled: Bool = false
|
||||||
@Published var updateAvailable: Bool = false
|
@Published var updateAvailable: Bool = false
|
||||||
|
@Published var permissionsOkay: Bool = true
|
||||||
|
@Published var permissionResults: PermissionsCheckResults?
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
init() {
|
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
|
// 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
|
@AppStorage("settings.sentinel.enable") var sentinel: Bool = false
|
||||||
if sentinel {
|
if sentinel {
|
||||||
launchctl(load: false)
|
launchctl(load: false)
|
||||||
@@ -214,13 +214,26 @@ func moveFilesToTrash(at fileURLs: [URL], completion: @escaping () -> Void = {})
|
|||||||
if let scriptObject = NSAppleScript(source: scriptSource) {
|
if let scriptObject = NSAppleScript(source: scriptSource) {
|
||||||
let output: NSAppleEventDescriptor = scriptObject.executeAndReturnError(&error)
|
let output: NSAppleEventDescriptor = scriptObject.executeAndReturnError(&error)
|
||||||
if let error = error {
|
if let error = error {
|
||||||
printOS("Error: \(error)")
|
checkAllPermissions(appState: appState) { results in
|
||||||
} else if let outputString = output.stringValue {
|
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)
|
printOS(outputString)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
DispatchQueue.main.async {
|
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) {
|
if let scriptObject = NSAppleScript(source: scriptSource) {
|
||||||
let output: NSAppleEventDescriptor = scriptObject.executeAndReturnError(&error)
|
let output: NSAppleEventDescriptor = scriptObject.executeAndReturnError(&error)
|
||||||
if let error = error {
|
if let error = error {
|
||||||
if let value = error["NSAppleScriptErrorNumber"] {
|
checkAllPermissions(appState: appState) { results in
|
||||||
if value as! Int == 1002 {
|
appState.permissionResults = results
|
||||||
_ = checkAndRequestAccessibilityAccess(appState: appState)
|
if !results.allPermissionsGranted {
|
||||||
|
updateOnMain {
|
||||||
|
appState.permissionsOkay = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
printOS("Error: \(error)")
|
printOS("Undo Trash Error: \(error)")
|
||||||
} else if let outputString = output.stringValue {
|
} else if let outputString = output.stringValue {
|
||||||
printOS(outputString)
|
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 {
|
struct UninstallButton: ButtonStyle {
|
||||||
@State private var hovered: Bool = false
|
@State private var hovered: Bool = false
|
||||||
var isEnabled: Bool
|
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
|
let lastFewReleases = Array(decodedResponse.prefix(3)) // Get only the last 3 recent releases
|
||||||
appState.releases = lastFewReleases
|
appState.releases = lastFewReleases
|
||||||
checkForUpdate(appState: appState, manual: manual)
|
checkForUpdate(appState: appState, manual: manual)
|
||||||
|
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -173,7 +172,7 @@ struct UpdateNotificationView: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
HStack {
|
HStack {
|
||||||
|
|
||||||
Text("Update Available")
|
Text("Update Available!")
|
||||||
.font(.callout)
|
.font(.callout)
|
||||||
.opacity(0.5)
|
.opacity(0.5)
|
||||||
.padding(.leading, 7)
|
.padding(.leading, 7)
|
||||||
@@ -199,14 +198,15 @@ struct UpdateNotificationView: View {
|
|||||||
hovered = hovering
|
hovered = hovering
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onTapGesture {
|
||||||
|
NewWin.show(appState: appState, width: 500, height: 440, newWin: .update)
|
||||||
|
}
|
||||||
.help("Download latest update")
|
.help("Download latest update")
|
||||||
.padding(.horizontal, 5)
|
.padding(.horizontal, 5)
|
||||||
.padding(.vertical, 4)
|
.padding(.vertical, 4)
|
||||||
.background(Color("pear"))
|
.background(Color("pear"))
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 6))
|
.clipShape(RoundedRectangle(cornerRadius: 6))
|
||||||
.onTapGesture {
|
|
||||||
NewWin.show(appState: appState, width: 500, height: 440, newWin: .update)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.frame(height: 30)
|
.frame(height: 30)
|
||||||
.padding(5)
|
.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
|
// Check app directory based on user permission
|
||||||
func checkAppDirectoryAndUserRole(completion: @escaping ((isInCorrectDirectory: Bool, isAdmin: Bool)) -> Void) {
|
func checkAppDirectoryAndUserRole(completion: @escaping ((isInCorrectDirectory: Bool, isAdmin: Bool)) -> Void) {
|
||||||
@@ -195,12 +115,22 @@ func hasWindowOpen() -> Bool {
|
|||||||
func findAndHideWindows(named titles: [String]) {
|
func findAndHideWindows(named titles: [String]) {
|
||||||
for title in titles {
|
for title in titles {
|
||||||
if let window = NSApp.windows.first(where: { $0.title == title }) {
|
if let window = NSApp.windows.first(where: { $0.title == title }) {
|
||||||
// window.orderOut(nil)
|
|
||||||
window.close()
|
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]) {
|
func findAndShowWindows(named titles: [String]) {
|
||||||
for title in titles {
|
for title in titles {
|
||||||
if let window = NSApp.windows.first(where: { $0.title == title }) {
|
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
|
// Brew cleanup
|
||||||
func caskCleanup(app: String) {
|
func caskCleanup(app: String) {
|
||||||
@@ -677,12 +586,12 @@ func uninstallPearcleaner(appState: AppState, locations: Locations) {
|
|||||||
// --- Create Application Support folder if it doesn't exist ---
|
// --- Create Application Support folder if it doesn't exist ---
|
||||||
func ensureApplicationSupportFolderExists(appState: AppState) {
|
func ensureApplicationSupportFolderExists(appState: AppState) {
|
||||||
let fileManager = FileManager.default
|
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
|
// Check to make sure Application Support/Pearcleaner folder exists
|
||||||
if !fileManager.fileExists(atPath: supportURL.path) {
|
if !fileManager.fileExists(atPath: supportURL.path) {
|
||||||
try! fileManager.createDirectory(at: supportURL, withIntermediateDirectories: true)
|
try! fileManager.createDirectory(at: supportURL, withIntermediateDirectories: true)
|
||||||
printOS("Created Application Support/Pearcleaner folder")
|
printOS("Created Application Support/com.alienator88.Pearcleaner folder")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,8 +17,6 @@ struct PearcleanerApp: App {
|
|||||||
@StateObject var fsm = FolderSettingsManager()
|
@StateObject var fsm = FolderSettingsManager()
|
||||||
@State private var windowSettings = WindowSettings()
|
@State private var windowSettings = WindowSettings()
|
||||||
@AppStorage("settings.updater.updateTimeframe") private var updateTimeframe: Int = 1
|
@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("settings.permissions.hasLaunched") private var hasLaunched: Bool = false
|
||||||
@AppStorage("displayMode") var displayMode: DisplayMode = .system
|
@AppStorage("displayMode") var displayMode: DisplayMode = .system
|
||||||
@AppStorage("settings.general.mini") private var mini: Bool = false
|
@AppStorage("settings.general.mini") private var mini: Bool = false
|
||||||
@@ -93,9 +91,9 @@ struct PearcleanerApp: App {
|
|||||||
// Disable tabbing
|
// Disable tabbing
|
||||||
NSWindow.allowsAutomaticWindowTabbing = false
|
NSWindow.allowsAutomaticWindowTabbing = false
|
||||||
|
|
||||||
|
|
||||||
// Set window size on load
|
// Set window size on load
|
||||||
let frame = windowSettings.loadWindowSettings()
|
// findAndSetWindowFrame(named: ["Pearcleaner"], windowSettings: windowSettings)
|
||||||
NSApplication.shared.windows.first?.setFrame(frame, display: true)
|
|
||||||
|
|
||||||
// Get Apps
|
// Get Apps
|
||||||
let sortedApps = getSortedApps(paths: fsm.folderPaths, appState: appState)
|
let sortedApps = getSortedApps(paths: fsm.folderPaths, appState: appState)
|
||||||
@@ -114,7 +112,7 @@ struct PearcleanerApp: App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#if !DEBUG
|
#if DEBUG
|
||||||
Task {
|
Task {
|
||||||
|
|
||||||
|
|
||||||
@@ -122,15 +120,12 @@ struct PearcleanerApp: App {
|
|||||||
ensureApplicationSupportFolderExists(appState: appState)
|
ensureApplicationSupportFolderExists(appState: appState)
|
||||||
|
|
||||||
// Check for updates after app launch
|
// Check for updates after app launch
|
||||||
if diskP {
|
checkAllPermissions(appState: appState) { results in
|
||||||
loadGithubReleases(appState: appState)
|
appState.permissionResults = results
|
||||||
getFeatures(appState: appState, show: $showFeature, features: $features)
|
if results.allPermissionsGranted {
|
||||||
}
|
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load extra conditions from GitHub
|
// Load extra conditions from GitHub
|
||||||
@@ -171,6 +166,7 @@ struct PearcleanerApp: App {
|
|||||||
.environmentObject(ThemeSettings.shared)
|
.environmentObject(ThemeSettings.shared)
|
||||||
.toolbarBackground(.clear)
|
.toolbarBackground(.clear)
|
||||||
.preferredColorScheme(displayMode.colorScheme)
|
.preferredColorScheme(displayMode.colorScheme)
|
||||||
|
.willRestore()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -180,6 +176,7 @@ struct PearcleanerApp: App {
|
|||||||
|
|
||||||
class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
|
class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
|
||||||
var observer: NSObjectProtocol?
|
var observer: NSObjectProtocol?
|
||||||
|
var windowSettings = WindowSettings()
|
||||||
|
|
||||||
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||||
let menubarEnabled = UserDefaults.standard.bool(forKey: "settings.menubar.enabled")
|
let menubarEnabled = UserDefaults.standard.bool(forKey: "settings.menubar.enabled")
|
||||||
@@ -188,6 +185,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
|
|||||||
|
|
||||||
func applicationDidFinishLaunching(_ notification: Notification) {
|
func applicationDidFinishLaunching(_ notification: Notification) {
|
||||||
let menubarEnabled = UserDefaults.standard.bool(forKey: "settings.menubar.enabled")
|
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 {
|
if UserDefaults.standard.object(forKey: "themeColor") == nil {
|
||||||
self.appearanceChanged()
|
self.appearanceChanged()
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ struct GeneralSettingsTab: View {
|
|||||||
@AppStorage("settings.general.sizeType") var sizeType: String = "Real"
|
@AppStorage("settings.general.sizeType") var sizeType: String = "Real"
|
||||||
@State private var diskStatus: Bool = false
|
@State private var diskStatus: Bool = false
|
||||||
@State private var accessStatus: 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 showPopover: Bool
|
||||||
@Binding var search: String
|
@Binding var search: String
|
||||||
@State var selectedIndex: Int?
|
@State var selectedIndex: Int?
|
||||||
@@ -177,58 +179,163 @@ struct GeneralSettingsTab: View {
|
|||||||
|
|
||||||
HStack() {
|
HStack() {
|
||||||
Text("Permissions").font(.title2)
|
Text("Permissions").font(.title2)
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
.padding(.leading)
|
|
||||||
|
|
||||||
HStack(spacing: 0) {
|
InfoButtonPerms()
|
||||||
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()
|
|
||||||
|
|
||||||
Button("") {
|
Button("Refresh") {
|
||||||
if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles") {
|
checkAllPermissions(appState: appState) { results in
|
||||||
NSWorkspace.shared.open(url)
|
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)
|
.padding(.leading)
|
||||||
|
|
||||||
|
|
||||||
HStack(spacing: 0) {
|
HStack {
|
||||||
Image(systemName: accessStatus ? "accessibility" : "accessibility")
|
HStack(spacing: 0) {
|
||||||
.resizable()
|
Image(systemName: "externaldrive")
|
||||||
.scaledToFit()
|
.resizable()
|
||||||
.frame(width: 20, height: 20)
|
.scaledToFit()
|
||||||
.padding(.trailing)
|
.frame(width: 20, height: 20)
|
||||||
.foregroundStyle(accessStatus ? .green : .red)
|
.padding(.trailing)
|
||||||
.saturation(displayMode.colorScheme == .dark ? 0.5 : 1)
|
.foregroundStyle(diskStatus ? .green : .red)
|
||||||
Text(accessStatus ? "Accessibility permission granted" : "Accessibility permission **NOT** granted")
|
.saturation(displayMode.colorScheme == .dark ? 0.5 : 1)
|
||||||
.font(.callout)
|
Text("Full Disk")
|
||||||
.foregroundStyle(Color("mode").opacity(0.5))
|
.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()
|
Spacer()
|
||||||
|
|
||||||
Button("") {
|
|
||||||
if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility") {
|
HStack(spacing: 0) {
|
||||||
NSWorkspace.shared.open(url)
|
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 =============================================================================================
|
// === Sentinel =============================================================================================
|
||||||
@@ -331,14 +438,26 @@ struct GeneralSettingsTab: View {
|
|||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
diskStatus = checkAndRequestFullDiskAccess(appState: appState, skipAlert: true)
|
checkAllPermissions(appState: appState) { results in
|
||||||
accessStatus = checkAndRequestAccessibilityAccess(appState: appState)
|
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()
|
appState.updateExtensionStatus()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
.padding(20)
|
.padding(20)
|
||||||
.frame(width: 500, height: 620)
|
.frame(width: 500, height: 650)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,6 @@ struct SettingsView: View {
|
|||||||
.tag(CurrentTabView.about)
|
.tag(CurrentTabView.about)
|
||||||
}
|
}
|
||||||
.background(backgroundView(themeSettings: themeSettings, glass: glass))
|
.background(backgroundView(themeSettings: themeSettings, glass: glass))
|
||||||
// .background(glass ? GlassEffect(material: .sidebar, blendingMode: .behindWindow).edgesIgnoringSafeArea(.all) : nil)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ struct AppSearchView: View {
|
|||||||
|
|
||||||
if appState.updateAvailable {
|
if appState.updateAvailable {
|
||||||
UpdateNotificationView(appState: appState)
|
UpdateNotificationView(appState: appState)
|
||||||
|
} else if !appState.permissionsOkay {
|
||||||
|
PermissionsNotificationView(appState: appState)
|
||||||
}
|
}
|
||||||
|
|
||||||
AppsListView(search: $search, showPopover: $showPopover, filteredApps: filteredApps)
|
AppsListView(search: $search, showPopover: $showPopover, filteredApps: filteredApps)
|
||||||
|
|||||||
@@ -263,29 +263,32 @@ struct FilesView: View {
|
|||||||
|
|
||||||
Button("\(sizeType == "Logical" ? totalSelectedSize.logical : sizeType == "Finder" ? totalSelectedSize.finder : totalSelectedSize.real)") {
|
Button("\(sizeType == "Logical" ? totalSelectedSize.logical : sizeType == "Finder" ? totalSelectedSize.finder : totalSelectedSize.real)") {
|
||||||
Task {
|
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)
|
let selectedItemsArray = Array(appState.selectedItems)
|
||||||
|
|
||||||
killApp(appId: appState.appInfo.bundleIdentifier) {
|
killApp(appId: appState.appInfo.bundleIdentifier) {
|
||||||
moveFilesToTrash(at: selectedItemsArray) {
|
|
||||||
withAnimation {
|
moveFilesToTrash(appState: appState, at: selectedItemsArray) { success in
|
||||||
showPopover = false
|
if sentinel {
|
||||||
// updateOnMain {
|
launchctl(load: true)
|
||||||
// appState.currentView = mini ? .apps : .empty
|
}
|
||||||
// }
|
|
||||||
if sentinel {
|
guard success else {
|
||||||
launchctl(load: true)
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -269,27 +269,34 @@ struct ZombieView: View {
|
|||||||
|
|
||||||
Button("\(sizeType == "Logical" ? totalLogicalSizeUninstallBtn : sizeType == "Finder" ? totalFinderSizeUninstallBtn : totalRealSizeUninstallBtn)") {
|
Button("\(sizeType == "Logical" ? totalLogicalSizeUninstallBtn : sizeType == "Finder" ? totalFinderSizeUninstallBtn : totalRealSizeUninstallBtn)") {
|
||||||
Task {
|
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)
|
let selectedItemsArray = Array(selectedZombieItemsLocal)
|
||||||
|
|
||||||
moveFilesToTrash(at: selectedItemsArray) {
|
moveFilesToTrash(appState: appState, at: selectedItemsArray) { success in
|
||||||
withAnimation {
|
|
||||||
showPopover = false
|
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 {
|
updateOnMain {
|
||||||
// Remove items from the list
|
// Remove items from the list
|
||||||
appState.zombieFile.fileSize = appState.zombieFile.fileSize.filter { !selectedZombieItemsLocal.contains($0.key) }
|
appState.zombieFile.fileSize = appState.zombieFile.fileSize.filter { !selectedZombieItemsLocal.contains($0.key) }
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import SwiftUI
|
|||||||
|
|
||||||
struct PermView: View {
|
struct PermView: View {
|
||||||
@EnvironmentObject var appState: AppState
|
@EnvironmentObject var appState: AppState
|
||||||
|
@AppStorage("settings.general.selectedTab") private var selectedTab: CurrentTabView = .general
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
HStack {
|
HStack {
|
||||||
@@ -55,6 +56,28 @@ struct PermView: View {
|
|||||||
.foregroundStyle(Color("mode").opacity(0.5))
|
.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) {
|
HStack(alignment: .top, spacing: 20) {
|
||||||
Image(systemName: "info.circle")
|
Image(systemName: "info.circle")
|
||||||
.resizable()
|
.resizable()
|
||||||
@@ -72,38 +95,74 @@ struct PermView: View {
|
|||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
|
|
||||||
Button(action: {
|
if #available(macOS 14.0, *) {
|
||||||
relaunchApp(afterDelay: 1)
|
SettingsLink {}
|
||||||
}) {
|
.buttonStyle(SimpleButtonBrightStyle(icon: "gear", label: "Settings", help: "Check permissions in Settings", color: .accentColor))
|
||||||
Text("Restart")
|
} else {
|
||||||
}
|
Button("Settings") {
|
||||||
.buttonStyle(SimpleButtonBrightStyle(icon: "arrow.uturn.left.circle", label: "Restart", help: "Restart", color: .red))
|
NSApp.sendAction(Selector(("showPreferencesWindow:")), to: NSApp.delegate, from: nil)
|
||||||
.padding()
|
|
||||||
|
|
||||||
Button(action: {
|
|
||||||
if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles") {
|
|
||||||
NSWorkspace.shared.open(url)
|
|
||||||
}
|
}
|
||||||
}) {
|
.buttonStyle(SimpleButtonBrightStyle(icon: "gear", label: "Settings", help: "Check permissions in Settings", color: .red))
|
||||||
Text("Full Disk Access")
|
|
||||||
}
|
}
|
||||||
.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: {
|
Button(action: {
|
||||||
if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility") {
|
NewWin.close()
|
||||||
NSWorkspace.shared.open(url)
|
|
||||||
}
|
|
||||||
}) {
|
}) {
|
||||||
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()
|
.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))
|
.padding(EdgeInsets(top: -25, leading: 0, bottom: 25, trailing: 0))
|
||||||
|
|||||||
@@ -35,13 +35,12 @@ class WindowSettings {
|
|||||||
var x = CGFloat(UserDefaults.standard.float(forKey: windowXKey))
|
var x = CGFloat(UserDefaults.standard.float(forKey: windowXKey))
|
||||||
var y = CGFloat(UserDefaults.standard.float(forKey: windowYKey))
|
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)
|
return NSRect(x: x, y: y, width: width, height: height)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,3 +63,23 @@ class WindowSettings {
|
|||||||
newWindow.makeKeyAndOrderFront(nil)
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user