This commit is contained in:
Alin
2024-03-22 18:16:55 -06:00
parent 5350a8f310
commit 7906cd70f7
13 changed files with 311 additions and 186 deletions
+5 -5
View File
@@ -245,12 +245,12 @@
isa = PBXGroup;
children = (
C77B90222AF2D616009CC655 /* FilesView.swift */,
C71848432B8D2D600046CB13 /* ZombieView.swift */,
C76D08542AF89CDE00D07867 /* RegularMode.swift */,
C7D31D472AFEB23700C7ED9E /* TopBar.swift */,
C7045A292B03FAF000376976 /* TopBarMini.swift */,
C7D31D492AFEB26700C7ED9E /* AppListItems.swift */,
C7045A272B03E71D00376976 /* MiniMode.swift */,
C71848432B8D2D600046CB13 /* ZombieView.swift */,
C7FB173A2B96321300B96F9A /* AppsListView.swift */,
C7DE672C2BA6356500EB1633 /* MenuBarMiniAppView.swift */,
);
@@ -575,7 +575,7 @@
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 26;
CURRENT_PROJECT_VERSION = 27;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = BK8443AXLU;
@@ -593,7 +593,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 3.3.0;
MARKETING_VERSION = 3.3.1;
PRODUCT_BUNDLE_IDENTIFIER = com.alienator88.Pearcleaner;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
@@ -610,7 +610,7 @@
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 26;
CURRENT_PROJECT_VERSION = 27;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = BK8443AXLU;
@@ -628,7 +628,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 3.3.0;
MARKETING_VERSION = 3.3.1;
PRODUCT_BUNDLE_IDENTIFIER = com.alienator88.Pearcleaner;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
+16 -5
View File
@@ -74,12 +74,12 @@ struct AppCommands: Commands {
{
undoTrash(appState: appState) {
let sortedApps = getSortedApps(paths: fsm.folderPaths, appState: appState)
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
appState.sortedApps = sortedApps
// if instantSearch {
// loadAllPaths(allApps: sortedApps.userApps + sortedApps.systemApps, appState: appState, locations: locations)
// }
for app in appState.trashedFiles {
let pathFinder = AppPathFinder(appInfo: app, appState: appState, locations: locations)
pathFinder.findPaths()
}
}
}
} label: {
@@ -88,6 +88,17 @@ struct AppCommands: Commands {
.keyboardShortcut("z", modifiers: .command)
// Button
// {
// updateOnMain {
// appState.currentView = .zombie
// }
// } label: {
// Label("Zombie", systemImage: "clear")
// }
// .keyboardShortcut("f", modifiers: .command)
}
+2 -1
View File
@@ -210,9 +210,10 @@ class AppPathFinder {
self.appState.appInfo = self.appInfo
self.appState.selectedItems = Set(updatedCollection)
}
self.appState.appInfoStore.append(self.appInfo)
// Only append object to store if instant search. Same for calculating progress.
if self.instantSearch {
self.appState.appInfoStore.append(self.appInfo)
self.appState.instantProgress += 1
}
+1
View File
@@ -14,6 +14,7 @@ class AppState: ObservableObject
{
@Published var appInfo: AppInfo
@Published var appInfoStore: [AppInfo] = []
@Published var trashedFiles: [AppInfo] = []
@Published var zombieFile: ZombieFile
@Published var sortedApps: [AppInfo] = []
@Published var selectedItems = Set<URL>()
+3 -1
View File
@@ -59,9 +59,11 @@ class ReversePathsSearcher {
private func processItem(_ itemName: String, itemURL: URL, allPaths: [String], allNames: [String]) {
let formattedItemName = itemName.pearFormat()
let itemPath = itemURL.path.pearFormat()
let itemLastPathComponent = itemURL.lastPathComponent.pearFormat()
guard !skipped.contains(where: { formattedItemName.contains($0) }),
!allPaths.contains(itemPath),
// !allPaths.contains(itemPath),
!allPaths.contains(where: { $0 == itemPath || $0.hasSuffix("/\(itemLastPathComponent)") }),
!allNames.contains(formattedItemName),
isSupportedFileType(at: itemURL.path) else {
return
+2 -2
View File
@@ -28,8 +28,8 @@ struct SimpleButtonStyle: ButtonStyle {
.resizable()
.scaledToFit()
.frame(width: 20)
if label != "" {
Text(label!)
if let label = label, !label.isEmpty {
Text(label)
}
}
.foregroundColor(hovered ? color : color.opacity(0.5))
+39 -5
View File
@@ -130,13 +130,47 @@ func checkAndRequestAccessibilityAccess(appState: AppState) -> Bool {
}
}
// Check app directory based on user permission
func checkAppDirectoryAndUserRole(completion: @escaping ((isInCorrectDirectory: Bool, isAdmin: Bool)) -> Void) {
isCurrentUserAdmin { isAdmin in
let bundlePath = Bundle.main.bundlePath as NSString
let applicationsDir = "/Applications"
let userApplicationsDir = "\(home)/Applications"
// Check if app is installed in /Applications directory
func isAppInApplicationsDir() -> Bool {
if let bundlePath = Bundle.main.bundlePath as NSString? {
return bundlePath.deletingLastPathComponent == "/Applications"
var isInCorrectDirectory = false
if isAdmin {
// Admins can have the app in either /Applications or ~/Applications
isInCorrectDirectory = bundlePath.deletingLastPathComponent == applicationsDir ||
bundlePath.deletingLastPathComponent == userApplicationsDir
} else {
// Standard users should only have the app in ~/Applications
isInCorrectDirectory = bundlePath.deletingLastPathComponent == userApplicationsDir
}
// Return both conditions: if the app is in the correct directory and if the user is an admin
completion((isInCorrectDirectory, isAdmin))
}
}
// Check if user is admin or standard user
func isCurrentUserAdmin(completion: @escaping (Bool) -> Void) {
let process = Process()
process.executableURL = URL(fileURLWithPath: "/bin/zsh") // Using zsh, macOS default shell
process.arguments = ["-c", "groups $(whoami) | grep -q ' admin '"]
process.terminationHandler = { process in
// On macOS, a process's exit status of 0 indicates success (admin group found in this context)
completion(process.terminationStatus == 0)
}
do {
try process.run()
} catch {
print("Failed to execute command: \(error)")
completion(false)
}
return false
}
// Check if appearance is dark mode
+22 -17
View File
@@ -118,6 +118,7 @@ struct PearcleanerApp: App {
}, icon: selectedMenubarIcon)
}
#if !DEBUG
Task {
@@ -157,7 +158,7 @@ struct PearcleanerApp: App {
.windowResizability(.contentMinSize)
.commands {
AppCommands(appState: appState, locations: locations, fsm: fsm)
CommandGroup(replacing: .newItem, addition: { })
// CommandGroup(replacing: .newItem, addition: { })
}
@@ -185,29 +186,33 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
return !menubarEnabled
}
//#if !DEBUG
// func windowShouldClose(_ sender: NSWindow) -> Bool {
// let menubarEnabled = UserDefaults.standard.bool(forKey: "settings.menubar.enabled")
// if menubarEnabled {
// findAndHideWindows(named: ["Pearcleaner"])
// return false
// } else {
// return true
// }
// }
//#endif
func applicationDidFinishLaunching(_ notification: Notification) {
let menubarEnabled = UserDefaults.standard.bool(forKey: "settings.menubar.enabled")
if menubarEnabled {
findAndHideWindows(named: ["Pearcleaner"])
NSApplication.shared.setActivationPolicy(.accessory)
}
// Link window to delegate
// let mainWindow = NSApp.windows[0]
// mainWindow.delegate = self
}
func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
let windowSettings = WindowSettings()
if !flag {
// No visible windows, so let's open a new one
for window in sender.windows {
window.title = "Pearcleaner"
window.makeKeyAndOrderFront(self)
print(windowSettings.loadWindowSettings())
updateOnMain(after: 0.1, {
resizeWindowAuto(windowSettings: windowSettings, title: "Pearcleaner")
print(window.title)
})
}
return true // Indicates you've handled the re-open
}
// Return true if you want the application to proceed with its default behavior
return false
}
}
+124 -74
View File
@@ -37,12 +37,16 @@ struct FilesView: View {
ProgressView()
.progressViewStyle(.linear)
.frame(width: 400, height: 10)
Image(systemName: "\(elapsedTime).circle")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 16, height: 16)
Text("\(elapsedTime)")
.font(.caption)
.foregroundStyle((.gray.opacity(0.8)))
.opacity(elapsedTime == 0 ? 0 : 1)
// Image(systemName: "\(elapsedTime).circle")
// .resizable()
// .aspectRatio(contentMode: .fit)
// .frame(width: 16, height: 16)
// .foregroundStyle((.gray.opacity(0.8)))
// .opacity(elapsedTime == 0 ? 0 : 1)
}
Spacer()
@@ -62,9 +66,26 @@ struct FilesView: View {
} else {
// Titlebar
if !regularWin {
HStack() {
HStack(spacing: 0) {
Spacer()
if instantSearch {
Button("Rescan") {
updateOnMain {
appState.showProgress.toggle()
let pathFinder = AppPathFinder(appInfo: appState.appInfo, appState: appState, locations: locations) {
updateOnMain {
appState.showProgress = false
}
}
pathFinder.findPaths()
}
}
.buttonStyle(NavButtonBottomBarStyle(image: "arrow.counterclockwise.circle.fill", help: "Rescan files"))
}
Button("Close") {
updateOnMain {
appState.appInfo = AppInfo.empty
@@ -122,61 +143,99 @@ struct FilesView: View {
Text("\(appState.appInfo.fileSize.count > 1 ? "\(appState.appInfo.fileSize.count) items" : "\(appState.appInfo.fileSize.count) item")").font(.callout).underline().foregroundStyle((.gray.opacity(0.8)))
}
}
HStack(alignment: .center, spacing: 10) {
Spacer()
if appState.appInfo.webApp {
Text("web")
.font(.footnote)
.foregroundStyle(Color("mode").opacity(0.5))
.frame(minWidth: 30, minHeight: 15)
.padding(2)
.background(Color("mode").opacity(0.1))
.clipShape(.capsule)
}
if appState.appInfo.wrapped {
Text("iOS")
.font(.footnote)
.foregroundStyle(Color("mode").opacity(0.5))
.frame(minWidth: 30, minHeight: 15)
.padding(2)
.background(Color("mode").opacity(0.1))
.clipShape(.capsule)
}
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)
}
// Item selection and sorting toolbar
HStack() {
Toggle("", isOn: Binding(
get: { self.appState.selectedItems.count == self.appState.appInfo.files.count },
set: { newValue in
updateOnMain {
self.appState.selectedItems = newValue ? Set(self.appState.appInfo.files) : []
}
}
))
Spacer()
HStack(alignment: .center, spacing: 10) {
if appState.appInfo.webApp {
Text("web")
.font(.footnote)
.foregroundStyle(Color("mode").opacity(0.5))
.frame(minWidth: 30, minHeight: 15)
.padding(2)
.background(Color("mode").opacity(0.1))
.clipShape(.capsule)
}
if appState.appInfo.wrapped {
Text("iOS")
.font(.footnote)
.foregroundStyle(Color("mode").opacity(0.5))
.frame(minWidth: 30, minHeight: 15)
.padding(2)
.background(Color("mode").opacity(0.1))
.clipShape(.capsule)
}
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)
}
Spacer()
Button("") {
selectedOption = selectedOption == "Default" ? "Size" : "Default"
}
.buttonStyle(SimpleButtonStyle(icon: selectedOption == "Default" ? "textformat.abc" : "textformat.123", help: selectedOption == "Default" ? "Sorted alphabetically" : "Sorted by size", color: Color("mode")))
}
.padding()
Divider()
.padding()
.padding(.horizontal)
ScrollView() {
VStack {
LazyVStack {
let sortedFilesSize = appState.appInfo.files.sorted(by: { appState.appInfo.fileSize[$0, default: 0] > appState.appInfo.fileSize[$1, default: 0] })
let sortedFilesAlpha = appState.appInfo.files
// let sortedFilesAlpha = appState.appInfo.files
let sortedFilesAlpha = appState.appInfo.files.sorted { firstURL, secondURL in
let isFirstPathApp = firstURL.pathExtension == "app"
let isSecondPathApp = secondURL.pathExtension == "app"
if isFirstPathApp, !isSecondPathApp {
return true // .app extension always comes first
} else if !isFirstPathApp, isSecondPathApp {
return false
} else {
// If neither or both are .app, sort alphabetically
return firstURL.lastPathComponent.pearFormat() < secondURL.lastPathComponent.pearFormat()
}
}
let sort = selectedOption == "Default" ? sortedFilesAlpha : sortedFilesSize
@@ -195,33 +254,17 @@ struct FilesView: View {
.padding()
}
Spacer()
HStack() {
Picker("", selection: Binding(
get: { appState.selectedItems.count == appState.appInfo.files.count ? true : false },
set: { newValue in
updateOnMain {
appState.selectedItems = newValue ? Set(appState.appInfo.files) : []
}
}
)) {
Image(systemName: "checkmark.square").tag(true)
Image(systemName: "square").tag(false)
}
.pickerStyle(SegmentedPickerStyle())
.frame(width: 100)
.offset(x: -8)
.help("Item Selection")
Spacer()
if !appState.selectedItems.isEmpty {
Button("Uninstall") {
Task {
updateOnMain {
// appState.appInfo = AppInfo.empty
search = ""
if !regularWin {
appState.currentView = .apps
@@ -254,13 +297,27 @@ struct FilesView: View {
launchctl(load: true)
}
}
// Remove app from app list
removeApp(appState: appState, withId: appState.appInfo.id)
// Brew cleanup if enabled
if brew {
caskCleanup(app: appState.appInfo.appName)
}
// Clear out AppInfo state
// Remove app from app list if all app files were removed
if appState.appInfo.files.count == selectedItemsArray.count {
removeApp(appState: appState, withId: appState.appInfo.id)
} else {
// Add deleted appInfo object to trashed array
appState.appInfo.files = []
appState.appInfo.fileSize = [:]
appState.trashedFiles.append(appState.appInfo)
// Clear out appInfoStore object
if let index = appState.appInfoStore.firstIndex(where: { $0.path == appState.appInfo.path }) {
appState.appInfoStore[index] = .empty
}
}
appState.appInfo = AppInfo.empty
}
}
@@ -278,13 +335,6 @@ struct FilesView: View {
Spacer()
Picker("", selection: $selectedOption) {
Image(systemName: "textformat.abc").tag("Default")
Image(systemName: "number").tag("Size")
}
.pickerStyle(SegmentedPickerStyle())
.frame(width: 100)
.help("Sorting alphabetically or by size")
}
}
@@ -111,37 +111,6 @@ struct MenuBarMiniAppView: View {
.buttonStyle(SimpleButtonStyle(icon: "gear", label: "Settings", help: "Settings", color: Color("mode")))
}
// Button("Leftover Files") {
// showMenu = false
// withAnimation(.easeInOut(duration: 0.5)) {
// showPopover = false
// updateOnMain() {
// appState.appInfo = .empty
// appState.selectedZombieItems = []
// if appState.zombieFile.fileSize.keys.count == 0 {
// appState.currentView = .zombie
// appState.showProgress.toggle()
// showPopover.toggle()
// if instantSearch {
// reversePathsSearch(appState: appState, locations: locations)
// } else {
// loadAllPaths(allApps: appState.sortedApps, appState: appState, locations: locations, reverseAddon: true)
// }
// } else {
// appState.currentView = .zombie
// showPopover.toggle()
// }
// }
//
// }
// }
// .buttonStyle(SimpleButtonStyle(icon: "clock.arrow.circlepath", label: "Leftover Files", help: "Leftover Files", color: Color("mode")))
Button("Quit") {
NSApp.terminate(nil)
}
+12 -3
View File
@@ -50,9 +50,18 @@ struct MiniMode: View {
}
.interactiveDismissDisabled(popoverStay)
.background(
Rectangle()
.fill(Color("pop"))
.padding(-80)
Group {
if glass {
GlassEffect(material: .sidebar, blendingMode: .behindWindow).edgesIgnoringSafeArea(.all)
} else {
Rectangle()
.fill(Color("pop"))
.padding(-80)
}
}
// Rectangle()
// .fill(Color("pop"))
// .padding(-80)
)
.frame(width: 650, height: 550)
+71 -36
View File
@@ -33,9 +33,11 @@ struct ZombieView: View {
}
let sortedFilteredFiles = filteredFiles.sorted(by: {
selectedOption == "Default" ?
$0.key.lastPathComponent < $1.key.lastPathComponent :
$0.value > $1.value
if selectedOption == "Default" {
return $0.key.lastPathComponent.pearFormat() < $1.key.lastPathComponent.pearFormat()
} else {
return $0.value > $1.value
}
}).map { $0.key }
let totalSize = filteredFiles.values.reduce(0, +)
@@ -56,12 +58,14 @@ struct ZombieView: View {
ProgressView()
.progressViewStyle(.linear)
.frame(width: 400, height: 10)
Image(systemName: "\(elapsedTime).circle")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 16, height: 16)
Text("\(elapsedTime)")
.font(.caption)
.foregroundStyle((.gray.opacity(0.8)))
.opacity(elapsedTime == 0 ? 0 : 1)
// Image(systemName: "\(elapsedTime).circle")
// .resizable()
// .aspectRatio(contentMode: .fit)
// .frame(width: 16, height: 16)
// .foregroundStyle((.gray.opacity(0.8)))
}
@@ -154,15 +158,45 @@ struct ZombieView: View {
.padding(.top, 0)
}
SearchBarMiniBottom(search: $searchZ)
.padding(.top)
// SearchBarMiniBottom(search: $searchZ)
// .padding(.top)
// .padding(.horizontal)
// Item selection and sorting toolbar
HStack {
Toggle("", isOn: Binding(
get: { appState.selectedZombieItems.count == appState.zombieFile.fileSize.count },
set: { newValue in
updateOnMain {
appState.selectedZombieItems = newValue ? Set(appState.zombieFile.fileSize.keys) : []
}
}
))
SearchBarMiniBottom(search: $searchZ)
// .padding(.top)
.padding(.horizontal)
// Spacer()
Button("") {
selectedOption = selectedOption == "Default" ? "Size" : "Default"
}
.buttonStyle(SimpleButtonStyle(icon: selectedOption == "Default" ? "textformat.abc" : "textformat.123", help: selectedOption == "Default" ? "Sorted alphabetically" : "Sorted by size", color: Color("mode")))
}
.padding(.horizontal)
.padding(.vertical)
Divider()
.padding()
.padding(.horizontal)
ScrollView() {
LazyVStack {
ForEach(filteredAndSortedFiles.0, id: \.self) { file in
if let fileSize = appState.zombieFile.fileSize[file], let fileIcon = appState.zombieFile.fileIcon[file] {
let iconImage = fileIcon.map(Image.init(nsImage:))
@@ -176,27 +210,28 @@ struct ZombieView: View {
}
}
}
.padding()
}
.padding()
Spacer()
HStack() {
Picker("", selection: Binding(
get: { appState.selectedZombieItems.count == appState.zombieFile.fileSize.count ? true : false },
set: { newValue in
updateOnMain {
appState.selectedZombieItems = newValue ? Set(appState.zombieFile.fileSize.keys) : []
}
}
)) {
Image(systemName: "checkmark.square").tag(true)
Image(systemName: "square").tag(false)
}
.pickerStyle(SegmentedPickerStyle())
.frame(width: 100)
.offset(x: -8)
.help("Item Selection")
// Picker("", selection: Binding(
// get: { appState.selectedZombieItems.count == appState.zombieFile.fileSize.count ? true : false },
// set: { newValue in
// updateOnMain {
// appState.selectedZombieItems = newValue ? Set(appState.zombieFile.fileSize.keys) : []
// }
// }
// )) {
// Image(systemName: "checkmark.square").tag(true)
// Image(systemName: "square").tag(false)
// }
// .pickerStyle(SegmentedPickerStyle())
// .frame(width: 100)
// .offset(x: -8)
// .help("Item Selection")
Spacer()
@@ -238,13 +273,13 @@ struct ZombieView: View {
Spacer()
Picker("", selection: $selectedOption) {
Image(systemName: "textformat.abc").tag("Default")
Image(systemName: "number").tag("Size")
}
.pickerStyle(SegmentedPickerStyle())
.frame(width: 100)
.help("Sorting alphabetically or by size")
// Picker("", selection: $selectedOption) {
// Image(systemName: "textformat.abc").tag("Default")
// Image(systemName: "number").tag("Size")
// }
// .pickerStyle(SegmentedPickerStyle())
// .frame(width: 100)
// .help("Sorting alphabetically or by size")
}
}
+12 -4
View File
@@ -10,7 +10,8 @@ import SwiftUI
struct UpdateView: View {
@EnvironmentObject var appState: AppState
let isAppInAppsDir = isAppInApplicationsDir()
@State private var isAppInCorrectDirectory: Bool = false
@State private var isUserAdmin: Bool = false
var body: some View {
VStack(spacing: 5) {
@@ -52,10 +53,11 @@ struct UpdateView: View {
Spacer()
if !isAppInAppsDir {
Text("Please move Pearcleaner to the **Applications** folder before updating!")
if !isAppInCorrectDirectory {
Text("To avoid permission issues, please move Pearcleaner to the \(isUserAdmin ? "/Applications" : "\(home)/Applications") folder before updating!")
.font(.callout)
.foregroundStyle(.red)
.foregroundStyle(Color.accentColor)
.padding(.horizontal)
.padding(.top, 0)
}
@@ -97,6 +99,12 @@ struct UpdateView: View {
}
.padding(EdgeInsets(top: -25, leading: 0, bottom: 25, trailing: 0))
.onAppear {
checkAppDirectoryAndUserRole { result in
isAppInCorrectDirectory = result.isInCorrectDirectory
isUserAdmin = result.isAdmin
}
}