diff --git a/Pearcleaner.xcodeproj/project.pbxproj b/Pearcleaner.xcodeproj/project.pbxproj index 365627a..d8d1f5c 100644 --- a/Pearcleaner.xcodeproj/project.pbxproj +++ b/Pearcleaner.xcodeproj/project.pbxproj @@ -37,9 +37,10 @@ C7D31D4A2AFEB26700C7ED9E /* AppListItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7D31D492AFEB26700C7ED9E /* AppListItems.swift */; }; C7D31D512AFF00F300C7ED9E /* Locations.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7D31D502AFF00F300C7ED9E /* Locations.swift */; }; C7DC1FEE2B9F7D3D009AC317 /* features.json in Resources */ = {isa = PBXBuildFile; fileRef = C7DC1FED2B9F7D3D009AC317 /* features.json */; }; - C7DE67272BA61E8D00EB1633 /* MenuBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7DE67262BA61E8D00EB1633 /* MenuBar.swift */; }; C7DE672B2BA6343D00EB1633 /* MenuBarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7DE672A2BA6343D00EB1633 /* MenuBarItem.swift */; }; C7DE672D2BA6356500EB1633 /* MenuBarMiniAppView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7DE672C2BA6356500EB1633 /* MenuBarMiniAppView.swift */; }; + C7ED86B12BA8A35500F13DE4 /* Interface.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7ED86B02BA8A35500F13DE4 /* Interface.swift */; }; + C7ED86B32BA8ED8800F13DE4 /* Trash.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7ED86B22BA8ED8800F13DE4 /* Trash.swift */; }; C7F539382AF60865007DF1B2 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7F539372AF60865007DF1B2 /* Utilities.swift */; }; C7FB173B2B96321300B96F9A /* AppsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7FB173A2B96321300B96F9A /* AppsListView.swift */; }; /* End PBXBuildFile section */ @@ -100,9 +101,10 @@ C7D31D492AFEB26700C7ED9E /* AppListItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppListItems.swift; sourceTree = ""; }; C7D31D502AFF00F300C7ED9E /* Locations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Locations.swift; sourceTree = ""; }; C7DC1FED2B9F7D3D009AC317 /* features.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = features.json; sourceTree = SOURCE_ROOT; }; - C7DE67262BA61E8D00EB1633 /* MenuBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBar.swift; sourceTree = ""; }; C7DE672A2BA6343D00EB1633 /* MenuBarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarItem.swift; sourceTree = ""; }; C7DE672C2BA6356500EB1633 /* MenuBarMiniAppView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarMiniAppView.swift; sourceTree = ""; }; + C7ED86B02BA8A35500F13DE4 /* Interface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Interface.swift; sourceTree = ""; }; + C7ED86B22BA8ED8800F13DE4 /* Trash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Trash.swift; sourceTree = ""; }; C7F539372AF60865007DF1B2 /* Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utilities.swift; sourceTree = ""; }; C7FB173A2B96321300B96F9A /* AppsListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppsListView.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -132,6 +134,8 @@ C7045A312B068AD700376976 /* NewWin.swift */, C7045A332B068B2900376976 /* UpdateView.swift */, C7045A352B068DD700376976 /* PermView.swift */, + C7DE672A2BA6343D00EB1633 /* MenuBarItem.swift */, + C7CF47232B3B3F1700979C5F /* WindowSettings.swift */, ); path = Windows; sourceTree = ""; @@ -140,16 +144,15 @@ isa = PBXGroup; children = ( C77B90192AF1938F009CC655 /* Logic.swift */, - C7DE672A2BA6343D00EB1633 /* MenuBarItem.swift */, C77B90172AF19382009CC655 /* AppCommands.swift */, C77B90152AF19377009CC655 /* AppState.swift */, C7F539372AF60865007DF1B2 /* Utilities.swift */, C72893042AFD42E600C8C1CD /* DeepLink.swift */, C77B901B2AF193A3009CC655 /* Styles.swift */, C7D31D502AFF00F300C7ED9E /* Locations.swift */, - C7CF47232B3B3F1700979C5F /* WindowSettings.swift */, C7A9CE462B89164700EB6E78 /* Authorization.swift */, C7ACADE52B92A737000B5845 /* Features.swift */, + C7ED86B22BA8ED8800F13DE4 /* Trash.swift */, ); path = Logic; sourceTree = ""; @@ -175,7 +178,7 @@ children = ( C77B901D2AF1A9DB009CC655 /* SettingsWindow.swift */, C76D08492AF83C6E00D07867 /* General.swift */, - C7DE67262BA61E8D00EB1633 /* MenuBar.swift */, + C7ED86B02BA8A35500F13DE4 /* Interface.swift */, C76D08472AF83C3F00D07867 /* Update.swift */, C76D08502AF850F300D07867 /* About.swift */, ); @@ -357,11 +360,13 @@ C77B90162AF19377009CC655 /* AppState.swift in Sources */, C7045A322B068AD700376976 /* NewWin.swift in Sources */, C7DE672D2BA6356500EB1633 /* MenuBarMiniAppView.swift in Sources */, + C7ED86B32BA8ED8800F13DE4 /* Trash.swift in Sources */, C7D31D512AFF00F300C7ED9E /* Locations.swift in Sources */, C77B901A2AF1938F009CC655 /* Logic.swift in Sources */, C7045A2A2B03FAF000376976 /* TopBarMini.swift in Sources */, C77B901C2AF193A3009CC655 /* Styles.swift in Sources */, C7045A282B03E71D00376976 /* MiniMode.swift in Sources */, + C7ED86B12BA8A35500F13DE4 /* Interface.swift in Sources */, C72893052AFD42E600C8C1CD /* DeepLink.swift in Sources */, C71848442B8D2D600046CB13 /* ZombieView.swift in Sources */, C7DE672B2BA6343D00EB1633 /* MenuBarItem.swift in Sources */, @@ -378,7 +383,6 @@ C76D08552AF89CDE00D07867 /* RegularMode.swift in Sources */, C7A9CE472B89164700EB6E78 /* Authorization.swift in Sources */, C7CF47242B3B3F1700979C5F /* WindowSettings.swift in Sources */, - C7DE67272BA61E8D00EB1633 /* MenuBar.swift in Sources */, C77B90232AF2D616009CC655 /* FilesView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -555,7 +559,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 23; + CURRENT_PROJECT_VERSION = 24; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = BK8443AXLU; @@ -573,7 +577,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 3.1.1; + MARKETING_VERSION = 3.2.0; PRODUCT_BUNDLE_IDENTIFIER = com.alienator88.Pearcleaner; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -590,7 +594,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 23; + CURRENT_PROJECT_VERSION = 24; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = BK8443AXLU; @@ -608,7 +612,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 3.1.1; + MARKETING_VERSION = 3.2.0; PRODUCT_BUNDLE_IDENTIFIER = com.alienator88.Pearcleaner; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; diff --git a/Pearcleaner/Logic/AppState.swift b/Pearcleaner/Logic/AppState.swift index bed5c96..d80e1f2 100644 --- a/Pearcleaner/Logic/AppState.swift +++ b/Pearcleaner/Logic/AppState.swift @@ -105,18 +105,14 @@ struct ZombieFile: Identifiable, Equatable, Hashable { enum CurrentTabView:Int { case general - case menubar -// case permissions -// case sentinel + case interface case update case about var title: String { switch self { case .general: return "General" - case .menubar: return "MenuBar" -// case .permissions: return "Permissions" -// case .sentinel: return "Sentinel" + case .interface: return "Interface" case .update: return "Update" case .about: return "About" } diff --git a/Pearcleaner/Logic/Authorization.swift b/Pearcleaner/Logic/Authorization.swift index a87a6c9..6203d1d 100644 --- a/Pearcleaner/Logic/Authorization.swift +++ b/Pearcleaner/Logic/Authorization.swift @@ -5,109 +5,109 @@ // Created by Alin Lupascu on 2/23/24. // -import Foundation -import Swift - -// https://github.com/x13a/authorization-swift - -public struct Authorization { - - public enum Error: Swift.Error { - case create(OSStatus) - case copyRights(OSStatus) - case exec(OSStatus) - } - - public static func executeWithPrivileges( - _ command: String - ) -> Result { - - let RTLD_DEFAULT = UnsafeMutableRawPointer(bitPattern: -2) - var fn: @convention(c) ( - AuthorizationRef, - UnsafePointer, // path - AuthorizationFlags, - UnsafePointer?>, // args - UnsafeMutablePointer>? - ) -> OSStatus - fn = unsafeBitCast( - dlsym(RTLD_DEFAULT, "AuthorizationExecuteWithPrivileges"), - to: type(of: fn) - ) - - var authorizationRef: AuthorizationRef? = nil - var err = AuthorizationCreate(nil, nil, [], &authorizationRef) - guard err == errAuthorizationSuccess else { - return .failure(.create(err)) - } - defer { AuthorizationFree(authorizationRef!, [.destroyRights]) } - - var components = command.components(separatedBy: " ") - var path = components.remove(at: 0).cString(using: .utf8)! - let name = kAuthorizationRightExecute.cString(using: .utf8)! - - var items: AuthorizationItem = name.withUnsafeBufferPointer { nameBuf in - path.withUnsafeBufferPointer { pathBuf in - let pathPtr = - UnsafeMutableRawPointer(mutating: pathBuf.baseAddress!) - return AuthorizationItem( - name: nameBuf.baseAddress!, - valueLength: path.count, - value: pathPtr, - flags: 0 - ) - } - } - - var rights: AuthorizationRights = - withUnsafeMutablePointer(to: &items) { items in - return AuthorizationRights(count: 1, items: items) - } - - let flags: AuthorizationFlags = [ - .interactionAllowed, - .preAuthorize, - .extendRights, - ] - - err = AuthorizationCopyRights( - authorizationRef!, - &rights, - nil, - flags, - nil - ) - guard err == errAuthorizationSuccess else { - return .failure(.copyRights(err)) - } - - let rest = components.map { $0.cString(using: .utf8)! } - var args = Array?>( - repeating: nil, - count: rest.count + 1 - ) - for (idx, arg) in rest.enumerated() { - args[idx] = UnsafePointer?(arg) - } - - var file = FILE() - let fh: FileHandle? - - (err, fh) = withUnsafeMutablePointer(to: &file) { file in - var pipe = file - let err = fn(authorizationRef!, &path, [], &args, &pipe) - guard err == errAuthorizationSuccess else { - return (err, nil) - } - let fh = FileHandle( - fileDescriptor: fileno(pipe), - closeOnDealloc: true - ) - return (err, fh) - } - guard err == errAuthorizationSuccess else { - return .failure(.exec(err)) - } - return .success(fh!) - } -} +//import Foundation +//import Swift +// +//// https://github.com/x13a/authorization-swift +// +//public struct Authorization { +// +// public enum Error: Swift.Error { +// case create(OSStatus) +// case copyRights(OSStatus) +// case exec(OSStatus) +// } +// +// public static func executeWithPrivileges( +// _ command: String +// ) -> Result { +// +// let RTLD_DEFAULT = UnsafeMutableRawPointer(bitPattern: -2) +// var fn: @convention(c) ( +// AuthorizationRef, +// UnsafePointer, // path +// AuthorizationFlags, +// UnsafePointer?>, // args +// UnsafeMutablePointer>? +// ) -> OSStatus +// fn = unsafeBitCast( +// dlsym(RTLD_DEFAULT, "AuthorizationExecuteWithPrivileges"), +// to: type(of: fn) +// ) +// +// var authorizationRef: AuthorizationRef? = nil +// var err = AuthorizationCreate(nil, nil, [], &authorizationRef) +// guard err == errAuthorizationSuccess else { +// return .failure(.create(err)) +// } +// defer { AuthorizationFree(authorizationRef!, [.destroyRights]) } +// +// var components = command.components(separatedBy: " ") +// var path = components.remove(at: 0).cString(using: .utf8)! +// let name = kAuthorizationRightExecute.cString(using: .utf8)! +// +// var items: AuthorizationItem = name.withUnsafeBufferPointer { nameBuf in +// path.withUnsafeBufferPointer { pathBuf in +// let pathPtr = +// UnsafeMutableRawPointer(mutating: pathBuf.baseAddress!) +// return AuthorizationItem( +// name: nameBuf.baseAddress!, +// valueLength: path.count, +// value: pathPtr, +// flags: 0 +// ) +// } +// } +// +// var rights: AuthorizationRights = +// withUnsafeMutablePointer(to: &items) { items in +// return AuthorizationRights(count: 1, items: items) +// } +// +// let flags: AuthorizationFlags = [ +// .interactionAllowed, +// .preAuthorize, +// .extendRights, +// ] +// +// err = AuthorizationCopyRights( +// authorizationRef!, +// &rights, +// nil, +// flags, +// nil +// ) +// guard err == errAuthorizationSuccess else { +// return .failure(.copyRights(err)) +// } +// +// let rest = components.map { $0.cString(using: .utf8)! } +// var args = Array?>( +// repeating: nil, +// count: rest.count + 1 +// ) +// for (idx, arg) in rest.enumerated() { +// args[idx] = UnsafePointer?(arg) +// } +// +// var file = FILE() +// let fh: FileHandle? +// +// (err, fh) = withUnsafeMutablePointer(to: &file) { file in +// var pipe = file +// let err = fn(authorizationRef!, &path, [], &args, &pipe) +// guard err == errAuthorizationSuccess else { +// return (err, nil) +// } +// let fh = FileHandle( +// fileDescriptor: fileno(pipe), +// closeOnDealloc: true +// ) +// return (err, fh) +// } +// guard err == errAuthorizationSuccess else { +// return .failure(.exec(err)) +// } +// return .success(fh!) +// } +//} diff --git a/Pearcleaner/Logic/Logic.swift b/Pearcleaner/Logic/Logic.swift index efa88c4..41fe35f 100644 --- a/Pearcleaner/Logic/Logic.swift +++ b/Pearcleaner/Logic/Logic.swift @@ -640,30 +640,6 @@ func reversePathsSearch(appState: AppState, locations: Locations, completion: @e // Move files to trash using applescript/Finder so it asks for user password if needed func moveFilesToTrash(at fileURLs: [URL], completion: @escaping () -> Void = {}) { @AppStorage("settings.sentinel.enable") var sentinel: Bool = false -// var filesFinder = fileURLs -// var filesSudo: [URL] = [] -// -// for file in fileURLs { -// if isSocketFile(at: file) { -// if let index = filesFinder.firstIndex(of: file) { -// filesFinder.remove(at: index) -// filesSudo.insert(file, at: 0) -// } -// } -// } -// -// if !filesSudo.isEmpty { -// // Remove socket files with rm -// let filesSudoPaths = filesSudo.map { $0.path } -// do { -// let fileHandler = try Authorization.executeWithPrivileges("/bin/rm -f \(filesSudoPaths.joined(separator: " "))").get() -// printOS(String(bytes: fileHandler.readDataToEndOfFile(), encoding: .utf8)!) -// } catch { -// printOS("Failed to remove socket file/s with privileges: \(error)") -// } -// } - - updateOnBackground { let posixFiles = fileURLs.map { "POSIX file \"\($0.path)\", " }.joined().dropLast(3) diff --git a/Pearcleaner/Logic/Trash.swift b/Pearcleaner/Logic/Trash.swift new file mode 100644 index 0000000..4d746e5 --- /dev/null +++ b/Pearcleaner/Logic/Trash.swift @@ -0,0 +1,100 @@ +// +// Trash.swift +// Pearcleaner +// +// Created by Alin Lupascu on 3/18/24. +// + + + +import Foundation + +//class TrashManager { +// private var fileLocations: [URL: URL] = [:] // Maps original location to trash location +// +// // Function to move files to Trash and track their original locations +// func delete(fileURLs: [URL]) { +// let fileManager = FileManager.default +// let trashURL = try! fileManager.url(for: .trashDirectory, in: .allDomainsMask, appropriateFor: nil, create: false) +// +// for originalURL in fileURLs { +// let destinationURL = trashURL.appendingPathComponent(originalURL.lastPathComponent) +// +// do { +// // The resultingItemURL is the new location of the item in the trash. +// var resultingItemURL: NSURL? +// try fileManager.trashItem(at: originalURL, resultingItemURL: &resultingItemURL) +// if let trashPath = resultingItemURL as URL? { +// // Track the original URL and its corresponding location in the Trash +// fileLocations[originalURL] = trashPath +// } +// } catch { +// print("Error moving file to Trash: \(error)") +// } +// } +// } +// +// // Function to undo the deletion of files, moving them back from Trash to their original locations +// func undoDelete() { +// let fileManager = FileManager.default +// +// for (originalURL, trashURL) in fileLocations { +// do { +// // Attempt to move the item back to its original location +// try fileManager.moveItem(at: trashURL, to: originalURL) +// } catch { +// print("Error moving file back from Trash: \(error)") +// } +// } +// +// // Clear the tracking dictionary after undoing the deletions +// fileLocations.removeAll() +// } +//} + + + + + + +//import Foundation +// +//func moveToTrash(fileURLs: [URL], completion: @escaping () -> Void = {}) { +// for fileURL in fileURLs { +// do { +// try _ = fileURL.checkResourceIsReachable() +// } catch { +// printOS(error.localizedDescription) +// } +// } +// let target: NSAppleEventDescriptor = .init(bundleIdentifier: "com.apple.finder") +// let event: NSAppleEventDescriptor = .init(eventClass: kAECoreSuite, +// eventID: AEEventID(kAEDelete), +// targetDescriptor: target, +// returnID: AEReturnID(kAutoGenerateReturnID), +// transactionID: AETransactionID(kAnyTransactionID)) +// let fileList: NSAppleEventDescriptor = fileURLs.enumerated().reduce(into: .init(listDescriptor: ())) { +// (result: inout NSAppleEventDescriptor, element: (offset: Int, element: URL)) in +// if let nativePath: NSAppleEventDescriptor = .init( +// descriptorType: typeFileURL, +// data: element.element.absoluteString.data(using: .utf8) +// ) { +// result.insert(nativePath, at: element.offset + 1) +// } +// } +// +// event.setParam(fileList, forKeyword: keyDirectObject) +// +// do { +// try event.sendEvent(options: .noReply, timeout: TimeInterval(kAEDefaultTimeout)) +// } catch let error as NSError { +// if case -600 = error.code { +// printOS("Finder is not running.") +// } else { +// printOS(error.description) +// } +// } +// +// completion() +//} + diff --git a/Pearcleaner/Logic/Utilities.swift b/Pearcleaner/Logic/Utilities.swift index 07f0ff8..f876c2d 100644 --- a/Pearcleaner/Logic/Utilities.swift +++ b/Pearcleaner/Logic/Utilities.swift @@ -156,6 +156,31 @@ func isDarkModeEnabled() -> Bool { } } +// Check if Pearcleaner has any windows open +func hasWindowOpen() -> Bool { + for window in NSApp.windows where window.title == "Pearcleaner" { + return true + } + return false +} + +// Find and hide/show main app window when using menubar item +func findAndHideWindows(named titles: [String]) { + for title in titles { + if let window = NSApp.windows.first(where: { $0.title == title }) { + window.orderOut(nil) + } + } +} + +func findAndShowWindows(named titles: [String]) { + for title in titles { + if let window = NSApp.windows.first(where: { $0.title == title }) { + window.makeKeyAndOrderFront(nil) + } + } +} + // Check if appearance is dark mode //func getCasks() -> [String] { diff --git a/Pearcleaner/PearcleanerApp.swift b/Pearcleaner/PearcleanerApp.swift index a664426..ce67f54 100644 --- a/Pearcleaner/PearcleanerApp.swift +++ b/Pearcleaner/PearcleanerApp.swift @@ -27,6 +27,7 @@ struct PearcleanerApp: App { @AppStorage("settings.general.brew") private var brew: Bool = false @AppStorage("settings.menubar.enabled") private var menubarEnabled: Bool = false @AppStorage("settings.menubar.mainWin") private var mainWinEnabled: Bool = false + @AppStorage("settings.interface.selectedMenubarIcon") var selectedMenubarIcon: String = "trash" @State private var search = "" @State private var showPopover: Bool = false @@ -41,10 +42,8 @@ struct PearcleanerApp: App { ZStack() { if !mini { RegularMode(search: $search, showPopover: $showPopover) - .environmentObject(locations) } else { MiniMode(search: $search, showPopover: $showPopover) - .environmentObject(locations) } if showFeature { @@ -57,8 +56,8 @@ struct PearcleanerApp: App { } .environmentObject(appState) + .environmentObject(locations) .preferredColorScheme(displayMode.colorScheme) -// .alert(isPresented: $appState.showAlert) { presentAlert(appState: appState) } .handlesExternalEvents(preferring: Set(arrayLiteral: "pear"), allowing: Set(arrayLiteral: "*")) .onOpenURL(perform: { url in let deeplinkManager = DeeplinkManager(showPopover: $showPopover) @@ -107,15 +106,14 @@ struct PearcleanerApp: App { } if menubarEnabled { - MenuBarExtraManager.shared.addMenuBarExtra { + MenuBarExtraManager.shared.addMenuBarExtra(withView: { MenuBarMiniAppView(search: $search, showPopover: $showPopover) .environmentObject(locations) .environmentObject(appState) - } + }, icon: selectedMenubarIcon) } - #if !DEBUG Task { @@ -175,7 +173,7 @@ struct PearcleanerApp: App { -class AppDelegate: NSObject, NSApplicationDelegate { +class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate { func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { let menubarEnabled = UserDefaults.standard.bool(forKey: "settings.menubar.enabled") @@ -183,15 +181,49 @@ class AppDelegate: NSObject, NSApplicationDelegate { } +#if !DEBUG + func windowShouldClose(_ sender: NSWindow) -> Bool { + let menubarEnabled = UserDefaults.standard.bool(forKey: "settings.menubar.enabled") +// let alert = NSAlert.init() +// alert.addButton(withTitle: "Return") +// alert.addButton(withTitle: "Quit") +// alert.informativeText = "Quit or return to application?" +// let response = alert.runModal() +// if response == NSApplication.ModalResponse.alertFirstButtonReturn { +// return false +// } else { +// NSApplication.shared.terminate(self) +// return true +// } + if menubarEnabled { + findAndHideWindows(named: ["Pearcleaner"]) + return false + } else { + return true + } + } +#endif + func applicationDidFinishLaunching(_ notification: Notification) { let menubarEnabled = UserDefaults.standard.bool(forKey: "settings.menubar.enabled") +// let dockEnabled = UserDefaults.standard.bool(forKey: "settings.dock.enabled") -#if !DEBUG if menubarEnabled { - NSApp.windows.first?.close() +#if !DEBUG + findAndHideWindows(named: ["Pearcleaner"]) +// NSApp.windows.first?.close() NSApplication.shared.setActivationPolicy(.accessory) - } #endif +// if dockEnabled { +// NSApplication.shared.setActivationPolicy(.regular) +// } else { +// NSApplication.shared.setActivationPolicy(.accessory) +// } + } + + // Link window to delegate + let mainWindow = NSApp.windows[0] + mainWindow.delegate = self } diff --git a/Pearcleaner/Resources/Assets.xcassets/pear-1.5.imageset/Contents.json b/Pearcleaner/Resources/Assets.xcassets/pear-1.5.imageset/Contents.json new file mode 100644 index 0000000..01d3ea9 --- /dev/null +++ b/Pearcleaner/Resources/Assets.xcassets/pear-1.5.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "pear-1.5.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Pearcleaner/Resources/Assets.xcassets/pear-1.5.imageset/pear-1.5.png b/Pearcleaner/Resources/Assets.xcassets/pear-1.5.imageset/pear-1.5.png new file mode 100644 index 0000000..0f348ca Binary files /dev/null and b/Pearcleaner/Resources/Assets.xcassets/pear-1.5.imageset/pear-1.5.png differ diff --git a/Pearcleaner/Resources/Assets.xcassets/pear-1.imageset/Contents.json b/Pearcleaner/Resources/Assets.xcassets/pear-1.imageset/Contents.json new file mode 100644 index 0000000..2cb0c80 --- /dev/null +++ b/Pearcleaner/Resources/Assets.xcassets/pear-1.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "pear-1.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Pearcleaner/Resources/Assets.xcassets/pear-1.imageset/pear-1.png b/Pearcleaner/Resources/Assets.xcassets/pear-1.imageset/pear-1.png new file mode 100644 index 0000000..d8daa10 Binary files /dev/null and b/Pearcleaner/Resources/Assets.xcassets/pear-1.imageset/pear-1.png differ diff --git a/Pearcleaner/Resources/Assets.xcassets/pear-2.imageset/Contents.json b/Pearcleaner/Resources/Assets.xcassets/pear-2.imageset/Contents.json new file mode 100644 index 0000000..149905f --- /dev/null +++ b/Pearcleaner/Resources/Assets.xcassets/pear-2.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "pear-2.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Pearcleaner/Resources/Assets.xcassets/pear-2.imageset/pear-2.png b/Pearcleaner/Resources/Assets.xcassets/pear-2.imageset/pear-2.png new file mode 100644 index 0000000..c12b8e2 Binary files /dev/null and b/Pearcleaner/Resources/Assets.xcassets/pear-2.imageset/pear-2.png differ diff --git a/Pearcleaner/Resources/Assets.xcassets/pear-3.imageset/Contents.json b/Pearcleaner/Resources/Assets.xcassets/pear-3.imageset/Contents.json new file mode 100644 index 0000000..d7ade1a --- /dev/null +++ b/Pearcleaner/Resources/Assets.xcassets/pear-3.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "pear-3.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Pearcleaner/Resources/Assets.xcassets/pear-3.imageset/pear-3.png b/Pearcleaner/Resources/Assets.xcassets/pear-3.imageset/pear-3.png new file mode 100644 index 0000000..e4208bf Binary files /dev/null and b/Pearcleaner/Resources/Assets.xcassets/pear-3.imageset/pear-3.png differ diff --git a/Pearcleaner/Resources/Assets.xcassets/pear-4.imageset/Contents.json b/Pearcleaner/Resources/Assets.xcassets/pear-4.imageset/Contents.json new file mode 100644 index 0000000..3a23ed9 --- /dev/null +++ b/Pearcleaner/Resources/Assets.xcassets/pear-4.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "pear-4.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Pearcleaner/Resources/Assets.xcassets/pear-4.imageset/pear-4.png b/Pearcleaner/Resources/Assets.xcassets/pear-4.imageset/pear-4.png new file mode 100644 index 0000000..f03db18 Binary files /dev/null and b/Pearcleaner/Resources/Assets.xcassets/pear-4.imageset/pear-4.png differ diff --git a/Pearcleaner/Settings/About.swift b/Pearcleaner/Settings/About.swift index 4d5fbed..744adc7 100644 --- a/Pearcleaner/Settings/About.swift +++ b/Pearcleaner/Settings/About.swift @@ -152,7 +152,7 @@ struct AboutSettingsTab: View { Text("Made with ❤️ by Alin Lupascu (dev@itsalin.com)").font(.footnote).padding(.bottom) } .padding(20) - .frame(width: 500, height: 650) + .frame(width: 500, height: 600) } } diff --git a/Pearcleaner/Settings/General.swift b/Pearcleaner/Settings/General.swift index b9afcf0..1ae6d1f 100644 --- a/Pearcleaner/Settings/General.swift +++ b/Pearcleaner/Settings/General.swift @@ -35,32 +35,11 @@ struct GeneralSettingsTab: View { VStack { HStack() { - Text("Appearance").font(.title2) + Text("Functionality").font(.title2) Spacer() } .padding(.leading) - HStack(spacing: 0) { - Image(systemName: glass ? "cube.transparent" : "cube.transparent.fill") - .resizable() - .scaledToFit() - .frame(width: 20, height: 20) - .padding(.trailing) - .foregroundStyle(.gray) - VStack(alignment: .leading, spacing: 5) { - Text("\(glass ? "Transparent material enabled" : "Transparent material disabled")") - .font(.callout) - .foregroundStyle(.gray) - } -// InfoButton(text: "When transparent material is enabled, sticky section headers (User/System) in app list are disabled to keep from showing app name text overlayed under the section header text with no background to separate the two.", color: nil) - Spacer() - Toggle(isOn: $glass, label: { - }) - .toggleStyle(.switch) - } - .padding(5) - .padding(.leading) - HStack(spacing: 0) { Image(systemName: instantSearch ? "bolt" : "bolt.slash") @@ -110,146 +89,6 @@ struct GeneralSettingsTab: View { .padding(.leading) - HStack(spacing: 0) { - Image(systemName: displayMode.colorScheme == .dark ? "moon.fill" : "sun.max.fill") - .resizable() - .scaledToFit() - .frame(width: 20, height: 20) - .padding(.trailing) - .foregroundStyle(.gray) - VStack(alignment: .leading, spacing: 5) { - Text("Set application color mode") - .font(.callout) - .foregroundStyle(.gray) - } - Spacer() - Picker("", selection: $selectedTheme) { - ForEach(themes, id: \.self) { theme in - Text(theme) - } - } - .pickerStyle(SegmentedPickerStyle()) - .frame(width: 200) - .onChange(of: selectedTheme) { newTheme in - switch newTheme { - case "Auto": - displayMode.colorScheme = nil - if isDarkModeEnabled() { - displayMode.colorScheme = .dark - } else { - displayMode.colorScheme = .light - } - case "Dark": - displayMode.colorScheme = .dark - case "Light": - displayMode.colorScheme = .light - default: - break - } - } - } - .padding(5) - .padding(.leading) - - // === Mini ================================================================================================= - - Divider() - .padding() - - - - HStack() { - Text("Mini Configuration").font(.title2) - Spacer() - } - .padding(.leading) - - HStack(spacing: 0) { - Image(systemName: mini ? "square.resize.up" : "square.resize.down") - .resizable() - .scaledToFit() - .frame(width: 20, height: 20) - .padding(.trailing) - .foregroundStyle(.gray) - VStack(alignment: .leading, spacing: 5) { - Text("\(mini ? "Mini window mode selected" : "Full size window mode selected")") - .font(.callout) - .foregroundStyle(.gray) - } - Spacer() - Toggle(isOn: $mini, label: { - }) - .toggleStyle(.switch) - .onChange(of: mini) { newVal in - if mini { - appState.currentView = miniView ? .apps : .empty - showPopover = false - resizeWindowAuto(windowSettings: windowSettings) - } else { - resizeWindowAuto(windowSettings: windowSettings) - if appState.appInfo.appName.isEmpty { - appState.currentView = .empty - } else { - appState.currentView = .files - } - } - } - } - .padding(5) - .padding(.leading) - - - HStack(spacing: 0) { - Image(systemName: miniView ? "square.grid.3x3.square" : "plus.square.dashed") - .resizable() - .scaledToFit() - .frame(width: 20, height: 20) - .padding(.trailing) - .foregroundStyle(.gray) - VStack(alignment: .leading, spacing: 5) { - Text("\(miniView ? "Show apps list on startup" : "Show drop target on startup")") - .font(.callout) - .foregroundStyle(.gray) - } - - InfoButton(text: "In mini window mode, you can have Pearcleaner startup to the Apps List view or the Drop Target view.", color: nil, label: "") - - Spacer() - Toggle(isOn: $miniView, label: { - }) - .toggleStyle(.switch) - .onChange(of: miniView) { newVal in - appState.currentView = newVal ? .apps : .empty - } - } - .padding(5) - .padding(.leading) - - - HStack(spacing: 0) { - Image(systemName: popoverStay ? "pin" : "pin.slash") - .resizable() - .scaledToFit() - .frame(width: 20, height: 20) - .padding(.trailing) - .foregroundStyle(.gray) - VStack(alignment: .leading, spacing: 5) { - Text("\(popoverStay ? "Popover window will stay on top" : "Popover window will not stay on top")") - .font(.callout) - .foregroundStyle(.gray) - } - - InfoButton(text: "In mini window mode, if you pin the Files popover on top, clicking away from the window will not dismiss it. Otherwise, it will dismiss by clicking anywhere outside the popover.", color: nil, label: "") - - Spacer() - Toggle(isOn: $popoverStay, label: { - }) - .toggleStyle(.switch) - } - .padding(5) - .padding(.leading) - - // === Perms ================================================================================================ @@ -369,7 +208,7 @@ struct GeneralSettingsTab: View { } .padding(20) - .frame(width: 500, height: 690) + .frame(width: 500, height: 420) } diff --git a/Pearcleaner/Settings/Interface.swift b/Pearcleaner/Settings/Interface.swift new file mode 100644 index 0000000..322f55c --- /dev/null +++ b/Pearcleaner/Settings/Interface.swift @@ -0,0 +1,396 @@ +// +// Interface.swift +// Pearcleaner +// +// Created by Alin Lupascu on 3/18/24. +// + + +import Foundation +import SwiftUI +import ServiceManagement + +struct InterfaceSettingsTab: View { + @EnvironmentObject var appState: AppState + @EnvironmentObject var locations: Locations + @State private var windowSettings = WindowSettings() + @AppStorage("settings.menubar.enabled") private var menubarEnabled: Bool = false +// @AppStorage("settings.dock.enabled") private var dockEnabled: Bool = false + @AppStorage("settings.general.mini") private var mini: Bool = false + @AppStorage("displayMode") var displayMode: DisplayMode = .system + @AppStorage("settings.general.glass") private var glass: Bool = true + @AppStorage("settings.general.dark") var isDark: Bool = true + @AppStorage("settings.general.popover") private var popoverStay: Bool = true + @AppStorage("settings.general.miniview") private var miniView: Bool = true + @AppStorage("settings.general.selectedTheme") var selectedTheme: String = "Auto" + @AppStorage("settings.interface.selectedMenubarIcon") var selectedMenubarIcon: String = "pear-4" + private let themes = ["Auto", "Dark", "Light"] + @State private var isLaunchAtLoginEnabled: Bool = false + let icons = ["externaldrive", "trash", "folder", "pear-1", "pear-1.5", "pear-2", "pear-3", "pear-4"] + + @Binding var showPopover: Bool + @Binding var search: String + + var body: some View { + Form { + VStack { + + HStack() { + Text("Appearance").font(.title2) + Spacer() + } + .padding(.leading) + + HStack(spacing: 0) { + Image(systemName: glass ? "cube.transparent" : "cube.transparent.fill") + .resizable() + .scaledToFit() + .frame(width: 20, height: 20) + .padding(.trailing) + .foregroundStyle(.gray) + VStack(alignment: .leading, spacing: 5) { + Text("\(glass ? "Transparent material enabled" : "Transparent material disabled")") + .font(.callout) + .foregroundStyle(.gray) + } + Spacer() + Toggle(isOn: $glass, label: { + }) + .toggleStyle(.switch) + } + .padding(5) + .padding(.leading) + + + + HStack(spacing: 0) { + Image(systemName: displayMode.colorScheme == .dark ? "moon.fill" : "sun.max.fill") + .resizable() + .scaledToFit() + .frame(width: 20, height: 20) + .padding(.trailing) + .foregroundStyle(.gray) + VStack(alignment: .leading, spacing: 5) { + Text("Application color mode") + .font(.callout) + .foregroundStyle(.gray) + } + Spacer() + Picker("", selection: $selectedTheme) { + ForEach(themes, id: \.self) { theme in + Text(theme) + } + } + .pickerStyle(SegmentedPickerStyle()) + .frame(width: 200) + .onChange(of: selectedTheme) { newTheme in + switch newTheme { + case "Auto": + displayMode.colorScheme = nil + if isDarkModeEnabled() { + displayMode.colorScheme = .dark + } else { + displayMode.colorScheme = .light + } + case "Dark": + displayMode.colorScheme = .dark + case "Light": + displayMode.colorScheme = .light + default: + break + } + } + } + .padding(5) + .padding(.leading) + + + + // === Mini ================================================================================================= + + Divider() + .padding() + + + + HStack() { + Text("Mini Configuration").font(.title2) + Spacer() + } + .padding(.leading) + + HStack(spacing: 0) { + Image(systemName: mini ? "square.resize.up" : "square.resize.down") + .resizable() + .scaledToFit() + .frame(width: 20, height: 20) + .padding(.trailing) + .foregroundStyle(.gray) + VStack(alignment: .leading, spacing: 5) { + Text("\(mini ? "Mini window mode" : "Full size window mode")") + .font(.callout) + .foregroundStyle(.gray) + } + Spacer() + Toggle(isOn: $mini, label: { + }) + .toggleStyle(.switch) + .onChange(of: mini) { newVal in + if mini { + appState.currentView = miniView ? .apps : .empty + showPopover = false + resizeWindowAuto(windowSettings: windowSettings) + } else { + resizeWindowAuto(windowSettings: windowSettings) + if appState.appInfo.appName.isEmpty { + appState.currentView = .empty + } else { + appState.currentView = .files + } + } + } + } + .padding(5) + .padding(.leading) + + + HStack(spacing: 0) { + Image(systemName: miniView ? "square.grid.3x3.square" : "plus.square.dashed") + .resizable() + .scaledToFit() + .frame(width: 20, height: 20) + .padding(.trailing) + .foregroundStyle(.gray) + VStack(alignment: .leading, spacing: 5) { + Text("\(miniView ? "Show apps list on startup" : "Show drop target on startup")") + .font(.callout) + .foregroundStyle(.gray) + } + + InfoButton(text: "In mini window mode, you can have Pearcleaner startup to the Apps List view or the Drop Target view.", color: nil, label: "") + + Spacer() + Toggle(isOn: $miniView, label: { + }) + .toggleStyle(.switch) + .onChange(of: miniView) { newVal in + appState.currentView = newVal ? .apps : .empty + } + } + .padding(5) + .padding(.leading) + + + HStack(spacing: 0) { + Image(systemName: popoverStay ? "pin" : "pin.slash") + .resizable() + .scaledToFit() + .frame(width: 20, height: 20) + .padding(.trailing) + .foregroundStyle(.gray) + VStack(alignment: .leading, spacing: 5) { + Text("\(popoverStay ? "Popover window will stay on top" : "Popover window will not stay on top")") + .font(.callout) + .foregroundStyle(.gray) + } + + InfoButton(text: "In mini window mode, if you pin the Files popover on top, clicking away from the window will not dismiss it. Otherwise, it will dismiss by clicking anywhere outside the popover.", color: nil, label: "") + + Spacer() + Toggle(isOn: $popoverStay, label: { + }) + .toggleStyle(.switch) + } + .padding(5) + .padding(.leading) + + + // === MenuBar=============================================================================================== + + Divider() + .padding() + + HStack() { + Text("Menubar").font(.title2) + Spacer() + } + .padding(.leading) + + + HStack(spacing: 0) { + Image(systemName: "menubar.rectangle") + .resizable() + .scaledToFit() + .frame(width: 20, height: 20) + .padding(.trailing) + .foregroundStyle(.gray) + VStack(alignment: .leading, spacing: 5) { + Text("\(menubarEnabled ? "Menubar icon enabled" : "Menubar icon disabled")") + .font(.callout) + .foregroundStyle(.gray) + } + InfoButton(text: "When menubar icon is enabled, the main app window and dock icon will be hidden. You can still pop-out the main app window from the menubar icon temporarily if you'd like.", color: nil, label: "") + Spacer() + Toggle(isOn: $menubarEnabled, label: { + }) + .toggleStyle(.switch) + .onChange(of: menubarEnabled) { newVal in + if newVal { + MenuBarExtraManager.shared.addMenuBarExtra(withView: { + MenuBarMiniAppView(search: $search, showPopover: $showPopover) + .environmentObject(locations) + .environmentObject(appState) + }, icon: selectedMenubarIcon) + NSApplication.shared.setActivationPolicy(.accessory) + findAndShowWindows(named: ["Pearcleaner", "Interface"]) + } else { + MenuBarExtraManager.shared.removeMenuBarExtra() +// dockEnabled = true + NSApplication.shared.setActivationPolicy(.regular) + if !hasWindowOpen() { + if mini { + windowSettings.newWindow { + MiniMode(search: $search, showPopover: $showPopover) + .environmentObject(locations) + .environmentObject(appState) + } + } else { + windowSettings.newWindow { + RegularMode(search: $search, showPopover: $showPopover) + .environmentObject(locations) + .environmentObject(appState) + } + } + } + + } + } + + } + .padding(5) + .padding(.leading) + + + HStack(spacing: 0) { + Image(systemName: isLaunchAtLoginEnabled ? "person" : "person.slash") + .resizable() + .scaledToFit() + .frame(width: 20, height: 20) + .padding(.trailing) + .foregroundStyle(.gray) + VStack(alignment: .leading, spacing: 5) { + Text("\(isLaunchAtLoginEnabled ? "Launch at login enabled" : "Launch at login disabled")") + .font(.callout) + .foregroundStyle(.gray) + } + InfoButton(text: "This setting will affect Pearcleaner whether you're running in menubar mode or regular mode. If you disable menubar icon, you might want to disable this as well so Pearcleaner doesn't start on login.", color: nil, label: "") + Spacer() + Toggle(isOn: $isLaunchAtLoginEnabled, label: { + }) + .toggleStyle(.switch) + .onAppear { + isLaunchAtLoginEnabled = SMAppService.mainApp.status == .enabled + } + .onChange(of: isLaunchAtLoginEnabled) { newValue in + do { + if newValue { + if SMAppService.mainApp.status == .enabled { + try? SMAppService.mainApp.unregister() + } + + try SMAppService.mainApp.register() + } else { + try SMAppService.mainApp.unregister() + } + } catch { + printOS("Failed to \(newValue ? "enable" : "disable") launch at login: \(error.localizedDescription)") + } + } + + } + .padding(5) + .padding(.leading) + + + HStack(spacing: 0) { + Image(systemName: "paintbrush") + .resizable() + .scaledToFit() + .frame(width: 20, height: 20) + .padding(.trailing) + .foregroundStyle(.gray) + VStack(alignment: .leading, spacing: 5) { + Text("Menubar icon") + .font(.callout) + .foregroundStyle(.gray) + } + Spacer() + Picker("", selection: $selectedMenubarIcon) { + ForEach(icons, id: \.self) { icon in + HStack { + if icon.contains("pear") { + Image(icon) + .resizable() + .scaledToFit() + } else { + Image(systemName: icon) + .resizable() + .scaledToFit() + } + } + .tag(icon) + + } + } + .frame(width: 60) + .onChange(of: selectedMenubarIcon) { newValue in + MenuBarExtraManager.shared.swapMenuBarIcon(icon: newValue) + } + + } + .padding(5) + .padding(.leading) + +// HStack(spacing: 0) { +// Image(systemName: "dock.rectangle") +// .resizable() +// .scaledToFit() +// .frame(width: 20, height: 20) +// .padding(.trailing) +// .foregroundStyle(.gray) +// VStack(alignment: .leading, spacing: 5) { +// Text("\(dockEnabled ? "Show dock icon" : "Hide dock icon")") +// .font(.callout) +// .foregroundStyle(.gray) +// } +// InfoButton(text: "This setting only affects Pearcleaner when the menubar icon is enabled, otherwise the dock icon will always show", color: nil, label: "") +// Spacer() +// Toggle(isOn: $dockEnabled, label: { +// }) +// .toggleStyle(.switch) +// .onChange(of: dockEnabled) { newValue in +// if newValue { +// NSApplication.shared.setActivationPolicy(.regular) +// } else { +// if menubarEnabled { +// NSApplication.shared.setActivationPolicy(.accessory) +// } +// +// } +// } +// +// } +// .padding(5) +// .padding(.leading) + + Spacer() + } + + } + .padding(20) + .frame(width: 500, height: 520) + + } + +} + diff --git a/Pearcleaner/Settings/MenuBar.swift b/Pearcleaner/Settings/MenuBar.swift deleted file mode 100644 index eb43329..0000000 --- a/Pearcleaner/Settings/MenuBar.swift +++ /dev/null @@ -1,112 +0,0 @@ -// -// MenuBar.swift -// Pearcleaner -// -// Created by Alin Lupascu on 3/16/24. -// -import Foundation -import SwiftUI -import ServiceManagement - -struct MenuBarSettingsTab: View { - @EnvironmentObject var appState: AppState - @EnvironmentObject var locations: Locations - @AppStorage("settings.menubar.enabled") private var menubarEnabled: Bool = false - @State private var isLaunchAtLoginEnabled: Bool = false - - @Binding var showPopover: Bool - @Binding var search: String - - var body: some View { - Form { - VStack { - - HStack() { - Text("Configuration").font(.title2) - Spacer() - } - .padding(.leading) - - - HStack(spacing: 0) { - Image(systemName: "menubar.rectangle") - .resizable() - .scaledToFit() - .frame(width: 20, height: 20) - .padding(.trailing) - .foregroundStyle(.gray) - VStack(alignment: .leading, spacing: 5) { - Text("\(menubarEnabled ? "Menubar icon enabled" : "Menubar icon disabled")") - .font(.callout) - .foregroundStyle(.gray) - } - Spacer() - Toggle(isOn: $menubarEnabled, label: { - }) - .toggleStyle(.switch) - .onChange(of: menubarEnabled) { newVal in - if newVal { - MenuBarExtraManager.shared.addMenuBarExtra { - MenuBarMiniAppView(search: $search, showPopover: $showPopover) - .environmentObject(locations) - .environmentObject(appState) - } - } else { - MenuBarExtraManager.shared.removeMenuBarExtra() - } - } - - } - .padding(5) - .padding(.leading) - - - HStack(spacing: 0) { - Image(systemName: isLaunchAtLoginEnabled ? "person.crop.circle.badge.checkmark" : "person.crop.circle") - .resizable() - .scaledToFit() - .frame(width: 20, height: 20) - .padding(.trailing) - .foregroundStyle(.gray) - VStack(alignment: .leading, spacing: 5) { - Text("\(isLaunchAtLoginEnabled ? "Launch at login enabled" : "Launch at login disabled")") - .font(.callout) - .foregroundStyle(.gray) - } - Spacer() - Toggle(isOn: $isLaunchAtLoginEnabled, label: { - }) - .toggleStyle(.switch) - .onAppear { - isLaunchAtLoginEnabled = SMAppService.mainApp.status == .enabled - } - .onChange(of: isLaunchAtLoginEnabled) { newValue in - do { - if newValue { - if SMAppService.mainApp.status == .enabled { - try? SMAppService.mainApp.unregister() - } - - try SMAppService.mainApp.register() - } else { - try SMAppService.mainApp.unregister() - } - } catch { - printOS("Failed to \(newValue ? "enable" : "disable") launch at login: \(error.localizedDescription)") - } - } - - } - .padding(5) - .padding(.leading) - - Spacer() - } - - } - .padding(20) - .frame(width: 500, height: 690) - - } - -} diff --git a/Pearcleaner/Settings/SettingsWindow.swift b/Pearcleaner/Settings/SettingsWindow.swift index 5b3e1ab..c45e3b6 100644 --- a/Pearcleaner/Settings/SettingsWindow.swift +++ b/Pearcleaner/Settings/SettingsWindow.swift @@ -23,11 +23,11 @@ struct SettingsView: View { } .tag(CurrentTabView.general) - MenuBarSettingsTab(showPopover: $showPopover, search: $search) + InterfaceSettingsTab(showPopover: $showPopover, search: $search) .tabItem { - Label(CurrentTabView.menubar.title, systemImage: "menubar.rectangle") + Label(CurrentTabView.interface.title, systemImage: "macwindow") } - .tag(CurrentTabView.menubar) + .tag(CurrentTabView.interface) UpdateSettingsTab(showFeature: $showFeature) .tabItem { diff --git a/Pearcleaner/Settings/Update.swift b/Pearcleaner/Settings/Update.swift index 0b54b91..8396e36 100644 --- a/Pearcleaner/Settings/Update.swift +++ b/Pearcleaner/Settings/Update.swift @@ -107,7 +107,7 @@ struct UpdateSettingsTab: View { } .padding(20) - .frame(width: 500, height: 650) + .frame(width: 500, height: 520) } } diff --git a/Pearcleaner/Views/MenuBarMiniAppView.swift b/Pearcleaner/Views/MenuBarMiniAppView.swift index 9b5b397..3070e60 100644 --- a/Pearcleaner/Views/MenuBarMiniAppView.swift +++ b/Pearcleaner/Views/MenuBarMiniAppView.swift @@ -12,7 +12,7 @@ struct MenuBarMiniAppView: View { @Environment(\.colorScheme) var colorScheme @EnvironmentObject var appState: AppState @EnvironmentObject var locations: Locations - @State private var windowSettings = WindowSettings() +// @State private var windowSettings = WindowSettings() @State private var animateGradient: Bool = false @Binding var search: String @State private var showSys: Bool = true @@ -56,6 +56,7 @@ struct MenuBarMiniAppView: View { AppsListView(search: $search, showPopover: $showPopover, filteredApps: filteredApps).padding(0) HStack(spacing: 10) { + if #available(macOS 14.0, *) { SettingsLink() .buttonStyle(SimpleButtonStyle(icon: "gear", help: "Settings", color: Color("mode"))) @@ -66,16 +67,51 @@ struct MenuBarMiniAppView: View { .buttonStyle(SimpleButtonStyle(icon: "gear", help: "Settings", color: Color("mode"))) } + Button("") { + withAnimation(.easeInOut(duration: 0.5)) { + showPopover = false + updateOnMain { + appState.appInfo = .empty + appState.selectedZombieItems = [] + if appState.zombieFile.fileSize.keys.count == 0 { + appState.currentView = .zombie + appState.showProgress.toggle() + showPopover.toggle() + if instantSearch { + reversePathsSearch(appState: appState, locations: locations) + } else { + loadAllPaths(allApps: appState.sortedApps, appState: appState, locations: locations, reverseAddon: true) + } + } else { + appState.currentView = .zombie + showPopover.toggle() + } + } + + } + } + .buttonStyle(SimpleButtonStyle(icon: "clock.arrow.circlepath", help: "Leftover Files", color: Color("mode"))) + SearchBarMiniBottom(search: $search) Button("Main") { - windowSettings.newWindow { - MiniMode(search: $search, showPopover: $showPopover) - .environmentObject(locations) - .environmentObject(appState) - } + findAndShowWindows(named: ["Pearcleaner"]) +// if mini { +// windowSettings.newWindow { +// MiniMode(search: $search, showPopover: $showPopover) +// .environmentObject(locations) +// .environmentObject(appState) +// } +// } else { +// windowSettings.newWindow { +// RegularMode(search: $search, showPopover: $showPopover) +// .environmentObject(locations) +// .environmentObject(appState) +// } +// } + } - .buttonStyle(SimpleButtonStyle(icon: "macwindow", help: "Show Main Window", color: Color("mode"))) + .buttonStyle(SimpleButtonStyle(icon: "macwindow.on.rectangle", help: "Pop Out Window", color: Color("mode"))) Button("Kill") { NSApp.terminate(nil) diff --git a/Pearcleaner/Views/MiniMode.swift b/Pearcleaner/Views/MiniMode.swift index 20f19c7..209de49 100644 --- a/Pearcleaner/Views/MiniMode.swift +++ b/Pearcleaner/Views/MiniMode.swift @@ -65,6 +65,7 @@ struct MiniMode: View { // MARK: Background for whole app // .background(Color("bg").opacity(1)) // .background(VisualEffect(material: .sidebar, blendingMode: .behindWindow).edgesIgnoringSafeArea(.all)) + } } diff --git a/Pearcleaner/Logic/MenuBarItem.swift b/Pearcleaner/Windows/MenuBarItem.swift similarity index 66% rename from Pearcleaner/Logic/MenuBarItem.swift rename to Pearcleaner/Windows/MenuBarItem.swift index b43bd4a..5796ab9 100644 --- a/Pearcleaner/Logic/MenuBarItem.swift +++ b/Pearcleaner/Windows/MenuBarItem.swift @@ -13,7 +13,7 @@ class MenuBarExtraManager { private var statusItem: NSStatusItem? private var popover = NSPopover() - func addMenuBarExtra(withView view: @escaping () -> V) { + func addMenuBarExtra(withView view: @escaping () -> V, icon: String) { guard statusItem == nil else { return } // Initialize the status item @@ -21,7 +21,12 @@ class MenuBarExtraManager { // Set up the status item's button if let button = statusItem?.button { - button.image = NSImage(systemSymbolName: "menubar.rectangle", accessibilityDescription: "Pearcleaner") + if NSImage(systemSymbolName: icon, accessibilityDescription: nil) != nil { + button.image = NSImage(systemSymbolName: icon, accessibilityDescription: "Pearcleaner") + } else { + button.image = NSImage(named: icon) + + } button.action = #selector(togglePopover(_:)) button.target = self } @@ -40,14 +45,23 @@ class MenuBarExtraManager { } } - func getStatus() -> Bool { - if let item = statusItem { - return true + func swapMenuBarIcon(icon: String) { + guard let button = statusItem?.button else { return } + if let image = NSImage(systemSymbolName: icon, accessibilityDescription: nil) { + button.image = image } else { - return false + button.image = NSImage(named: icon) } } +// func getStatus() -> Bool { +// if let item = statusItem { +// return true +// } else { +// return false +// } +// } + @objc func togglePopover(_ sender: AnyObject?) { if let button = statusItem?.button { if popover.isShown { diff --git a/Pearcleaner/Logic/WindowSettings.swift b/Pearcleaner/Windows/WindowSettings.swift similarity index 94% rename from Pearcleaner/Logic/WindowSettings.swift rename to Pearcleaner/Windows/WindowSettings.swift index 03ddeba..ac6226c 100644 --- a/Pearcleaner/Logic/WindowSettings.swift +++ b/Pearcleaner/Windows/WindowSettings.swift @@ -15,7 +15,7 @@ class WindowSettings { private let windowXKey = "windowXKey" private let windowYKey = "windowYKey" @AppStorage("settings.general.mini") private var mini: Bool = false - var window: NSWindow? + var windows: [NSWindow] = [] func saveWindowSettings(frame: NSRect) { @@ -37,7 +37,6 @@ class WindowSettings { func newWindow(withView view: @escaping () -> V) { let contentView = view let frame = self.loadWindowSettings() - // Create the window and set the content view let newWindow = NSWindow( contentRect: frame, styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView], @@ -47,7 +46,8 @@ class WindowSettings { newWindow.center() newWindow.setFrameAutosaveName("Main Window") newWindow.contentView = NSHostingView(rootView: contentView()) - self.window = newWindow +// self.window = newWindow + self.windows.append(newWindow) newWindow.makeKeyAndOrderFront(nil) } } diff --git a/features.json b/features.json index 5f6b82e..8d3111c 100644 --- a/features.json +++ b/features.json @@ -1,4 +1,5 @@ { + "3.2.0": "- Menubar Item: You can choose to access Pearcleaner from the menubar. This will show a slightly modified Mini Mode view.", "3.1.0": "- Homebrew Cleanup: If you install apps using brew, you can now enable homebrew cleanup in the settings to have Pearcleaner alert Homebrew that the app was removed externally from Homebrew and keep the brew app list synced.", "3.0.0": "- Instant Search: Enable in settings to load app files on startup|- Semantic Versioning: Going forward will use semver (Ex. v0.0.0)|-Feature Alert: For each version, a feature alert will popup once on startup to show details|- Leftover File Cleaning: Search your mac for files leftover by uninstalled apps|- Sidebar Drag Handle: Resize the sidebar in regular mode|- Redesign UI/Settings/Icon: Some new buttons, layout changes, new official app icon and pear color theme accents|- File Sort: Sort files alphabetically or by size|- Socket File Removal: Finds socket files that aren't even visible in Finder with show hidden files enabled|- OSLog Output: Will print errors to the Console app for easier troubleshooting" }