mirror of
https://github.com/exituser/Pearcleaner.git
synced 2026-09-17 04:58:56 +00:00
Updates
This commit is contained in:
@@ -4,9 +4,7 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>com.apple.security.app-sandbox</key>
|
<key>com.apple.security.app-sandbox</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.files.user-selected.read-write</key>
|
<key>com.apple.security.temporary-exception.files.absolute-path.read-only</key>
|
||||||
<true/>
|
|
||||||
<key>com.apple.security.temporary-exception.files.home-relative-path.read-write</key>
|
|
||||||
<array>
|
<array>
|
||||||
<string>/</string>
|
<string>/</string>
|
||||||
</array>
|
</array>
|
||||||
|
|||||||
@@ -42,6 +42,7 @@
|
|||||||
C7DD49EE2BAB7F6000CCBA16 /* ReversePathsFetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7DD49ED2BAB7F6000CCBA16 /* ReversePathsFetch.swift */; };
|
C7DD49EE2BAB7F6000CCBA16 /* ReversePathsFetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7DD49ED2BAB7F6000CCBA16 /* ReversePathsFetch.swift */; };
|
||||||
C7DD49F02BABA14400CCBA16 /* Folders.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7DD49EF2BABA14400CCBA16 /* Folders.swift */; };
|
C7DD49F02BABA14400CCBA16 /* Folders.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7DD49EF2BABA14400CCBA16 /* Folders.swift */; };
|
||||||
C7DE672B2BA6343D00EB1633 /* MenuBarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7DE672A2BA6343D00EB1633 /* MenuBarItem.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 */; };
|
C7EA06332C1CBE2000F872FC /* ConditionBuilderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7EA06322C1CBE2000F872FC /* ConditionBuilderView.swift */; };
|
||||||
C7ED86B12BA8A35500F13DE4 /* Interface.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7ED86B02BA8A35500F13DE4 /* Interface.swift */; };
|
C7ED86B12BA8A35500F13DE4 /* Interface.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7ED86B02BA8A35500F13DE4 /* Interface.swift */; };
|
||||||
C7F539382AF60865007DF1B2 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7F539372AF60865007DF1B2 /* Utilities.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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
C7F539372AF60865007DF1B2 /* Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utilities.swift; sourceTree = "<group>"; };
|
||||||
@@ -266,6 +268,7 @@
|
|||||||
C77B901F2AF1B390009CC655 /* Views */ = {
|
C77B901F2AF1B390009CC655 /* Views */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
C7DFBDE62CAF5ACB0013C825 /* CustomTabView.swift */,
|
||||||
C77B90222AF2D616009CC655 /* FilesView.swift */,
|
C77B90222AF2D616009CC655 /* FilesView.swift */,
|
||||||
C71848432B8D2D600046CB13 /* ZombieView.swift */,
|
C71848432B8D2D600046CB13 /* ZombieView.swift */,
|
||||||
C76D08542AF89CDE00D07867 /* RegularMode.swift */,
|
C76D08542AF89CDE00D07867 /* RegularMode.swift */,
|
||||||
@@ -435,6 +438,7 @@
|
|||||||
C764680F2CA7790200DC72DF /* PearGroupBox.swift in Sources */,
|
C764680F2CA7790200DC72DF /* PearGroupBox.swift in Sources */,
|
||||||
C79947962C34A794007279CC /* Tips.swift in Sources */,
|
C79947962C34A794007279CC /* Tips.swift in Sources */,
|
||||||
C76D08482AF83C3F00D07867 /* Update.swift in Sources */,
|
C76D08482AF83C3F00D07867 /* Update.swift in Sources */,
|
||||||
|
C7DFBDE72CAF5AD30013C825 /* CustomTabView.swift in Sources */,
|
||||||
C7FB173B2B96321300B96F9A /* AppsListView.swift in Sources */,
|
C7FB173B2B96321300B96F9A /* AppsListView.swift in Sources */,
|
||||||
C77B90162AF19377009CC655 /* AppState.swift in Sources */,
|
C77B90162AF19377009CC655 /* AppState.swift in Sources */,
|
||||||
C7DD49EE2BAB7F6000CCBA16 /* ReversePathsFetch.swift in Sources */,
|
C7DD49EE2BAB7F6000CCBA16 /* ReversePathsFetch.swift in Sources */,
|
||||||
@@ -791,6 +795,7 @@
|
|||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = "";
|
||||||
"DEVELOPMENT_TEAM[sdk=macosx*]" = BK8443AXLU;
|
"DEVELOPMENT_TEAM[sdk=macosx*]" = BK8443AXLU;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
|
GCC_OPTIMIZATION_LEVEL = "";
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = FinderOpen/Info.plist;
|
INFOPLIST_FILE = FinderOpen/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = FinderOpen;
|
INFOPLIST_KEY_CFBundleDisplayName = FinderOpen;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"location" : "https://github.com/alienator88/AlinFoundation",
|
"location" : "https://github.com/alienator88/AlinFoundation",
|
||||||
"state" : {
|
"state" : {
|
||||||
"branch" : "main",
|
"branch" : "main",
|
||||||
"revision" : "579f5dd4730f085ad0163442a6c81d1c5fb65e90"
|
"revision" : "9e22796895c78f8ecce0b4d8c58917ec36f7c1b0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -71,6 +71,13 @@
|
|||||||
ReferencedContainer = "container:Pearcleaner.xcodeproj">
|
ReferencedContainer = "container:Pearcleaner.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</MacroExpansion>
|
</MacroExpansion>
|
||||||
|
<AdditionalOptions>
|
||||||
|
<AdditionalOption
|
||||||
|
key = "APP_DISTRIBUTOR_ID_OVERRIDE"
|
||||||
|
value = "com.apple.AppStore"
|
||||||
|
isEnabled = "YES">
|
||||||
|
</AdditionalOption>
|
||||||
|
</AdditionalOptions>
|
||||||
</LaunchAction>
|
</LaunchAction>
|
||||||
<ProfileAction
|
<ProfileAction
|
||||||
buildConfiguration = "Release"
|
buildConfiguration = "Release"
|
||||||
|
|||||||
@@ -37,15 +37,6 @@ struct AppCommands: Commands {
|
|||||||
}
|
}
|
||||||
.keyboardShortcut("u", modifiers: .command)
|
.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 {
|
Button {
|
||||||
appState.triggerUninstallAlert()
|
appState.triggerUninstallAlert()
|
||||||
} label: {
|
} label: {
|
||||||
@@ -93,6 +84,15 @@ struct AppCommands: Commands {
|
|||||||
// Tools Menu
|
// Tools Menu
|
||||||
CommandMenu(Text("Tools", comment: "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
|
Button
|
||||||
{
|
{
|
||||||
if !appState.appInfo.bundleIdentifier.isEmpty {
|
if !appState.appInfo.bundleIdentifier.isEmpty {
|
||||||
|
|||||||
@@ -9,10 +9,78 @@ import Foundation
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import AlinFoundation
|
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 {
|
class AppInfoFetcher {
|
||||||
static let fileManager = FileManager.default
|
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) {
|
if isDirectoryWrapped(path: path) {
|
||||||
return handleWrappedDirectory(atPath: path)
|
return handleWrappedDirectory(atPath: path)
|
||||||
} else {
|
} 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")
|
let wrapperURL = path.appendingPathComponent("Wrapper")
|
||||||
return fileManager.fileExists(atPath: wrapperURL.path)
|
return fileManager.fileExists(atPath: wrapperURL.path)
|
||||||
}
|
}
|
||||||
@@ -53,17 +121,36 @@ class AppInfoFetcher {
|
|||||||
? bundle.infoDictionary?["CFBundleVersion"] as? String ?? ""
|
? bundle.infoDictionary?["CFBundleVersion"] as? String ?? ""
|
||||||
: bundle.infoDictionary?["CFBundleShortVersionString"] as? String ?? ""
|
: bundle.infoDictionary?["CFBundleShortVersionString"] as? String ?? ""
|
||||||
|
|
||||||
let appIcon = fetchAppIcon(for: path, wrapped: wrapped)
|
let appIcon = AppInfoUtils.fetchAppIcon(for: path, wrapped: wrapped)
|
||||||
let webApp = (bundle.infoDictionary?["LSTemplateApplication"] as? Bool ?? false || bundle.infoDictionary?["CFBundleExecutable"] as? String == "app_mode_loader")
|
let webApp = AppInfoUtils.isWebApp(bundle: bundle)
|
||||||
|
|
||||||
|
|
||||||
let system = !path.path.contains(NSHomeDirectory())
|
let system = !path.path.contains(NSHomeDirectory())
|
||||||
|
|
||||||
return AppInfo(id: UUID(), path: path, bundleIdentifier: bundleIdentifier, appName: appName, appVersion: appVersion, appIcon: appIcon,
|
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) {
|
if let appIcon = getIconForFileOrFolderNS(atPath: iconPath) {
|
||||||
return convertICNSToPNG(icon: appIcon, size: NSSize(width: 100, height: 100))
|
return convertICNSToPNG(icon: appIcon, size: NSSize(width: 100, height: 100))
|
||||||
} else {
|
} else {
|
||||||
@@ -71,5 +158,101 @@ class AppInfoFetcher {
|
|||||||
return nil
|
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 sortedApps: [AppInfo] = []
|
||||||
@Published var selectedItems = Set<URL>()
|
@Published var selectedItems = Set<URL>()
|
||||||
@Published var currentView = CurrentDetailsView.empty
|
@Published var currentView = CurrentDetailsView.empty
|
||||||
|
@Published var currentPage = CurrentPage.installed
|
||||||
@Published var showAlert: Bool = false
|
@Published var showAlert: Bool = false
|
||||||
@Published var sidebar: Bool = true
|
@Published var sidebar: Bool = true
|
||||||
@Published var reload: Bool = false
|
@Published var reload: Bool = false
|
||||||
@@ -67,10 +68,13 @@ class AppState: ObservableObject {
|
|||||||
system: false,
|
system: false,
|
||||||
arch: .empty,
|
arch: .empty,
|
||||||
bundleSize: 0,
|
bundleSize: 0,
|
||||||
files: [],
|
// files: [],
|
||||||
fileSize: [:],
|
fileSize: [:],
|
||||||
fileSizeLogical: [:],
|
fileSizeLogical: [:],
|
||||||
fileIcon: [:]
|
fileIcon: [:],
|
||||||
|
creationDate: nil,
|
||||||
|
contentChangeDate: nil,
|
||||||
|
lastUsedDate: nil
|
||||||
)
|
)
|
||||||
|
|
||||||
self.zombieFile = ZombieFile(
|
self.zombieFile = ZombieFile(
|
||||||
@@ -137,11 +141,14 @@ struct AppInfo: Identifiable, Equatable, Hashable {
|
|||||||
let wrapped: Bool
|
let wrapped: Bool
|
||||||
let system: Bool
|
let system: Bool
|
||||||
var arch: Arch
|
var arch: Arch
|
||||||
var bundleSize: Int64
|
var bundleSize: Int64 // Only used in the app list view
|
||||||
var files: [URL]
|
// var files: [URL]
|
||||||
var fileSize: [URL:Int64]
|
var fileSize: [URL:Int64]
|
||||||
var fileSizeLogical: [URL:Int64]
|
var fileSizeLogical: [URL:Int64]
|
||||||
var fileIcon: [URL:NSImage?]
|
var fileIcon: [URL:NSImage?]
|
||||||
|
let creationDate: Date?
|
||||||
|
let contentChangeDate: Date?
|
||||||
|
let lastUsedDate: Date?
|
||||||
var totalSize: Int64
|
var totalSize: Int64
|
||||||
{
|
{
|
||||||
return fileSize.values.reduce(0, +)
|
return fileSize.values.reduce(0, +)
|
||||||
@@ -152,7 +159,8 @@ struct AppInfo: Identifiable, Equatable, Hashable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
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
|
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
|
enum CurrentTabView:Int
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -37,9 +37,40 @@ func getSortedApps(paths: [String]) -> [AppInfo] {
|
|||||||
// Collect system applications
|
// Collect system applications
|
||||||
paths.forEach { collectAppPaths(at: $0) }
|
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
|
// Get app info and sort
|
||||||
let sortedApps = apps
|
// let sortedApps = apps
|
||||||
.compactMap { AppInfoFetcher.getAppInfo(atPath: $0) }
|
// .compactMap { AppInfoFetcher.getAppInfo(atPath: $0) }
|
||||||
|
|
||||||
|
|
||||||
|
// let elapsedTime = Date().timeIntervalSince(startTime)
|
||||||
|
// print("Time taken for mdls metadata extraction: \(elapsedTime) seconds")
|
||||||
|
|
||||||
return sortedApps
|
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
|
// Stop Sentinel FileWatcher momentarily to ignore .app bundle being sent to Trash
|
||||||
sendStopNotificationFW()
|
sendStopNotificationFW()
|
||||||
|
|
||||||
updateOnBackground {
|
updateOnMain {
|
||||||
let posixFiles = fileURLs.map { item in
|
let posixFiles = fileURLs.map { item in
|
||||||
return "POSIX file \"\(item.path)\"" + (item == fileURLs.last ? "" : ", ")}.joined()
|
return "POSIX file \"\(item.path)\"" + (item == fileURLs.last ? "" : ", ")}.joined()
|
||||||
let scriptSource = """
|
let scriptSource = """
|
||||||
@@ -182,19 +213,15 @@ func moveFilesToTrash(appState: AppState, at fileURLs: [URL], completion: @escap
|
|||||||
|
|
||||||
// Handle any AppleScript errors
|
// Handle any AppleScript errors
|
||||||
if let error = error {
|
if let error = error {
|
||||||
DispatchQueue.main.async {
|
|
||||||
printOS("Trash Error: \(error)")
|
printOS("Trash Error: \(error)")
|
||||||
completion(false) // Indicate failure
|
completion(false) // Indicate failure
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if output is null, indicating the user canceled the operation
|
// Check if output is null, indicating the user canceled the operation
|
||||||
if output.descriptorType == typeNull {
|
if output.descriptorType == typeNull {
|
||||||
DispatchQueue.main.async {
|
|
||||||
printOS("Trash Error: operation canceled by the user")
|
printOS("Trash Error: operation canceled by the user")
|
||||||
completion(false) // Indicate failure due to cancellation
|
completion(false) // Indicate failure due to cancellation
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,10 +230,8 @@ func moveFilesToTrash(appState: AppState, at fileURLs: [URL], completion: @escap
|
|||||||
printOS("Trash: \(outputString)")
|
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?
|
var error: NSDictionary?
|
||||||
|
|
||||||
updateOnBackground {
|
updateOnMain {
|
||||||
if let scriptObject = NSAppleScript(source: scriptSource) {
|
if let scriptObject = NSAppleScript(source: scriptSource) {
|
||||||
let output: NSAppleEventDescriptor = scriptObject.executeAndReturnError(&error)
|
let output: NSAppleEventDescriptor = scriptObject.executeAndReturnError(&error)
|
||||||
if let error = error {
|
if let error = error {
|
||||||
@@ -274,10 +299,8 @@ func undoTrash(appState: AppState, completion: @escaping () -> Void = {}) {
|
|||||||
printOS(outputString)
|
printOS(outputString)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
DispatchQueue.main.async {
|
|
||||||
completion()
|
completion()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -303,6 +303,7 @@ func manageSymlink(install: Bool) {
|
|||||||
"""
|
"""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateOnMain {
|
||||||
// Execute the AppleScript
|
// Execute the AppleScript
|
||||||
var error: NSDictionary?
|
var error: NSDictionary?
|
||||||
if let scriptObject = NSAppleScript(source: script) {
|
if let scriptObject = NSAppleScript(source: script) {
|
||||||
@@ -322,6 +323,8 @@ func manageSymlink(install: Bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// FinderExtension Sequoia Fix
|
// FinderExtension Sequoia Fix
|
||||||
func manageFinderPlugin(install: Bool) {
|
func manageFinderPlugin(install: Bool) {
|
||||||
@@ -361,6 +364,7 @@ func caskCleanup(app: String) {
|
|||||||
end tell
|
end tell
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
updateOnMain {
|
||||||
var error: NSDictionary?
|
var error: NSDictionary?
|
||||||
if let appleScript = NSAppleScript(source: script) {
|
if let appleScript = NSAppleScript(source: script) {
|
||||||
appleScript.executeAndReturnError(&error)
|
appleScript.executeAndReturnError(&error)
|
||||||
@@ -369,6 +373,8 @@ func caskCleanup(app: String) {
|
|||||||
if let error = error {
|
if let error = error {
|
||||||
printOS("AppleScript Error: \(error)")
|
printOS("AppleScript Error: \(error)")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
printOS("Brew cleanup: No cask found for \(app).")
|
printOS("Brew cleanup: No cask found for \(app).")
|
||||||
}
|
}
|
||||||
@@ -453,7 +459,7 @@ func caskCleanup2(app: String) {
|
|||||||
fi'" in front window
|
fi'" in front window
|
||||||
end tell
|
end tell
|
||||||
"""
|
"""
|
||||||
|
updateOnMain {
|
||||||
var error: NSDictionary?
|
var error: NSDictionary?
|
||||||
if let appleScript = NSAppleScript(source: script) {
|
if let appleScript = NSAppleScript(source: script) {
|
||||||
appleScript.executeAndReturnError(&error)
|
appleScript.executeAndReturnError(&error)
|
||||||
@@ -463,6 +469,8 @@ func caskCleanup2(app: String) {
|
|||||||
print("AppleScript 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
|
// --- Extend String to remove periods, spaces and lowercase the string
|
||||||
extension String {
|
extension String {
|
||||||
|
|||||||
@@ -35,12 +35,12 @@ class WindowSettings: ObservableObject {
|
|||||||
func newWindow<V: View>(mini: Bool, withView view: @escaping () -> V) {
|
func newWindow<V: View>(mini: Bool, withView view: @escaping () -> V) {
|
||||||
let frame = self.resetWindowSettings(mini: mini)
|
let frame = self.resetWindowSettings(mini: mini)
|
||||||
|
|
||||||
if menubarEnabled {
|
// if menubarEnabled {
|
||||||
windowRef = NSWindow(
|
// windowRef = NSWindow(
|
||||||
contentRect: .zero,
|
// contentRect: .zero,
|
||||||
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
|
// styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
|
||||||
backing: .buffered, defer: false)
|
// backing: .buffered, defer: false)
|
||||||
}
|
// }
|
||||||
// Update the existing windowRef with all desired settings
|
// Update the existing windowRef with all desired settings
|
||||||
windowRef?.contentView = NSHostingView(rootView: view())
|
windowRef?.contentView = NSHostingView(rootView: view())
|
||||||
windowRef?.setFrame(frame, display: true, animate: true)
|
windowRef?.setFrame(frame, display: true, animate: true)
|
||||||
|
|||||||
@@ -98,21 +98,6 @@ struct PearcleanerApp: App {
|
|||||||
})
|
})
|
||||||
.onAppear {
|
.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
|
// Enable menubar item
|
||||||
if menubarEnabled {
|
if menubarEnabled {
|
||||||
MenuBarExtraManager.shared.addMenuBarExtra(withView: {
|
MenuBarExtraManager.shared.addMenuBarExtra(withView: {
|
||||||
@@ -125,12 +110,36 @@ struct PearcleanerApp: App {
|
|||||||
.environmentObject(permissionManager)
|
.environmentObject(permissionManager)
|
||||||
.preferredColorScheme(themeManager.displayMode.colorScheme)
|
.preferredColorScheme(themeManager.displayMode.colorScheme)
|
||||||
})
|
})
|
||||||
|
|
||||||
findAndHideWindows(named: ["Pearcleaner"])
|
|
||||||
NSApplication.shared.setActivationPolicy(.accessory)
|
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)
|
.windowStyle(.hiddenTitleBar)
|
||||||
@@ -197,6 +206,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -272,6 +272,7 @@ struct GeneralSettingsTab: View {
|
|||||||
|
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
|
Task {
|
||||||
appState.updateExtensionStatus()
|
appState.updateExtensionStatus()
|
||||||
isCLISymlinked = checkCLISymlink()
|
isCLISymlinked = checkCLISymlink()
|
||||||
}
|
}
|
||||||
@@ -279,3 +280,5 @@ struct GeneralSettingsTab: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -283,7 +283,7 @@ struct InterfaceSettingsTab: View {
|
|||||||
.preferredColorScheme(themeManager.displayMode.colorScheme)
|
.preferredColorScheme(themeManager.displayMode.colorScheme)
|
||||||
})
|
})
|
||||||
NSApplication.shared.setActivationPolicy(.accessory)
|
NSApplication.shared.setActivationPolicy(.accessory)
|
||||||
findAndHideWindows(named: ["Pearcleaner"])
|
findAndHideWindows(named: ["Pearcleaner", ""])
|
||||||
} else {
|
} else {
|
||||||
MenuBarExtraManager.shared.removeMenuBarExtra()
|
MenuBarExtraManager.shared.removeMenuBarExtra()
|
||||||
NSApplication.shared.setActivationPolicy(.regular)
|
NSApplication.shared.setActivationPolicy(.regular)
|
||||||
|
|||||||
@@ -82,31 +82,29 @@ struct AppListItems: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
if appInfo.webApp {
|
if appInfo.webApp {
|
||||||
Text("web")
|
Image(systemName: "safari")
|
||||||
.font(.footnote)
|
.resizable()
|
||||||
|
.aspectRatio(contentMode: .fit)
|
||||||
|
.frame(width: 16, height: 16)
|
||||||
.foregroundStyle(.primary.opacity(0.3))
|
.foregroundStyle(.primary.opacity(0.3))
|
||||||
.frame(minWidth: 30, minHeight: 15)
|
.symbolRenderingMode(.monochrome)
|
||||||
.padding(2)
|
.help("Web app")
|
||||||
.background(
|
|
||||||
Capsule().strokeBorder(.primary.opacity(0.3), lineWidth: 1)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
if appInfo.wrapped {
|
if appInfo.wrapped {
|
||||||
Text("iOS")
|
Image(systemName: "iphone")
|
||||||
.font(.footnote)
|
.resizable()
|
||||||
|
.aspectRatio(contentMode: .fit)
|
||||||
|
.frame(width: 16, height: 16)
|
||||||
.foregroundStyle(.primary.opacity(0.3))
|
.foregroundStyle(.primary.opacity(0.3))
|
||||||
.frame(minWidth: 30, minHeight: 15)
|
.symbolRenderingMode(.monochrome)
|
||||||
.padding(2)
|
.help("iOS app")
|
||||||
.background(
|
|
||||||
Capsule().strokeBorder(.primary.opacity(0.3), lineWidth: 1)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
if minimalEnabled {
|
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))
|
.font(.system(size: (isHovered || isSelected) ? 12 : 10))
|
||||||
.foregroundStyle(.primary.opacity(0.5))
|
.foregroundStyle(.primary.opacity(0.5))
|
||||||
}
|
}
|
||||||
@@ -119,7 +117,7 @@ struct AppListItems: View {
|
|||||||
.frame(height: 35)
|
.frame(height: 35)
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
.padding(.vertical, 5)
|
.padding(.vertical, 5)
|
||||||
.help(appInfo.appName)
|
// .help(appInfo.appName)
|
||||||
.onHover { hovering in
|
.onHover { hovering in
|
||||||
withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) {
|
withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) {
|
||||||
self.isHovered = hovering
|
self.isHovered = hovering
|
||||||
@@ -155,9 +153,10 @@ struct AppListItems: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
if self.bundleSize == 0 {
|
if appInfo.bundleSize == 0 {
|
||||||
appState.getBundleSize(for: appInfo) { size in
|
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 {
|
var body: some View {
|
||||||
VStack(alignment: .center, spacing: 0) {
|
VStack(alignment: .center, spacing: 0) {
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
.frame(height: 10)
|
.frame(height: !isMenuBar ? 30 : 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
AppsListView(search: $search, showPopover: $showPopover, filteredApps: filteredApps)
|
searchBarComponent
|
||||||
|
.padding(.horizontal, search.isEmpty ? 10 : 5)
|
||||||
|
.padding(.bottom, 5)
|
||||||
|
|
||||||
Divider()
|
Divider()
|
||||||
|
|
||||||
@@ -55,6 +46,29 @@ struct AppSearchView: View {
|
|||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private var searchBarComponent: some View {
|
||||||
HStack(spacing: 10) {
|
HStack(spacing: 10) {
|
||||||
|
|
||||||
if search.isEmpty {
|
if search.isEmpty {
|
||||||
@@ -148,11 +162,8 @@ struct AppSearchView: View {
|
|||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
.padding(.horizontal, search.isEmpty ? 10 : 5)
|
|
||||||
.padding(.vertical, 5)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private var filteredApps: [AppInfo] {
|
private var filteredApps: [AppInfo] {
|
||||||
let apps: [AppInfo]
|
let apps: [AppInfo]
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ struct AppsListView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
LazyVStack(alignment: .leading, spacing: 0, pinnedViews: [.sectionHeaders]) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
let filteredUserApps = filteredApps.filter { !$0.system }
|
let filteredUserApps = filteredApps.filter { !$0.system }
|
||||||
let filteredSystemApps = 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
|
@AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
LazyVStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
Header(title: title, count: count, showPopover: $showPopover)
|
Header(title: title, count: count, showPopover: $showPopover)
|
||||||
.padding(.leading, 5)
|
.padding(.leading, 5)
|
||||||
.onTapGesture {
|
.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("•").foregroundStyle(Color("AccentColor"))
|
||||||
Text("v\(appState.appInfo.appVersion)").font(.title3)
|
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)")
|
Text("\(appState.appInfo.bundleIdentifier)")
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
.font(.title3)
|
.font(.title3)
|
||||||
.foregroundStyle((.primary.opacity(0.5)))
|
.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)
|
.padding(.leading)
|
||||||
|
|
||||||
@@ -153,6 +162,8 @@ struct FilesView: View {
|
|||||||
Text("\(displaySizeTotal)").font(.title).fontWeight(.bold).help("Total size on disk")
|
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")")
|
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)))
|
.font(.callout).foregroundStyle((.primary.opacity(0.5)))
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,10 +13,12 @@ struct RegularMode: View {
|
|||||||
@EnvironmentObject var appState: AppState
|
@EnvironmentObject var appState: AppState
|
||||||
@EnvironmentObject var themeManager: ThemeManager
|
@EnvironmentObject var themeManager: ThemeManager
|
||||||
@EnvironmentObject var locations: Locations
|
@EnvironmentObject var locations: Locations
|
||||||
|
@EnvironmentObject var fsm: FolderSettingsManager
|
||||||
@AppStorage("settings.general.glass") private var glass: Bool = false
|
@AppStorage("settings.general.glass") private var glass: Bool = false
|
||||||
@AppStorage("settings.general.sidebarWidth") private var sidebarWidth: Double = 280
|
@AppStorage("settings.general.sidebarWidth") private var sidebarWidth: Double = 280
|
||||||
@AppStorage("settings.menubar.enabled") private var menubarEnabled: Bool = false
|
@AppStorage("settings.menubar.enabled") private var menubarEnabled: Bool = false
|
||||||
@AppStorage("settings.general.mini") private var mini: Bool = false
|
@AppStorage("settings.general.mini") private var mini: Bool = false
|
||||||
|
@AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true
|
||||||
@Binding var search: String
|
@Binding var search: String
|
||||||
@State private var showSys: Bool = true
|
@State private var showSys: Bool = true
|
||||||
@State private var showUsr: Bool = true
|
@State private var showUsr: Bool = true
|
||||||
@@ -26,6 +28,15 @@ struct RegularMode: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
|
||||||
|
// Main App Window
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
|
||||||
|
// Tab View
|
||||||
|
CustomTabView(content: [
|
||||||
|
(
|
||||||
|
title: "Installed",
|
||||||
|
icon: "externaldrive",
|
||||||
|
view: AnyView (
|
||||||
HStack(alignment: .center, spacing: 0) {
|
HStack(alignment: .center, spacing: 0) {
|
||||||
|
|
||||||
// App List
|
// App List
|
||||||
@@ -47,6 +58,7 @@ struct RegularMode: View {
|
|||||||
} else {
|
} else {
|
||||||
AppSearchView(glass: glass, menubarEnabled: menubarEnabled, mini: mini, search: $search, showPopover: $showPopover, isMenuBar: $isMenuBar)
|
AppSearchView(glass: glass, menubarEnabled: menubarEnabled, mini: mini, search: $search, showPopover: $showPopover, isMenuBar: $isMenuBar)
|
||||||
.frame(width: sidebarWidth)
|
.frame(width: sidebarWidth)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -78,6 +90,29 @@ struct RegularMode: View {
|
|||||||
.zIndex(2)
|
.zIndex(2)
|
||||||
.background(backgroundView(themeManager: themeManager))
|
.background(backgroundView(themeManager: themeManager))
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
),
|
||||||
|
(
|
||||||
|
title: "Uninstalled",
|
||||||
|
icon: "externaldrive.badge.minus",
|
||||||
|
view: AnyView(
|
||||||
|
ZombieView(showPopover: $showPopover, search: $search)
|
||||||
|
.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)
|
.frame(minWidth: 900, minHeight: 600)
|
||||||
.edgesIgnoringSafeArea(.all)
|
.edgesIgnoringSafeArea(.all)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user