mirror of
https://github.com/exituser/Pearcleaner.git
synced 2026-09-17 06:09:01 +00:00
v3.7.0
This commit is contained in:
+9
-9
@@ -10,17 +10,17 @@ GEM
|
|||||||
artifactory (3.0.17)
|
artifactory (3.0.17)
|
||||||
atomos (0.1.3)
|
atomos (0.1.3)
|
||||||
aws-eventstream (1.3.0)
|
aws-eventstream (1.3.0)
|
||||||
aws-partitions (1.935.0)
|
aws-partitions (1.943.0)
|
||||||
aws-sdk-core (3.196.1)
|
aws-sdk-core (3.197.0)
|
||||||
aws-eventstream (~> 1, >= 1.3.0)
|
aws-eventstream (~> 1, >= 1.3.0)
|
||||||
aws-partitions (~> 1, >= 1.651.0)
|
aws-partitions (~> 1, >= 1.651.0)
|
||||||
aws-sigv4 (~> 1.8)
|
aws-sigv4 (~> 1.8)
|
||||||
jmespath (~> 1, >= 1.6.1)
|
jmespath (~> 1, >= 1.6.1)
|
||||||
aws-sdk-kms (1.82.0)
|
aws-sdk-kms (1.83.0)
|
||||||
aws-sdk-core (~> 3, >= 3.193.0)
|
aws-sdk-core (~> 3, >= 3.197.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
aws-sdk-s3 (1.151.0)
|
aws-sdk-s3 (1.152.1)
|
||||||
aws-sdk-core (~> 3, >= 3.194.0)
|
aws-sdk-core (~> 3, >= 3.197.0)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.8)
|
aws-sigv4 (~> 1.8)
|
||||||
aws-sigv4 (1.8.0)
|
aws-sigv4 (1.8.0)
|
||||||
@@ -147,7 +147,7 @@ GEM
|
|||||||
os (>= 0.9, < 2.0)
|
os (>= 0.9, < 2.0)
|
||||||
signet (>= 0.16, < 2.a)
|
signet (>= 0.16, < 2.a)
|
||||||
highline (2.0.3)
|
highline (2.0.3)
|
||||||
http-cookie (1.0.5)
|
http-cookie (1.0.6)
|
||||||
domain_name (~> 0.5)
|
domain_name (~> 0.5)
|
||||||
httpclient (2.8.3)
|
httpclient (2.8.3)
|
||||||
jmespath (1.6.2)
|
jmespath (1.6.2)
|
||||||
@@ -171,8 +171,8 @@ GEM
|
|||||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||||
uber (< 0.2.0)
|
uber (< 0.2.0)
|
||||||
retriable (3.1.2)
|
retriable (3.1.2)
|
||||||
rexml (3.2.8)
|
rexml (3.2.9)
|
||||||
strscan (>= 3.0.9)
|
strscan
|
||||||
rouge (2.0.7)
|
rouge (2.0.7)
|
||||||
ruby2_keywords (0.0.5)
|
ruby2_keywords (0.0.5)
|
||||||
rubyzip (2.3.2)
|
rubyzip (2.3.2)
|
||||||
|
|||||||
@@ -305,10 +305,10 @@
|
|||||||
C77B90222AF2D616009CC655 /* FilesView.swift */,
|
C77B90222AF2D616009CC655 /* FilesView.swift */,
|
||||||
C71848432B8D2D600046CB13 /* ZombieView.swift */,
|
C71848432B8D2D600046CB13 /* ZombieView.swift */,
|
||||||
C76D08542AF89CDE00D07867 /* RegularMode.swift */,
|
C76D08542AF89CDE00D07867 /* RegularMode.swift */,
|
||||||
C7D31D492AFEB26700C7ED9E /* AppListItems.swift */,
|
|
||||||
C7045A272B03E71D00376976 /* MiniMode.swift */,
|
C7045A272B03E71D00376976 /* MiniMode.swift */,
|
||||||
C7FB173A2B96321300B96F9A /* AppsListView.swift */,
|
C7FB173A2B96321300B96F9A /* AppsListView.swift */,
|
||||||
C7FEBA102BDC422200AE195F /* AppSearchView.swift */,
|
C7FEBA102BDC422200AE195F /* AppSearchView.swift */,
|
||||||
|
C7D31D492AFEB26700C7ED9E /* AppListItems.swift */,
|
||||||
);
|
);
|
||||||
path = Views;
|
path = Views;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -568,8 +568,8 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
APP_BUILD = 44;
|
APP_BUILD = 45;
|
||||||
APP_VERSION = 3.6.5;
|
APP_VERSION = 3.7.0;
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
@@ -638,8 +638,8 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
APP_BUILD = 44;
|
APP_BUILD = 45;
|
||||||
APP_VERSION = 3.6.5;
|
APP_VERSION = 3.7.0;
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
|
|||||||
@@ -33,16 +33,7 @@ struct AppCommands: Commands {
|
|||||||
|
|
||||||
Button {
|
Button {
|
||||||
withAnimation(.easeInOut(duration: 0.5)) {
|
withAnimation(.easeInOut(duration: 0.5)) {
|
||||||
// Refresh Apps list
|
reloadAppsList(appState: appState, fsm: fsm)
|
||||||
updateOnMain {
|
|
||||||
appState.reload.toggle()
|
|
||||||
}
|
|
||||||
let sortedApps = getSortedApps(paths: fsm.folderPaths, appState: appState)
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
|
||||||
appState.sortedApps = []
|
|
||||||
appState.sortedApps = sortedApps
|
|
||||||
appState.reload.toggle()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
Text("Refresh Apps")
|
Text("Refresh Apps")
|
||||||
@@ -65,12 +56,9 @@ struct AppCommands: Commands {
|
|||||||
Button
|
Button
|
||||||
{
|
{
|
||||||
undoTrash(appState: appState) {
|
undoTrash(appState: appState) {
|
||||||
let sortedApps = getSortedApps(paths: fsm.folderPaths, appState: appState)
|
reloadAppsList(appState: appState, fsm: fsm)
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
for app in appState.trashedFiles {
|
||||||
appState.sortedApps = sortedApps
|
AppPathFinder(appInfo: app, appState: appState, locations: locations, undo: true).findPaths()
|
||||||
for app in appState.trashedFiles {
|
|
||||||
AppPathFinder(appInfo: app, appState: appState, locations: locations, undo: true).findPaths()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
|
|||||||
@@ -46,22 +46,19 @@ class AppInfoFetcher {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// let appName = bundle.infoDictionary?["CFBundleDisplayName"] as? String ?? bundle.infoDictionary?["CFBundleName"] as? String
|
|
||||||
// ?? bundle.localizedInfoDictionary?[kCFBundleNameKey as String] as? String ?? path.deletingPathExtension().lastPathComponent
|
|
||||||
|
|
||||||
let appName = wrapped ? path.deletingLastPathComponent().deletingLastPathComponent().deletingPathExtension().lastPathComponent.capitalizingFirstLetter() : path.deletingPathExtension().lastPathComponent.capitalizingFirstLetter()
|
let appName = wrapped ? path.deletingLastPathComponent().deletingLastPathComponent().deletingPathExtension().lastPathComponent.capitalizingFirstLetter() : path.deletingPathExtension().lastPathComponent.capitalizingFirstLetter()
|
||||||
|
|
||||||
let appVersion = bundle.infoDictionary?["CFBundleShortVersionString"] as? String
|
let appVersion = (bundle.infoDictionary?["CFBundleShortVersionString"] as? String)?.isEmpty ?? true
|
||||||
?? bundle.infoDictionary?["CFBundleVersion"] as? String
|
? bundle.infoDictionary?["CFBundleVersion"] as? String ?? ""
|
||||||
?? ""
|
: bundle.infoDictionary?["CFBundleShortVersionString"] as? String ?? ""
|
||||||
|
|
||||||
let appIcon = fetchAppIcon(for: path, wrapped: wrapped)
|
let appIcon = fetchAppIcon(for: path, wrapped: wrapped)
|
||||||
|
let webApp = (bundle.infoDictionary?["LSTemplateApplication"] as? Bool ?? false || bundle.infoDictionary?["CFBundleExecutable"] as? String == "app_mode_loader")
|
||||||
|
|
||||||
let webApp = bundle.infoDictionary?["LSTemplateApplication"] as? Bool ?? false
|
|
||||||
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, files: [], fileSize: [:], fileSizeLogical: [:], fileIcon: [:])
|
webApp: webApp, wrapped: wrapped, system: system, bundleSize: 0, files: [], fileSize: [:], fileSizeLogical: [:], fileIcon: [:])
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func fetchAppIcon(for path: URL, wrapped: Bool) -> NSImage? {
|
private static func fetchAppIcon(for path: URL, wrapped: Bool) -> NSImage? {
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ class AppState: ObservableObject {
|
|||||||
webApp: false,
|
webApp: false,
|
||||||
wrapped: false,
|
wrapped: false,
|
||||||
system: false,
|
system: false,
|
||||||
|
bundleSize: 0,
|
||||||
files: [],
|
files: [],
|
||||||
fileSize: [:],
|
fileSize: [:],
|
||||||
fileSizeLogical: [:],
|
fileSizeLogical: [:],
|
||||||
@@ -98,6 +99,7 @@ struct AppInfo: Identifiable, Equatable, Hashable {
|
|||||||
let webApp: Bool
|
let webApp: Bool
|
||||||
let wrapped: Bool
|
let wrapped: Bool
|
||||||
let system: Bool
|
let system: Bool
|
||||||
|
var bundleSize: Int64
|
||||||
var files: [URL]
|
var files: [URL]
|
||||||
var fileSize: [URL:Int64]
|
var fileSize: [URL:Int64]
|
||||||
var fileSizeLogical: [URL:Int64]
|
var fileSizeLogical: [URL:Int64]
|
||||||
@@ -112,7 +114,7 @@ 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, files: [], fileSize: [:], fileSizeLogical: [:], fileIcon: [:])
|
static let empty = AppInfo(id: UUID(), path: URL(fileURLWithPath: ""), bundleIdentifier: "", appName: "", appVersion: "", appIcon: nil, webApp: false, wrapped: false, system: false, bundleSize: 0, files: [], fileSize: [:], fileSizeLogical: [:], fileIcon: [:])
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,8 +39,6 @@ func getSortedApps(paths: [String], appState: AppState) -> [AppInfo] {
|
|||||||
// 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) }
|
||||||
.sorted { $0.appName.replacingOccurrences(of: ".", with: "").lowercased() < $1.appName.replacingOccurrences(of: ".", with: "").lowercased() }
|
|
||||||
|
|
||||||
|
|
||||||
return sortedApps
|
return sortedApps
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -570,13 +570,7 @@ struct AnimatedSearchStyle: TextFieldStyle {
|
|||||||
.foregroundStyle(isHovered ? Color("mode").opacity(0.8) : Color("mode").opacity(0.5))
|
.foregroundStyle(isHovered ? Color("mode").opacity(0.8) : Color("mode").opacity(0.5))
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
withAnimation {
|
withAnimation {
|
||||||
// Refresh Apps list
|
reloadAppsList(appState: appState, fsm: fsm)
|
||||||
appState.reload.toggle()
|
|
||||||
let sortedApps = getSortedApps(paths: fsm.folderPaths, appState: appState)
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
|
||||||
appState.sortedApps = sortedApps
|
|
||||||
appState.reload.toggle()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,19 +27,25 @@ func updateOnMain(after delay: Double? = nil, _ updates: @escaping () -> Void) {
|
|||||||
|
|
||||||
// Execute functions on background thread
|
// Execute functions on background thread
|
||||||
func updateOnBackground(_ updates: @escaping () -> Void) {
|
func updateOnBackground(_ updates: @escaping () -> Void) {
|
||||||
DispatchQueue.global(qos: .background).async {
|
DispatchQueue.global(qos: .userInitiated).async {
|
||||||
updates()
|
updates()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reload apps list
|
||||||
|
func reloadAppsList(appState: AppState, fsm: FolderSettingsManager) {
|
||||||
|
appState.reload = true
|
||||||
|
updateOnBackground {
|
||||||
|
let sortedApps = getSortedApps(paths: fsm.folderPaths, appState: appState)
|
||||||
|
// Update UI on the main thread
|
||||||
|
updateOnMain {
|
||||||
|
appState.sortedApps = sortedApps
|
||||||
|
appState.reload = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Resize window
|
|
||||||
//func resizeWindow(width: CGFloat, height: CGFloat) {
|
|
||||||
// if let window = NSApplication.shared.windows.first {
|
|
||||||
// let newSize = NSSize(width: width, height: height)
|
|
||||||
// window.setContentSize(newSize)
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
|
|
||||||
func resizeWindowAuto(windowSettings: WindowSettings, title: String) {
|
func resizeWindowAuto(windowSettings: WindowSettings, title: String) {
|
||||||
if let window = NSApplication.shared.windows.first(where: { $0.title == title }) {
|
if let window = NSApplication.shared.windows.first(where: { $0.title == title }) {
|
||||||
@@ -160,16 +166,26 @@ func caskCleanup(app: String) {
|
|||||||
#elseif arch(arm64)
|
#elseif arch(arm64)
|
||||||
let cmd = "/opt/homebrew/bin/brew"
|
let cmd = "/opt/homebrew/bin/brew"
|
||||||
#endif
|
#endif
|
||||||
|
let formattedApp = app.lowercased().replacingOccurrences(of: " ", with: "-")
|
||||||
process.executableURL = URL(fileURLWithPath: "/bin/zsh")
|
process.executableURL = URL(fileURLWithPath: "/bin/zsh")
|
||||||
process.arguments = ["-c", "\(cmd) uninstall --cask \(app) --force; \(cmd) cleanup"]
|
// process.arguments = ["-c", "\(cmd) uninstall --cask \"\(formattedApp)\" --force; \(cmd) cleanup"]
|
||||||
// let pipe = Pipe()
|
process.arguments = ["-c", """
|
||||||
process.standardOutput = FileHandle.nullDevice//pipe
|
found_cask=$(\(cmd) list --cask | grep "\(formattedApp)")
|
||||||
process.standardError = FileHandle.nullDevice//pipe
|
if [ -n "$found_cask" ]; then
|
||||||
|
\(cmd) uninstall --cask "$found_cask" --force;
|
||||||
|
\(cmd) cleanup;
|
||||||
|
else
|
||||||
|
echo "Cask not found for \(formattedApp)";
|
||||||
|
fi
|
||||||
|
"""]
|
||||||
|
let pipe = Pipe()
|
||||||
|
process.standardOutput = pipe
|
||||||
|
process.standardError = pipe
|
||||||
try? process.run()
|
try? process.run()
|
||||||
process.waitUntilExit() // Ensure the process completes
|
process.waitUntilExit() // Ensure the process completes
|
||||||
// let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
||||||
// let output = (String(data: data, encoding: .utf8) ?? "") as String
|
let output = (String(data: data, encoding: .utf8) ?? "") as String
|
||||||
// print(output)
|
print(output)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -499,6 +515,7 @@ func formatByte(size: Int64) -> (human: String, byte: String) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Only process supported files
|
// Only process supported files
|
||||||
func isSupportedFileType(at path: String) -> Bool {
|
func isSupportedFileType(at path: String) -> Bool {
|
||||||
let fileManager = FileManager.default
|
let fileManager = FileManager.default
|
||||||
|
|||||||
@@ -88,9 +88,9 @@ struct PearcleanerApp: App {
|
|||||||
// Disable tabbing
|
// Disable tabbing
|
||||||
NSWindow.allowsAutomaticWindowTabbing = false
|
NSWindow.allowsAutomaticWindowTabbing = false
|
||||||
|
|
||||||
// Get Apps
|
// Load apps list on startup
|
||||||
let sortedApps = getSortedApps(paths: fsm.folderPaths, appState: appState)
|
reloadAppsList(appState: appState, fsm: fsm)
|
||||||
appState.sortedApps = sortedApps
|
|
||||||
|
|
||||||
// Enable menubar item
|
// Enable menubar item
|
||||||
if menubarEnabled {
|
if menubarEnabled {
|
||||||
|
|||||||
@@ -24,10 +24,10 @@ struct AppListItems: View {
|
|||||||
@EnvironmentObject var locations: Locations
|
@EnvironmentObject var locations: Locations
|
||||||
let itemId = UUID()
|
let itemId = UUID()
|
||||||
let appInfo: AppInfo
|
let appInfo: AppInfo
|
||||||
var isSelected: Bool {
|
var isSelected: Bool { appState.appInfo.path == appInfo.path }
|
||||||
appState.appInfo.path == appInfo.path
|
|
||||||
}
|
|
||||||
@State private var hoveredItemPath: URL? = nil
|
@State private var hoveredItemPath: URL? = nil
|
||||||
|
@State private var bundleSize: Int64 = 0
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
|
||||||
ZStack() {
|
ZStack() {
|
||||||
@@ -40,9 +40,7 @@ struct AppListItems: View {
|
|||||||
.resizable()
|
.resizable()
|
||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
.frame(width: 30)
|
.frame(width: 30)
|
||||||
// .frame(width: (isHovered || isSelected) ? 32 : 30, height: (isHovered || isSelected) ? 32 : 30)
|
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||||
// .shadow(color: Color("pop").opacity(0.8), radius: isSelected ? 2 : 0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -80,17 +78,13 @@ struct AppListItems: View {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(appInfo.appVersion)
|
if bundleSize == 0 {
|
||||||
.font(.system(size: (isHovered || isSelected) ? 12 : 10))
|
ProgressView().controlSize(.mini)
|
||||||
.foregroundStyle(Color("mode").opacity(0.5))
|
} else {
|
||||||
|
Text("\(isHovered ? "v\(appInfo.appVersion)" : formatByte(size: bundleSize).human)")
|
||||||
// if (isHovered || isSelected) {
|
.font(.system(size: (isHovered || isSelected) ? 12 : 10))
|
||||||
// RoundedRectangle(cornerRadius: 50)
|
.foregroundStyle(Color("mode").opacity(0.5))
|
||||||
// .fill(isSelected ? Color("pear") : Color("mode").opacity(0.5))
|
}
|
||||||
// .frame(width: isSelected ? 4 : 2, height: 25)
|
|
||||||
// .padding(.trailing, 5)
|
|
||||||
// }
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -99,7 +93,7 @@ struct AppListItems: View {
|
|||||||
.padding(.vertical, 5)
|
.padding(.vertical, 5)
|
||||||
.help(appInfo.appName)
|
.help(appInfo.appName)
|
||||||
.onHover { hovering in
|
.onHover { hovering in
|
||||||
withAnimation(Animation.easeIn(duration: 0.2)) {
|
withAnimation(Animation.easeIn(duration: 0.3)) {
|
||||||
self.isHovered = hovering
|
self.isHovered = hovering
|
||||||
self.hoveredItemPath = isHovered ? appInfo.path : nil
|
self.hoveredItemPath = isHovered ? appInfo.path : nil
|
||||||
}
|
}
|
||||||
@@ -132,6 +126,19 @@ struct AppListItems: View {
|
|||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onAppear {
|
||||||
|
DispatchQueue.global(qos: .userInitiated).async {
|
||||||
|
let size = totalSizeOnDisk(for: appInfo.path).logical
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.bundleSize = size
|
||||||
|
|
||||||
|
// Optionally, update the appInfo in the appState array
|
||||||
|
if let index = appState.sortedApps.firstIndex(where: { $0.path == appInfo.path }) {
|
||||||
|
appState.sortedApps[index].bundleSize = size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ struct AppSearchView: View {
|
|||||||
@State private var showSys: Bool = true
|
@State private var showSys: Bool = true
|
||||||
@State private var showUsr: Bool = true
|
@State private var showUsr: Bool = true
|
||||||
@Binding var isMenuBar: Bool
|
@Binding var isMenuBar: Bool
|
||||||
|
@AppStorage("settings.general.selectedSortAppsList") var selectedSortAlpha: Bool = true
|
||||||
|
@State private var progress: Double = 0.0
|
||||||
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .center, spacing: 0) {
|
VStack(alignment: .center, spacing: 0) {
|
||||||
@@ -53,17 +56,11 @@ struct AppSearchView: View {
|
|||||||
if search.isEmpty {
|
if search.isEmpty {
|
||||||
Button("Refresh") {
|
Button("Refresh") {
|
||||||
withAnimation {
|
withAnimation {
|
||||||
// Refresh Apps list
|
|
||||||
appState.reload.toggle()
|
|
||||||
showPopover = false
|
showPopover = false
|
||||||
let sortedApps = getSortedApps(paths: fsm.folderPaths, appState: appState)
|
reloadAppsList(appState: appState, fsm: fsm)
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|
||||||
appState.sortedApps = sortedApps
|
|
||||||
appState.reload.toggle()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.buttonStyle(SimpleButtonStyle(icon: "arrow.uturn.backward.circle", help: "Refresh apps (⌘+R)", size: 16))
|
.buttonStyle(SimpleButtonStyle(icon: "arrow.counterclockwise.circle", help: "Refresh apps (⌘+R)", size: 16))
|
||||||
}
|
}
|
||||||
|
|
||||||
SearchBar(search: $search, darker: (mini || menubarEnabled) ? false : true, glass: glass)
|
SearchBar(search: $search, darker: (mini || menubarEnabled) ? false : true, glass: glass)
|
||||||
@@ -77,6 +74,13 @@ struct AppSearchView: View {
|
|||||||
.popover(isPresented: $showMenu) {
|
.popover(isPresented: $showMenu) {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
|
|
||||||
|
Button("") {
|
||||||
|
withAnimation(.easeInOut(duration: 0.5)) {
|
||||||
|
selectedSortAlpha.toggle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(SimpleButtonStyle(icon: "circle.fill", label: "App List Sorting", help: "Can also click on User/System headers to toggle this", size: 5))
|
||||||
|
|
||||||
if mini && !menubarEnabled {
|
if mini && !menubarEnabled {
|
||||||
Button("") {
|
Button("") {
|
||||||
withAnimation(.easeInOut(duration: 0.5)) {
|
withAnimation(.easeInOut(duration: 0.5)) {
|
||||||
@@ -142,12 +146,21 @@ struct AppSearchView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var filteredApps: [AppInfo] {
|
private var filteredApps: [AppInfo] {
|
||||||
|
let apps: [AppInfo]
|
||||||
if search.isEmpty {
|
if search.isEmpty {
|
||||||
return appState.sortedApps
|
apps = appState.sortedApps
|
||||||
} else {
|
} else {
|
||||||
return appState.sortedApps.filter { $0.appName.localizedCaseInsensitiveContains(search) }
|
apps = appState.sortedApps.filter { $0.appName.localizedCaseInsensitiveContains(search) }
|
||||||
|
}
|
||||||
|
|
||||||
|
switch selectedSortAlpha {
|
||||||
|
case true:
|
||||||
|
return apps.sorted { $0.appName.replacingOccurrences(of: ".", with: "").lowercased() < $1.appName.replacingOccurrences(of: ".", with: "").lowercased() }
|
||||||
|
case false:
|
||||||
|
return apps.sorted { $0.bundleSize > $1.bundleSize }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ struct AppsListView: View {
|
|||||||
@Binding var search: String
|
@Binding var search: String
|
||||||
@Binding var showPopover: Bool
|
@Binding var showPopover: Bool
|
||||||
@AppStorage("settings.general.mini") private var mini: Bool = false
|
@AppStorage("settings.general.mini") private var mini: Bool = false
|
||||||
|
@AppStorage("settings.general.selectedSort") var selectedSortAlpha: Bool = true
|
||||||
|
|
||||||
var filteredApps: [AppInfo]
|
var filteredApps: [AppInfo]
|
||||||
|
|
||||||
@@ -62,12 +63,12 @@ struct SectionView: View {
|
|||||||
struct Header: View {
|
struct Header: View {
|
||||||
let title: String
|
let title: String
|
||||||
let count: Int
|
let count: Int
|
||||||
// @State private var hovered = false
|
|
||||||
@EnvironmentObject var appState: AppState
|
@EnvironmentObject var appState: AppState
|
||||||
@EnvironmentObject var locations: Locations
|
@EnvironmentObject var locations: Locations
|
||||||
@EnvironmentObject var fsm: FolderSettingsManager
|
@EnvironmentObject var fsm: FolderSettingsManager
|
||||||
@Binding var showPopover: Bool
|
@Binding var showPopover: Bool
|
||||||
@AppStorage("settings.general.glass") private var glass: Bool = true
|
@AppStorage("settings.general.glass") private var glass: Bool = true
|
||||||
|
@AppStorage("settings.general.selectedSortAppsList") var selectedSortAlpha: Bool = true
|
||||||
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -87,6 +88,10 @@ struct Header: View {
|
|||||||
}
|
}
|
||||||
.frame(minHeight: 20)
|
.frame(minHeight: 20)
|
||||||
.padding(5)
|
.padding(5)
|
||||||
|
.help("Click header to change sorting order")
|
||||||
|
.onTapGesture {
|
||||||
|
selectedSortAlpha.toggle()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ struct FilesView: View {
|
|||||||
HStack {
|
HStack {
|
||||||
Text("\(appState.appInfo.appName)").font(.title).fontWeight(.bold).lineLimit(1)
|
Text("\(appState.appInfo.appName)").font(.title).fontWeight(.bold).lineLimit(1)
|
||||||
Text("•").foregroundStyle(Color("AccentColor"))
|
Text("•").foregroundStyle(Color("AccentColor"))
|
||||||
Text("\(appState.appInfo.appVersion)").font(.title3)
|
Text("v\(appState.appInfo.appVersion)").font(.title3)
|
||||||
if appState.appInfo.appName.count < 5 {
|
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.")
|
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.")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -173,7 +173,12 @@ struct MiniAppView: View {
|
|||||||
if appState.reload {
|
if appState.reload {
|
||||||
VStack {
|
VStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
ProgressView("Refreshing app list")
|
ProgressView() {
|
||||||
|
Text("Gathering app details")
|
||||||
|
.font(.callout)
|
||||||
|
.foregroundStyle(Color("mode").opacity(0.5))
|
||||||
|
.padding(5)
|
||||||
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
.padding(.vertical)
|
.padding(.vertical)
|
||||||
|
|||||||
@@ -33,7 +33,12 @@ struct RegularMode: View {
|
|||||||
if appState.reload {
|
if appState.reload {
|
||||||
VStack {
|
VStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
ProgressView("Refreshing app list")
|
ProgressView() {
|
||||||
|
Text("Gathering app details")
|
||||||
|
.font(.callout)
|
||||||
|
.foregroundStyle(Color("mode").opacity(0.5))
|
||||||
|
.padding(5)
|
||||||
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
.frame(width: sidebarWidth)
|
.frame(width: sidebarWidth)
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ struct ZombieView: View {
|
|||||||
@EnvironmentObject var locations: Locations
|
@EnvironmentObject var locations: Locations
|
||||||
@EnvironmentObject var fsm: FolderSettingsManager
|
@EnvironmentObject var fsm: FolderSettingsManager
|
||||||
@State private var showPop: Bool = false
|
@State private var showPop: Bool = false
|
||||||
|
@AppStorage("settings.general.leftoverWarning") private var warning: Bool = false
|
||||||
|
@State private var showAlert = false
|
||||||
@AppStorage("settings.general.mini") private var mini: Bool = false
|
@AppStorage("settings.general.mini") private var mini: Bool = false
|
||||||
@AppStorage("settings.general.glass") private var glass: Bool = false
|
@AppStorage("settings.general.glass") private var glass: Bool = false
|
||||||
@AppStorage("settings.sentinel.enable") private var sentinel: Bool = false
|
@AppStorage("settings.sentinel.enable") private var sentinel: Bool = false
|
||||||
@@ -36,45 +38,6 @@ struct ZombieView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
|
||||||
// let totalSelectedZombieSize: (real: String, logical: String, finder: String) = {
|
|
||||||
// var totalReal: Int64 = 0
|
|
||||||
// var totalLogical: Int64 = 0
|
|
||||||
//
|
|
||||||
// for url in selectedZombieItemsLocal {
|
|
||||||
// let realSize = appState.zombieFile.fileSize[url] ?? 0
|
|
||||||
// let logicalSize = appState.zombieFile.fileSizeLogical[url] ?? 0
|
|
||||||
// totalReal += realSize
|
|
||||||
// totalLogical += logicalSize
|
|
||||||
// }
|
|
||||||
// return (formatByte(size: totalReal).human, formatByte(size:totalLogical).human, "\(formatByte(size: totalLogical).byte) (\(formatByte(size: totalReal).human))")
|
|
||||||
// }()
|
|
||||||
|
|
||||||
// let filteredAndSortedFiles: ([URL], Int64, Int64) = {
|
|
||||||
// let fileSizeReal = appState.zombieFile.fileSize
|
|
||||||
// let fileSizeLogical = appState.zombieFile.fileSizeLogical
|
|
||||||
// let filteredFilesReal = fileSizeReal.filter { (url, _) in
|
|
||||||
// searchZ.isEmpty || url.lastPathComponent.localizedCaseInsensitiveContains(searchZ)
|
|
||||||
// }
|
|
||||||
// let filteredFilesLogical = fileSizeLogical.filter { (url, _) in
|
|
||||||
// searchZ.isEmpty || url.lastPathComponent.localizedCaseInsensitiveContains(searchZ)
|
|
||||||
// }
|
|
||||||
// let filesToSort = (sizeType == "Real" || sizeType == "Finder" ? filteredFilesReal : filteredFilesLogical)
|
|
||||||
// let sortedFilteredFiles = filesToSort.sorted(by: {
|
|
||||||
// if selectedSortAlpha {
|
|
||||||
// return $0.key.lastPathComponent.pearFormat() < $1.key.lastPathComponent.pearFormat()
|
|
||||||
// } else {
|
|
||||||
// return $0.value > $1.value
|
|
||||||
// }
|
|
||||||
// }).map { $0.key }
|
|
||||||
// let totalSize = filteredFilesReal.values.reduce(0, +)
|
|
||||||
// let totalSizeL = filteredFilesLogical.values.reduce(0, +)
|
|
||||||
// return (sortedFilteredFiles, totalSize, totalSizeL)
|
|
||||||
// }()
|
|
||||||
|
|
||||||
// let displaySizeTotal = sizeType == "Real" ? formatByte(size: filteredAndSortedFiles.1).human :
|
|
||||||
// sizeType == "Logical" ? formatByte(size: filteredAndSortedFiles.2).human :
|
|
||||||
// "\(formatByte(size: filteredAndSortedFiles.2).byte) (\(formatByte(size: filteredAndSortedFiles.1).human))"
|
|
||||||
|
|
||||||
VStack(alignment: .center) {
|
VStack(alignment: .center) {
|
||||||
if appState.showProgress {
|
if appState.showProgress {
|
||||||
VStack {
|
VStack {
|
||||||
@@ -150,7 +113,7 @@ struct ZombieView: View {
|
|||||||
VStack(alignment: .leading, spacing: 10){
|
VStack(alignment: .leading, spacing: 10){
|
||||||
HStack {
|
HStack {
|
||||||
Text("Leftover Files").font(.title).fontWeight(.bold)
|
Text("Leftover Files").font(.title).fontWeight(.bold)
|
||||||
InfoButton(text: "Leftover file search is not and can never be 100% accurate as it doesn't have any old/uninstalled app bundles to check against for file exclusion. This does a best guess search for files/folders and excludes the ones that have overlap with your currently installed applications. Please confirm files marked for deletion really do belong to old/uninstalled applications.", color: .orange, warning: true)
|
InfoButton(text: "Leftover file search is not 100% accurate as it doesn't have any uninstalled app bundles to check against for file exclusion. This does a best guess search for files/folders and excludes the ones that have overlap with your currently installed applications. Please confirm files marked for deletion really do belong to uninstalled applications.", color: .orange, warning: true)
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -326,9 +289,34 @@ struct ZombieView: View {
|
|||||||
.onAppear {
|
.onAppear {
|
||||||
updateMemoizedFiles(for: searchZ, sizeType: sizeType, selectedSortAlpha: selectedSortAlpha, force: true)
|
updateMemoizedFiles(for: searchZ, sizeType: sizeType, selectedSortAlpha: selectedSortAlpha, force: true)
|
||||||
}
|
}
|
||||||
|
.sheet(isPresented: $showAlert, content: {
|
||||||
|
VStack(spacing: 10) {
|
||||||
|
Text("Important")
|
||||||
|
.font(.headline)
|
||||||
|
Spacer()
|
||||||
|
Text("Leftover file search is not 100% accurate as it doesn't have any uninstalled app bundles to check against for file exclusion. This does a best guess search for files/folders and excludes the ones that have overlap with your currently installed applications. Please confirm files marked for deletion really do belong to uninstalled applications.")
|
||||||
|
.font(.subheadline)
|
||||||
|
Spacer()
|
||||||
|
Button("Close") {
|
||||||
|
warning = true
|
||||||
|
showAlert = false
|
||||||
|
}
|
||||||
|
.buttonStyle(SimpleButtonStyle(icon: "x.circle.fill", label: "Close", help: "Dismiss"))
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding(15)
|
||||||
|
.frame(width: 400, height: 250)
|
||||||
|
.background(GlassEffect(material: .hudWindow, blendingMode: .behindWindow))
|
||||||
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onAppear {
|
||||||
|
if !warning {
|
||||||
|
showAlert = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func binding(for file: URL) -> Binding<Bool> {
|
private func binding(for file: URL) -> Binding<Bool> {
|
||||||
|
|||||||
+6
-1
@@ -11,7 +11,7 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
# Uncomment the line if you want fastlane to automatically update itself
|
# Uncomment the line if you want fastlane to automatically update itself
|
||||||
# update_fastlane
|
update_fastlane
|
||||||
|
|
||||||
opt_out_usage
|
opt_out_usage
|
||||||
|
|
||||||
@@ -51,6 +51,11 @@ platform :mac do
|
|||||||
UI.user_error!("Failed to find .app file in #{output_path}")
|
UI.user_error!("Failed to find .app file in #{output_path}")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Empty the Archives folder
|
||||||
|
archives_path = "#{Dir.home}/Library/Developer/Xcode/Archives"
|
||||||
|
FileUtils.rm_rf(Dir.glob("#{archives_path}/*"))
|
||||||
|
|
||||||
|
|
||||||
# Open Finder at the Downloads folder
|
# Open Finder at the Downloads folder
|
||||||
system("open #{output_path}")
|
system("open #{output_path}")
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"3.7.0": "- App List Size Sorting: You can now sort the sidebar app list alphabetically or by descending size. Click the User/System header to toggle or from the searchbar menu|- Leftover Files: On first use, a warning/explanation sheet will be presented to the user",
|
||||||
"3.5.0": "- Finder Extension: When enabled, you can right click an app in finder and uninstall with Pearcleaner directly|- Theme System: You can now select a base theme color for the application window",
|
"3.5.0": "- Finder Extension: When enabled, you can right click an app in finder and uninstall with Pearcleaner directly|- Theme System: You can now select a base theme color for the application window",
|
||||||
"3.3.0": "- Folder Settings: Add more directories where Pearcleaner should search for app files|- Progress: Show progress bar on startup when loading all app files with instant search enabled|- App Icon: Show background color behind app icons in FilesView based on icon's average color mapping|- Progress: Show time counter on regular file search views",
|
"3.3.0": "- Folder Settings: Add more directories where Pearcleaner should search for app files|- Progress: Show progress bar on startup when loading all app files with instant search enabled|- App Icon: Show background color behind app icons in FilesView based on icon's average color mapping|- Progress: Show time counter on regular file search views",
|
||||||
"3.2.0": "- Menubar Item: You can choose to access Pearcleaner from the menubar. This will show a slightly modified Mini Mode view.",
|
"3.2.0": "- Menubar Item: You can choose to access Pearcleaner from the menubar. This will show a slightly modified Mini Mode view.",
|
||||||
|
|||||||
Reference in New Issue
Block a user