diff --git a/Pearcleaner.xcodeproj/project.pbxproj b/Pearcleaner.xcodeproj/project.pbxproj index 9ad3464..18d507c 100644 --- a/Pearcleaner.xcodeproj/project.pbxproj +++ b/Pearcleaner.xcodeproj/project.pbxproj @@ -48,6 +48,7 @@ C7DD49F02BABA14400CCBA16 /* Folders.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7DD49EF2BABA14400CCBA16 /* Folders.swift */; }; C7DE17652BE54F4900197715 /* Updater.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7DE17642BE54F4900197715 /* Updater.swift */; }; C7DE672B2BA6343D00EB1633 /* MenuBarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7DE672A2BA6343D00EB1633 /* MenuBarItem.swift */; }; + C7EA06332C1CBE2000F872FC /* ConditionBuilderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7EA06322C1CBE2000F872FC /* ConditionBuilderView.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 */; }; @@ -145,6 +146,7 @@ C7DD49EF2BABA14400CCBA16 /* Folders.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Folders.swift; sourceTree = ""; }; C7DE17642BE54F4900197715 /* Updater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Updater.swift; sourceTree = ""; }; C7DE672A2BA6343D00EB1633 /* MenuBarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarItem.swift; sourceTree = ""; }; + C7EA06322C1CBE2000F872FC /* ConditionBuilderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConditionBuilderView.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 = ""; }; @@ -309,6 +311,7 @@ C7FB173A2B96321300B96F9A /* AppsListView.swift */, C7FEBA102BDC422200AE195F /* AppSearchView.swift */, C7D31D492AFEB26700C7ED9E /* AppListItems.swift */, + C7EA06322C1CBE2000F872FC /* ConditionBuilderView.swift */, ); path = Views; sourceTree = ""; @@ -471,6 +474,7 @@ C7DE17652BE54F4900197715 /* Updater.swift in Sources */, C7DD49EE2BAB7F6000CCBA16 /* ReversePathsFetch.swift in Sources */, C7045A322B068AD700376976 /* NewWin.swift in Sources */, + C7EA06332C1CBE2000F872FC /* ConditionBuilderView.swift in Sources */, C7DD49F02BABA14400CCBA16 /* Folders.swift in Sources */, C7ED86B32BA8ED8800F13DE4 /* Trash.swift in Sources */, C7D31D512AFF00F300C7ED9E /* Locations.swift in Sources */, @@ -568,8 +572,8 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - APP_BUILD = 47; - APP_VERSION = 3.7.2; + APP_BUILD = 48; + APP_VERSION = 3.7.3; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -638,8 +642,8 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - APP_BUILD = 47; - APP_VERSION = 3.7.2; + APP_BUILD = 48; + APP_VERSION = 3.7.3; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; diff --git a/Pearcleaner/Logic/AppPathsFetch.swift b/Pearcleaner/Logic/AppPathsFetch.swift index 99e77fb..9895626 100644 --- a/Pearcleaner/Logic/AppPathsFetch.swift +++ b/Pearcleaner/Logic/AppPathsFetch.swift @@ -153,17 +153,15 @@ class AppPathFinder { for condition in conditions { if bundleIdentifierL.contains(condition.bundle_id) { - // Exclude and include keywords - let hasIncludeKeyword = condition.include.contains(where: itemL.contains) + // Exclude keywords let hasExcludeKeyword = condition.exclude.contains(where: itemL.contains) - if hasExcludeKeyword { return false } + // Include keywords + let hasIncludeKeyword = condition.include.contains(where: itemL.contains) if hasIncludeKeyword { - if !condition.exclude.contains(where: itemL.contains) { - return true - } + return true } } } @@ -230,7 +228,7 @@ class AppPathFinder { } - private func handleOutliers() -> [URL] { + private func handleOutliers(include: Bool = true) -> [URL] { var outliers: [URL] = [] let bundleIdentifier = self.appInfo.bundleIdentifier.pearFormat() @@ -239,14 +237,24 @@ class AppPathFinder { bundleIdentifier.contains(condition.bundle_id) } + for condition in matchingConditions { - if let forceIncludes = condition.includeForce { - for path in forceIncludes { - if let url = URL(string: path), FileManager.default.fileExists(atPath: url.path) { - outliers.append(url) + if include { + // Handle includeForce + if let forceIncludes = condition.includeForce { + for path in forceIncludes { + outliers.append(path) + } + } + } else { + // Handle excludeForce + if let excludeForce = condition.excludeForce { + for path in excludeForce { + outliers.append(path) } } } + } return outliers @@ -256,6 +264,7 @@ class AppPathFinder { DispatchQueue.global(qos: .userInitiated).async { let allContainers = self.getAllContainers(bundleURL: self.appInfo.path) let outliers = self.handleOutliers() + let outliersEx = self.handleOutliers(include: false) var tempCollection: [URL] = [] self.collectionAccessQueue.sync { tempCollection = self.collection @@ -263,6 +272,12 @@ class AppPathFinder { tempCollection.append(contentsOf: allContainers) tempCollection.append(contentsOf: outliers) + // Remove URLs based on outliersExcludes + let excludePaths = outliersEx.map { $0.path } + tempCollection.removeAll { url in + excludePaths.contains(url.path) + } + // Sort and standardize URLs to ensure consistent comparisons let sortedCollection = tempCollection.map { $0.standardizedFileURL }.sorted(by: { $0.path < $1.path }) var filteredCollection: [URL] = [] diff --git a/Pearcleaner/Logic/Conditions.swift b/Pearcleaner/Logic/Conditions.swift index 988c490..4822412 100644 --- a/Pearcleaner/Logic/Conditions.swift +++ b/Pearcleaner/Logic/Conditions.swift @@ -6,13 +6,32 @@ // import Foundation +import SwiftData - -struct Condition: Decodable { +struct Condition: Codable { var bundle_id: String var include: [String] var exclude: [String] - var includeForce: [String]? + var includeForce: [URL]? + var excludeForce: [URL]? + + init(bundle_id: String, include: [String], exclude: [String], includeForce: [String]? = nil, excludeForce: [String]? = nil) { + self.bundle_id = bundle_id.pearFormat() + self.include = include.map { $0.pearFormat() } + self.exclude = exclude.map { $0.pearFormat() } + self.includeForce = includeForce?.compactMap { path in + if let url = URL(string: path), FileManager.default.fileExists(atPath: url.path) { + return url + } + return nil + } + self.excludeForce = excludeForce?.compactMap { path in + if let url = URL(string: path), FileManager.default.fileExists(atPath: url.path) { + return url + } + return nil + } + } } struct SkipCondition { @@ -25,70 +44,75 @@ struct SkipCondition { // Conditions for some apps that need to include/exclude certain files/folders when names are more complicated var conditions: [Condition] = [ Condition( - bundle_id: "comappledtxcode", - include: ["comappledt", "xcode", "simulator"], - exclude: ["comrobotsandpencilsxcodesapp", "comoneminutegamesxcodecleaner", "iohyperappxcodecleaner", "xcodesjson"], + bundle_id: "com.apple.dt.xcode", + include: ["com.apple.dt", "xcode", "simulator"], + exclude: ["com.robotsandpencils.xcodesapp", "com.oneminutegames.xcodecleaner", "io.hyperapp.xcodecleaner", "xcodes.json"], includeForce: ["\(home)/Library/Containers/com.apple.iphonesimulator.ShareExtension"] ), Condition( - bundle_id: "comrobotsandpencilsxcodesapp", + bundle_id: "com.robotsandpencils.xcodesapp", include: [], - exclude: ["comappledtxcode", "comoneminutegamesxcodecleaner", "iohyperappxcodecleaner"] + exclude: ["com.apple.dt.xcode", "com.oneminutegames.xcodecleaner", "io.hyperapp.xcodecleaner"] ), Condition( - bundle_id: "iohyperappxcodecleaner", + bundle_id: "io.hyperapp.xcodecleaner", include: [], - exclude: ["comrobotsandpencilsxcodesapp", "comoneminutegamesxcodecleaner", "comappledtxcode", "xcodesjson"] + exclude: ["com.robotsandpencils.xcodesapp", "com.oneminutegames.xcodecleaner", "com.apple.dt.xcode", "xcodes.json"] ), Condition( - bundle_id: "uszoomxos", + bundle_id: "us.zoom.xos", include: ["zoom"], exclude: [] ), Condition( - bundle_id: "combravebrowser", + bundle_id: "com.brave.browser", include: ["brave"], exclude: [] ), Condition( - bundle_id: "comoktamobile", + bundle_id: "com.okta.mobile", include: ["okta"], exclude: [] ), Condition( - bundle_id: "comgooglechrome", + bundle_id: "com.google.chrome", include: ["google", "chrome"], exclude: ["iterm", "chromefeaturestate"] ), Condition( - bundle_id: "commicrosoftedgemac", + bundle_id: "com.microsoft.edgemac", include: ["microsoft"], exclude: ["vscode", "rdc", "appcenter", "office", "oneauth"] ), Condition( - bundle_id: "orgmozillafirefox", + bundle_id: "org.mozilla.firefox", include: ["mozilla", "firefox"], exclude: [] ), Condition( - bundle_id: "comlogioptionsplus", + bundle_id: "org.mozilla.firefox.nightly", + include: ["mozilla", "firefox"], + exclude: [] + ), + Condition( + bundle_id: "com.logi.optionsplus", include: ["logi"], exclude: ["login", "logic"], includeForce: [] ), Condition( - bundle_id: "commicrosoftvscode", + bundle_id: "com.microsoft.vscode", include: ["vscode"], exclude: [], - includeForce: ["\(home)/Library/Application Support/Code"] + includeForce: ["\(home)/Library/Application Support/Code/"] ), Condition( - bundle_id: "comfacebookarchondeveloperid", - include: ["archonloginhelper"], + bundle_id: "com.facebook.archon.developerid", + include: ["archon.loginhelper"], exclude: [] ), Condition( - bundle_id: "euexelbanstats", + bundle_id: "eu.exelban.stats", include: [], exclude: ["video"] ), @@ -114,36 +138,59 @@ let skipConditions: [SkipCondition] = [ let skipReverse = ["apple", "temporary", "btserver", "proapps", "scripteditor", "ilife", "livefsd", "siritoday", "addressbook", "animoji", "appstore", "askpermission", "callhistory", "clouddocs", "diskimages", "dock", "facetime", "fileprovider", "instruments", "knowledge", "mobilesync", "syncservices", "homeenergyd", "icloud", "icdd", "networkserviceproxy", "familycircle", "geoservices", "installation", "passkit", "sharedimagecache", "desktop", "mbuseragent", "swiftpm", "baseband", "coresimulator", "photoslegacyupgrade", "photosupgrade", "siritts", "ipod", "globalpreferences", "apmanalytics", "apmexperiment", "avatarcache", "byhost", "contextstoreagent", "mobilemeaccounts", "intentbuilderc", "loginwindow", "momc", "replayd", "sharedfilelistd", "clang", "audiocomponent", "csexattrcryptoservice", "livetranscriptionagent", "sandboxhelper", "statuskitagent", "betaenrollmentd", "contentlinkingd", "diagnosticextensionsd", "gamed", "heard", "homed", "itunescloudd", "lldb", "mds", "mediaanalysisd", "metrickitd", "mobiletimerd", "proactived", "ptpcamerad", "studentd", "talagent", "watchlistd", "apptranslocation", "xcrun", "ds_store", "caches", "crashreporter", "trash", "pearcleaner", "amsdatamigratortool", "arfilecache", "assistant", "chromium", "cloudkit", "webkit", "databases", "diagnostic", "cache", "gamekit", "homebrew", "logi", "microsoft", "mozilla", "sync", "google", "sentinel", "hexnode", "sentry", "tvappservices", "reminders"] -// Function to load additional conditions from a GitHub JSON file -func loadConditionsFromGitHub() { - let url = URL(string: "https://api.github.com/repos/alienator88/Pearcleaner/contents/conditions.json")! - var request = URLRequest(url: url) - request.setValue("application/vnd.github.VERSION.raw", forHTTPHeaderField: "Accept") - request.setValue("2022-11-28", forHTTPHeaderField: "X-GitHub-Api-Version") - URLSession.shared.dataTask(with: request) { data, response, error in - if let data = data { - do { - // Assuming the JSON structure directly maps to an array of Condition - let jsonObject = try JSONSerialization.jsonObject(with: data, options: []) - if let conditionArray = jsonObject as? [[String: Any]] { - let jsonData = try JSONSerialization.data(withJSONObject: conditionArray, options: []) - let additionalConditions = try JSONDecoder().decode([Condition].self, from: jsonData) - if !additionalConditions.isEmpty { - DispatchQueue.main.async { - conditions += additionalConditions - } - } - } else { - printOS("The data format is incorrect or empty for GitHub conditions processing.") - } - } catch { - printOS("Failed to decode conditions JSON: \(error.localizedDescription)") - } - } else { - printOS("Failed to fetch conditions data: \(error?.localizedDescription ?? "Unknown error")") + + +// Store and load conditions locally via SwiftData +class ConditionManager { + static let shared = ConditionManager() + + private init() { + loadConditions() + } + + // Function to save a condition + func saveCondition(_ condition: Condition) { + if condition.include.isEmpty && condition.exclude.isEmpty && (condition.includeForce?.isEmpty ?? true) { + deleteCondition(bundle_id: condition.bundle_id) + return } - }.resume() -} + let defaults = UserDefaults.standard + let encoder = JSONEncoder() + let key = "Condition-\(condition.bundle_id)" + if let encoded = try? encoder.encode(condition) { + defaults.set(encoded, forKey: key) + conditions.append(condition) + } + } + + // Function to delete a condition from defaults and conditions variable + func deleteCondition(bundle_id: String) { + let defaults = UserDefaults.standard + let key = "Condition-\(bundle_id.pearFormat())" + + // Remove from UserDefaults + defaults.removeObject(forKey: key) + + // Remove from conditions variable + conditions.removeAll { $0.bundle_id == bundle_id.pearFormat() } + } + + // Function to load a condition and append to the global variable + func loadConditions() { + let defaults = UserDefaults.standard + let decoder = JSONDecoder() + + for (key, value) in defaults.dictionaryRepresentation() { + if key.starts(with: "Condition-"), let savedCondition = value as? Data { + if let loadedCondition = try? decoder.decode(Condition.self, from: savedCondition) { + conditions.append(loadedCondition) + } + } + } + } + + +} diff --git a/Pearcleaner/Logic/Locations.swift b/Pearcleaner/Logic/Locations.swift index 326977c..eb3dc7f 100644 --- a/Pearcleaner/Logic/Locations.swift +++ b/Pearcleaner/Logic/Locations.swift @@ -26,6 +26,7 @@ class Locations: ObservableObject { self.tempDir = tempDir self.apps = Category(name: "Apps", paths: [ + "\(home)", "\(home)/Library", "\(home)/Library/Application Scripts", "\(home)/Library/Application Support", diff --git a/Pearcleaner/Logic/Styles.swift b/Pearcleaner/Logic/Styles.swift index 32a83ba..2846f56 100644 --- a/Pearcleaner/Logic/Styles.swift +++ b/Pearcleaner/Logic/Styles.swift @@ -630,6 +630,20 @@ struct AnimatedSearchStyle: TextFieldStyle { } +struct RoundedTextFieldStyle: TextFieldStyle { + func _body(configuration: TextField) -> some View { + configuration + .padding(8) + .cornerRadius(6) + .textFieldStyle(.plain) + .overlay( + RoundedRectangle(cornerRadius: 8) + .strokeBorder(Color("mode").opacity(0.4), lineWidth: 0.8) + ) + } +} + + struct GlassEffect: NSViewRepresentable { var material: NSVisualEffectView.Material // Choose the material you want diff --git a/Pearcleaner/Logic/Updater.swift b/Pearcleaner/Logic/Updater.swift index 1099d6a..348b391 100644 --- a/Pearcleaner/Logic/Updater.swift +++ b/Pearcleaner/Logic/Updater.swift @@ -52,10 +52,10 @@ func loadGithubReleases(appState: AppState, manual: Bool = false, releaseOnly: B }.resume() } -func checkForUpdate(appState: AppState, manual: Bool = false) { +func checkForUpdate(appState: AppState, manual: Bool) { guard let latestRelease = appState.releases.first else { return } - let currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String - if latestRelease.tag_name > currentVersion ?? "" { + let currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0.0" + if latestRelease.tag_name > currentVersion { updateOnMain { appState.updateAvailable = true } @@ -198,14 +198,6 @@ enum UpdateFrequency: String, CaseIterable, Identifiable { } } -//func updateNextUpdateDate() { -// @AppStorage("settings.updater.updateFrequency") var updateFrequency: UpdateFrequency = .daily -// @AppStorage("settings.updater.nextUpdateDate") var nextUpdateDate = Date.now.timeIntervalSinceReferenceDate -// -// guard let updateInterval = updateFrequency.interval else { return } -// let newUpdateDate = Calendar.current.startOfDay(for: Date().addingTimeInterval(updateInterval)) -// nextUpdateDate = newUpdateDate.timeIntervalSinceReferenceDate -//} func checkAndUpdateIfNeeded(appState: AppState) { @AppStorage("settings.updater.updateFrequency") var updateFrequency: UpdateFrequency = .daily @@ -230,7 +222,7 @@ func checkAndUpdateIfNeeded(appState: AppState) { func updateApp(appState: AppState) { // Perform your update logic here - printOS("Updater: performing update") + printOS("Updater: checking for update") loadGithubReleases(appState: appState) } @@ -243,54 +235,6 @@ func isSameDay(date1: Date, date2: Date) -> Bool { return Calendar.current.isDate(date1, inSameDayAs: date2) } -//func updateNextUpdateDate() { -// @AppStorage("settings.updater.updateTimeframe") var updateTimeframe: Int = 1 -// @AppStorage("settings.updater.nextUpdateDate") var nextUpdateDate = Date.now.timeIntervalSinceReferenceDate -// let updateSeconds = updateTimeframe.daysToSeconds -// let newUpdateDate = Calendar.current.startOfDay(for: Date().addingTimeInterval(updateSeconds)) -// nextUpdateDate = newUpdateDate.timeIntervalSinceReferenceDate -//} -// -//func checkAndUpdateIfNeeded(appState: AppState) { -// @AppStorage("settings.updater.updateTimeframe") var updateTimeframe: Int = 1 -// @AppStorage("settings.updater.nextUpdateDate") var nextUpdateDate = Date.now.timeIntervalSinceReferenceDate -// -// let updateSeconds = updateTimeframe.daysToSeconds -// let now = Date() -// -// // Retrieve the next update date from UserDefaults -// let nextUpdateDateLocal = Date(timeIntervalSinceReferenceDate: nextUpdateDate) -//// let nextUpdateDate = UserDefaults.standard.object(forKey: "settings.updater.nextUpdateDate") as? Date -// -// // If there's no stored next update date or it's in the past, update immediately -// if !isSameDay(date1: nextUpdateDateLocal, date2: now) { -// // Next update date is in the future, no need to update -// printOS("Updater: next update date is in the future, skipping") -// return -// } -// -// // Update immediately and set next update date -// updateApp(appState: appState) -// setNextUpdateDate(interval: updateSeconds) -//} -// -//func updateApp(appState: AppState) { -// // Perform your update logic here -// printOS("Updater: performing update") -// loadGithubReleases(appState: appState) -//} -// -//func setNextUpdateDate(interval: TimeInterval) { -// let newUpdateDate = Calendar.current.startOfDay(for: Date().addingTimeInterval(interval)) -// UserDefaults.standard.set(newUpdateDate.timeIntervalSinceReferenceDate, forKey: "settings.updater.nextUpdateDate") -//// UserDefaults.standard.set(newUpdateDate, forKey: "settings.updater.nextUpdateDate") -//} -// -//func isSameDay(date1: Date, date2: Date) -> Bool { -// return Calendar.current.isDate(date1, inSameDayAs: date2) -//} - - // --- Updater Badge View diff --git a/Pearcleaner/Logic/Utilities.swift b/Pearcleaner/Logic/Utilities.swift index 200fff6..ecb4016 100644 --- a/Pearcleaner/Logic/Utilities.swift +++ b/Pearcleaner/Logic/Utilities.swift @@ -427,6 +427,31 @@ extension String { } } +// --- Returns comma separated string as array of strings +extension String { + func toConditionFormat() -> [String] { + if self.isEmpty { + return [] + } + return self.components(separatedBy: ",").map { $0.trimmingCharacters(in: .whitespaces) } + } +} + + +// --- Overload the greater than operator ">" to do a semantic check on the string versions +extension String { + func versionStringToTuple() -> (Int, Int, Int) { + let components = self.split(separator: ".").compactMap { Int($0) } + return (components[0], components[1], components[2]) + } + + static func > (lhs: String, rhs: String) -> Bool { + let lhsVersion = lhs.versionStringToTuple() + let rhsVersion = rhs.versionStringToTuple() + return lhsVersion > rhsVersion + } +} + // --- Trash Relationship --- //extension FileManager { diff --git a/Pearcleaner/PearcleanerApp.swift b/Pearcleaner/PearcleanerApp.swift index a95ff55..af831f1 100644 --- a/Pearcleaner/PearcleanerApp.swift +++ b/Pearcleaner/PearcleanerApp.swift @@ -27,7 +27,7 @@ struct PearcleanerApp: App { @AppStorage("settings.interface.selectedMenubarIcon") var selectedMenubarIcon: String = "trash" @State private var search = "" @State private var showPopover: Bool = false - + let conditionManager = ConditionManager.shared var body: some Scene { @@ -127,7 +127,7 @@ struct PearcleanerApp: App { // Get new features getFeatures(appState: appState, features: $features) // Load extra conditions from GitHub - loadConditionsFromGitHub() +// loadConditionsFromGitHub() // REPLACE THIS WITH NEW CONDITION MANAGER } } diff --git a/Pearcleaner/Views/AppSearchView.swift b/Pearcleaner/Views/AppSearchView.swift index 15dcc87..6a7630d 100644 --- a/Pearcleaner/Views/AppSearchView.swift +++ b/Pearcleaner/Views/AppSearchView.swift @@ -238,7 +238,7 @@ struct SimpleSearchStyle: TextFieldStyle { extension NSTextView { open override var frame: CGRect { didSet { - insertionPointColor = .clear + insertionPointColor = NSColor(Color("mode").opacity(0.2))//.clear } } } diff --git a/Pearcleaner/Views/ConditionBuilderView.swift b/Pearcleaner/Views/ConditionBuilderView.swift new file mode 100644 index 0000000..c1a21f2 --- /dev/null +++ b/Pearcleaner/Views/ConditionBuilderView.swift @@ -0,0 +1,154 @@ +// +// ConditionBuilderView.swift +// Pearcleaner +// +// Created by Alin Lupascu on 6/14/24. +// + +import Foundation +import SwiftUI + +struct ConditionBuilderView: View { + @Binding var showAlert:Bool + @State private var include = "" + @State private var exclude = "" + @State private var paths = "" + @State private var pathsEx = "" + @State var bundle: String + @State private var conditionExists = false + + + var body: some View { + VStack(spacing: 10) { + HStack { + Spacer() + Text("Condition Builder") + .font(.headline) + Spacer() + Button("Close") { + showAlert = false + } + .buttonStyle(SimpleButtonStyle(icon: "x.circle", iconFlip: "x.circle.fill", help: "Close")) + } + + Divider() + Spacer() + InfoButton(text: "Some files/folders are not similar to the app name or bundle id, causing Pearcleaner to either not find them or find unrelated files. \nTo combat this, you may create a custom condition for each application bundle using keywords or direct paths. \n\n- Can add file/folder keywords that you want to either include or exclude in fuzzy searches. \n\n- Can explicitly add or remove a full Finder path to search results if you want a direct search.", label: "Instructions", edge: .bottom) + Spacer() + + VStack { + HStack { + Text("Include Keywords:").font(.callout) + Spacer() + } + HStack { + TextField("keyword-1, keyword-2", text: $include) + .textFieldStyle(RoundedTextFieldStyle()) + Button("Clear") { + include = "" + } + .buttonStyle(SimpleButtonStyle(icon: "xmark.circle.fill", help: "Clear", size: 15)) + } + + HStack { + Text("Exclude Keywords:").font(.callout) + Spacer() + } + HStack { + TextField("keyword-1, keyword-2", text: $exclude) + .textFieldStyle(RoundedTextFieldStyle()) + Button("Clear") { + exclude = "" + } + .buttonStyle(SimpleButtonStyle(icon: "xmark.circle.fill", help: "Clear", size: 15)) + } + + HStack { + Text("Add Direct Paths:").font(.callout) + Spacer() + } + HStack { + TextField("/Full/Path/example-1.txt, /Full/Path/example-2.txt", text: $paths) + .textFieldStyle(RoundedTextFieldStyle()) + Button("Clear") { + paths = "" + } + .buttonStyle(SimpleButtonStyle(icon: "xmark.circle.fill", help: "Clear", size: 15)) + } + HStack { + Text("Remove Direct Paths:").font(.callout) + Spacer() + } + HStack { + TextField("/Full/Path/example-1.txt, /Full/Path/example-2.txt", text: $pathsEx) + .textFieldStyle(RoundedTextFieldStyle()) + Button("Clear") { + pathsEx = "" + } + .buttonStyle(SimpleButtonStyle(icon: "xmark.circle.fill", help: "Clear", size: 15)) + } + } + .padding(.horizontal) + + Spacer() + + HStack { + + Spacer() + + Button("Add/Save") { + showAlert = false + let newCondition = Condition(bundle_id: bundle, include: include.toConditionFormat(), exclude: exclude.toConditionFormat(), includeForce: paths.toConditionFormat(), excludeForce: pathsEx.toConditionFormat()) + ConditionManager.shared.saveCondition(newCondition) + } + .buttonStyle(SimpleButtonStyle(icon: "plus.square.fill", label: conditionExists ? "Save" : "Add", help: "Save the condition for this application")) + + Spacer() + + Button("Remove") { + showAlert = false + include = "" + exclude = "" + paths = "" + pathsEx = "" + ConditionManager.shared.deleteCondition(bundle_id: bundle) + } + .buttonStyle(SimpleButtonStyle(icon: "minus.square.fill", label: "Remove", help: "Remove the condition from this application")) + .disabled(!conditionExists) + + Spacer() + + } + + Spacer() + } + .padding(15) + .frame(width: 500, height: 500) + .background(GlassEffect(material: .hudWindow, blendingMode: .behindWindow)) + .onAppear { + loadCondition() + } + } + + private func loadCondition() { + let defaults = UserDefaults.standard + let decoder = JSONDecoder() + + let key = "Condition-\(bundle.pearFormat())" + if let savedCondition = defaults.object(forKey: key) as? Data { + if let loadedCondition = try? decoder.decode(Condition.self, from: savedCondition) { + include = loadedCondition.include.joined(separator: ", ") + exclude = loadedCondition.exclude.joined(separator: ", ") + if let includeForce = loadedCondition.includeForce { + paths = includeForce.map { $0.absoluteString }.joined(separator: ", ") + } + if let excludeForce = loadedCondition.excludeForce { + pathsEx = excludeForce.map { $0.absoluteString }.joined(separator: ", ") + } + conditionExists = true + } + } + } +} + + diff --git a/Pearcleaner/Views/FilesView.swift b/Pearcleaner/Views/FilesView.swift index 37a9f88..cd8040c 100644 --- a/Pearcleaner/Views/FilesView.swift +++ b/Pearcleaner/Views/FilesView.swift @@ -20,6 +20,7 @@ struct FilesView: View { @AppStorage("settings.general.sizeType") var sizeType: String = "Real" @AppStorage("settings.general.filesWarning") private var warning: Bool = false @State private var showAlert = false + @State private var showConditionBuilder = false @Environment(\.colorScheme) var colorScheme @Binding var showPopover: Bool @Binding var search: String @@ -277,6 +278,14 @@ struct FilesView: View { .padding(.top) + Button("Builder") { + showConditionBuilder = true + } + .buttonStyle(SimpleButtonStyle(icon: "hammer.fill", help: "Condition Builder", size: 14)) + .padding(.top) + .padding(.trailing, 5) + + Button("\(sizeType == "Logical" ? totalSelectedSize.logical : sizeType == "Finder" ? totalSelectedSize.finder : totalSelectedSize.real)") { Task { @@ -389,6 +398,9 @@ struct FilesView: View { .frame(width: 400, height: 200) .background(GlassEffect(material: .hudWindow, blendingMode: .behindWindow)) }) + .sheet(isPresented: $showConditionBuilder, content: { + ConditionBuilderView(showAlert: $showConditionBuilder, bundle: appState.appInfo.bundleIdentifier) + }) .onAppear { if !warning { showAlert = true diff --git a/conditions.json b/conditions.json deleted file mode 100644 index e9cb33f..0000000 --- a/conditions.json +++ /dev/null @@ -1,8 +0,0 @@ -[ - { - "bundle_id": "orgmozillanightly", - "include": ["mozilla", "firefox"], - "exclude": [], - "includeForce": [] - } -]