This commit is contained in:
Alin
2024-02-12 18:09:49 -07:00
parent 18b9ceb564
commit 09b2785cd2
13 changed files with 230 additions and 107 deletions
+4 -2
View File
@@ -10,9 +10,11 @@ import SwiftUI
struct AppCommands: Commands {
let appState: AppState
let locations: Locations
init(appState: AppState) {
init(appState: AppState, locations: Locations) {
self.appState = appState
self.locations = locations
}
var body: some Commands {
@@ -44,7 +46,7 @@ struct AppCommands: Commands {
.keyboardShortcut("r", modifiers: .command)
Button {
uninstallPearcleaner(appState: appState)
uninstallPearcleaner(appState: appState, locations: locations)
} label: {
Text("Uninstall Pearcleaner")
}
+34 -3
View File
@@ -12,20 +12,18 @@ let home = FileManager.default.homeDirectoryForCurrentUser.path
class AppState: ObservableObject
{
// @Published var showFiles: Bool = false
@Published var appInfo: AppInfo
@Published var paths: [URL] = []
@Published var sortedApps: (userApps: [AppInfo], systemApps: [AppInfo]) = ([], [])
@Published var selectedItems = Set<URL>()
@Published var alertType = AlertType.off
@Published var currentView = CurrentDetailsView.empty
// @Published var currentMode = CurrentMode.regular
@Published var showAlert: Bool = false
@Published var sidebar: Bool = true
// @Published var showPopover: Bool = false
@Published var isReminderVisible: Bool = false
@Published var releases = [Release]()
@Published var progressBar: (String, Double) = ("Ready", 0.0)
// @Published var progressManager = ProgressManager()
@Published var reload: Bool = false
@@ -46,6 +44,39 @@ class AppState: ObservableObject
}
}
class ProgressManager: ObservableObject {
@Published var progress: Double = 0.0
@Published var total: Double = 0.0
@Published var status: String = "Ready"
func setTotal(_ total: Double) {
DispatchQueue.main.async {
self.total = total
}
}
func updateProgress() {
DispatchQueue.main.async {
self.progress = min(max(0.0, self.progress + 1.0), Double(self.total))
}
}
func updateStatus(status: String) {
DispatchQueue.main.async {
self.status = status
}
}
func resetProgress() {
DispatchQueue.main.async {
self.progress = 0.0
}
}
}
struct AppInfo: Identifiable, Hashable {
let id: UUID
let path: URL
+5 -5
View File
@@ -22,9 +22,9 @@ class DeeplinkManager {
static let query = "path"
}
func manage(url: URL, appState: AppState) {
func manage(url: URL, appState: AppState, locations: Locations) {
if url.pathExtension == "app" {
handleAppBundle(url: url, appState: appState)
handleAppBundle(url: url, appState: appState, locations: locations)
} else if url.scheme == DeepLinkConstants.scheme,
url.host == DeepLinkConstants.host,
let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
@@ -34,7 +34,7 @@ class DeeplinkManager {
let appInfo = getAppInfo(atPath: pathURL)
updateOnMain {
appState.appInfo = appInfo!
findPathsForApp(appState: appState, appInfo: appState.appInfo)
findPathsForApp(appState: appState, locations: locations)
if self.mini {
self.showPopover = true
} else {
@@ -72,11 +72,11 @@ class DeeplinkManager {
// }
// }
func handleAppBundle(url: URL, appState: AppState) {
func handleAppBundle(url: URL, appState: AppState, locations: Locations) {
let appInfo = getAppInfo(atPath: url)
updateOnMain {
appState.appInfo = appInfo!
findPathsForApp(appState: appState, appInfo: appState.appInfo)
findPathsForApp(appState: appState, locations: locations)
if self.mini {
self.showPopover = true
} else {
+44 -34
View File
@@ -8,7 +8,7 @@
import Foundation
struct Locations {
class Locations: ObservableObject {
struct Category {
let name: String
var paths: [String]
@@ -19,8 +19,8 @@ struct Locations {
let tempDir: String
var apps: Category
var widgets: Category
var plugins: Category
// var widgets: Category
// var plugins: Category
init() {
self.home = FileManager.default.homeDirectoryForCurrentUser.path
@@ -32,7 +32,7 @@ struct Locations {
"\(home)/Library",
"\(home)/Library/Application Scripts",
"\(home)/Library/Application Support",
"\(home)/Library/Application Support/CrashReporter",
// "\(home)/Library/Application Support/CrashReporter",
"\(home)/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.ApplicationRecentDocuments",
"\(home)/Library/Containers",
// "\(home)/Library/Group Containers", // This is now handled by the function getGroupContainers()
@@ -70,35 +70,45 @@ struct Locations {
cacheDir,
tempDir
])
self.widgets = Category(name: "Widgets", paths: [
// User
"~/Library/Widgets",
// System
"/Library/Widgets"
])
self.plugins = Category(name: "Plugins", paths: [
// User
"~/Library/Contextual Menu Items",
"~/Library/InputManagers",
"~/Library/Internet Plug-Ins",
"~/Library/Mail/Bundles",
"~/Library/PreferencePanes",
"~/Library/QuickLook",
"~/Library/QuickTime",
"~/Library/Screen Savers",
"~/Library/Spotlight",
// System
"/Library/Contextual Menu Items",
"/Library/InputManagers",
"/Library/Internet Plug-Ins",
"/Library/Mail/Bundles",
"/Library/PreferencePanes",
"/Library/QuickLook",
"/Library/QuickTime",
"/Library/Screen Savers",
"/Library/Spotlight",
])
// Append Application Support subfolders for deeper search
do {
let subfolders = try appSupSubfolders()
for folder in subfolders {
self.apps.paths.append("\(home)/Library/Application Support/\(folder)")
}
} catch {
print("Error getting subfolders: \(error)")
}
// self.widgets = Category(name: "Widgets", paths: [
// // User
// "~/Library/Widgets",
// // System
// "/Library/Widgets"
// ])
//
// self.plugins = Category(name: "Plugins", paths: [
// // User
// "~/Library/Contextual Menu Items",
// "~/Library/InputManagers",
// "~/Library/Internet Plug-Ins",
// "~/Library/Mail/Bundles",
// "~/Library/PreferencePanes",
// "~/Library/QuickLook",
// "~/Library/QuickTime",
// "~/Library/Screen Savers",
// "~/Library/Spotlight",
// // System
// "/Library/Contextual Menu Items",
// "/Library/InputManagers",
// "/Library/Internet Plug-Ins",
// "/Library/Mail/Bundles",
// "/Library/PreferencePanes",
// "/Library/QuickLook",
// "/Library/QuickTime",
// "/Library/Screen Savers",
// "/Library/Spotlight",
// ])
}
}
+40 -7
View File
@@ -268,6 +268,24 @@ func darwinCT() -> (String, String) {
}
// Add subfolders of ~/Library/Application Support/ to locations for deeper search
func appSupSubfolders() throws -> [String] {
let fileManager = FileManager.default
let appSup = "\(home)/Library/Application Support/"
let subfolders = try fileManager.contentsOfDirectory(atPath: appSup)
let exclusionRegex = try NSRegularExpression(pattern: "\\bcom\\.apple\\b", options: [])
let exclusions = ["MobileSync", ".DS_Store", "Xcode", "SyncServices", "networkserviceproxy", "DiskImages", "CallHistoryTransactions", "App Store", "CloudDocs", "icdd", "iCloud", "Instruments", "AddressBook", "FaceTime", "AskPermission", "CallHistoryDB"]
let allowedFolders = subfolders.filter { folder in
let range = NSRange(location: 0, length: folder.utf16.count)
return exclusionRegex.firstMatch(in: folder, options: [], range: range) == nil && !exclusions.contains(folder)
}
return allowedFolders
}
// Check if app is running before deleting app files
func killApp(appId: String, completion: @escaping () -> Void = {}) {
@@ -284,17 +302,19 @@ func killApp(appId: String, completion: @escaping () -> Void = {}) {
// Find all possible paths for an app based on name/bundle id
func findPathsForApp(appState: AppState, appInfo: AppInfo) {
func findPathsForApp(appState: AppState, locations: Locations) {
Task(priority: .high) {
updateOnMain {
appState.paths = []
}
let appInfo = appState.appInfo
var collection: [URL] = []
if let url = URL(string: appInfo.path.absoluteString) {
collection.insert(url, at: 0)
}
let fileManager = FileManager.default
// let progressManager = appState.progressManager
let dispatchGroup = DispatchGroup()
var bundleComponents = appInfo.bundleIdentifier.components(separatedBy: ".")
if let lastComponent = bundleComponents.last, let rangeOfDash = lastComponent.range(of: "-") {
@@ -310,27 +330,37 @@ func findPathsForApp(appState: AppState, appInfo: AppInfo) {
let nameL = appInfo.appName.pearFormat()
let nameP = appInfo.path.lastPathComponent.replacingOccurrences(of: ".app", with: "")
let bundleIdentifierL = appInfo.bundleIdentifier.pearFormat()
let locations = Locations()
for location in locations.apps.paths {
if !fileManager.fileExists(atPath: location) {
continue
}
// DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
// withAnimation {
// progressManager.updateStatus(status: location)
// progressManager.updateProgress()
// progressManager.objectWillChange.send()
// }
// }
dispatchGroup.enter() // Enter the dispatch group
do {
let contents = try fileManager.contentsOfDirectory(atPath: location)
for item in contents {
let itemURL = URL(fileURLWithPath: location).appendingPathComponent(item)
let itemL = ("\(item)").replacingOccurrences(of: ".", with: "").replacingOccurrences(of: " ", with: "").lowercased()
if collection.contains(itemURL) {
return
break
}
// Catch web app plist files
if appInfo.webApp {
@@ -348,7 +378,7 @@ func findPathsForApp(appState: AppState, appInfo: AppInfo) {
}
}
// Catch Xcode files
else if itemL.contains("xcode") {
else if itemL.contains("xcode") {
if itemL.contains(bundle) || itemL.contains(bundleIdentifierL) || (nameL.count > 4 && itemL.contains(nameL)) {
collection.append(itemURL)
}
@@ -361,9 +391,12 @@ func findPathsForApp(appState: AppState, appInfo: AppInfo) {
}
} catch {
print("Error processing location:", location, error)
continue
}
// try await Task.sleep(nanoseconds: 50_000_000)
dispatchGroup.leave() // Leave the dispatch group
}
+3 -2
View File
@@ -396,16 +396,17 @@ func presentAlert(appState: AppState) -> Alert {
// --- Pearcleaner Uninstall --
func uninstallPearcleaner(appState: AppState) {
func uninstallPearcleaner(appState: AppState, locations: Locations) {
// Unload Sentinel Monitor if running
launchctl(load: false)
// Get app info for Pearcleaner
let appInfo = getAppInfo(atPath: Bundle.main.bundleURL)
appState.appInfo = appInfo!
// Find application files for Pearcleaner
findPathsForApp(appState: appState, appInfo: appInfo!)
findPathsForApp(appState: appState, locations: locations)
// Kill Pearcleaner and tell Finder to trash the files
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
+10 -4
View File
@@ -12,6 +12,7 @@ import AppKit
struct PearcleanerApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
@StateObject var appState = AppState()
@StateObject var locations = Locations()
@State private var windowSettings = WindowSettings()
@AppStorage("settings.updater.updateTimeframe") private var updateTimeframe: Int = 1
@AppStorage("settings.permissions.disk") private var diskP: Bool = false
@@ -31,8 +32,10 @@ struct PearcleanerApp: App {
if !mini {
AppListView(search: $search, showPopover: $showPopover)
.environmentObject(locations)
} else {
MiniMode(search: $search, showPopover: $showPopover)
.environmentObject(locations)
}
}
@@ -42,14 +45,14 @@ struct PearcleanerApp: App {
.handlesExternalEvents(preferring: Set(arrayLiteral: "pear"), allowing: Set(arrayLiteral: "*"))
.onOpenURL(perform: { url in
let deeplinkManager = DeeplinkManager(showPopover: $showPopover)
deeplinkManager.manage(url: url, appState: appState)
deeplinkManager.manage(url: url, appState: appState, locations: locations)
})
.onDrop(of: ["public.file-url"], isTargeted: nil) { providers, _ in
for provider in providers {
provider.loadItem(forTypeIdentifier: "public.file-url") { data, error in
if let data = data as? Data, let url = URL(dataRepresentation: data, relativeTo: nil) {
let deeplinkManager = DeeplinkManager(showPopover: $showPopover)
deeplinkManager.manage(url: url, appState: appState)
deeplinkManager.manage(url: url, appState: appState, locations: locations)
}
}
}
@@ -73,7 +76,10 @@ struct PearcleanerApp: App {
let sortedApps = getSortedApps()
appState.sortedApps.userApps = sortedApps.userApps
appState.sortedApps.systemApps = sortedApps.systemApps
// Load progressbar total
// appState.progressManager.total = Double(locations.apps.paths.count)
Task {
#if !DEBUG
@@ -112,7 +118,7 @@ struct PearcleanerApp: App {
.windowStyle(.hiddenTitleBar)
.windowResizability(.contentMinSize)
.commands {
AppCommands(appState: appState)
AppCommands(appState: appState, locations: locations)
CommandGroup(replacing: .newItem, addition: { })
}
+38 -24
View File
@@ -12,10 +12,13 @@ struct AppListItems: View {
@EnvironmentObject var appState: AppState
@Binding var search: String
@State private var isHovered = false
@State private var windowSettings = WindowSettings()
@Environment(\.colorScheme) var colorScheme
@AppStorage("settings.general.mini") private var mini: Bool = false
@AppStorage("settings.general.popover") private var popoverStay: Bool = true
@Binding var showPopover: Bool
@EnvironmentObject var locations: Locations
let itemId = UUID()
let appInfo: AppInfo
var isSelected: Bool {
appState.appInfo == appInfo
@@ -74,20 +77,6 @@ struct AppListItems: View {
}
// .background(
// RoundedRectangle(cornerRadius: 6)
// .fill(isSelected ? Color("AccentColor").opacity(0.15) : (isHovered ? Color("AccentColor").opacity(0.1) : Color.white.opacity(colorScheme == .dark ? 0.05 : 0.8)))
// .shadow(color: .black.opacity(0.2), radius: 2, x: 0, y: 1)
// .overlay(
// RoundedRectangle(cornerRadius: 6)
// .strokeBorder(Color("AccentColor").opacity(isHovered ? 0.7 : 0.1), lineWidth: 1)
// )
// )
// Rectangle()
// .frame(maxWidth: isSelected ? .infinity : (isHovered ? .infinity : 30), maxHeight: 2)
// .foregroundStyle(Color("AccentColor"))
}
.padding(.horizontal, 5)
.help(appInfo.appName)
@@ -97,18 +86,43 @@ struct AppListItems: View {
}
}
.onTapGesture {
updateOnMain {
appState.appInfo = .empty
appState.appInfo = appInfo
findPathsForApp(appState: appState, appInfo: appState.appInfo)
withAnimation(Animation.easeIn(duration: 0.4)) {
if mini {
showPopover.toggle()
} else {
appState.currentView = .files
showPopover = false
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
updateOnMain {
appState.appInfo = .empty
appState.appInfo = appInfo
// appState.progressManager.resetProgress()
findPathsForApp(appState: appState, locations: locations)
withAnimation(Animation.easeIn(duration: 0.4)) {
if mini {
showPopover.toggle()
} else {
appState.currentView = .files
}
}
}
}
}
.popover(isPresented: Binding(
get: { showPopover && appState.appInfo.id == appInfo.id},
set: { _ in showPopover = false}
), attachmentAnchor: .rect(.rect(CGRect(x: windowSettings.loadWindowSettings().width - 17, y: 15, width: 0, height: 0))), arrowEdge: .trailing) {
// .popover(isPresented: $showPopover, arrowEdge: .trailing) {
VStack {
FilesView(showPopover: $showPopover, search: $search)
.id(appState.appInfo.id)
}
.interactiveDismissDisabled(popoverStay)
.background(
Rectangle()
.fill(Color("pop"))
.padding(-80)
)
.frame(minWidth: 650, minHeight: 500)
}
}
}
+2 -1
View File
@@ -183,6 +183,7 @@ struct AppListView: View {
struct AppDetailsEmptyView: View {
@Environment(\.colorScheme) var colorScheme
@EnvironmentObject var appState: AppState
@EnvironmentObject var locations: Locations
@State private var animateGradient: Bool = false
@Binding var showPopover: Bool
@@ -191,7 +192,7 @@ struct AppDetailsEmptyView: View {
Spacer()
DropTarget(appState: appState, showPopover: $showPopover)
DropTarget(appState: appState, locations: locations, showPopover: $showPopover)
Spacer()
+4 -2
View File
@@ -13,14 +13,16 @@ struct DropTarget: View, DropDelegate {
@AppStorage("settings.general.ants") private var ants: Bool = false
@AppStorage("settings.general.mini") private var mini: Bool = false
@ObservedObject var appState: AppState
@ObservedObject var locations: Locations
@State private var shouldFlash = false
private var types: [UTType] = [.fileURL]
@Environment(\.colorScheme) var colorScheme
@State private var phase: CGFloat = 4
@Binding var showPopover: Bool
public init(appState: AppState, showPopover: Binding<Bool>) {
public init(appState: AppState, locations: Locations, showPopover: Binding<Bool>) {
self.appState = appState
self.locations = locations
_showPopover = showPopover
}
@@ -132,7 +134,7 @@ struct DropTarget: View, DropDelegate {
DispatchQueue.main.async {
self.ants = false
appState.appInfo = appInfo!
findPathsForApp(appState: appState, appInfo: appState.appInfo)
findPathsForApp(appState: appState, locations: locations)
if mini {
showPopover = true
} else {
+26 -4
View File
@@ -10,6 +10,7 @@ import SwiftUI
struct FilesView: View {
@EnvironmentObject var appState: AppState
@EnvironmentObject var locations: Locations
@State private var appSize: String = ""
@State private var showDetails: Bool = false
@State private var showPop: Bool = false
@@ -25,7 +26,28 @@ struct FilesView: View {
if !self.showDetails {
VStack {
Spacer()
ProgressView("Finding application files..")
// ProgressView("Finding application files..")
// .progressViewStyle(.linear)
// Spacer()
Text("Finding application files..").font(.title3)
.foregroundStyle((.gray.opacity(0.8)))
ProgressView()
.progressViewStyle(.linear)
.frame(width: 400, height: 10)
// ProgressView("\(appState.progressManager.status)", value: appState.progressManager.progress, total: Double(appState.progressManager.total))
// .progressViewStyle(LinearProgressViewStyle(tint: .blue))
// .frame(width: 400, height: 10)
// .padding(.top)
// .onReceive(appState.progressManager.$progress) { newProgress in
// DispatchQueue.main.async {
// appState.objectWillChange.send()
// }
// }
// .onReceive(appState.progressManager.$status) { newStatus in
// DispatchQueue.main.async {
// appState.objectWillChange.send()
// }
// }
Spacer()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
@@ -134,7 +156,7 @@ struct FilesView: View {
ScrollView {
ScrollView() {
VStack {
ForEach(Array(zip(appState.paths, itemDetails)), id: \.0) { path, details in
if let firstPath = appState.paths.first, path == firstPath {
@@ -250,10 +272,10 @@ struct FilesView: View {
}
if let appSize = totalSizeOnDisk(for: appState.paths) {
self.appSize = "\(appSize)"
self.showDetails = true
} else {
print("Error calculating the total size on disk.")
}
self.showDetails = true
}
} else {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
@@ -326,7 +348,7 @@ struct FileDetailsItem: View {
.help(path.lastPathComponent)
Text(path.path)
.font(.footnote)
.lineLimit(1)
.lineLimit(2)
.truncationMode(.tail)
.opacity(0.5)
// .foregroundStyle(Color("AccentColor"))
+16 -15
View File
@@ -39,20 +39,20 @@ struct MiniMode: View {
}
// .padding(.leading, appState.sidebar ? 0 : 10)
.transition(.move(edge: .leading))
.popover(isPresented: $showPopover, arrowEdge: .trailing) {
VStack {
FilesView(showPopover: $showPopover, search: $search)
.id(appState.appInfo.id)
}
.interactiveDismissDisabled(popoverStay)
.background(
Rectangle()
.fill(Color("pop"))
.padding(-80)
)
.frame(minWidth: 600, minHeight: 500)
}
// .popover(isPresented: $showPopover, arrowEdge: .trailing) {
// VStack {
// FilesView(showPopover: $showPopover, search: $search)
// .id(appState.appInfo.id)
// }
// .interactiveDismissDisabled(popoverStay)
// .background(
// Rectangle()
// .fill(Color("pop"))
// .padding(-80)
// )
// .frame(minWidth: 600, minHeight: 500)
//
// }
}
@@ -97,6 +97,7 @@ struct MiniMode: View {
struct MiniEmptyView: View {
@Environment(\.colorScheme) var colorScheme
@EnvironmentObject var appState: AppState
@EnvironmentObject var locations: Locations
@State private var animateGradient: Bool = false
@AppStorage("settings.general.mini") private var mini: Bool = false
@Binding var showPopover: Bool
@@ -106,7 +107,7 @@ struct MiniEmptyView: View {
Spacer()
DropTarget(appState: appState, showPopover: $showPopover)
DropTarget(appState: appState, locations: locations, showPopover: $showPopover)
.frame(maxWidth: mini ? 300 : 500)
Text("Drop an app to begin")