mirror of
https://github.com/exituser/Pearcleaner.git
synced 2026-09-17 06:09:01 +00:00
v1.8
This commit is contained in:
@@ -535,7 +535,7 @@
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 8;
|
||||
CURRENT_PROJECT_VERSION = 9;
|
||||
DEVELOPMENT_ASSET_PATHS = "";
|
||||
DEVELOPMENT_TEAM = BK8443AXLU;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
@@ -552,7 +552,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||
MARKETING_VERSION = 1.7;
|
||||
MARKETING_VERSION = 1.8;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.alienator88.Pearcleaner;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
@@ -569,7 +569,7 @@
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 8;
|
||||
CURRENT_PROJECT_VERSION = 9;
|
||||
DEVELOPMENT_ASSET_PATHS = "";
|
||||
DEVELOPMENT_TEAM = BK8443AXLU;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
@@ -586,7 +586,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||
MARKETING_VERSION = 1.7;
|
||||
MARKETING_VERSION = 1.8;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.alienator88.Pearcleaner;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
|
||||
@@ -38,7 +38,17 @@ struct AppCommands: Commands {
|
||||
Text("Refresh Apps")
|
||||
}
|
||||
.keyboardShortcut("r", modifiers: .command)
|
||||
|
||||
|
||||
Button {
|
||||
uninstallPearcleaner(appState: appState)
|
||||
} label: {
|
||||
Text("Uninstall Pearcleaner")
|
||||
}
|
||||
// .keyboardShortcut("C", modifiers: .command)
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -39,7 +39,8 @@ class AppState: ObservableObject
|
||||
appName: "",
|
||||
appVersion: "",
|
||||
appIcon: nil,
|
||||
webApp: false
|
||||
webApp: false,
|
||||
wrapped: false
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -52,8 +53,9 @@ struct AppInfo: Identifiable, Hashable {
|
||||
let appVersion: String
|
||||
let appIcon: NSImage?
|
||||
let webApp: Bool
|
||||
|
||||
static let empty = AppInfo(id: UUID(), path: URL(fileURLWithPath: ""), bundleIdentifier: "", appName: "", appVersion: "", appIcon: nil, webApp: false)
|
||||
let wrapped: Bool
|
||||
|
||||
static let empty = AppInfo(id: UUID(), path: URL(fileURLWithPath: ""), bundleIdentifier: "", appName: "", appVersion: "", appIcon: nil, webApp: false, wrapped: false)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ func getSortedApps() -> (systemApps: [AppInfo], userApps: [AppInfo]) {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -78,6 +78,7 @@ func getAppInfo(atPath path: URL) -> AppInfo? {
|
||||
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 {
|
||||
@@ -119,9 +120,86 @@ func getAppInfo(atPath path: URL) -> AppInfo? {
|
||||
} else {
|
||||
webApp = false
|
||||
}
|
||||
|
||||
return AppInfo(id: UUID(), path: path, bundleIdentifier: bundleIdentifier, appName: appName ?? "", appVersion: appVersion, appIcon: appIcon, webApp: webApp ?? false)
|
||||
|
||||
|
||||
|
||||
return AppInfo(id: UUID(), path: path, bundleIdentifier: bundleIdentifier, appName: appName ?? "", appVersion: appVersion, appIcon: appIcon, webApp: webApp ?? false, wrapped: false)
|
||||
|
||||
} else {
|
||||
// print("One or more variables missing for app at path: \(path)")
|
||||
let wrapperURL = path.appendingPathComponent("Wrapper").appendingPathComponent(path.lastPathComponent)
|
||||
if let wrappedAppInfo = getWrappedAppInfo(atPath: wrapperURL) {
|
||||
return wrappedAppInfo
|
||||
}
|
||||
}
|
||||
} else {
|
||||
print("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 {
|
||||
print("No matching image found for \(primaryIconFile).")
|
||||
}
|
||||
} else {
|
||||
print("Unable to access the directory at \(primaryIconFile).")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
print("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: true)
|
||||
|
||||
} else {
|
||||
print("One or more variables missing for app at path: \(path)")
|
||||
}
|
||||
|
||||
@@ -394,6 +394,38 @@ func presentAlert(appState: AppState) -> Alert {
|
||||
|
||||
|
||||
|
||||
// --- Pearcleaner Uninstall --
|
||||
func uninstallPearcleaner(appState: AppState) {
|
||||
|
||||
// Unload Sentinel Monitor if running
|
||||
launchctl(load: false)
|
||||
|
||||
// Get app info for Pearcleaner
|
||||
let appInfo = getAppInfo(atPath: Bundle.main.bundleURL)
|
||||
|
||||
// Find application files for Pearcleaner
|
||||
findPathsForApp(appState: appState, appInfo: appInfo!)
|
||||
|
||||
// Kill Pearcleaner and tell Finder to trash the files
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
|
||||
let selectedItemsArray = Array(appState.selectedItems).filter { !$0.path.contains(".Trash") }
|
||||
let posixFiles = selectedItemsArray.map { "POSIX file \"\($0.path)\", " }.joined().dropLast(3)
|
||||
let scriptSource = """
|
||||
tell application \"Finder\" to delete { \(posixFiles)" }
|
||||
"""
|
||||
let task = Process()
|
||||
task.launchPath = "/bin/sh"
|
||||
task.arguments = ["-c", "sleep 1; osascript -e '\(scriptSource)'"]
|
||||
task.launch()
|
||||
|
||||
NSApp.terminate(nil)
|
||||
exit(0)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// --- Create Application Support folder if it doesn't exist ---
|
||||
func ensureApplicationSupportFolderExists(appState: AppState) {
|
||||
let fileManager = FileManager.default
|
||||
|
||||
@@ -71,9 +71,9 @@ struct PearcleanerApp: App {
|
||||
appState.sortedApps.systemApps = sortedApps.systemApps
|
||||
|
||||
Task {
|
||||
|
||||
|
||||
#if !DEBUG
|
||||
|
||||
|
||||
// Make sure App Support folder exists in the future if needed for storage
|
||||
ensureApplicationSupportFolderExists(appState: appState)
|
||||
|
||||
|
||||
@@ -48,7 +48,16 @@ struct AppListItems: View {
|
||||
.background(Color("mode").opacity(0.1))
|
||||
.clipShape(.capsule)
|
||||
}
|
||||
|
||||
if 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)
|
||||
}
|
||||
|
||||
|
||||
Spacer()
|
||||
Text(appInfo.appVersion)
|
||||
|
||||
@@ -133,7 +133,7 @@ struct AppListView: View {
|
||||
VStack(spacing: 0) {
|
||||
if appState.currentView == .empty {
|
||||
TopBar(reload: $reload)
|
||||
AppDetailsEmptyView()
|
||||
AppDetailsEmptyView(showPopover: $showPopover)
|
||||
} else if appState.currentView == .files {
|
||||
TopBar(reload: $reload)
|
||||
FilesView(showPopover: $showPopover)
|
||||
@@ -184,14 +184,15 @@ struct AppDetailsEmptyView: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@EnvironmentObject var appState: AppState
|
||||
@State private var animateGradient: Bool = false
|
||||
@Binding var showPopover: Bool
|
||||
|
||||
var body: some View {
|
||||
VStack() {
|
||||
|
||||
Spacer()
|
||||
|
||||
DropTarget(appState: appState)
|
||||
|
||||
DropTarget(appState: appState, showPopover: $showPopover)
|
||||
|
||||
Spacer()
|
||||
|
||||
// if appState.isReminderVisible {
|
||||
|
||||
@@ -17,9 +17,11 @@ struct DropTarget: View, DropDelegate {
|
||||
private var types: [UTType] = [.fileURL]
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@State private var phase: CGFloat = 4
|
||||
|
||||
public init(appState: AppState) {
|
||||
@Binding var showPopover: Bool
|
||||
|
||||
public init(appState: AppState, showPopover: Binding<Bool>) {
|
||||
self.appState = appState
|
||||
_showPopover = showPopover
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -126,7 +128,11 @@ struct DropTarget: View, DropDelegate {
|
||||
self.ants = false
|
||||
appState.appInfo = appInfo!
|
||||
findPathsForApp(appState: appState, appInfo: appState.appInfo)
|
||||
appState.currentView = .files
|
||||
if mini {
|
||||
showPopover = true
|
||||
} else {
|
||||
appState.currentView = .files
|
||||
}
|
||||
}
|
||||
} else {
|
||||
print("Error: Dropped file is not an application bundle")
|
||||
|
||||
@@ -170,7 +170,12 @@ struct FilesView: View {
|
||||
}
|
||||
}
|
||||
|
||||
let selectedItemsArray = Array(appState.selectedItems).filter { !$0.path.contains(".Trash") }
|
||||
let selectedItemsArray = Array(appState.selectedItems)
|
||||
.filter { !$0.path.contains(".Trash") }
|
||||
.map { path in
|
||||
return path.path.contains("Wrapper") ? path.deletingLastPathComponent().deletingLastPathComponent() : path
|
||||
}
|
||||
|
||||
killApp(appId: appState.appInfo.bundleIdentifier) {
|
||||
moveFilesToTrash(at: selectedItemsArray) {
|
||||
withAnimation {
|
||||
@@ -259,7 +264,14 @@ struct FileDetailsItem: View {
|
||||
let icon: Image?
|
||||
let path: URL
|
||||
|
||||
// init(size: String, icon: Image?, path: URL) {
|
||||
// self.size = size
|
||||
// self.icon = icon
|
||||
// self.path = path.path.contains("Wrapper") ? path.deletingLastPathComponent().deletingLastPathComponent() : path
|
||||
// }
|
||||
|
||||
var body: some View {
|
||||
|
||||
HStack(alignment: .center, spacing: 20) {
|
||||
Toggle("", isOn: Binding(
|
||||
get: { self.appState.selectedItems.contains(self.path) },
|
||||
|
||||
@@ -27,7 +27,7 @@ struct MiniMode: View {
|
||||
VStack(spacing: 0) {
|
||||
if appState.currentView == .empty {
|
||||
TopBarMini(reload: $reload, search: $search)
|
||||
MiniEmptyView()
|
||||
MiniEmptyView(showPopover: $showPopover)
|
||||
} else if appState.currentView == .files {
|
||||
TopBarMini(reload: $reload, search: $search)
|
||||
FilesView(showPopover: $showPopover)
|
||||
@@ -98,13 +98,14 @@ struct MiniEmptyView: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
@State private var animateGradient: Bool = false
|
||||
@AppStorage("settings.general.mini") private var mini: Bool = false
|
||||
|
||||
@Binding var showPopover: Bool
|
||||
|
||||
var body: some View {
|
||||
VStack() {
|
||||
|
||||
Spacer()
|
||||
|
||||
DropTarget(appState: appState)
|
||||
DropTarget(appState: appState, showPopover: $showPopover)
|
||||
.frame(maxWidth: mini ? 300 : 500)
|
||||
|
||||
Text("Drop an app to begin")
|
||||
|
||||
Reference in New Issue
Block a user