This commit is contained in:
Alin
2024-05-06 16:37:42 -06:00
parent fe14df080f
commit 5a8eb2d365
12 changed files with 171 additions and 326 deletions
+1
View File
@@ -27,6 +27,7 @@ class DeeplinkManager {
if url.pathExtension == "app" {
handleAppBundle(url: url, appState: appState, locations: locations)
} else if url.scheme == DeepLinkConstants.scheme,
// This handles SentinelMonitor FileWatcher
url.host == DeepLinkConstants.host,
let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
let queryItems = components.queryItems {
+6 -6
View File
@@ -200,14 +200,14 @@ 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(appState: AppState, at fileURLs: [URL], completion: @escaping (Bool) -> Void = {_ in }) {
@AppStorage("settings.sentinel.enable") var sentinel: Bool = false
if sentinel {
launchctl(load: false)
}
// Stop Sentinel FileWatcher momentarily to ignore .app bundle being sent to Trash
sendStopNotificationFW()
updateOnBackground {
let posixFiles = fileURLs.map { "POSIX file \"\($0.path)\", " }.joined().dropLast(3)
let posixFiles = fileURLs.map { item in
return "POSIX file \"\(item.path)\"" + (item == fileURLs.last ? "" : ", ")}.joined()
let scriptSource = """
tell application \"Finder\" to delete { \(posixFiles)" }
tell application \"Finder\" to delete { \(posixFiles) }
"""
var error: NSDictionary?
+23 -20
View File
@@ -66,10 +66,10 @@ func checkForUpdate(appState: AppState, manual: Bool = false) {
func downloadUpdate(appState: AppState) {
updateOnMain {
appState.progressBar.0 = "Getting update file links ready"
appState.progressBar.0 = "UPDATER: Getting update link"
appState.progressBar.1 = 0.1
}
let fileManager = FileManager.default
guard let latestRelease = appState.releases.first else { return }
guard let asset = latestRelease.assets.first else { return }
guard let url = URL(string: asset.url) else { return }
@@ -78,38 +78,37 @@ func downloadUpdate(appState: AppState) {
let downloadTask = URLSession.shared.downloadTask(with: request) { localURL, urlResponse, error in
updateOnMain {
appState.progressBar.0 = "Downloading update file"
appState.progressBar.0 = "UPDATER: Starting download of update file"
appState.progressBar.1 = 0.2
}
guard let localURL = localURL else { return }
let fileManager = FileManager.default
let destinationURL = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!.appendingPathComponent("Pearcleaner").appendingPathComponent("\(asset.name)")
let destinationURL = FileManager.default.temporaryDirectory.appendingPathComponent("\(asset.name)")
do {
if fileManager.fileExists(atPath: destinationURL.path) {
try? fileManager.removeItem(at: destinationURL)
}
updateOnMain {
appState.progressBar.0 = "Moving update file to Application Support"
appState.progressBar.0 = "UPDATER: File downloaded to temp directory"
appState.progressBar.1 = 0.3
}
try fileManager.moveItem(at: localURL, to: destinationURL)
updateOnMain {
appState.progressBar.0 = "UPDATER: File renamed using asset name"
appState.progressBar.1 = 0.4
}
try fileManager.moveItem(at: localURL, to: destinationURL)
UnzipAndReplace(DownloadedFileURL: destinationURL.path, appState: appState)
updateOnMain {
appState.progressBar.0 = "Done, please restart!"
appState.progressBar.1 = 1.0
appState.updateAvailable = false
}
} catch {
printOS("Error moving downloaded file: \(error.localizedDescription)")
}
}
downloadTask.resume()
@@ -122,7 +121,7 @@ func UnzipAndReplace(DownloadedFileURL fileURL: String, appState: AppState) {
do {
updateOnMain {
appState.progressBar.0 = "Deleting existing application"
appState.progressBar.0 = "UPDATER: Removing currently installed application bundle"
appState.progressBar.1 = 0.5
}
@@ -130,11 +129,10 @@ func UnzipAndReplace(DownloadedFileURL fileURL: String, appState: AppState) {
try fileManager.removeItem(atPath: appBundle)
updateOnMain {
appState.progressBar.0 = "Unziping new update file to original Pearcleaner location"
appState.progressBar.0 = "UPDATER: Unziping file to original install location"
appState.progressBar.1 = 0.6
}
// Unzip the downloaded update file to your app's bundle path
let process = Process()
process.executableURL = URL(fileURLWithPath: "/usr/bin/ditto")
@@ -146,13 +144,18 @@ func UnzipAndReplace(DownloadedFileURL fileURL: String, appState: AppState) {
process.waitUntilExit()
updateOnMain {
appState.progressBar.0 = "Deleting update file"
appState.progressBar.0 = "UPDATER: Removing file from temp directory"
appState.progressBar.1 = 0.8
}
// After unzipping, remove the update file
try fileManager.removeItem(atPath: fileURL)
updateOnMain {
appState.progressBar.0 = "UPDATER: Completed, please restart!"
appState.progressBar.1 = 1.0
appState.updateAvailable = false
}
} catch {
printOS("Error updating the app: \(error)")
+28 -51
View File
@@ -121,11 +121,13 @@ func findAndHideWindows(named titles: [String]) {
}
func findAndSetWindowFrame(named titles: [String], windowSettings: WindowSettings) {
for title in titles {
if let window = NSApp.windows.first(where: { $0.title == title }) {
window.isRestorable = false
let frame = windowSettings.loadWindowSettings()
window.setFrame(frame, display: true)
windowSettings.registerDefaultWindowSettings() {
for title in titles {
if let window = NSApp.windows.first(where: { $0.title == title }) {
window.isRestorable = false
let frame = windowSettings.loadWindowSettings()
window.setFrame(frame, display: true)
}
}
}
}
@@ -279,16 +281,17 @@ func killApp(appId: String, completion: @escaping () -> Void = {}) {
}
// Remove app from cache
func removeApp(appState: AppState, withId id: UUID) {
func removeApp(appState: AppState, withPath path: URL) {
@AppStorage("settings.general.brew") var brew: Bool = false
DispatchQueue.main.async {
// Remove from sortedApps if found
if let index = appState.sortedApps.firstIndex(where: { $0.id == id }) {
if let index = appState.sortedApps.firstIndex(where: { $0.path == path }) {
appState.sortedApps.remove(at: index)
return // Exit the function if the app was found and removed
// return // Exit the function if the app was found and removed
}
// Remove from appInfoStore if found
if let index = appState.appInfoStore.firstIndex(where: { $0.id == id }) {
if let index = appState.appInfoStore.firstIndex(where: { $0.path == path }) {
appState.appInfoStore.remove(at: index)
}
// Brew cleanup if enabled
@@ -410,13 +413,13 @@ extension String {
// --- Trash Relationship ---
extension FileManager {
public func isInTrash(_ file: URL) -> Bool {
var relationship: URLRelationship = .other
try? getRelationship(&relationship, of: .trashDirectory, in: .userDomainMask, toItemAt: file)
return relationship == .contains
}
}
//extension FileManager {
// public func isInTrash(_ file: URL) -> Bool {
// var relationship: URLRelationship = .other
// try? getRelationship(&relationship, of: .trashDirectory, in: .userDomainMask, toItemAt: file)
// return relationship == .contains
// }
//}
// --- Extend print command to also output to the Console ---
func printOS(_ items: Any..., separator: String = " ", terminator: String = "\n") {
@@ -518,37 +521,6 @@ func isSupportedFileType(at path: String) -> Bool {
}
// Alerts
func presentAlert(appState: AppState) -> Alert {
switch appState.alertType {
case .update:
return Alert(title: Text("Update Available 🥳"), message: Text("You may choose to install the update now, otherwise you may check again later from Settings"), primaryButton: .default(Text("Install")) {
downloadUpdate(appState: appState)
appState.alertType = .off
}, secondaryButton: .cancel())
case .no_update:
return Alert(title: Text("No Updates 😌"), message: Text("Pearcleaner is on the latest release available"), primaryButton: .cancel(Text("Okay")), secondaryButton: .default(Text("Force Update")) {
downloadUpdate(appState: appState)
appState.alertType = .off
})
case .diskAccess:
return Alert(title: Text("Permissions"), message: Text("Pearcleaner requires Full Disk and Accessibility permissions. Drag the app into the Full Disk and Accessibility pane to enable or toggle On if already present."), primaryButton: .default(Text("Allow in Settings")) {
if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles") {
NSWorkspace.shared.open(url)
}
appState.alertType = .off
}, secondaryButton: .cancel(Text("Later")))
case .restartApp:
return Alert(title: Text("Update Completed!"), message: Text("The application has been updated to the latest version, would you like to restart now?"), primaryButton: .default(Text("Restart")) {
appState.alertType = .off
relaunchApp()
}, secondaryButton: .cancel(Text("Later")))
case .off:
return Alert(title: Text(""))
}
}
// --- Pearcleaner Uninstall --
@@ -564,16 +536,15 @@ func uninstallPearcleaner(appState: AppState, locations: Locations) {
AppPathFinder(appInfo: appInfo!, appState: appState, locations: locations, completion: {
// Kill Pearcleaner and tell Finder to trash the files
let selectedItemsArray = Array(appState.selectedItems).filter { !$0.path.contains(".Trash") }
let posixFiles = selectedItemsArray.map { "POSIX file \"\($0.path)\", " }.joined().dropLast(3)
let posixFiles = selectedItemsArray.map { item in
return "POSIX file \"\(item.path)\"" + (item == selectedItemsArray.last ? "" : ", ")}.joined()
let scriptSource = """
tell application \"Finder\" to delete { \(posixFiles)" }
tell application \"Finder\" to delete { \(posixFiles) }
"""
let task = Process()
task.launchPath = "/bin/sh"
task.arguments = ["-c", "sleep 1; osascript -e '\(scriptSource)'"]
task.launch()
NSApp.terminate(nil)
exit(0)
}).findPaths()
}
@@ -653,7 +624,13 @@ func launchctl(load: Bool, completion: @escaping () -> Void = {}) {
}
func sendStartNotificationFW() {
DistributedNotificationCenter.default().postNotificationName(Notification.Name("Pearcleaner.StartFileWatcher"), object: nil, userInfo: nil, deliverImmediately: true)
}
func sendStopNotificationFW() {
DistributedNotificationCenter.default().postNotificationName(Notification.Name("Pearcleaner.StopFileWatcher"), object: nil, userInfo: nil, deliverImmediately: true)
}
func getCurrentTimestamp() -> String {
+3 -3
View File
@@ -7,7 +7,6 @@
import SwiftUI
import AppKit
//import ServiceManagement
@main
struct PearcleanerApp: App {
@@ -111,7 +110,8 @@ struct PearcleanerApp: App {
// Make sure App Support folder exists in the future if needed for storage
ensureApplicationSupportFolderExists(appState: appState)
//MARK: This is not needed any longer as the update file is stored in /tmp directory
// ensureApplicationSupportFolderExists(appState: appState)
// Check for updates after app launch
checkAllPermissions(appState: appState) { results in
@@ -184,7 +184,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
if UserDefaults.standard.object(forKey: "themeColor") == nil {
self.appearanceChanged()
}
if menubarEnabled {
findAndHideWindows(named: ["Pearcleaner"])
NSApplication.shared.setActivationPolicy(.accessory)
+5 -6
View File
@@ -198,7 +198,7 @@ struct GeneralSettingsTab: View {
}
}
}
.buttonStyle(SimpleButtonStyle(icon: "arrow.triangle.2.circlepath", help: "Refresh permissions"))
.buttonStyle(SimpleButtonStyle(icon: "arrow.triangle.2.circlepath", label: "Refresh", help: "Refresh permissions"))
.padding(.trailing, 5)
}
@@ -412,11 +412,7 @@ struct GeneralSettingsTab: View {
private func resetUserDefaults() {
isResetting = true
DispatchQueue.global(qos: .background).async {
let defaults = UserDefaults.standard
let dictionary = defaults.dictionaryRepresentation()
dictionary.keys.forEach { key in
defaults.removeObject(forKey: key)
}
UserDefaults.standard.dictionaryRepresentation().keys.forEach(UserDefaults.standard.removeObject(forKey:))
DispatchQueue.main.async {
isResetting = false
}
@@ -424,3 +420,6 @@ struct GeneralSettingsTab: View {
}
}
+5
View File
@@ -42,6 +42,11 @@ struct AppSearchView: View {
HStack(spacing: 10) {
#if DEBUG
Image(systemName: "ant.fill")
.foregroundStyle(.orange)
.help("DEBUG")
#endif
SearchBar(search: $search, darker: (mini || menubarEnabled) ? false : true, glass: glass)
+14 -6
View File
@@ -257,22 +257,30 @@ struct FilesView: View {
Spacer()
HStack() {
HStack(alignment: .center) {
Spacer()
if appState.appInfo.fileSize.keys.count == 0 {
Text("Sentinel Monitor found no other files to remove")
.font(.title2)
.opacity(0.5)
.padding(.top)
}
Spacer()
Button("\(sizeType == "Logical" ? totalSelectedSize.logical : sizeType == "Finder" ? totalSelectedSize.finder : totalSelectedSize.real)") {
Task {
let selectedItemsArray = Array(appState.selectedItems)
killApp(appId: appState.appInfo.bundleIdentifier) {
moveFilesToTrash(appState: appState, at: selectedItemsArray) { success in
if sentinel {
launchctl(load: true)
}
// Send Sentinel FileWatcher start notification
sendStartNotificationFW()
guard success else {
return
@@ -296,7 +304,7 @@ struct FilesView: View {
if (appState.appInfo.wrapped && selectedItemsArray.contains(where: { $0.absoluteString == appState.appInfo.path.deletingLastPathComponent().deletingLastPathComponent().absoluteString })) ||
(!appState.appInfo.wrapped && selectedItemsArray.contains(where: { $0.absoluteString == appState.appInfo.path.absoluteString })) {
// Match found, remove the app
removeApp(appState: appState, withId: appState.appInfo.id)
removeApp(appState: appState, withPath: appState.appInfo.path)
} else {
// Add deleted appInfo object to trashed array
appState.trashedFiles.append(appState.appInfo)
-1
View File
@@ -111,7 +111,6 @@ struct AppDetailsEmptyView: View {
PearDropView()
}
Spacer()
Text("Drop an app here")
+44 -13
View File
@@ -17,33 +17,64 @@ class WindowSettings {
@AppStorage("settings.general.mini") private var mini: Bool = false
var windows: [NSWindow] = []
func saveWindowSettings(frame: NSRect) {
func registerDefaultWindowSettings(completion: @escaping () -> Void = {}) {
let defaults = UserDefaults.standard
// Get primary screen
let screenFrame = NSScreen.main?.visibleFrame ?? NSRect(x: 0, y: 0, width: 800, height: 600)
// Calculate default window sizes and x/y coordinates
let defaultWidth = Float(900) // Default width for regular window
let defaultHeight = Float(600) // Default height for regular window
let defaultWidthMini = Float(300) // Default width for mini window
let defaultHeightMini = Float(370) // Default height for mini window
let defaultX = Float((screenFrame.width - CGFloat(defaultWidth)) / 2 + screenFrame.origin.x) // Default X coordinate
let defaultY = Float((screenFrame.height - CGFloat(defaultHeight)) / 2 + screenFrame.origin.y) // Default Y coordinate
// Set defaults only if they are not already set
if defaults.object(forKey: windowWidthKey) == nil {
defaults.set(defaultWidth, forKey: windowWidthKey)
}
if defaults.object(forKey: windowHeightKey) == nil {
defaults.set(defaultHeight, forKey: windowHeightKey)
}
if defaults.object(forKey: windowWidthKeyMini) == nil {
defaults.set(defaultWidthMini, forKey: windowWidthKeyMini)
}
if defaults.object(forKey: windowHeightKeyMini) == nil {
defaults.set(defaultHeightMini, forKey: windowHeightKeyMini)
}
if defaults.object(forKey: windowXKey) == nil {
defaults.set(defaultX, forKey: windowXKey)
}
if defaults.object(forKey: windowYKey) == nil {
defaults.set(defaultY, forKey: windowYKey)
}
completion()
}
// Save user window settings
func saveWindowSettings(frame: NSRect) {
UserDefaults.standard.set(Float(frame.size.width), forKey: mini ? windowWidthKeyMini : windowWidthKey)
UserDefaults.standard.set(Float(frame.size.height), forKey: mini ? windowHeightKeyMini : windowHeightKey)
UserDefaults.standard.set(Float(frame.origin.x), forKey: windowXKey)
UserDefaults.standard.set(Float(frame.origin.y), forKey: windowYKey)
}
// Load default window settings or user defined settings
func loadWindowSettings() -> NSRect {
// Retrieve window size
let width = CGFloat(UserDefaults.standard.float(forKey: mini ? windowWidthKeyMini : windowWidthKey))
let height = CGFloat(UserDefaults.standard.float(forKey: mini ? windowHeightKeyMini : windowHeightKey))
let x = CGFloat(UserDefaults.standard.float(forKey: windowXKey))
let y = CGFloat(UserDefaults.standard.float(forKey: windowYKey))
// Set default middle position if not set in UserDefaults
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 {
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)
}
// Launch new app windows on demand
func newWindow<V: View>(withView view: @escaping () -> V, completion: @escaping () -> Void = {}) {
findAndHideWindows(named: ["Pearcleaner"])
let contentView = view