This commit is contained in:
Alin
2024-06-14 17:44:33 -06:00
parent ca0785f275
commit 1f2e1202a8
12 changed files with 345 additions and 137 deletions
+26 -11
View File
@@ -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] = []
+98 -51
View File
@@ -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)
}
}
}
}
}
+1
View File
@@ -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",
+14
View File
@@ -630,6 +630,20 @@ struct AnimatedSearchStyle: TextFieldStyle {
}
struct RoundedTextFieldStyle: TextFieldStyle {
func _body(configuration: TextField<Self._Label>) -> 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
+4 -60
View File
@@ -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
+25
View File
@@ -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 {
+2 -2
View File
@@ -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
}
}
+1 -1
View File
@@ -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
}
}
}
@@ -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
}
}
}
}
+12
View File
@@ -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