diff --git a/Pearcleaner.xcodeproj/project.pbxproj b/Pearcleaner.xcodeproj/project.pbxproj index e837724..2ec85eb 100644 --- a/Pearcleaner.xcodeproj/project.pbxproj +++ b/Pearcleaner.xcodeproj/project.pbxproj @@ -19,7 +19,7 @@ C76D08482AF83C3F00D07867 /* Update.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76D08472AF83C3F00D07867 /* Update.swift */; }; C76D084A2AF83C6E00D07867 /* General.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76D08492AF83C6E00D07867 /* General.swift */; }; C76D08512AF850F300D07867 /* About.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76D08502AF850F300D07867 /* About.swift */; }; - C76D08552AF89CDE00D07867 /* AppListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76D08542AF89CDE00D07867 /* AppListView.swift */; }; + C76D08552AF89CDE00D07867 /* RegularMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76D08542AF89CDE00D07867 /* RegularMode.swift */; }; C77B90042AF18E2E009CC655 /* PearcleanerApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C77B90032AF18E2E009CC655 /* PearcleanerApp.swift */; }; C77B90082AF18E2F009CC655 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C77B90072AF18E2F009CC655 /* Assets.xcassets */; }; C77B90162AF19377009CC655 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = C77B90152AF19377009CC655 /* AppState.swift */; }; @@ -75,7 +75,7 @@ C76D08472AF83C3F00D07867 /* Update.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Update.swift; sourceTree = ""; }; C76D08492AF83C6E00D07867 /* General.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = General.swift; sourceTree = ""; }; C76D08502AF850F300D07867 /* About.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = About.swift; sourceTree = ""; }; - C76D08542AF89CDE00D07867 /* AppListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppListView.swift; sourceTree = ""; }; + C76D08542AF89CDE00D07867 /* RegularMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegularMode.swift; sourceTree = ""; }; C77B90002AF18E2E009CC655 /* Pearcleaner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Pearcleaner.app; sourceTree = BUILT_PRODUCTS_DIR; }; C77B90032AF18E2E009CC655 /* PearcleanerApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PearcleanerApp.swift; sourceTree = ""; }; C77B90072AF18E2F009CC655 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -219,7 +219,7 @@ isa = PBXGroup; children = ( C77B90222AF2D616009CC655 /* FilesView.swift */, - C76D08542AF89CDE00D07867 /* AppListView.swift */, + C76D08542AF89CDE00D07867 /* RegularMode.swift */, C7D31D472AFEB23700C7ED9E /* TopBar.swift */, C7045A292B03FAF000376976 /* TopBarMini.swift */, C7D31D492AFEB26700C7ED9E /* AppListItems.swift */, @@ -360,7 +360,7 @@ C7D31D482AFEB23700C7ED9E /* TopBar.swift in Sources */, C7ACADE62B92A737000B5845 /* Features.swift in Sources */, C7F539382AF60865007DF1B2 /* Utilities.swift in Sources */, - C76D08552AF89CDE00D07867 /* AppListView.swift in Sources */, + C76D08552AF89CDE00D07867 /* RegularMode.swift in Sources */, C7A9CE472B89164700EB6E78 /* Authorization.swift in Sources */, C7CF47242B3B3F1700979C5F /* WindowSettings.swift in Sources */, C77B90232AF2D616009CC655 /* FilesView.swift in Sources */, diff --git a/Pearcleaner/Logic/AppCommands.swift b/Pearcleaner/Logic/AppCommands.swift index a2c873a..9b1e811 100644 --- a/Pearcleaner/Logic/AppCommands.swift +++ b/Pearcleaner/Logic/AppCommands.swift @@ -38,13 +38,13 @@ struct AppCommands: Commands { } let sortedApps = getSortedApps() DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - appState.sortedApps.userApps = [] - appState.sortedApps.systemApps = [] - appState.sortedApps.userApps = sortedApps.userApps - appState.sortedApps.systemApps = sortedApps.systemApps + appState.sortedApps = [] +// appState.sortedApps.systemApps = [] + appState.sortedApps = sortedApps +// appState.sortedApps.systemApps = sortedApps.systemApps if instantSearch { Task(priority: .high){ - loadAllPaths(allApps: sortedApps.userApps + sortedApps.systemApps, appState: appState, locations: locations) + loadAllPaths(allApps: sortedApps, appState: appState, locations: locations) } } appState.reload.toggle() @@ -73,8 +73,7 @@ struct AppCommands: Commands { undoTrash(appState: appState) { let sortedApps = getSortedApps() DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - appState.sortedApps.userApps = sortedApps.userApps - appState.sortedApps.systemApps = sortedApps.systemApps + appState.sortedApps = sortedApps // if instantSearch { // loadAllPaths(allApps: sortedApps.userApps + sortedApps.systemApps, appState: appState, locations: locations) // } diff --git a/Pearcleaner/Logic/AppState.swift b/Pearcleaner/Logic/AppState.swift index e311ca4..f125300 100644 --- a/Pearcleaner/Logic/AppState.swift +++ b/Pearcleaner/Logic/AppState.swift @@ -15,7 +15,7 @@ class AppState: ObservableObject @Published var appInfo: AppInfo @Published var appInfoStore: [AppInfo] = [] @Published var zombieFile: ZombieFile - @Published var sortedApps: (userApps: [AppInfo], systemApps: [AppInfo]) = ([], []) + @Published var sortedApps: [AppInfo] = [] @Published var selectedItems = Set() @Published var selectedZombieItems = Set() @Published var alertType = AlertType.off @@ -41,6 +41,7 @@ class AppState: ObservableObject appIcon: nil, webApp: false, wrapped: false, + system: false, files: [], fileSize: [:], fileIcon: [:] @@ -68,6 +69,7 @@ struct AppInfo: Identifiable, Equatable, Hashable { let appIcon: NSImage? let webApp: Bool let wrapped: Bool + let system: Bool var files: [URL] var fileSize: [URL:Int64] var fileIcon: [URL:NSImage?] @@ -77,7 +79,7 @@ struct AppInfo: Identifiable, Equatable, Hashable { } - static let empty = AppInfo(id: UUID(), path: URL(fileURLWithPath: ""), bundleIdentifier: "", appName: "", appVersion: "", appIcon: nil, webApp: false, wrapped: false, files: [], fileSize: [:], fileIcon: [:]) + static let empty = AppInfo(id: UUID(), path: URL(fileURLWithPath: ""), bundleIdentifier: "", appName: "", appVersion: "", appIcon: nil, webApp: false, wrapped: false, system: false, files: [], fileSize: [:], fileIcon: [:]) } diff --git a/Pearcleaner/Logic/Logic.swift b/Pearcleaner/Logic/Logic.swift index 18bcd20..8b84d77 100644 --- a/Pearcleaner/Logic/Logic.swift +++ b/Pearcleaner/Logic/Logic.swift @@ -8,46 +8,27 @@ import Foundation import SwiftUI -// Get list of apps and sort it -func getSortedApps() -> (systemApps: [AppInfo], userApps: [AppInfo]) { - let apps = getApplications() - let sortedSystemApps = apps.systemApps - .compactMap { getAppInfo(atPath: $0) } - .sorted { $0.appName.replacingOccurrences(of: ".", with: "").lowercased() < $1.appName.replacingOccurrences(of: ".", with: "").lowercased() } - let sortedUserApps = apps.userApps - .compactMap { getAppInfo(atPath: $0) } - .sorted { $0.appName.replacingOccurrences(of: ".", with: "").lowercased() < $1.appName.replacingOccurrences(of: ".", with: "").lowercased() } - - return (systemApps: sortedSystemApps, userApps: sortedUserApps) -} - - -// Get all applications from /Applications and ~/Applications -func getApplications() -> (systemApps: [URL], userApps: [URL]) { +// Get all apps from /Applications and ~/Applications +func getSortedApps() -> [AppInfo] { let fileManager = FileManager.default // Define the paths for system and user applications let systemAppsPath = "/Applications" let userAppsPath = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Applications").path - var systemApps: [URL] = [] - var userApps: [URL] = [] + var apps: [URL] = [] - func collectAppPaths(at directoryPath: String, isSystem: Bool) { + func collectAppPaths(at directoryPath: String) { do { let appURLs = try fileManager.contentsOfDirectory(at: URL(fileURLWithPath: directoryPath), includingPropertiesForKeys: nil, options: []) for appURL in appURLs { - if appURL.pathExtension == "app" { - // Add the path to the appropriate list - if isSystem && !isRestricted(atPath: appURL) { - systemApps.append(appURL) - } else if !isSystem && !isRestricted(atPath: appURL) { - userApps.append(appURL) - } + if appURL.pathExtension == "app" && !isRestricted(atPath: appURL) { + // Add the path to the array + apps.append(appURL) } else if appURL.hasDirectoryPath { // If it's a directory, recursively explore it - collectAppPaths(at: appURL.path, isSystem: isSystem) + collectAppPaths(at: appURL.path) } } } catch { @@ -56,183 +37,154 @@ func getApplications() -> (systemApps: [URL], userApps: [URL]) { } } + // Collect system applications - collectAppPaths(at: systemAppsPath, isSystem: true) + collectAppPaths(at: systemAppsPath) // Collect user applications - collectAppPaths(at: userAppsPath, isSystem: false) - - // Filter out unwanted system folders like /Applications/Utilities - systemApps = systemApps.filter { !$0.absoluteString.contains("/Applications/Utilities/") } - return (systemApps, userApps) + collectAppPaths(at: userAppsPath) + + // Get app info and sort + let sortedApps = apps + .compactMap { getAppInfo(atPath: $0) } + .sorted { $0.appName.replacingOccurrences(of: ".", with: "").lowercased() < $1.appName.replacingOccurrences(of: ".", with: "").lowercased() } + + return sortedApps } // Get app bundle information from provided path -func getAppInfo(atPath path: URL) -> AppInfo? { - if let bundle = Bundle(url: path) { - if let bundleIdentifier = bundle.bundleIdentifier, - var appIconFileName = bundle.infoDictionary?["CFBundleIconFile"] as? String { - var appVersion = "0.0.0" - var appIcon: NSImage? - var appName: String? - var webApp: Bool? +func getAppInfo(atPath path: URL, wrapped: Bool = false) -> AppInfo? { + let filemanager = FileManager.default + let wrapperURL = path.appendingPathComponent("Wrapper") + if filemanager.fileExists(atPath: wrapperURL.path) { + do { + let contents = try filemanager.contentsOfDirectory(at: wrapperURL, includingPropertiesForKeys: nil, options: []) + let appFiles = contents.filter { $0.pathExtension == "app" } - - if let shortVersion = bundle.infoDictionary?["CFBundleShortVersionString"] as? String { - appVersion = shortVersion - } else { - if let bundleVersion = bundle.infoDictionary?["CFBundleVersion"] as? String { - appVersion = bundleVersion - } else { - printOS("Failed to retrieve bundle version") + if let firstAppFile = appFiles.first { + let fullPath = wrapperURL.appendingPathComponent(firstAppFile.lastPathComponent) + if let wrappedAppInfo = getAppInfo(atPath: fullPath, wrapped: true) { + return wrappedAppInfo } - } - - if let localizedName = bundle.localizedInfoDictionary?[kCFBundleNameKey as String] as? String { - appName = localizedName - } else if let bundleName = bundle.infoDictionary?["CFBundleName"] as? String { - appName = bundleName } else { - appName = path.deletingPathExtension().lastPathComponent + printOS("No .app files found in the 'Wrapper' directory: \(wrapperURL)") } - // Check if the icon file name has an .icns extension, if not, add it - if !appIconFileName.hasSuffix(".icns") { - appIconFileName += ".icns" - } - - // Try to find the icon in the main bundle - if let iconPath = bundle.path(forResource: appIconFileName, ofType: nil), - let icon = NSImage(contentsOfFile: iconPath) { - appIcon = icon - } - - // If not found, try to find it in the resources directory - if appIcon == nil, - let resourcesPath = bundle.resourcePath { - let iconURL = URL(fileURLWithPath: resourcesPath).appendingPathComponent(appIconFileName) - if let icon = NSImage(contentsOfFile: iconURL.path) { + } catch { + printOS("Error reading contents of 'Wrapper' directory: \(error.localizedDescription)\n\(wrapperURL)") + } + } else { + if let bundle = Bundle(url: path) { + if let bundleIdentifier = bundle.bundleIdentifier { + var appIconFileName = bundle.infoDictionary?["CFBundleIconFile"] as? String ?? "" + var appVersion: String? + var appIcon: NSImage? + var appName: String? + var webApp: Bool? + var wrappedApp: Bool? + var system: Bool? + + if let shortVersion = bundle.infoDictionary?["CFBundleShortVersionString"] as? String, !shortVersion.isEmpty { + appVersion = shortVersion + } else { + if let bundleVersion = bundle.infoDictionary?["CFBundleVersion"] as? String, !bundleVersion.isEmpty { + appVersion = bundleVersion + } else { + printOS("Failed to retrieve bundle version") + } + } + + if let localizedName = bundle.localizedInfoDictionary?[kCFBundleNameKey as String] as? String { + appName = localizedName + } else if let bundleName = bundle.infoDictionary?["CFBundleName"] as? String { + appName = bundleName + } else { + appName = path.deletingPathExtension().lastPathComponent + } + + if appIconFileName.isEmpty { + if let iconsDict = bundle.infoDictionary?["CFBundleIcons"] as? [String: Any] { + if let primaryIconDict = iconsDict["CFBundlePrimaryIcon"] as? [String: Any], + let iconFiles = primaryIconDict["CFBundleIconFiles"] as? [String], + let primaryIconFile = iconFiles.first { + // Now, you can use the primaryIconFile to construct the path to the icon file + let iconPath = path.appendingPathComponent(primaryIconFile) + appIconFileName = iconPath.path + + if let contents = try? FileManager.default.contentsOfDirectory(at: path, includingPropertiesForKeys: nil) { + // Find the first file that matches the specified prefix + if let foundURL = contents.first(where: { $0.lastPathComponent.hasPrefix(primaryIconFile) }), + let image = NSImage(contentsOfFile: foundURL.path) { + appIcon = image + + + } else { + printOS("No matching image found for \(primaryIconFile) in wrapped app.") + } + } else { + printOS("Unable to access the directory at \(primaryIconFile) for wrapped app.") + } + + } + } + } + + // Check if the icon file name has an .icns extension, if not, add it + if !appIconFileName.hasSuffix(".icns") { + appIconFileName += ".icns" + } + + // Try to find the icon in the main bundle + if let iconPath = bundle.path(forResource: appIconFileName, ofType: nil), + let icon = NSImage(contentsOfFile: iconPath) { appIcon = icon } - } - - // Convert the icon to a 100x100 PNG image - if let pngIcon = appIcon.flatMap({ convertICNSToPNG(icon: $0, size: NSSize(width: 100, height: 100)) }) { - appIcon = pngIcon - } - - if appIcon == nil { - printOS("App Icon not found for app at path: \(path)") - } - - if bundle.infoDictionary?["LSTemplateApplication"] is Bool { - webApp = true - } else { - webApp = false - } - - - return AppInfo(id: UUID(), path: path, bundleIdentifier: bundleIdentifier, appName: appName ?? "", appVersion: appVersion, appIcon: appIcon, webApp: webApp ?? false, wrapped: false, files: [], fileSize: [:], fileIcon: [:]) - - } else { - let wrapperURL = path.appendingPathComponent("Wrapper") - - // Check that file path exists, exit if not - if FileManager.default.fileExists(atPath: wrapperURL.path) { - do { - let contents = try FileManager.default.contentsOfDirectory(at: wrapperURL, includingPropertiesForKeys: nil, options: []) - let appFiles = contents.filter { $0.pathExtension == "app" } - - if let firstAppFile = appFiles.first { - let fullPath = wrapperURL.appendingPathComponent(firstAppFile.lastPathComponent) - if let wrappedAppInfo = getWrappedAppInfo(atPath: fullPath) { - return wrappedAppInfo - } - } else { - printOS("No .app files found in the 'Wrapper' directory.") - } - } catch { - printOS("Error reading contents of 'Wrapper' directory: \(error.localizedDescription)") - } - } else { - printOS("Error: 'Wrapper' directory not found at path: \(wrapperURL.path)") - } - - } - } else { - printOS("Bundle not found at path: \(path)") - } - return nil -} - - -func getWrappedAppInfo(atPath path: URL) -> AppInfo? { - if let bundle = Bundle(url: path) { - if let bundleIdentifier = bundle.bundleIdentifier, - let appVersion = bundle.infoDictionary?["CFBundleShortVersionString"] as? String { - var appIconFileName = bundle.infoDictionary?["CFBundleIconFile"] as? String - var appIcon: NSImage? - var appName: String? - var webApp: Bool? - - if let localizedName = bundle.localizedInfoDictionary?[kCFBundleNameKey as String] as? String { - appName = localizedName - } else if let bundleName = bundle.infoDictionary?["CFBundleName"] as? String { - appName = bundleName - } else { - appName = path.deletingPathExtension().lastPathComponent - } - - if appIconFileName == nil { - if let iconsDict = bundle.infoDictionary?["CFBundleIcons"] as? [String: Any] { - if let primaryIconDict = iconsDict["CFBundlePrimaryIcon"] as? [String: Any], - let iconFiles = primaryIconDict["CFBundleIconFiles"] as? [String], - let primaryIconFile = iconFiles.first { - // Now, you can use the primaryIconFile to construct the path to the icon file - let iconPath = path.appendingPathComponent(primaryIconFile) - appIconFileName = iconPath.path - - if let contents = try? FileManager.default.contentsOfDirectory(at: path, includingPropertiesForKeys: nil) { - // Find the first file that matches the specified prefix - if let foundURL = contents.first(where: { $0.lastPathComponent.hasPrefix(primaryIconFile) }), - let image = NSImage(contentsOfFile: foundURL.path) { - appIcon = image - - - } else { - printOS("No matching image found for \(primaryIconFile).") - } - } else { - printOS("Unable to access the directory at \(primaryIconFile).") - } + // If not found, try to find it in the resources directory + if appIcon == nil, + let resourcesPath = bundle.resourcePath { + let iconURL = URL(fileURLWithPath: resourcesPath).appendingPathComponent(appIconFileName) + if let icon = NSImage(contentsOfFile: iconURL.path) { + appIcon = icon } } - } - // Convert the icon to a 100x100 PNG image - if let pngIcon = appIcon.flatMap({ convertICNSToPNG(icon: $0, size: NSSize(width: 100, height: 100)) }) { - appIcon = pngIcon - } + // Convert the icon to a 100x100 PNG image + if let pngIcon = appIcon.flatMap({ convertICNSToPNG(icon: $0, size: NSSize(width: 100, height: 100)) }) { + appIcon = pngIcon + } - if appIcon == nil { - printOS("App Icon not found for app at path: \(path)") - } + if appIcon == nil { + printOS("App Icon not found for app at path: \(path)") + } + + if bundle.infoDictionary?["LSTemplateApplication"] is Bool { + webApp = true + } else { + webApp = false + } + + if wrapped { + wrappedApp = true + } else { + wrappedApp = false + } + + if !path.path.contains(home) { + system = true + } else { + system = false + } + + + return AppInfo(id: UUID(), path: path, bundleIdentifier: bundleIdentifier, appName: appName ?? "", appVersion: appVersion ?? "", appIcon: appIcon, webApp: webApp ?? false, wrapped: wrappedApp ?? false, system: system ?? false, files: [], fileSize: [:], fileIcon: [:]) - if bundle.infoDictionary?["LSTemplateApplication"] is Bool { - webApp = true } else { - webApp = false + printOS("Bundle identifier not found at path: \(path)") } - - - return AppInfo(id: UUID(), path: path, bundleIdentifier: bundleIdentifier, appName: appName ?? "", appVersion: appVersion, appIcon: appIcon, webApp: webApp ?? false, wrapped: true, files: [], fileSize: [:], fileIcon: [:]) - } else { - printOS("One or more variables missing for app at path: \(path)") + printOS("Bundle not found at path: \(path)") } - } else { - printOS("Bundle not found at path: \(path)") } return nil } @@ -487,6 +439,7 @@ func loadAllPaths(allApps: [AppInfo], appState: AppState, locations: Locations, } } else { printOS("loadAllPaths - Retry limit timed out. Unable to load all paths within 1 minute.") + completion() } } } diff --git a/Pearcleaner/Logic/Styles.swift b/Pearcleaner/Logic/Styles.swift index 21e8133..020ceca 100644 --- a/Pearcleaner/Logic/Styles.swift +++ b/Pearcleaner/Logic/Styles.swift @@ -317,8 +317,7 @@ struct AnimatedSearchStyle: TextFieldStyle { appState.reload.toggle() let sortedApps = getSortedApps() DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - appState.sortedApps.userApps = sortedApps.userApps - appState.sortedApps.systemApps = sortedApps.systemApps + appState.sortedApps = sortedApps appState.reload.toggle() } } @@ -431,10 +430,15 @@ struct SimpleSearchStyle: TextFieldStyle { .onHover { hovering in withAnimation(Animation.easeInOut(duration: 0.15)) { self.isHovered = hovering - self.isFocused = hovering + self.isFocused = true } } .focused($isFocused) + .onAppear { + updateOnMain { + self.isFocused = false + } + } } } diff --git a/Pearcleaner/Logic/Utilities.swift b/Pearcleaner/Logic/Utilities.swift index 4e255d2..6ae415d 100644 --- a/Pearcleaner/Logic/Utilities.swift +++ b/Pearcleaner/Logic/Utilities.swift @@ -8,11 +8,19 @@ import Foundation import SwiftUI import OSLog +import CoreImage +import AppKit // Make updates on main thread -func updateOnMain(_ updates: @escaping () -> Void) { - DispatchQueue.main.async { - updates() +func updateOnMain(after delay: Double? = nil, _ updates: @escaping () -> Void) { + if let delay = delay { + DispatchQueue.main.asyncAfter(deadline: .now() + delay) { + updates() + } + } else { + DispatchQueue.main.async { + updates() + } } } @@ -170,7 +178,7 @@ func openTrash() { // Check if restricted app func isRestricted(atPath path: URL) -> Bool { - if path.path.contains("Safari") || path.path.contains(Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String ?? "") { + if path.path.contains("Safari") || path.path.contains(Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String ?? "") || path.path.contains("/Applications/Utilities") { return true } else { return false @@ -209,6 +217,48 @@ func getIconForFileOrFolderNS(atPath path: URL) -> NSImage? { return nsImage } +// Get average color from image +//extension NSImage { +// var averageColor2: NSColor? { +// guard let inputImage = CIImage(image: self) else { return nil } +// let extentVector = CIVector(x: inputImage.extent.origin.x, y: inputImage.extent.origin.y, z: inputImage.extent.size.width, w: inputImage.extent.size.height) +// +// guard let filter = CIFilter(name: "CIAreaAverage", parameters: [kCIInputImageKey: inputImage, kCIInputExtentKey: extentVector]) else { return nil } +// guard let outputImage = filter.outputImage else { return nil } +// +// var bitmap = [UInt8](repeating: 0, count: 4) +// let context = CIContext(options: [.workingColorSpace: kCFNull!]) +// context.render(outputImage, toBitmap: &bitmap, rowBytes: 4, bounds: CGRect(x: 0, y: 0, width: 1, height: 1), format: .RGBA8, colorSpace: nil) +// +// return NSColor(red: CGFloat(bitmap[0]) / 255, green: CGFloat(bitmap[1]) / 255, blue: CGFloat(bitmap[2]) / 255, alpha: CGFloat(bitmap[3]) / 255) +// } +//} + +extension NSImage { + var averageColor: NSColor? { + guard let tiffData = self.tiffRepresentation, let bitmapImage = NSBitmapImageRep(data: tiffData), let inputImage = CIImage(bitmapImageRep: bitmapImage) else { return nil } + + let extentVector = CIVector(x: inputImage.extent.origin.x, y: inputImage.extent.origin.y, z: inputImage.extent.size.width, w: inputImage.extent.size.height) + + guard let filter = CIFilter(name: "CIAreaAverage", parameters: [kCIInputImageKey: inputImage, kCIInputExtentKey: extentVector]) else { return nil } + guard let outputImage = filter.outputImage else { return nil } + + var bitmap = [UInt8](repeating: 0, count: 4) + let context = CIContext(options: [.workingColorSpace: NSNull()]) + context.render(outputImage, toBitmap: &bitmap, rowBytes: 4, bounds: CGRect(x: 0, y: 0, width: 1, height: 1), format: CIFormat.RGBA8, colorSpace: nil) + + return NSColor(red: CGFloat(bitmap[0]) / 255, green: CGFloat(bitmap[1]) / 255, blue: CGFloat(bitmap[2]) / 255, alpha: CGFloat(bitmap[3]) / 255) + } +} + +func startEnd(_ function: @escaping () -> Void) { + let startTime = Date() // Capture start time + function() + let endTime = Date() + let executionTime = endTime.timeIntervalSince(startTime) + print("Function executed in: \n\(executionTime) seconds") +} + // Relaunch app func relaunchApp(afterDelay seconds: TimeInterval = 0.5) -> Never { @@ -221,6 +271,25 @@ func relaunchApp(afterDelay seconds: TimeInterval = 0.5) -> Never { exit(0) } +// Remove app from cache +func removeApp(appState: AppState, withId id: UUID) { + DispatchQueue.main.async { + // Remove from sortedApps if found + if let index = appState.sortedApps.firstIndex(where: { $0.id == id }) { + appState.sortedApps.remove(at: index) + 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 }) { + appState.appInfoStore.remove(at: index) + } + // Clear out AppInfo state + appState.appInfo = AppInfo.empty + } +} + + // --- Extend Int to convert hours to seconds --- extension Int { diff --git a/Pearcleaner/PearcleanerApp.swift b/Pearcleaner/PearcleanerApp.swift index 14daf02..3db173f 100644 --- a/Pearcleaner/PearcleanerApp.swift +++ b/Pearcleaner/PearcleanerApp.swift @@ -33,7 +33,7 @@ struct PearcleanerApp: App { Group { ZStack() { if !mini { - AppListView(search: $search, showPopover: $showPopover) + RegularMode(search: $search, showPopover: $showPopover) .environmentObject(locations) } else { MiniMode(search: $search, showPopover: $showPopover) @@ -91,16 +91,17 @@ struct PearcleanerApp: App { // Get Apps let sortedApps = getSortedApps() - appState.sortedApps.userApps = sortedApps.userApps - appState.sortedApps.systemApps = sortedApps.systemApps + appState.sortedApps = sortedApps // Find all app paths on load if instantSearch { - loadAllPaths(allApps: sortedApps.userApps + sortedApps.systemApps, appState: appState, locations: locations) +// startEnd { + loadAllPaths(allApps: sortedApps, appState: appState, locations: locations) +// } } - + #if !DEBUG Task { diff --git a/Pearcleaner/Resources/Info.plist b/Pearcleaner/Resources/Info.plist index 7f3e25f..16c1874 100644 --- a/Pearcleaner/Resources/Info.plist +++ b/Pearcleaner/Resources/Info.plist @@ -32,13 +32,5 @@ - SUPublicEDKey - cD156JWtfwtgFJH2RApyj9Bgc0fCqLBGEf9C9IRMCWE= - SUFeedURL - https://raw.githubusercontent.com/alienator88/Pearcleaner/main/appcast.xml - SUEnableInstallerLauncherService - - SUEnableDownloaderService - diff --git a/Pearcleaner/Views/AppListItems.swift b/Pearcleaner/Views/AppListItems.swift index e78ce99..10d591f 100644 --- a/Pearcleaner/Views/AppListItems.swift +++ b/Pearcleaner/Views/AppListItems.swift @@ -16,6 +16,7 @@ struct AppListItems: View { @Environment(\.colorScheme) var colorScheme @AppStorage("settings.general.mini") private var mini: Bool = false @AppStorage("settings.general.popover") private var popoverStay: Bool = true + @AppStorage("displayMode") var displayMode: DisplayMode = .system @Binding var showPopover: Bool @EnvironmentObject var locations: Locations let itemId = UUID() @@ -25,7 +26,7 @@ struct AppListItems: View { } var body: some View { - VStack(alignment: .leading, spacing: 5) { +// VStack(alignment: .leading, spacing: 5) { @@ -39,11 +40,27 @@ struct AppListItems: View { } if let appIcon = appInfo.appIcon { - Image(nsImage: appIcon) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 30, height: 30) - .clipShape(RoundedRectangle(cornerRadius: 8)) + ZStack { +// RoundedRectangle(cornerRadius: 8) +// .fill(Color(appIcon.averageColor!)) +// .frame(width: 35, height: 35) +// .saturation(3) +// .opacity(0.5) +//// .brightness(displayMode.colorScheme == .dark ? 0 : 0.5) +//// .shadow(color: .black, radius: 1, y: 2) +// RoundedRectangle(cornerRadius: 8) +// .strokeBorder(Color("mode").opacity(0.1), lineWidth: 1) +// .frame(width: 35, height: 35) + Image(nsImage: appIcon) + .resizable() + .aspectRatio(contentMode: .fit) +// .scaledToFit() + .frame(width: 30, height: 30) + .clipShape(RoundedRectangle(cornerRadius: 8)) + .shadow(color: .black.opacity(0.5), radius: 2, y: 2) +// .grayscale(opacityForItem()) + } + } VStack(alignment: .leading, spacing: 5) { @@ -84,17 +101,30 @@ struct AppListItems: View { .foregroundStyle(Color("mode").opacity(0.5)) } - - } - .padding(.horizontal, 5) - .help(appInfo.appName) - .onHover { hovering in - withAnimation(Animation.easeIn(duration: 0.2)) { - self.isHovered = hovering + .padding(.horizontal, 5) + .help(appInfo.appName) + .onHover { hovering in + withAnimation(Animation.easeIn(duration: 0.2)) { + self.isHovered = hovering + } } - } - .onTapGesture { - showAppInFiles(appInfo: appInfo, mini: mini, appState: appState, locations: locations, showPopover: $showPopover) - } + .onTapGesture { + withAnimation(Animation.easeInOut(duration: 0.4)) { + showAppInFiles(appInfo: appInfo, mini: mini, appState: appState, locations: locations, showPopover: $showPopover) + } + } + } + + func opacityForItem() -> Double { + // Check if any item is selected + let isAnyItemSelected = appState.sortedApps.contains(where: { $0.path == appState.appInfo.path }) + // If this item is selected or no items are selected, keep full opacity + // Otherwise, reduce opacity + return isAnyItemSelected ? (appState.appInfo.path == appInfo.path ? 0 : 1.0) : 0 + } + } + + + diff --git a/Pearcleaner/Views/AppsListView.swift b/Pearcleaner/Views/AppsListView.swift index 6e15b87..ecd9208 100644 --- a/Pearcleaner/Views/AppsListView.swift +++ b/Pearcleaner/Views/AppsListView.swift @@ -11,17 +11,19 @@ import SwiftUI struct AppsListView: View { @Binding var search: String @Binding var showPopover: Bool - var filteredUserApps: [AppInfo] - var filteredSystemApps: [AppInfo] + var filteredApps: [AppInfo] var body: some View { ScrollView { LazyVStack(alignment: .leading, spacing: 0, pinnedViews: [.sectionHeaders]) { - if filteredUserApps.count > 0 { + let filteredUserApps = filteredApps.filter { !$0.system } + let filteredSystemApps = filteredApps.filter { $0.system } + + if !filteredUserApps.isEmpty { SectionView(title: "User", count: filteredUserApps.count, apps: filteredUserApps, search: $search, showPopover: $showPopover) } - if filteredSystemApps.count > 0 { + if !filteredSystemApps.isEmpty { SectionView(title: "System", count: filteredSystemApps.count, apps: filteredSystemApps, search: $search, showPopover: $showPopover) } } diff --git a/Pearcleaner/Views/FilesView.swift b/Pearcleaner/Views/FilesView.swift index 46ff7e7..3ab8811 100644 --- a/Pearcleaner/Views/FilesView.swift +++ b/Pearcleaner/Views/FilesView.swift @@ -54,6 +54,7 @@ struct FilesView: View { .padding([.horizontal, .top], 5) } VStack(spacing: 0) { + // Main Group HStack(alignment: .center) { @@ -93,8 +94,6 @@ struct FilesView: View { } - - if appState.appInfo.webApp || appState.appInfo.wrapped { HStack(alignment: .center, spacing: 10) { Spacer() @@ -121,15 +120,21 @@ struct FilesView: View { } + Text(appState.appInfo.system ? "system" : "user") + .font(.footnote) + .foregroundStyle(Color("mode").opacity(0.5)) + .frame(minWidth: 30, minHeight: 15) + .padding(2) + .padding(.horizontal, 2) + .background(Color("mode").opacity(0.1)) + .clipShape(.capsule) + } - } - - - } .padding(.horizontal, 20) .padding(.top, 0) + } Divider() @@ -184,7 +189,7 @@ struct FilesView: View { Button("Uninstall") { Task { updateOnMain { - appState.appInfo = AppInfo.empty +// appState.appInfo = AppInfo.empty search = "" if mini { appState.currentView = .apps @@ -214,7 +219,9 @@ struct FilesView: View { appState.isReminderVisible.toggle() } } - refreshAppList(appState.appInfo) + removeApp(appState: appState, withId: appState.appInfo.id) + +// refreshAppList(appState.appInfo) } } @@ -243,6 +250,7 @@ struct FilesView: View { } .transition(.opacity) .padding([.horizontal, .bottom], 20) + .padding(.top, !mini ? 10 : 0) } @@ -253,8 +261,8 @@ struct FilesView: View { showPopover = false let sortedApps = getSortedApps() DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - appState.sortedApps.userApps = sortedApps.userApps - appState.sortedApps.systemApps = sortedApps.systemApps + appState.sortedApps = sortedApps +// appState.sortedApps.systemApps = sortedApps.systemApps // if instantSearch { // loadAllPaths(allApps: sortedApps.userApps + sortedApps.systemApps, appState: appState, locations: locations) // } diff --git a/Pearcleaner/Views/MiniMode.swift b/Pearcleaner/Views/MiniMode.swift index d992684..b36bc92 100644 --- a/Pearcleaner/Views/MiniMode.swift +++ b/Pearcleaner/Views/MiniMode.swift @@ -25,16 +25,17 @@ struct MiniMode: View { // Main Mini View VStack(spacing: 0) { - if appState.currentView == .empty { - TopBarMini(search: $search, showPopover: $showPopover) - MiniEmptyView(showPopover: $showPopover) - } else { - TopBarMini(search: $search, showPopover: $showPopover) - MiniAppView(search: $search, showPopover: $showPopover) + Group { + if appState.currentView == .empty { + TopBarMini(search: $search, showPopover: $showPopover) + MiniEmptyView(showPopover: $showPopover) + } else { + TopBarMini(search: $search, showPopover: $showPopover) + MiniAppView(search: $search, showPopover: $showPopover) + } } - + .transition(.opacity) } - .transition(.move(edge: .leading)) .popover(isPresented: $showPopover, arrowEdge: .trailing) { VStack { if appState.currentView == .files { @@ -149,19 +150,11 @@ struct MiniAppView: View { var body: some View { - var filteredUserApps: [AppInfo] { + var filteredApps: [AppInfo] { if search.isEmpty { - return appState.sortedApps.userApps + return appState.sortedApps } else { - return appState.sortedApps.userApps.filter { $0.appName.localizedCaseInsensitiveContains(search) } - } - } - - var filteredSystemApps: [AppInfo] { - if search.isEmpty { - return appState.sortedApps.systemApps - } else { - return appState.sortedApps.systemApps.filter { $0.appName.localizedCaseInsensitiveContains(search) } + return appState.sortedApps.filter { $0.appName.localizedCaseInsensitiveContains(search) } } } @@ -186,7 +179,7 @@ struct MiniAppView: View { } else { VStack(alignment: .center) { - AppsListView(search: $search, showPopover: $showPopover, filteredUserApps: filteredUserApps, filteredSystemApps: filteredSystemApps) + AppsListView(search: $search, showPopover: $showPopover, filteredApps: filteredApps) if appState.currentView != .empty { SearchBarMiniBottom(search: $search).background(.ultraThinMaterial) @@ -198,7 +191,7 @@ struct MiniAppView: View { } } - .transition(.move(edge: .leading)) + .transition(.opacity) } } diff --git a/Pearcleaner/Views/AppListView.swift b/Pearcleaner/Views/RegularMode.swift similarity index 77% rename from Pearcleaner/Views/AppListView.swift rename to Pearcleaner/Views/RegularMode.swift index 4146108..5d2e51a 100644 --- a/Pearcleaner/Views/AppListView.swift +++ b/Pearcleaner/Views/RegularMode.swift @@ -8,7 +8,7 @@ import Foundation import SwiftUI -struct AppListView: View { +struct RegularMode: View { @EnvironmentObject var appState: AppState @AppStorage("settings.general.glass") private var glass: Bool = false @AppStorage("settings.general.sidebarWidth") private var sidebarWidth: Double = 280 @@ -17,22 +17,18 @@ struct AppListView: View { @State private var showSys: Bool = true @State private var showUsr: Bool = true @Binding var showPopover: Bool +// @State private var isHovering = false +// @State private var isHovering2 = false - var filteredUserApps: [AppInfo] { + + var filteredApps: [AppInfo] { if search.isEmpty { - return appState.sortedApps.userApps + return appState.sortedApps } else { - return appState.sortedApps.userApps.filter { $0.appName.localizedCaseInsensitiveContains(search) } + return appState.sortedApps.filter { $0.appName.localizedCaseInsensitiveContains(search) } } } - var filteredSystemApps: [AppInfo] { - if search.isEmpty { - return appState.sortedApps.systemApps - } else { - return appState.sortedApps.systemApps.filter { $0.appName.localizedCaseInsensitiveContains(search) } - } - } var body: some View { @@ -66,37 +62,54 @@ struct AppListView: View { .padding(.top, 20) - AppsListView(search: $search, showPopover: $showPopover, filteredUserApps: filteredUserApps, filteredSystemApps: filteredSystemApps) + AppsListView(search: $search, showPopover: $showPopover, filteredApps: filteredApps) } .frame(width: sidebarWidth) .padding(.vertical) +// .onHover { hovering in +// withAnimation(Animation.easeInOut(duration: 0.4)) { +// isHovering = hovering +// } +// } +// .opacity(!isHovering ? 1 : 0.5) + } } .background(glass ? GlassEffect(material: .sidebar, blendingMode: .behindWindow).edgesIgnoringSafeArea(.all) : nil) - .transition(.move(edge: .leading)) + .transition(.opacity) SlideableDivider(dimension: $sidebarWidth) // Details View VStack(spacing: 0) { - if appState.currentView == .empty || appState.currentView == .apps { - TopBar(showPopover: $showPopover) - AppDetailsEmptyView(showPopover: $showPopover) - } else if appState.currentView == .files { - TopBar(showPopover: $showPopover) - FilesView(showPopover: $showPopover, search: $search) - .id(appState.appInfo.id) - } else if appState.currentView == .zombie { - TopBar(showPopover: $showPopover) - ZombieView(showPopover: $showPopover, search: $search) - .id(appState.appInfo.id) + Group { + if appState.currentView == .empty || appState.currentView == .apps { + TopBar(showPopover: $showPopover) + AppDetailsEmptyView(showPopover: $showPopover) + } else if appState.currentView == .files { + TopBar(showPopover: $showPopover) + FilesView(showPopover: $showPopover, search: $search) + .id(appState.appInfo.id) + } else if appState.currentView == .zombie { + TopBar(showPopover: $showPopover) + ZombieView(showPopover: $showPopover, search: $search) + .id(appState.appInfo.id) + } } + .transition(.opacity) } - .transition(.move(edge: .leading)) +// .onHover { hovering in +// withAnimation(Animation.easeInOut(duration: 0.4)) { +// isHovering = hovering +// } +// } +// .opacity((isHovering2 || (!isHovering && !isHovering2)) ? 1 : 0.5) + + } .frame(minWidth: 900, minHeight: 600) .edgesIgnoringSafeArea(.all) @@ -187,10 +200,9 @@ struct Header: View { showPopover = false let sortedApps = getSortedApps() DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - appState.sortedApps.userApps = sortedApps.userApps - appState.sortedApps.systemApps = sortedApps.systemApps + appState.sortedApps = sortedApps if instantSearch { - loadAllPaths(allApps: sortedApps.userApps + sortedApps.systemApps, appState: appState, locations: locations) { + loadAllPaths(allApps: sortedApps, appState: appState, locations: locations) { appState.reload.toggle() } diff --git a/Pearcleaner/Views/TopBar.swift b/Pearcleaner/Views/TopBar.swift index a25b218..423e7e8 100644 --- a/Pearcleaner/Views/TopBar.swift +++ b/Pearcleaner/Views/TopBar.swift @@ -53,7 +53,7 @@ struct TopBar: View { if instantSearch { reversePathsSearch(appState: appState, locations: locations) } else { - loadAllPaths(allApps: appState.sortedApps.userApps + appState.sortedApps.systemApps, appState: appState, locations: locations, reverseAddon: true) + loadAllPaths(allApps: appState.sortedApps, appState: appState, locations: locations, reverseAddon: true) } } else { appState.currentView = .zombie diff --git a/Pearcleaner/Views/TopBarMini.swift b/Pearcleaner/Views/TopBarMini.swift index 84004cf..76f61a0 100644 --- a/Pearcleaner/Views/TopBarMini.swift +++ b/Pearcleaner/Views/TopBarMini.swift @@ -39,7 +39,7 @@ struct TopBarMini: View { if instantSearch { reversePathsSearch(appState: appState, locations: locations) } else { - loadAllPaths(allApps: appState.sortedApps.userApps + appState.sortedApps.systemApps, appState: appState, locations: locations, reverseAddon: true) + loadAllPaths(allApps: appState.sortedApps, appState: appState, locations: locations, reverseAddon: true) } } else { appState.currentView = .zombie @@ -78,7 +78,7 @@ struct TopBarMini: View { if instantSearch { reversePathsSearch(appState: appState, locations: locations) } else { - loadAllPaths(allApps: appState.sortedApps.userApps + appState.sortedApps.systemApps, appState: appState, locations: locations, reverseAddon: true) + loadAllPaths(allApps: appState.sortedApps, appState: appState, locations: locations, reverseAddon: true) } } else { appState.currentView = .zombie diff --git a/Pearcleaner/Views/ZombieView.swift b/Pearcleaner/Views/ZombieView.swift index 9ca1c71..ada94ff 100644 --- a/Pearcleaner/Views/ZombieView.swift +++ b/Pearcleaner/Views/ZombieView.swift @@ -24,18 +24,21 @@ struct ZombieView: View { VStack(alignment: .center) { if appState.showProgress { VStack { - Spacer() + Group { + Spacer() - Text("Searching the file system").font(.title3) - .foregroundStyle((.gray.opacity(0.8))) - ProgressView() - .progressViewStyle(.linear) - .frame(width: 400, height: 10) + Text("Searching the file system").font(.title3) + .foregroundStyle((.gray.opacity(0.8))) + ProgressView() + .progressViewStyle(.linear) + .frame(width: 400, height: 10) + + Spacer() + } + .transition(.opacity) - Spacer() } .frame(maxWidth: .infinity, maxHeight: .infinity) - .transition(.opacity) } else { // Titlebar if mini { @@ -49,7 +52,7 @@ struct ZombieView: View { if instantSearch { reversePathsSearch(appState: appState, locations: locations) } else { - loadAllPaths(allApps: appState.sortedApps.userApps + appState.sortedApps.systemApps, appState: appState, locations: locations, reverseAddon: true) + loadAllPaths(allApps: appState.sortedApps, appState: appState, locations: locations, reverseAddon: true) } } } @@ -206,6 +209,7 @@ struct ZombieView: View { } .transition(.opacity) .padding([.horizontal, .bottom], 20) + .padding(.top, !mini ? 10 : 0) } diff --git a/Pearcleaner/Windows/UpdateView.swift b/Pearcleaner/Windows/UpdateView.swift index ca664de..72461f5 100644 --- a/Pearcleaner/Windows/UpdateView.swift +++ b/Pearcleaner/Windows/UpdateView.swift @@ -16,8 +16,14 @@ struct UpdateView: View { VStack(spacing: 5) { HStack { + Text("App: \(Bundle.main.version)") + .font(.title3) + .fontWeight(.semibold) + .opacity(0.5) + .padding() + Spacer() - .frame(width: appState.progressBar.1 != 1.0 ? 150 : 180) +// .frame(width: appState.progressBar.1 != 1.0 ? 150 : 180) Text("\(appState.progressBar.1 != 1.0 ? "Update Available 🥳" : "Completed 🚀")") .font(.title) @@ -26,7 +32,7 @@ struct UpdateView: View { Spacer() - Text("v\(appState.releases.first?.tag_name ?? "")") + Text("GitHub: \(appState.releases.first?.tag_name ?? "")") .font(.title3) .fontWeight(.semibold) .opacity(0.5) @@ -107,17 +113,23 @@ struct NoUpdateView: View { VStack(spacing: 5) { HStack { + Text("App: \(Bundle.main.version)") + .font(.title3) + .fontWeight(.semibold) + .opacity(0.5) + .padding() + Spacer() - .frame(width: appState.progressBar.1 != 1.0 ? 140 : 180) +// .frame(width: appState.progressBar.1 != 1.0 ? 140 : 180) - Text("\(appState.progressBar.1 != 1.0 ? "No Update Available 😌" : "Completed 🚀")") + Text("\(appState.progressBar.1 != 1.0 ? "No Update 😌" : "Completed 🚀")") .font(.title) .bold() .padding(.vertical) - + Spacer() - Text("v\(appState.releases.first?.tag_name ?? "")") + Text("GitHub: \(appState.releases.first?.tag_name ?? "")") .font(.title3) .fontWeight(.semibold) .opacity(0.5) @@ -125,6 +137,12 @@ struct NoUpdateView: View { } +// Text("App: \(Bundle.main.version) | GitHub: \(appState.releases.first?.tag_name ?? "")") +// .font(.title3) +// .fontWeight(.semibold) +// .opacity(0.5) +// .padding(.vertical, 5) + Divider() .padding([.horizontal])