mirror of
https://github.com/exituser/Pearcleaner.git
synced 2026-09-17 02:38:54 +00:00
Updates
This commit is contained in:
@@ -4,9 +4,7 @@
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-write</key>
|
||||
<true/>
|
||||
<key>com.apple.security.temporary-exception.files.home-relative-path.read-write</key>
|
||||
<key>com.apple.security.temporary-exception.files.absolute-path.read-only</key>
|
||||
<array>
|
||||
<string>/</string>
|
||||
</array>
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
C7DD49EE2BAB7F6000CCBA16 /* ReversePathsFetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7DD49ED2BAB7F6000CCBA16 /* ReversePathsFetch.swift */; };
|
||||
C7DD49F02BABA14400CCBA16 /* Folders.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7DD49EF2BABA14400CCBA16 /* Folders.swift */; };
|
||||
C7DE672B2BA6343D00EB1633 /* MenuBarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7DE672A2BA6343D00EB1633 /* MenuBarItem.swift */; };
|
||||
C7DFBDE72CAF5AD30013C825 /* CustomTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7DFBDE62CAF5ACB0013C825 /* CustomTabView.swift */; };
|
||||
C7EA06332C1CBE2000F872FC /* ConditionBuilderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7EA06322C1CBE2000F872FC /* ConditionBuilderView.swift */; };
|
||||
C7ED86B12BA8A35500F13DE4 /* Interface.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7ED86B02BA8A35500F13DE4 /* Interface.swift */; };
|
||||
C7F539382AF60865007DF1B2 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7F539372AF60865007DF1B2 /* Utilities.swift */; };
|
||||
@@ -132,6 +133,7 @@
|
||||
C7DD49ED2BAB7F6000CCBA16 /* ReversePathsFetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReversePathsFetch.swift; sourceTree = "<group>"; };
|
||||
C7DD49EF2BABA14400CCBA16 /* Folders.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Folders.swift; sourceTree = "<group>"; };
|
||||
C7DE672A2BA6343D00EB1633 /* MenuBarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarItem.swift; sourceTree = "<group>"; };
|
||||
C7DFBDE62CAF5ACB0013C825 /* CustomTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTabView.swift; sourceTree = "<group>"; };
|
||||
C7EA06322C1CBE2000F872FC /* ConditionBuilderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConditionBuilderView.swift; sourceTree = "<group>"; };
|
||||
C7ED86B02BA8A35500F13DE4 /* Interface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Interface.swift; sourceTree = "<group>"; };
|
||||
C7F539372AF60865007DF1B2 /* Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utilities.swift; sourceTree = "<group>"; };
|
||||
@@ -266,6 +268,7 @@
|
||||
C77B901F2AF1B390009CC655 /* Views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C7DFBDE62CAF5ACB0013C825 /* CustomTabView.swift */,
|
||||
C77B90222AF2D616009CC655 /* FilesView.swift */,
|
||||
C71848432B8D2D600046CB13 /* ZombieView.swift */,
|
||||
C76D08542AF89CDE00D07867 /* RegularMode.swift */,
|
||||
@@ -435,6 +438,7 @@
|
||||
C764680F2CA7790200DC72DF /* PearGroupBox.swift in Sources */,
|
||||
C79947962C34A794007279CC /* Tips.swift in Sources */,
|
||||
C76D08482AF83C3F00D07867 /* Update.swift in Sources */,
|
||||
C7DFBDE72CAF5AD30013C825 /* CustomTabView.swift in Sources */,
|
||||
C7FB173B2B96321300B96F9A /* AppsListView.swift in Sources */,
|
||||
C77B90162AF19377009CC655 /* AppState.swift in Sources */,
|
||||
C7DD49EE2BAB7F6000CCBA16 /* ReversePathsFetch.swift in Sources */,
|
||||
@@ -791,6 +795,7 @@
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=macosx*]" = BK8443AXLU;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = "";
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = FinderOpen/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = FinderOpen;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"location" : "https://github.com/alienator88/AlinFoundation",
|
||||
"state" : {
|
||||
"branch" : "main",
|
||||
"revision" : "579f5dd4730f085ad0163442a6c81d1c5fb65e90"
|
||||
"revision" : "9e22796895c78f8ecce0b4d8c58917ec36f7c1b0"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -71,6 +71,13 @@
|
||||
ReferencedContainer = "container:Pearcleaner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
<AdditionalOption
|
||||
key = "APP_DISTRIBUTOR_ID_OVERRIDE"
|
||||
value = "com.apple.AppStore"
|
||||
isEnabled = "YES">
|
||||
</AdditionalOption>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
|
||||
@@ -36,15 +36,6 @@ struct AppCommands: Commands {
|
||||
Text("Check for Updates")
|
||||
}
|
||||
.keyboardShortcut("u", modifiers: .command)
|
||||
|
||||
Button {
|
||||
withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) {
|
||||
reloadAppsList(appState: appState, fsm: fsm)
|
||||
}
|
||||
} label: {
|
||||
Text("Refresh Apps")
|
||||
}
|
||||
.keyboardShortcut("r", modifiers: .command)
|
||||
|
||||
Button {
|
||||
appState.triggerUninstallAlert()
|
||||
@@ -93,6 +84,15 @@ struct AppCommands: Commands {
|
||||
// Tools Menu
|
||||
CommandMenu(Text("Tools", comment: "Tools Menu")) {
|
||||
|
||||
Button {
|
||||
withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) {
|
||||
reloadAppsList(appState: appState, fsm: fsm)
|
||||
}
|
||||
} label: {
|
||||
Text("Refresh Apps")
|
||||
}
|
||||
.keyboardShortcut("r", modifiers: .command)
|
||||
|
||||
Button
|
||||
{
|
||||
if !appState.appInfo.bundleIdentifier.isEmpty {
|
||||
|
||||
@@ -9,10 +9,78 @@ import Foundation
|
||||
import SwiftUI
|
||||
import AlinFoundation
|
||||
|
||||
|
||||
// Metadata-based AppInfo Fetcher Class
|
||||
class MetadataAppInfoFetcher {
|
||||
static func getAppInfo(fromMetadata metadata: [String: Any], atPath path: URL) -> AppInfo? {
|
||||
// Extract metadata attributes for known fields
|
||||
let displayName = metadata["kMDItemDisplayName"] as? String ?? ""
|
||||
let fsName = metadata["kMDItemFSName"] as? String ?? path.lastPathComponent
|
||||
let appName = displayName.isEmpty ? fsName : displayName
|
||||
|
||||
let bundleIdentifier = metadata["kMDItemCFBundleIdentifier"] as? String ?? ""
|
||||
let version = metadata["kMDItemVersion"] as? String ?? ""
|
||||
|
||||
// Sizes
|
||||
let logicalSize = metadata["kMDItemLogicalSize"] as? Int64 ?? 0
|
||||
let physicalSize = metadata["kMDItemPhysicalSize"] as? Int64 ?? 0
|
||||
|
||||
// Check if any of the critical fields are missing or invalid
|
||||
if appName.isEmpty || bundleIdentifier.isEmpty || version.isEmpty || logicalSize == 0 || physicalSize == 0 {
|
||||
// print("Metadata is missing critical fields for \(path). Falling back to AppInfoFetcher.")
|
||||
// Fallback to the regular AppInfoFetcher for this app
|
||||
return AppInfoFetcher.getAppInfo(atPath: path)
|
||||
}
|
||||
|
||||
// Extract optional date fields
|
||||
let creationDate = metadata["kMDItemFSCreationDate"] as? Date
|
||||
let contentChangeDate = metadata["kMDItemFSContentChangeDate"] as? Date
|
||||
let lastUsedDate = metadata["kMDItemLastUsedDate"] as? Date
|
||||
|
||||
// Determine architecture type
|
||||
let arch = determineArchitecture(from: metadata)
|
||||
|
||||
// Use similar helper functions as `AppInfoFetcher` for attributes not found in metadata
|
||||
let wrapped = AppInfoFetcher.isDirectoryWrapped(path: path)
|
||||
let appIcon = AppInfoUtils.fetchAppIcon(for: path, wrapped: wrapped, md: true)
|
||||
let webApp = AppInfoUtils.isWebApp(appPath: path)
|
||||
let system = !path.path.contains(NSHomeDirectory())
|
||||
|
||||
return AppInfo(id: UUID(), path: path, bundleIdentifier: bundleIdentifier, appName: appName,
|
||||
appVersion: version, appIcon: appIcon, webApp: webApp, wrapped: wrapped, system: system,
|
||||
arch: arch, bundleSize: logicalSize, fileSize: [:],
|
||||
fileSizeLogical: [:], fileIcon: [:], creationDate: creationDate, contentChangeDate: contentChangeDate, lastUsedDate: lastUsedDate)
|
||||
}
|
||||
|
||||
/// Determine the architecture type based on metadata
|
||||
private static func determineArchitecture(from metadata: [String: Any]) -> Arch {
|
||||
guard let architectures = metadata["kMDItemExecutableArchitectures"] as? [String] else {
|
||||
return .empty
|
||||
}
|
||||
|
||||
// Check for ARM and Intel presence
|
||||
let containsArm = architectures.contains("arm64")
|
||||
let containsIntel = architectures.contains("x86_64")
|
||||
|
||||
// Determine the Arch type based on available architectures
|
||||
if containsArm && containsIntel {
|
||||
return .universal
|
||||
} else if containsArm {
|
||||
return .arm
|
||||
} else if containsIntel {
|
||||
return .intel
|
||||
} else {
|
||||
return .empty
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
class AppInfoFetcher {
|
||||
static let fileManager = FileManager.default
|
||||
|
||||
static func getAppInfo(atPath path: URL, wrapped: Bool = false) -> AppInfo? {
|
||||
public static func getAppInfo(atPath path: URL, wrapped: Bool = false) -> AppInfo? {
|
||||
if isDirectoryWrapped(path: path) {
|
||||
return handleWrappedDirectory(atPath: path)
|
||||
} else {
|
||||
@@ -20,7 +88,7 @@ class AppInfoFetcher {
|
||||
}
|
||||
}
|
||||
|
||||
private static func isDirectoryWrapped(path: URL) -> Bool {
|
||||
public static func isDirectoryWrapped(path: URL) -> Bool {
|
||||
let wrapperURL = path.appendingPathComponent("Wrapper")
|
||||
return fileManager.fileExists(atPath: wrapperURL.path)
|
||||
}
|
||||
@@ -53,17 +121,36 @@ class AppInfoFetcher {
|
||||
? bundle.infoDictionary?["CFBundleVersion"] as? String ?? ""
|
||||
: bundle.infoDictionary?["CFBundleShortVersionString"] as? String ?? ""
|
||||
|
||||
let appIcon = fetchAppIcon(for: path, wrapped: wrapped)
|
||||
let webApp = (bundle.infoDictionary?["LSTemplateApplication"] as? Bool ?? false || bundle.infoDictionary?["CFBundleExecutable"] as? String == "app_mode_loader")
|
||||
let appIcon = AppInfoUtils.fetchAppIcon(for: path, wrapped: wrapped)
|
||||
let webApp = AppInfoUtils.isWebApp(bundle: bundle)
|
||||
|
||||
|
||||
let system = !path.path.contains(NSHomeDirectory())
|
||||
|
||||
return AppInfo(id: UUID(), path: path, bundleIdentifier: bundleIdentifier, appName: appName, appVersion: appVersion, appIcon: appIcon,
|
||||
webApp: webApp, wrapped: wrapped, system: system, arch: .empty, bundleSize: 0, files: [], fileSize: [:], fileSizeLogical: [:], fileIcon: [:])
|
||||
webApp: webApp, wrapped: wrapped, system: system, arch: .empty, bundleSize: 0, fileSize: [:], fileSizeLogical: [:], fileIcon: [:], creationDate: nil, contentChangeDate: nil, lastUsedDate: nil)
|
||||
}
|
||||
|
||||
private static func fetchAppIcon(for path: URL, wrapped: Bool) -> NSImage? {
|
||||
let iconPath = wrapped ? path.deletingLastPathComponent().deletingLastPathComponent() : path
|
||||
}
|
||||
|
||||
|
||||
class AppInfoUtils {
|
||||
/// Determines if the app is a web application by directly reading its `Info.plist` using the app path.
|
||||
static func isWebApp(appPath: URL) -> Bool {
|
||||
guard let bundle = Bundle(url: appPath) else { return false }
|
||||
return isWebApp(bundle: bundle)
|
||||
}
|
||||
|
||||
/// Determines if the app is a web application based on its bundle.
|
||||
static func isWebApp(bundle: Bundle?) -> Bool {
|
||||
guard let infoDict = bundle?.infoDictionary else { return false }
|
||||
return (infoDict["LSTemplateApplication"] as? Bool ?? false) ||
|
||||
(infoDict["CFBundleExecutable"] as? String == "app_mode_loader")
|
||||
}
|
||||
|
||||
/// Fetch app icon.
|
||||
static func fetchAppIcon(for path: URL, wrapped: Bool, md: Bool = false) -> NSImage? {
|
||||
let iconPath = wrapped ? (md ? path : path.deletingLastPathComponent().deletingLastPathComponent()) : path
|
||||
if let appIcon = getIconForFileOrFolderNS(atPath: iconPath) {
|
||||
return convertICNSToPNG(icon: appIcon, size: NSSize(width: 100, height: 100))
|
||||
} else {
|
||||
@@ -71,5 +158,101 @@ class AppInfoFetcher {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/// Executes `mdls` with `-plist -` and `-nullMarker ""` options and returns metadata in a structured dictionary.
|
||||
func getMDLSMetadataAsPlist(for paths: [String]) -> [String: [String: Any]]? {
|
||||
let task = Process()
|
||||
task.executableURL = URL(fileURLWithPath: "/usr/bin/mdls")
|
||||
|
||||
// Define the required metadata attributes to include in the output
|
||||
let attributes = [
|
||||
"kMDItemFSCreationDate",
|
||||
"kMDItemFSContentChangeDate",
|
||||
"kMDItemLastUsedDate",
|
||||
"kMDItemDisplayName",
|
||||
"kMDItemAppStoreCategory",
|
||||
"kMDItemCFBundleIdentifier",
|
||||
"kMDItemExecutableArchitectures",
|
||||
"kMDItemFSName",
|
||||
"kMDItemVersion",
|
||||
"kMDItemLogicalSize",
|
||||
"kMDItemPhysicalSize"
|
||||
]
|
||||
|
||||
// Construct the `mdls` arguments: paths + -name for each attribute + -plist - + -nullMarker ""
|
||||
var arguments = paths
|
||||
for attribute in attributes {
|
||||
arguments.append("-name")
|
||||
arguments.append(attribute)
|
||||
}
|
||||
arguments.append("-plist") // Use plist format
|
||||
arguments.append("-") // Output to stdout
|
||||
arguments.append("-nullMarker") // Replace null attributes
|
||||
arguments.append("") // Substitute null values with empty string
|
||||
|
||||
task.arguments = arguments
|
||||
|
||||
// Use Pipe to capture the output
|
||||
let pipe = Pipe()
|
||||
let errorPipe = Pipe()
|
||||
task.standardOutput = pipe
|
||||
task.standardError = errorPipe // Capture stderr in case there are errors
|
||||
|
||||
do {
|
||||
// Run the task
|
||||
try task.run()
|
||||
|
||||
// Read the data from the pipe
|
||||
let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
||||
|
||||
let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile()
|
||||
if let errorOutput = String(data: errorData, encoding: .utf8), !errorOutput.isEmpty {
|
||||
print("Error Output from mdls:\n\(errorOutput)\n")
|
||||
}
|
||||
|
||||
// Check if there's any output captured
|
||||
if data.isEmpty {
|
||||
print("No output captured from mdls.")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Attempt to parse the plist output into a Swift array of dictionaries
|
||||
guard let plistArray = try PropertyListSerialization.propertyList(from: data, format: nil) as? [[String: Any]] else {
|
||||
print("Failed to parse plist output into expected format.")
|
||||
return nil
|
||||
}
|
||||
|
||||
// print("Parsed plist array count: \(plistArray.count)")
|
||||
|
||||
// Ensure the number of plist items matches the number of paths
|
||||
if plistArray.count != paths.count {
|
||||
print("Warning: Number of plist items (\(plistArray.count)) does not match the number of paths (\(paths.count)).")
|
||||
}
|
||||
|
||||
var metadataDictionary = [String: [String: Any]]()
|
||||
|
||||
// Map each metadata dictionary to its corresponding path using indices
|
||||
for (index, appMetadata) in plistArray.enumerated() {
|
||||
// Match metadata to path using the index
|
||||
if index < paths.count {
|
||||
let path = paths[index]
|
||||
metadataDictionary[path] = appMetadata
|
||||
// print("Mapped metadata to path: \(path)")
|
||||
} else {
|
||||
print("Warning: More metadata entries than paths.")
|
||||
}
|
||||
}
|
||||
return metadataDictionary
|
||||
|
||||
} catch {
|
||||
print("Error running mdls: \(error)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ class AppState: ObservableObject {
|
||||
@Published var sortedApps: [AppInfo] = []
|
||||
@Published var selectedItems = Set<URL>()
|
||||
@Published var currentView = CurrentDetailsView.empty
|
||||
@Published var currentPage = CurrentPage.installed
|
||||
@Published var showAlert: Bool = false
|
||||
@Published var sidebar: Bool = true
|
||||
@Published var reload: Bool = false
|
||||
@@ -67,10 +68,13 @@ class AppState: ObservableObject {
|
||||
system: false,
|
||||
arch: .empty,
|
||||
bundleSize: 0,
|
||||
files: [],
|
||||
// files: [],
|
||||
fileSize: [:],
|
||||
fileSizeLogical: [:],
|
||||
fileIcon: [:]
|
||||
fileIcon: [:],
|
||||
creationDate: nil,
|
||||
contentChangeDate: nil,
|
||||
lastUsedDate: nil
|
||||
)
|
||||
|
||||
self.zombieFile = ZombieFile(
|
||||
@@ -137,12 +141,15 @@ struct AppInfo: Identifiable, Equatable, Hashable {
|
||||
let wrapped: Bool
|
||||
let system: Bool
|
||||
var arch: Arch
|
||||
var bundleSize: Int64
|
||||
var files: [URL]
|
||||
var bundleSize: Int64 // Only used in the app list view
|
||||
// var files: [URL]
|
||||
var fileSize: [URL:Int64]
|
||||
var fileSizeLogical: [URL:Int64]
|
||||
var fileIcon: [URL:NSImage?]
|
||||
var totalSize: Int64
|
||||
let creationDate: Date?
|
||||
let contentChangeDate: Date?
|
||||
let lastUsedDate: Date?
|
||||
var totalSize: Int64
|
||||
{
|
||||
return fileSize.values.reduce(0, +)
|
||||
}
|
||||
@@ -150,9 +157,10 @@ struct AppInfo: Identifiable, Equatable, Hashable {
|
||||
{
|
||||
return fileSizeLogical.values.reduce(0, +)
|
||||
}
|
||||
|
||||
|
||||
|
||||
static let empty = AppInfo(id: UUID(), path: URL(fileURLWithPath: ""), bundleIdentifier: "", appName: "", appVersion: "", appIcon: nil, webApp: false, wrapped: false, system: false, arch: .empty, bundleSize: 0, files: [], fileSize: [:], fileSizeLogical: [:], fileIcon: [:])
|
||||
static let empty = AppInfo(id: UUID(), path: URL(fileURLWithPath: ""), bundleIdentifier: "", appName: "", appVersion: "", appIcon: nil, webApp: false, wrapped: false, system: false, arch: .empty, bundleSize: 0, fileSize: [:], fileSizeLogical: [:], fileIcon: [:], creationDate: nil, contentChangeDate: nil, lastUsedDate: nil)
|
||||
|
||||
}
|
||||
|
||||
@@ -185,6 +193,19 @@ enum Arch {
|
||||
case empty
|
||||
}
|
||||
|
||||
enum CurrentPage:Int
|
||||
{
|
||||
case installed
|
||||
case uninstalled
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .installed: return "Installed"
|
||||
case .uninstalled: return "Uninstalled"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
enum CurrentTabView:Int
|
||||
{
|
||||
|
||||
@@ -37,9 +37,40 @@ func getSortedApps(paths: [String]) -> [AppInfo] {
|
||||
// Collect system applications
|
||||
paths.forEach { collectAppPaths(at: $0) }
|
||||
|
||||
// let startTime = Date()
|
||||
|
||||
// Convert collected paths to string format for metadata query
|
||||
let combinedPaths = apps.map { $0.path }
|
||||
|
||||
// Get metadata for all collected app paths
|
||||
var metadataDictionary: [String: [String: Any]] = [:]
|
||||
if let metadata = getMDLSMetadataAsPlist(for: combinedPaths) {
|
||||
metadataDictionary = metadata
|
||||
}
|
||||
|
||||
// Process each app path and construct AppInfo using metadata first, then fallback if necessary
|
||||
let appInfos: [AppInfo] = apps.compactMap { appURL in
|
||||
let appPath = appURL.path
|
||||
|
||||
if let appMetadata = metadataDictionary[appPath] {
|
||||
// Use `MetadataAppInfoFetcher` first
|
||||
return MetadataAppInfoFetcher.getAppInfo(fromMetadata: appMetadata, atPath: appURL)
|
||||
} else {
|
||||
// Fallback to `AppInfoFetcher` if no metadata found
|
||||
return AppInfoFetcher.getAppInfo(atPath: appURL)
|
||||
}
|
||||
}
|
||||
|
||||
// Sort apps by display name
|
||||
let sortedApps = appInfos.sorted { $0.appName.lowercased() < $1.appName.lowercased() }
|
||||
|
||||
// Get app info and sort
|
||||
let sortedApps = apps
|
||||
.compactMap { AppInfoFetcher.getAppInfo(atPath: $0) }
|
||||
// let sortedApps = apps
|
||||
// .compactMap { AppInfoFetcher.getAppInfo(atPath: $0) }
|
||||
|
||||
|
||||
// let elapsedTime = Date().timeIntervalSince(startTime)
|
||||
// print("Time taken for mdls metadata extraction: \(elapsedTime) seconds")
|
||||
|
||||
return sortedApps
|
||||
}
|
||||
@@ -169,7 +200,7 @@ func moveFilesToTrash(appState: AppState, at fileURLs: [URL], completion: @escap
|
||||
// Stop Sentinel FileWatcher momentarily to ignore .app bundle being sent to Trash
|
||||
sendStopNotificationFW()
|
||||
|
||||
updateOnBackground {
|
||||
updateOnMain {
|
||||
let posixFiles = fileURLs.map { item in
|
||||
return "POSIX file \"\(item.path)\"" + (item == fileURLs.last ? "" : ", ")}.joined()
|
||||
let scriptSource = """
|
||||
@@ -182,19 +213,15 @@ func moveFilesToTrash(appState: AppState, at fileURLs: [URL], completion: @escap
|
||||
|
||||
// Handle any AppleScript errors
|
||||
if let error = error {
|
||||
DispatchQueue.main.async {
|
||||
printOS("Trash Error: \(error)")
|
||||
completion(false) // Indicate failure
|
||||
}
|
||||
printOS("Trash Error: \(error)")
|
||||
completion(false) // Indicate failure
|
||||
return
|
||||
}
|
||||
|
||||
// Check if output is null, indicating the user canceled the operation
|
||||
if output.descriptorType == typeNull {
|
||||
DispatchQueue.main.async {
|
||||
printOS("Trash Error: operation canceled by the user")
|
||||
completion(false) // Indicate failure due to cancellation
|
||||
}
|
||||
printOS("Trash Error: operation canceled by the user")
|
||||
completion(false) // Indicate failure due to cancellation
|
||||
return
|
||||
}
|
||||
|
||||
@@ -203,9 +230,7 @@ func moveFilesToTrash(appState: AppState, at fileURLs: [URL], completion: @escap
|
||||
printOS("Trash: \(outputString)")
|
||||
}
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
completion(true) // Indicate success
|
||||
}
|
||||
completion(true) // Indicate success
|
||||
}
|
||||
|
||||
}
|
||||
@@ -265,7 +290,7 @@ func undoTrash(appState: AppState, completion: @escaping () -> Void = {}) {
|
||||
"""
|
||||
var error: NSDictionary?
|
||||
|
||||
updateOnBackground {
|
||||
updateOnMain {
|
||||
if let scriptObject = NSAppleScript(source: scriptSource) {
|
||||
let output: NSAppleEventDescriptor = scriptObject.executeAndReturnError(&error)
|
||||
if let error = error {
|
||||
@@ -274,9 +299,7 @@ func undoTrash(appState: AppState, completion: @escaping () -> Void = {}) {
|
||||
printOS(outputString)
|
||||
}
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
completion()
|
||||
}
|
||||
completion()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -303,23 +303,26 @@ func manageSymlink(install: Bool) {
|
||||
"""
|
||||
}
|
||||
|
||||
// Execute the AppleScript
|
||||
var error: NSDictionary?
|
||||
if let scriptObject = NSAppleScript(source: script) {
|
||||
scriptObject.executeAndReturnError(&error)
|
||||
updateOnMain {
|
||||
// Execute the AppleScript
|
||||
var error: NSDictionary?
|
||||
if let scriptObject = NSAppleScript(source: script) {
|
||||
scriptObject.executeAndReturnError(&error)
|
||||
|
||||
if let error = error {
|
||||
printOS("AppleScript Error: \(error)")
|
||||
} else {
|
||||
if install {
|
||||
printOS("Symlink created successfully at \(symlinkPath).")
|
||||
if let error = error {
|
||||
printOS("AppleScript Error: \(error)")
|
||||
} else {
|
||||
printOS("Symlink removed successfully from \(symlinkPath).")
|
||||
if install {
|
||||
printOS("Symlink created successfully at \(symlinkPath).")
|
||||
} else {
|
||||
printOS("Symlink removed successfully from \(symlinkPath).")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
printOS("Error: Unable to create the AppleScript object.")
|
||||
}
|
||||
} else {
|
||||
printOS("Error: Unable to create the AppleScript object.")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -361,14 +364,17 @@ func caskCleanup(app: String) {
|
||||
end tell
|
||||
"""
|
||||
|
||||
var error: NSDictionary?
|
||||
if let appleScript = NSAppleScript(source: script) {
|
||||
appleScript.executeAndReturnError(&error)
|
||||
updateOnMain {
|
||||
var error: NSDictionary?
|
||||
if let appleScript = NSAppleScript(source: script) {
|
||||
appleScript.executeAndReturnError(&error)
|
||||
}
|
||||
|
||||
if let error = error {
|
||||
printOS("AppleScript Error: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
if let error = error {
|
||||
printOS("AppleScript Error: \(error)")
|
||||
}
|
||||
} else {
|
||||
printOS("Brew cleanup: No cask found for \(app).")
|
||||
}
|
||||
@@ -453,15 +459,17 @@ func caskCleanup2(app: String) {
|
||||
fi'" in front window
|
||||
end tell
|
||||
"""
|
||||
updateOnMain {
|
||||
var error: NSDictionary?
|
||||
if let appleScript = NSAppleScript(source: script) {
|
||||
appleScript.executeAndReturnError(&error)
|
||||
}
|
||||
|
||||
var error: NSDictionary?
|
||||
if let appleScript = NSAppleScript(source: script) {
|
||||
appleScript.executeAndReturnError(&error)
|
||||
if let error = error {
|
||||
print("AppleScript Error: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
if let error = error {
|
||||
print("AppleScript Error: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -796,6 +804,15 @@ func isNested(path: URL) -> Bool {
|
||||
}
|
||||
|
||||
|
||||
// Date formatter for metadata
|
||||
func formattedMDDate(from date: Date) -> String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "yyyy-MM-dd HH:mm zzz"
|
||||
formatter.timeZone = .current // Use the current timezone
|
||||
return formatter.string(from: date)
|
||||
}
|
||||
|
||||
|
||||
|
||||
// --- Extend String to remove periods, spaces and lowercase the string
|
||||
extension String {
|
||||
|
||||
@@ -35,12 +35,12 @@ class WindowSettings: ObservableObject {
|
||||
func newWindow<V: View>(mini: Bool, withView view: @escaping () -> V) {
|
||||
let frame = self.resetWindowSettings(mini: mini)
|
||||
|
||||
if menubarEnabled {
|
||||
windowRef = NSWindow(
|
||||
contentRect: .zero,
|
||||
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
|
||||
backing: .buffered, defer: false)
|
||||
}
|
||||
// if menubarEnabled {
|
||||
// windowRef = NSWindow(
|
||||
// contentRect: .zero,
|
||||
// styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
|
||||
// backing: .buffered, defer: false)
|
||||
// }
|
||||
// Update the existing windowRef with all desired settings
|
||||
windowRef?.contentView = NSHostingView(rootView: view())
|
||||
windowRef?.setFrame(frame, display: true, animate: true)
|
||||
|
||||
@@ -98,21 +98,6 @@ struct PearcleanerApp: App {
|
||||
})
|
||||
.onAppear {
|
||||
|
||||
if miniView {
|
||||
appState.currentView = .apps
|
||||
} else {
|
||||
appState.currentView = .empty
|
||||
}
|
||||
|
||||
// Track main window within windowSettings class
|
||||
windowSettings.trackMainWindow()
|
||||
|
||||
// Disable tabbing
|
||||
NSWindow.allowsAutomaticWindowTabbing = false
|
||||
|
||||
// Load apps list on startup
|
||||
reloadAppsList(appState: appState, fsm: fsm)
|
||||
|
||||
// Enable menubar item
|
||||
if menubarEnabled {
|
||||
MenuBarExtraManager.shared.addMenuBarExtra(withView: {
|
||||
@@ -125,12 +110,36 @@ struct PearcleanerApp: App {
|
||||
.environmentObject(permissionManager)
|
||||
.preferredColorScheme(themeManager.displayMode.colorScheme)
|
||||
})
|
||||
|
||||
findAndHideWindows(named: ["Pearcleaner"])
|
||||
NSApplication.shared.setActivationPolicy(.accessory)
|
||||
|
||||
windowSettings.trackMainWindow()
|
||||
findAndHideWindows(named: ["Pearcleaner", ""])
|
||||
// Catch windows in case something gets opened from SwiftUI lifecycle
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: {
|
||||
findAndHideWindows(named: ["Pearcleaner", ""])
|
||||
})
|
||||
}
|
||||
|
||||
// Set mini view
|
||||
if mini && miniView {
|
||||
appState.currentView = .apps
|
||||
} else {
|
||||
appState.currentView = .empty
|
||||
}
|
||||
|
||||
// Disable tabbing
|
||||
NSWindow.allowsAutomaticWindowTabbing = false
|
||||
|
||||
// Load apps list on startup
|
||||
reloadAppsList(appState: appState, fsm: fsm)
|
||||
|
||||
if !menubarEnabled {
|
||||
Task {
|
||||
// Track main window within windowSettings class
|
||||
windowSettings.trackMainWindow()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
.windowStyle(.hiddenTitleBar)
|
||||
@@ -197,6 +206,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -272,8 +272,11 @@ struct GeneralSettingsTab: View {
|
||||
|
||||
}
|
||||
.onAppear {
|
||||
appState.updateExtensionStatus()
|
||||
isCLISymlinked = checkCLISymlink()
|
||||
Task {
|
||||
appState.updateExtensionStatus()
|
||||
isCLISymlinked = checkCLISymlink()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -283,7 +283,7 @@ struct InterfaceSettingsTab: View {
|
||||
.preferredColorScheme(themeManager.displayMode.colorScheme)
|
||||
})
|
||||
NSApplication.shared.setActivationPolicy(.accessory)
|
||||
findAndHideWindows(named: ["Pearcleaner"])
|
||||
findAndHideWindows(named: ["Pearcleaner", ""])
|
||||
} else {
|
||||
MenuBarExtraManager.shared.removeMenuBarExtra()
|
||||
NSApplication.shared.setActivationPolicy(.regular)
|
||||
|
||||
@@ -82,31 +82,29 @@ struct AppListItems: View {
|
||||
}
|
||||
|
||||
|
||||
Spacer()
|
||||
|
||||
if appInfo.webApp {
|
||||
Text("web")
|
||||
.font(.footnote)
|
||||
Image(systemName: "safari")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 16, height: 16)
|
||||
.foregroundStyle(.primary.opacity(0.3))
|
||||
.frame(minWidth: 30, minHeight: 15)
|
||||
.padding(2)
|
||||
.background(
|
||||
Capsule().strokeBorder(.primary.opacity(0.3), lineWidth: 1)
|
||||
)
|
||||
.symbolRenderingMode(.monochrome)
|
||||
.help("Web app")
|
||||
}
|
||||
if appInfo.wrapped {
|
||||
Text("iOS")
|
||||
.font(.footnote)
|
||||
Image(systemName: "iphone")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 16, height: 16)
|
||||
.foregroundStyle(.primary.opacity(0.3))
|
||||
.frame(minWidth: 30, minHeight: 15)
|
||||
.padding(2)
|
||||
.background(
|
||||
Capsule().strokeBorder(.primary.opacity(0.3), lineWidth: 1)
|
||||
)
|
||||
.symbolRenderingMode(.monochrome)
|
||||
.help("iOS app")
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
if minimalEnabled {
|
||||
Text(bundleSize == 0 ? "v\(appInfo.appVersion)" : (isHovered ? "v\(appInfo.appVersion)" : formatByte(size: bundleSize).human))
|
||||
Text(appInfo.bundleSize == 0 ? "v\(appInfo.appVersion)" : (isHovered ? "v\(appInfo.appVersion)" : formatByte(size: appInfo.bundleSize).human))
|
||||
.font(.system(size: (isHovered || isSelected) ? 12 : 10))
|
||||
.foregroundStyle(.primary.opacity(0.5))
|
||||
}
|
||||
@@ -119,7 +117,7 @@ struct AppListItems: View {
|
||||
.frame(height: 35)
|
||||
.padding(.horizontal)
|
||||
.padding(.vertical, 5)
|
||||
.help(appInfo.appName)
|
||||
// .help(appInfo.appName)
|
||||
.onHover { hovering in
|
||||
withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) {
|
||||
self.isHovered = hovering
|
||||
@@ -155,9 +153,10 @@ struct AppListItems: View {
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if self.bundleSize == 0 {
|
||||
if appInfo.bundleSize == 0 {
|
||||
appState.getBundleSize(for: appInfo) { size in
|
||||
self.bundleSize = size
|
||||
// print("Getting size for: \(appInfo.appName)")
|
||||
// appInfo.bundleSize = size
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,23 +28,14 @@ struct AppSearchView: View {
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .center, spacing: 0) {
|
||||
|
||||
Spacer()
|
||||
.frame(height: 10)
|
||||
.padding(.top, !isMenuBar ? 25 : 0)
|
||||
|
||||
if updater.updateAvailable {
|
||||
UpdateBadge(updater: updater)
|
||||
.padding(.horizontal)
|
||||
} else if let _ = permissionManager.results, !permissionManager.allPermissionsGranted {
|
||||
PermissionsBadge()
|
||||
.padding(.horizontal)
|
||||
} else if updater.announcementAvailable {
|
||||
FeatureBadge(updater: updater)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.frame(height: !isMenuBar ? 30 : 10)
|
||||
|
||||
|
||||
AppsListView(search: $search, showPopover: $showPopover, filteredApps: filteredApps)
|
||||
searchBarComponent
|
||||
.padding(.horizontal, search.isEmpty ? 10 : 5)
|
||||
.padding(.bottom, 5)
|
||||
|
||||
Divider()
|
||||
|
||||
@@ -55,105 +46,125 @@ struct AppSearchView: View {
|
||||
|
||||
#endif
|
||||
|
||||
HStack(spacing: 10) {
|
||||
|
||||
if search.isEmpty {
|
||||
Button("Refresh") {
|
||||
withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) {
|
||||
showPopover = false
|
||||
reloadAppsList(appState: appState, fsm: fsm)
|
||||
}
|
||||
}
|
||||
.buttonStyle(SimpleButtonStyle(icon: "arrow.counterclockwise.circle", help: "Refresh apps (⌘+R)", size: 16))
|
||||
}
|
||||
|
||||
SearchBar(search: $search, darker: (mini || menubarEnabled) ? false : true, glass: glass)
|
||||
|
||||
|
||||
if search.isEmpty {
|
||||
Button("More") {
|
||||
self.showMenu.toggle()
|
||||
}
|
||||
.buttonStyle(SimpleButtonStyle(icon: "ellipsis.circle", help: "More", size: 16, rotate: true))
|
||||
.popover(isPresented: $showMenu) {
|
||||
VStack(alignment: .leading) {
|
||||
|
||||
Button("") {
|
||||
withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) {
|
||||
selectedSortAlpha.toggle()
|
||||
}
|
||||
}
|
||||
.buttonStyle(SimpleButtonStyle(icon: "circle.fill", label: "Sorting: \(selectedSortAlpha ? "ABC" : "123")", help: "Can also click on User/System headers to toggle this", size: 5))
|
||||
|
||||
if mini && !menubarEnabled {
|
||||
Button("") {
|
||||
withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) {
|
||||
appState.currentView = .empty
|
||||
appState.appInfo = AppInfo.empty
|
||||
showPopover = false
|
||||
}
|
||||
}
|
||||
.buttonStyle(SimpleButtonStyle(icon: "circle.fill", label: "Drop Target", help: "Drop Target", size: 5))
|
||||
}
|
||||
|
||||
|
||||
Button("Leftover Files") {
|
||||
showMenu = false
|
||||
withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) {
|
||||
showPopover = false
|
||||
appState.appInfo = .empty
|
||||
if appState.zombieFile.fileSize.keys.isEmpty {
|
||||
appState.currentView = .zombie
|
||||
appState.showProgress.toggle()
|
||||
showPopover.toggle()
|
||||
reversePreloader(allApps: appState.sortedApps, appState: appState, locations: locations, fsm: fsm)
|
||||
} else {
|
||||
appState.currentView = .zombie
|
||||
showPopover.toggle()
|
||||
}
|
||||
}
|
||||
}
|
||||
.buttonStyle(SimpleButtonStyle(icon: "circle.fill", label: "Leftover Files", help: "Leftover Files", size: 5))
|
||||
|
||||
|
||||
if #available(macOS 14.0, *) {
|
||||
SettingsLink {}
|
||||
.buttonStyle(SimpleButtonStyle(icon: "circle.fill", label: "Settings", help: "Settings", size: 5))
|
||||
} else {
|
||||
Button("Settings") {
|
||||
if #available(macOS 13.0, *) {
|
||||
NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil)
|
||||
} else {
|
||||
NSApp.sendAction(Selector(("showPreferencesWindow:")), to: nil, from: nil)
|
||||
}
|
||||
showMenu = false
|
||||
}
|
||||
.buttonStyle(SimpleButtonStyle(icon: "circle.fill", label: "Settings", help: "Settings", size: 5))
|
||||
}
|
||||
|
||||
if menubarEnabled {
|
||||
Button("Quit") {
|
||||
NSApp.terminate(nil)
|
||||
}
|
||||
.buttonStyle(SimpleButtonStyle(icon: "circle.fill", label: "Quit Pearcleaner", help: "Quit Pearcleaner", size: 5))
|
||||
}
|
||||
|
||||
}
|
||||
.padding()
|
||||
.background(backgroundView(themeManager: themeManager, glass: glass).padding(-80))
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
AppsListView(search: $search, showPopover: $showPopover, filteredApps: filteredApps)
|
||||
.padding(.vertical, 4)
|
||||
|
||||
if updater.updateAvailable {
|
||||
Divider()
|
||||
UpdateBadge(updater: updater)
|
||||
.padding(8)
|
||||
} else if let _ = permissionManager.results, !permissionManager.allPermissionsGranted {
|
||||
Divider()
|
||||
PermissionsBadge()
|
||||
.padding(8)
|
||||
} else if updater.announcementAvailable {
|
||||
Divider()
|
||||
FeatureBadge(updater: updater)
|
||||
.padding(8)
|
||||
}
|
||||
.padding(.horizontal, search.isEmpty ? 10 : 5)
|
||||
.padding(.vertical, 5)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private var searchBarComponent: some View {
|
||||
HStack(spacing: 10) {
|
||||
|
||||
if search.isEmpty {
|
||||
Button("Refresh") {
|
||||
withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) {
|
||||
showPopover = false
|
||||
reloadAppsList(appState: appState, fsm: fsm)
|
||||
}
|
||||
}
|
||||
.buttonStyle(SimpleButtonStyle(icon: "arrow.counterclockwise.circle", help: "Refresh apps (⌘+R)", size: 16))
|
||||
}
|
||||
|
||||
SearchBar(search: $search, darker: (mini || menubarEnabled) ? false : true, glass: glass)
|
||||
|
||||
|
||||
if search.isEmpty {
|
||||
Button("More") {
|
||||
self.showMenu.toggle()
|
||||
}
|
||||
.buttonStyle(SimpleButtonStyle(icon: "ellipsis.circle", help: "More", size: 16, rotate: true))
|
||||
.popover(isPresented: $showMenu) {
|
||||
VStack(alignment: .leading) {
|
||||
|
||||
Button("") {
|
||||
withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) {
|
||||
selectedSortAlpha.toggle()
|
||||
}
|
||||
}
|
||||
.buttonStyle(SimpleButtonStyle(icon: "circle.fill", label: "Sorting: \(selectedSortAlpha ? "ABC" : "123")", help: "Can also click on User/System headers to toggle this", size: 5))
|
||||
|
||||
if mini && !menubarEnabled {
|
||||
Button("") {
|
||||
withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) {
|
||||
appState.currentView = .empty
|
||||
appState.appInfo = AppInfo.empty
|
||||
showPopover = false
|
||||
}
|
||||
}
|
||||
.buttonStyle(SimpleButtonStyle(icon: "circle.fill", label: "Drop Target", help: "Drop Target", size: 5))
|
||||
}
|
||||
|
||||
|
||||
Button("Leftover Files") {
|
||||
showMenu = false
|
||||
withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) {
|
||||
showPopover = false
|
||||
appState.appInfo = .empty
|
||||
if appState.zombieFile.fileSize.keys.isEmpty {
|
||||
appState.currentView = .zombie
|
||||
appState.showProgress.toggle()
|
||||
showPopover.toggle()
|
||||
reversePreloader(allApps: appState.sortedApps, appState: appState, locations: locations, fsm: fsm)
|
||||
} else {
|
||||
appState.currentView = .zombie
|
||||
showPopover.toggle()
|
||||
}
|
||||
}
|
||||
}
|
||||
.buttonStyle(SimpleButtonStyle(icon: "circle.fill", label: "Leftover Files", help: "Leftover Files", size: 5))
|
||||
|
||||
|
||||
if #available(macOS 14.0, *) {
|
||||
SettingsLink {}
|
||||
.buttonStyle(SimpleButtonStyle(icon: "circle.fill", label: "Settings", help: "Settings", size: 5))
|
||||
} else {
|
||||
Button("Settings") {
|
||||
if #available(macOS 13.0, *) {
|
||||
NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil)
|
||||
} else {
|
||||
NSApp.sendAction(Selector(("showPreferencesWindow:")), to: nil, from: nil)
|
||||
}
|
||||
showMenu = false
|
||||
}
|
||||
.buttonStyle(SimpleButtonStyle(icon: "circle.fill", label: "Settings", help: "Settings", size: 5))
|
||||
}
|
||||
|
||||
if menubarEnabled {
|
||||
Button("Quit") {
|
||||
NSApp.terminate(nil)
|
||||
}
|
||||
.buttonStyle(SimpleButtonStyle(icon: "circle.fill", label: "Quit Pearcleaner", help: "Quit Pearcleaner", size: 5))
|
||||
}
|
||||
|
||||
}
|
||||
.padding()
|
||||
.background(backgroundView(themeManager: themeManager, glass: glass).padding(-80))
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var filteredApps: [AppInfo] {
|
||||
let apps: [AppInfo]
|
||||
if search.isEmpty {
|
||||
|
||||
@@ -18,7 +18,7 @@ struct AppsListView: View {
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
LazyVStack(alignment: .leading, spacing: 0, pinnedViews: [.sectionHeaders]) {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
let filteredUserApps = filteredApps.filter { !$0.system }
|
||||
let filteredSystemApps = filteredApps.filter { $0.system }
|
||||
|
||||
@@ -48,7 +48,7 @@ struct SectionView: View {
|
||||
@AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true
|
||||
|
||||
var body: some View {
|
||||
LazyVStack(spacing: 0) {
|
||||
VStack(spacing: 0) {
|
||||
Header(title: title, count: count, showPopover: $showPopover)
|
||||
.padding(.leading, 5)
|
||||
.onTapGesture {
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
//
|
||||
// CustomTabView.swift
|
||||
// Pearcleaner
|
||||
//
|
||||
// Created by Alin Lupascu on 10/3/24.
|
||||
//
|
||||
import SwiftUI
|
||||
|
||||
public struct CustomTabView: View {
|
||||
private let titles: [String]
|
||||
private let icons: [String]
|
||||
private let tabViews: [AnyView]
|
||||
|
||||
@State private var selection = 0
|
||||
@State private var indexHovered = -1
|
||||
|
||||
public init(content: [(title: String, icon: String, view: AnyView)]) {
|
||||
self.titles = content.map{ $0.title }
|
||||
self.icons = content.map{ $0.icon }
|
||||
self.tabViews = content.map{ $0.view }
|
||||
}
|
||||
|
||||
public var tabBar: some View {
|
||||
HStack {
|
||||
Spacer()
|
||||
ForEach(0..<titles.count, id: \.self) { index in
|
||||
|
||||
HStack {
|
||||
Image(systemName: self.icons[index])
|
||||
.font(.title2)
|
||||
Text(self.titles[index])
|
||||
}
|
||||
// .frame(height: 30)
|
||||
.padding(5)
|
||||
.background(Color.gray.opacity(((self.selection == index) || (self.indexHovered == index)) ? 0.3 : 0),
|
||||
in: RoundedRectangle(cornerRadius: 8, style: .continuous))
|
||||
|
||||
.frame(height: 40)
|
||||
.padding(.horizontal, 0)
|
||||
.foregroundColor(self.selection == index ? .accent : .primary.opacity(0.8))
|
||||
.onHover(perform: { hovering in
|
||||
if hovering {
|
||||
indexHovered = index
|
||||
} else {
|
||||
indexHovered = -1
|
||||
}
|
||||
})
|
||||
.onTapGesture {
|
||||
self.selection = index
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.padding(0)
|
||||
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
tabBar
|
||||
|
||||
tabViews[selection]
|
||||
.padding(0)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
}
|
||||
.padding(0)
|
||||
}
|
||||
}
|
||||
@@ -134,15 +134,24 @@ struct FilesView: View {
|
||||
Text("•").foregroundStyle(Color("AccentColor"))
|
||||
Text("v\(appState.appInfo.appVersion)").font(.title3)
|
||||
|
||||
// if appState.appInfo.appName.count < 5 {
|
||||
// InfoButton(text: "Pearcleaner searches for files via a combination of bundle id and app name. \(appState.appInfo.appName) has a common or short app name so there might be unrelated files found. Please check the list thoroughly before uninstalling.")
|
||||
// }
|
||||
|
||||
}
|
||||
Text("\(appState.appInfo.bundleIdentifier)")
|
||||
.lineLimit(1)
|
||||
.font(.title3)
|
||||
.foregroundStyle((.primary.opacity(0.5)))
|
||||
|
||||
if let creationDate = appState.appInfo.creationDate {
|
||||
Text("Installed: \(formattedMDDate(from: creationDate))")
|
||||
.font(.footnote)
|
||||
}
|
||||
if let modificationDate = appState.appInfo.contentChangeDate {
|
||||
Text("Modified: \(formattedMDDate(from: modificationDate))")
|
||||
.font(.footnote)
|
||||
}
|
||||
if let lastUsedDate = appState.appInfo.lastUsedDate {
|
||||
Text("Last Used: \(formattedMDDate(from: lastUsedDate))")
|
||||
.font(.footnote)
|
||||
}
|
||||
}
|
||||
.padding(.leading)
|
||||
|
||||
@@ -153,6 +162,8 @@ struct FilesView: View {
|
||||
Text("\(displaySizeTotal)").font(.title).fontWeight(.bold).help("Total size on disk")
|
||||
Text("\(appState.appInfo.fileSize.count == 1 ? "\(appState.selectedItems.count) / \(appState.appInfo.fileSize.count) item" : "\(appState.selectedItems.count) / \(appState.appInfo.fileSize.count) items")")
|
||||
.font(.callout).foregroundStyle((.primary.opacity(0.5)))
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -13,10 +13,12 @@ struct RegularMode: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
@EnvironmentObject var themeManager: ThemeManager
|
||||
@EnvironmentObject var locations: Locations
|
||||
@EnvironmentObject var fsm: FolderSettingsManager
|
||||
@AppStorage("settings.general.glass") private var glass: Bool = false
|
||||
@AppStorage("settings.general.sidebarWidth") private var sidebarWidth: Double = 280
|
||||
@AppStorage("settings.menubar.enabled") private var menubarEnabled: Bool = false
|
||||
@AppStorage("settings.general.mini") private var mini: Bool = false
|
||||
@AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true
|
||||
@Binding var search: String
|
||||
@State private var showSys: Bool = true
|
||||
@State private var showUsr: Bool = true
|
||||
@@ -26,57 +28,90 @@ struct RegularMode: View {
|
||||
|
||||
var body: some View {
|
||||
|
||||
HStack(alignment: .center, spacing: 0) {
|
||||
// Main App Window
|
||||
VStack(spacing: 0) {
|
||||
|
||||
// App List
|
||||
HStack(spacing: 0){
|
||||
// Tab View
|
||||
CustomTabView(content: [
|
||||
(
|
||||
title: "Installed",
|
||||
icon: "externaldrive",
|
||||
view: AnyView (
|
||||
HStack(alignment: .center, spacing: 0) {
|
||||
|
||||
if appState.reload {
|
||||
VStack {
|
||||
Spacer()
|
||||
ProgressView() {
|
||||
Text("Gathering app details")
|
||||
.font(.callout)
|
||||
.foregroundStyle(.primary.opacity(0.5))
|
||||
.padding(5)
|
||||
// App List
|
||||
HStack(spacing: 0){
|
||||
|
||||
if appState.reload {
|
||||
VStack {
|
||||
Spacer()
|
||||
ProgressView() {
|
||||
Text("Gathering app details")
|
||||
.font(.callout)
|
||||
.foregroundStyle(.primary.opacity(0.5))
|
||||
.padding(5)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.frame(width: sidebarWidth)
|
||||
.padding(.vertical)
|
||||
} else {
|
||||
AppSearchView(glass: glass, menubarEnabled: menubarEnabled, mini: mini, search: $search, showPopover: $showPopover, isMenuBar: $isMenuBar)
|
||||
.frame(width: sidebarWidth)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
.background(backgroundView(themeManager: themeManager, darker: true, glass: glass))
|
||||
.transition(.opacity)
|
||||
|
||||
SlideableDivider(dimension: $sidebarWidth)
|
||||
.background(backgroundView(themeManager: themeManager))
|
||||
.zIndex(3)
|
||||
|
||||
|
||||
// Details View
|
||||
HStack(spacing: 0) {
|
||||
Spacer()
|
||||
Group {
|
||||
if appState.currentView == .empty || appState.currentView == .apps {
|
||||
AppDetailsEmptyView(showPopover: $showPopover)
|
||||
} else if appState.currentView == .files {
|
||||
FilesView(showPopover: $showPopover, search: $search)
|
||||
.id(appState.appInfo.id)
|
||||
} else if appState.currentView == .zombie {
|
||||
ZombieView(showPopover: $showPopover, search: $search)
|
||||
.id(appState.appInfo.id)
|
||||
}
|
||||
}
|
||||
.transition(.opacity)
|
||||
Spacer()
|
||||
}
|
||||
.zIndex(2)
|
||||
.background(backgroundView(themeManager: themeManager))
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.frame(width: sidebarWidth)
|
||||
.padding(.vertical)
|
||||
} else {
|
||||
AppSearchView(glass: glass, menubarEnabled: menubarEnabled, mini: mini, search: $search, showPopover: $showPopover, isMenuBar: $isMenuBar)
|
||||
.frame(width: sidebarWidth)
|
||||
}
|
||||
|
||||
}
|
||||
.background(backgroundView(themeManager: themeManager, darker: true, glass: glass))
|
||||
.transition(.opacity)
|
||||
|
||||
SlideableDivider(dimension: $sidebarWidth)
|
||||
.background(backgroundView(themeManager: themeManager))
|
||||
.zIndex(3)
|
||||
|
||||
|
||||
// Details View
|
||||
HStack(spacing: 0) {
|
||||
Spacer()
|
||||
Group {
|
||||
if appState.currentView == .empty || appState.currentView == .apps {
|
||||
AppDetailsEmptyView(showPopover: $showPopover)
|
||||
} else if appState.currentView == .files {
|
||||
FilesView(showPopover: $showPopover, search: $search)
|
||||
.id(appState.appInfo.id)
|
||||
} else if appState.currentView == .zombie {
|
||||
)
|
||||
),
|
||||
(
|
||||
title: "Uninstalled",
|
||||
icon: "externaldrive.badge.minus",
|
||||
view: AnyView(
|
||||
ZombieView(showPopover: $showPopover, search: $search)
|
||||
.id(appState.appInfo.id)
|
||||
}
|
||||
}
|
||||
.transition(.opacity)
|
||||
Spacer()
|
||||
}
|
||||
.zIndex(2)
|
||||
.background(backgroundView(themeManager: themeManager))
|
||||
.background(backgroundView(themeManager: themeManager))
|
||||
.onAppear {
|
||||
if appState.zombieFile.fileSize.keys.isEmpty {
|
||||
appState.showProgress.toggle()
|
||||
}
|
||||
withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) {
|
||||
if appState.zombieFile.fileSize.keys.isEmpty {
|
||||
reversePreloader(allApps: appState.sortedApps, appState: appState, locations: locations, fsm: fsm)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
),
|
||||
])
|
||||
|
||||
}
|
||||
.frame(minWidth: 900, minHeight: 600)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
|
||||
Reference in New Issue
Block a user