mirror of
https://github.com/exituser/Pearcleaner.git
synced 2026-09-17 06:09:01 +00:00
updates
This commit is contained in:
@@ -16,11 +16,13 @@
|
|||||||
C72893052AFD42E600C8C1CD /* DeepLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = C72893042AFD42E600C8C1CD /* DeepLink.swift */; };
|
C72893052AFD42E600C8C1CD /* DeepLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = C72893042AFD42E600C8C1CD /* DeepLink.swift */; };
|
||||||
C72893122AFD51EA00C8C1CD /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = C72893112AFD51EA00C8C1CD /* main.swift */; };
|
C72893122AFD51EA00C8C1CD /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = C72893112AFD51EA00C8C1CD /* main.swift */; };
|
||||||
C74412C72BBF249000DDFCA8 /* AppPathsFetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C74412C62BBF249000DDFCA8 /* AppPathsFetch.swift */; };
|
C74412C72BBF249000DDFCA8 /* AppPathsFetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C74412C62BBF249000DDFCA8 /* AppPathsFetch.swift */; };
|
||||||
|
C74FDC632BCD833D00B8960F /* Conditions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C74FDC622BCD833D00B8960F /* Conditions.swift */; };
|
||||||
C7575D392B0182DE006A600A /* PearcleanerSentinel in CopyFiles */ = {isa = PBXBuildFile; fileRef = C728930F2AFD51EA00C8C1CD /* PearcleanerSentinel */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
|
C7575D392B0182DE006A600A /* PearcleanerSentinel in CopyFiles */ = {isa = PBXBuildFile; fileRef = C728930F2AFD51EA00C8C1CD /* PearcleanerSentinel */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
|
||||||
C76D08482AF83C3F00D07867 /* Update.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76D08472AF83C3F00D07867 /* Update.swift */; };
|
C76D08482AF83C3F00D07867 /* Update.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76D08472AF83C3F00D07867 /* Update.swift */; };
|
||||||
C76D084A2AF83C6E00D07867 /* General.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76D08492AF83C6E00D07867 /* General.swift */; };
|
C76D084A2AF83C6E00D07867 /* General.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76D08492AF83C6E00D07867 /* General.swift */; };
|
||||||
C76D08512AF850F300D07867 /* About.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76D08502AF850F300D07867 /* About.swift */; };
|
C76D08512AF850F300D07867 /* About.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76D08502AF850F300D07867 /* About.swift */; };
|
||||||
C76D08552AF89CDE00D07867 /* RegularMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76D08542AF89CDE00D07867 /* RegularMode.swift */; };
|
C76D08552AF89CDE00D07867 /* RegularMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76D08542AF89CDE00D07867 /* RegularMode.swift */; };
|
||||||
|
C771CB762BD028AF00C33A18 /* SegmentedPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = C771CB752BD028AF00C33A18 /* SegmentedPicker.swift */; };
|
||||||
C77B90042AF18E2E009CC655 /* PearcleanerApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C77B90032AF18E2E009CC655 /* PearcleanerApp.swift */; };
|
C77B90042AF18E2E009CC655 /* PearcleanerApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C77B90032AF18E2E009CC655 /* PearcleanerApp.swift */; };
|
||||||
C77B90082AF18E2F009CC655 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C77B90072AF18E2F009CC655 /* Assets.xcassets */; };
|
C77B90082AF18E2F009CC655 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C77B90072AF18E2F009CC655 /* Assets.xcassets */; };
|
||||||
C77B90162AF19377009CC655 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = C77B90152AF19377009CC655 /* AppState.swift */; };
|
C77B90162AF19377009CC655 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = C77B90152AF19377009CC655 /* AppState.swift */; };
|
||||||
@@ -106,10 +108,12 @@
|
|||||||
C728930F2AFD51EA00C8C1CD /* PearcleanerSentinel */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = PearcleanerSentinel; sourceTree = BUILT_PRODUCTS_DIR; };
|
C728930F2AFD51EA00C8C1CD /* PearcleanerSentinel */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = PearcleanerSentinel; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
C72893112AFD51EA00C8C1CD /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
|
C72893112AFD51EA00C8C1CD /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
|
||||||
C74412C62BBF249000DDFCA8 /* AppPathsFetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPathsFetch.swift; sourceTree = "<group>"; };
|
C74412C62BBF249000DDFCA8 /* AppPathsFetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPathsFetch.swift; sourceTree = "<group>"; };
|
||||||
|
C74FDC622BCD833D00B8960F /* Conditions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Conditions.swift; sourceTree = "<group>"; };
|
||||||
C76D08472AF83C3F00D07867 /* Update.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Update.swift; sourceTree = "<group>"; };
|
C76D08472AF83C3F00D07867 /* Update.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Update.swift; sourceTree = "<group>"; };
|
||||||
C76D08492AF83C6E00D07867 /* General.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = General.swift; sourceTree = "<group>"; };
|
C76D08492AF83C6E00D07867 /* General.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = General.swift; sourceTree = "<group>"; };
|
||||||
C76D08502AF850F300D07867 /* About.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = About.swift; sourceTree = "<group>"; };
|
C76D08502AF850F300D07867 /* About.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = About.swift; sourceTree = "<group>"; };
|
||||||
C76D08542AF89CDE00D07867 /* RegularMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegularMode.swift; sourceTree = "<group>"; };
|
C76D08542AF89CDE00D07867 /* RegularMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegularMode.swift; sourceTree = "<group>"; };
|
||||||
|
C771CB752BD028AF00C33A18 /* SegmentedPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentedPicker.swift; sourceTree = "<group>"; };
|
||||||
C77B90002AF18E2E009CC655 /* Pearcleaner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Pearcleaner.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
C77B90002AF18E2E009CC655 /* Pearcleaner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Pearcleaner.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
C77B90032AF18E2E009CC655 /* PearcleanerApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PearcleanerApp.swift; sourceTree = "<group>"; };
|
C77B90032AF18E2E009CC655 /* PearcleanerApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PearcleanerApp.swift; sourceTree = "<group>"; };
|
||||||
C77B90072AF18E2F009CC655 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
C77B90072AF18E2F009CC655 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
@@ -197,6 +201,7 @@
|
|||||||
C72893042AFD42E600C8C1CD /* DeepLink.swift */,
|
C72893042AFD42E600C8C1CD /* DeepLink.swift */,
|
||||||
C77B901B2AF193A3009CC655 /* Styles.swift */,
|
C77B901B2AF193A3009CC655 /* Styles.swift */,
|
||||||
C7D31D502AFF00F300C7ED9E /* Locations.swift */,
|
C7D31D502AFF00F300C7ED9E /* Locations.swift */,
|
||||||
|
C74FDC622BCD833D00B8960F /* Conditions.swift */,
|
||||||
C7A9CE462B89164700EB6E78 /* Authorization.swift */,
|
C7A9CE462B89164700EB6E78 /* Authorization.swift */,
|
||||||
C7ACADE52B92A737000B5845 /* Features.swift */,
|
C7ACADE52B92A737000B5845 /* Features.swift */,
|
||||||
C7ED86B22BA8ED8800F13DE4 /* Trash.swift */,
|
C7ED86B22BA8ED8800F13DE4 /* Trash.swift */,
|
||||||
@@ -246,6 +251,14 @@
|
|||||||
path = Resources;
|
path = Resources;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
C771CB742BD028A000C33A18 /* Picker */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
C771CB752BD028AF00C33A18 /* SegmentedPicker.swift */,
|
||||||
|
);
|
||||||
|
path = Picker;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
C77B8FF72AF18E2E009CC655 = {
|
C77B8FF72AF18E2E009CC655 = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -283,6 +296,7 @@
|
|||||||
C77B901F2AF1B390009CC655 /* Views */ = {
|
C77B901F2AF1B390009CC655 /* Views */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
C771CB742BD028A000C33A18 /* Picker */,
|
||||||
C77B90222AF2D616009CC655 /* FilesView.swift */,
|
C77B90222AF2D616009CC655 /* FilesView.swift */,
|
||||||
C71848432B8D2D600046CB13 /* ZombieView.swift */,
|
C71848432B8D2D600046CB13 /* ZombieView.swift */,
|
||||||
C76D08542AF89CDE00D07867 /* RegularMode.swift */,
|
C76D08542AF89CDE00D07867 /* RegularMode.swift */,
|
||||||
@@ -471,9 +485,11 @@
|
|||||||
C77B90182AF19382009CC655 /* AppCommands.swift in Sources */,
|
C77B90182AF19382009CC655 /* AppCommands.swift in Sources */,
|
||||||
C7D31D4A2AFEB26700C7ED9E /* AppListItems.swift in Sources */,
|
C7D31D4A2AFEB26700C7ED9E /* AppListItems.swift in Sources */,
|
||||||
C77B901E2AF1A9DB009CC655 /* SettingsWindow.swift in Sources */,
|
C77B901E2AF1A9DB009CC655 /* SettingsWindow.swift in Sources */,
|
||||||
|
C771CB762BD028AF00C33A18 /* SegmentedPicker.swift in Sources */,
|
||||||
C74412C72BBF249000DDFCA8 /* AppPathsFetch.swift in Sources */,
|
C74412C72BBF249000DDFCA8 /* AppPathsFetch.swift in Sources */,
|
||||||
C79D05202BC99EB40083F976 /* ThemeManager.swift in Sources */,
|
C79D05202BC99EB40083F976 /* ThemeManager.swift in Sources */,
|
||||||
C7D31D482AFEB23700C7ED9E /* TopBar.swift in Sources */,
|
C7D31D482AFEB23700C7ED9E /* TopBar.swift in Sources */,
|
||||||
|
C74FDC632BCD833D00B8960F /* Conditions.swift in Sources */,
|
||||||
C7ACADE62B92A737000B5845 /* Features.swift in Sources */,
|
C7ACADE62B92A737000B5845 /* Features.swift in Sources */,
|
||||||
C7F539382AF60865007DF1B2 /* Utilities.swift in Sources */,
|
C7F539382AF60865007DF1B2 /* Utilities.swift in Sources */,
|
||||||
C76D08552AF89CDE00D07867 /* RegularMode.swift in Sources */,
|
C76D08552AF89CDE00D07867 /* RegularMode.swift in Sources */,
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct AppCommands: Commands {
|
struct AppCommands: Commands {
|
||||||
@AppStorage("settings.general.instant") private var instantSearch: Bool = true
|
|
||||||
|
|
||||||
let appState: AppState
|
let appState: AppState
|
||||||
let locations: Locations
|
let locations: Locations
|
||||||
@@ -42,17 +41,9 @@ struct AppCommands: Commands {
|
|||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||||
appState.sortedApps = []
|
appState.sortedApps = []
|
||||||
appState.sortedApps = sortedApps
|
appState.sortedApps = sortedApps
|
||||||
if instantSearch {
|
|
||||||
Task(priority: .high){
|
|
||||||
loadAllPaths(allApps: sortedApps, appState: appState, locations: locations) {
|
|
||||||
appState.reload.toggle()
|
appState.reload.toggle()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
appState.reload.toggle()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} label: {
|
} label: {
|
||||||
Text("Refresh Apps")
|
Text("Refresh Apps")
|
||||||
}
|
}
|
||||||
@@ -78,8 +69,7 @@ struct AppCommands: Commands {
|
|||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||||
appState.sortedApps = sortedApps
|
appState.sortedApps = sortedApps
|
||||||
for app in appState.trashedFiles {
|
for app in appState.trashedFiles {
|
||||||
let pathFinder = AppPathFinder(appInfo: app, appState: appState, locations: locations)
|
AppPathFinder(appInfo: app, appState: appState, locations: locations).findPaths()
|
||||||
pathFinder.findPaths()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ class AppInfoFetcher {
|
|||||||
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: [:], fileIcon: [:])
|
webApp: webApp, wrapped: wrapped, system: system, files: [], fileSize: [:], fileSizeLogical: [:], fileIcon: [:])
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func fetchAppIcon(for path: URL, wrapped: Bool) -> NSImage? {
|
private static func fetchAppIcon(for path: URL, wrapped: Bool) -> NSImage? {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import Foundation
|
|||||||
import AppKit
|
import AppKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
|
||||||
class AppPathFinder {
|
class AppPathFinder {
|
||||||
private var appInfo: AppInfo
|
private var appInfo: AppInfo
|
||||||
private var appState: AppState
|
private var appState: AppState
|
||||||
@@ -18,7 +19,6 @@ class AppPathFinder {
|
|||||||
private var completion: () -> Void = {}
|
private var completion: () -> Void = {}
|
||||||
private var collection: [URL] = []
|
private var collection: [URL] = []
|
||||||
private let collectionAccessQueue = DispatchQueue(label: "com.alienator88.Pearcleaner.appPathFinder.collectionAccess")
|
private let collectionAccessQueue = DispatchQueue(label: "com.alienator88.Pearcleaner.appPathFinder.collectionAccess")
|
||||||
@AppStorage("settings.general.instant") var instantSearch: Bool = true
|
|
||||||
|
|
||||||
init(appInfo: AppInfo = .empty, appState: AppState, locations: Locations, backgroundRun: Bool = false, reverseAddon: Bool = false, completion: @escaping () -> Void = {}) {
|
init(appInfo: AppInfo = .empty, appState: AppState, locations: Locations, backgroundRun: Bool = false, reverseAddon: Bool = false, completion: @escaping () -> Void = {}) {
|
||||||
self.appInfo = appInfo
|
self.appInfo = appInfo
|
||||||
@@ -124,10 +124,21 @@ class AppPathFinder {
|
|||||||
collectionAccessQueue.sync {
|
collectionAccessQueue.sync {
|
||||||
containsItem = self.collection.contains(itemURL)
|
containsItem = self.collection.contains(itemURL)
|
||||||
}
|
}
|
||||||
return itemL.hasPrefix("comapple") && !["comappleconfigurator", "comappledt", "comappleiwork", "comapplesfsymbols", "comappletestflight"].contains(where: itemL.hasPrefix) || containsItem || !isSupportedFileType(at: itemURL.path)
|
if containsItem || !isSupportedFileType(at: itemURL.path) {
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for skipCondition in skipConditions {
|
||||||
|
if itemL.hasPrefix(skipCondition.skipPrefix) {
|
||||||
|
let isAllowed = skipCondition.allowPrefixes.contains(where: itemL.hasPrefix)
|
||||||
|
if !isAllowed {
|
||||||
|
return true // Skip because it starts with a base prefix but is not in the allowed list
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -138,85 +149,20 @@ class AppPathFinder {
|
|||||||
let nameL = self.appInfo.appName.pearFormat()
|
let nameL = self.appInfo.appName.pearFormat()
|
||||||
let nameP = self.appInfo.path.lastPathComponent.replacingOccurrences(of: ".app", with: "")
|
let nameP = self.appInfo.path.lastPathComponent.replacingOccurrences(of: ".app", with: "")
|
||||||
|
|
||||||
let exclusions = [
|
for condition in conditions {
|
||||||
"comappledtxcode": ["comrobotsandpencilsxcodesapp", "comoneminutegamesxcodecleaner", "iohyperappxcodecleaner", "xcodesjson"],
|
if bundleIdentifierL.contains(condition.bundle_id) {
|
||||||
"comrobotsandpencilsxcodesapp": ["comappledtxcode", "comoneminutegamesxcodecleaner", "iohyperappxcodecleaner"],
|
// Exclude and include keywords
|
||||||
"iohyperappxcodecleaner": ["comrobotsandpencilsxcodesapp", "comoneminutegamesxcodecleaner", "comappledtxcode", "xcodesjson"]
|
let hasIncludeKeyword = condition.include.contains(where: itemL.contains)
|
||||||
]
|
let hasExcludeKeyword = condition.exclude.contains(where: itemL.contains)
|
||||||
|
|
||||||
if let excludedApps = exclusions[bundleIdentifierL], excludedApps.contains(where: { itemL.contains($0) }) {
|
if hasExcludeKeyword {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if hasIncludeKeyword {
|
||||||
if bundleIdentifierL.contains("comappledtxcode") {
|
if !condition.exclude.contains(where: itemL.contains) {
|
||||||
// Include items that are part of the Xcode ecosystem
|
|
||||||
if itemL.contains("comappledt") || itemL.contains("xcode") || itemL.contains("simulator") {
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if bundleIdentifierL.contains("uszoomxos") {
|
|
||||||
// Include items for Zoom that are not similar to the app name and/or bundle id
|
|
||||||
if itemL.contains("zoom") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if bundleIdentifierL.contains("combravebrowser") {
|
|
||||||
// Include items for Brave that are not similar to the app name and/or bundle id
|
|
||||||
if itemL.contains("brave") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if bundleIdentifierL.contains("comgooglechrome") {
|
|
||||||
// Include items for Chrome that are not similar to the app name and/or bundle id
|
|
||||||
if (itemL.contains("google") && !itemL.contains("iterm")) || (itemL.contains("chrome") && !itemL.contains("chromefeaturestate")) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if bundleIdentifierL.contains("commicrosoftedgemac") {
|
|
||||||
// Include items for Edge that are not similar to the app name and/or bundle id
|
|
||||||
let exclusions = ["vscode","rdc","appcenter","office","oneauth"]
|
|
||||||
if itemL.contains("microsoft") && !exclusions.contains(where: itemL.contains) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if bundleIdentifierL.contains("orgmozillafirefox") {
|
|
||||||
// Include items for Firefox that are not similar to the app name and/or bundle id
|
|
||||||
if itemL.contains("mozilla") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if bundleIdentifierL.contains("comlogioptionsplus") {
|
|
||||||
// Include items for Zoom that are not similar to the app name and/or bundle id
|
|
||||||
if itemL.contains("logi") && !itemL.contains("login") && !itemL.contains("logic") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if bundleIdentifierL.contains("commicrosoftvscode") {
|
|
||||||
// Include items for vscode
|
|
||||||
if itemL.contains("vscode") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if bundleIdentifierL.contains("comfacebookarchondeveloperid") {
|
|
||||||
// Include items for FB Messenger
|
|
||||||
if itemL.contains("archonloginhelper") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if bundleIdentifierL.contains("euexelbanstats") {
|
|
||||||
// Include items for Stats that are not similar to the app name and/or bundle id
|
|
||||||
if itemL.contains("video") {
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,37 +209,35 @@ class AppPathFinder {
|
|||||||
|
|
||||||
private func handleOutliers() -> [URL] {
|
private func handleOutliers() -> [URL] {
|
||||||
var outliers: [URL] = []
|
var outliers: [URL] = []
|
||||||
|
let bundleIdentifier = self.appInfo.bundleIdentifier.pearFormat()
|
||||||
|
|
||||||
// Handle VSCode folder
|
// Find conditions that match the current app's bundle identifier
|
||||||
if self.appInfo.bundleIdentifier.pearFormat().contains("commicrosoftvscode") {
|
let matchingConditions = conditions.filter { condition in
|
||||||
let appSupportCodePath = "\(home)/Library/Application Support/Code"
|
bundleIdentifier.contains(condition.bundle_id)
|
||||||
if let codeDirectoryURL = URL(string: appSupportCodePath), FileManager.default.fileExists(atPath: codeDirectoryURL.path) {
|
|
||||||
outliers.append(codeDirectoryURL)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle Xcode folder
|
for condition in matchingConditions {
|
||||||
if self.appInfo.bundleIdentifier.pearFormat().contains("comappledtxcode") {
|
if let forceIncludes = condition.includeForce {
|
||||||
let simulators = "\(home)/Library/Containers/com.apple.iphonesimulator.ShareExtension"
|
for path in forceIncludes {
|
||||||
if let simulatorsURL = URL(string: simulators), FileManager.default.fileExists(atPath: simulatorsURL.path) {
|
if let url = URL(string: path), FileManager.default.fileExists(atPath: url.path) {
|
||||||
outliers.append(simulatorsURL)
|
outliers.append(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add other outlier cases here as needed
|
|
||||||
|
|
||||||
return outliers
|
return outliers
|
||||||
}
|
}
|
||||||
|
|
||||||
private func finalizeCollection() {
|
private func finalizeCollection() {
|
||||||
DispatchQueue.global(qos: .userInitiated).async {
|
DispatchQueue.global(qos: .userInitiated).async {
|
||||||
let groupContainers = self.getGroupContainers(bundleURL: self.appInfo.path)
|
// let groupContainers = self.getGroupContainers(bundleURL: self.appInfo.path)
|
||||||
let outliers = self.handleOutliers()
|
let outliers = self.handleOutliers()
|
||||||
var tempCollection: [URL] = []
|
var tempCollection: [URL] = []
|
||||||
self.collectionAccessQueue.sync {
|
self.collectionAccessQueue.sync {
|
||||||
tempCollection = self.collection
|
tempCollection = self.collection
|
||||||
}
|
}
|
||||||
tempCollection.append(contentsOf: groupContainers)
|
// tempCollection.append(contentsOf: groupContainers)
|
||||||
tempCollection.append(contentsOf: outliers)
|
tempCollection.append(contentsOf: outliers)
|
||||||
|
|
||||||
// Sort and standardize URLs to ensure consistent comparisons
|
// Sort and standardize URLs to ensure consistent comparisons
|
||||||
@@ -318,10 +262,13 @@ class AppPathFinder {
|
|||||||
private func handlePostProcessing(sortedCollection: [URL]) {
|
private func handlePostProcessing(sortedCollection: [URL]) {
|
||||||
// Calculate file details (sizes and icons), update app state, and call completion
|
// Calculate file details (sizes and icons), update app state, and call completion
|
||||||
var fileSize: [URL: Int64] = [:]
|
var fileSize: [URL: Int64] = [:]
|
||||||
|
var fileSizeLogical: [URL: Int64] = [:]
|
||||||
var fileIcon: [URL: NSImage?] = [:]
|
var fileIcon: [URL: NSImage?] = [:]
|
||||||
|
|
||||||
for path in sortedCollection {
|
for path in sortedCollection {
|
||||||
fileSize[path] = totalSizeOnDisk(for: path)
|
let size = totalSizeOnDisk(for: path)
|
||||||
|
fileSize[path] = size.real
|
||||||
|
fileSizeLogical[path] = size.logical
|
||||||
fileIcon[path] = getIconForFileOrFolderNS(atPath: path)
|
fileIcon[path] = getIconForFileOrFolderNS(atPath: path)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -332,8 +279,8 @@ class AppPathFinder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update appInfo and appState with the new values
|
// Update appInfo and appState with the new values
|
||||||
self.appInfo.files = updatedCollection
|
|
||||||
self.appInfo.fileSize = fileSize
|
self.appInfo.fileSize = fileSize
|
||||||
|
self.appInfo.fileSizeLogical = fileSizeLogical
|
||||||
self.appInfo.fileIcon = fileIcon
|
self.appInfo.fileIcon = fileIcon
|
||||||
|
|
||||||
if !self.backgroundRun {
|
if !self.backgroundRun {
|
||||||
@@ -341,12 +288,6 @@ class AppPathFinder {
|
|||||||
self.appState.selectedItems = Set(updatedCollection)
|
self.appState.selectedItems = Set(updatedCollection)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only append object to store if instant search. Same for calculating progress.
|
|
||||||
if self.instantSearch {
|
|
||||||
self.appState.appInfoStore.append(self.appInfo)
|
|
||||||
self.appState.instantProgress += 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append object to store if running reverse search with empty store
|
// Append object to store if running reverse search with empty store
|
||||||
if self.reverseAddon {
|
if self.reverseAddon {
|
||||||
self.appState.appInfoStore.append(self.appInfo)
|
self.appState.appInfoStore.append(self.appInfo)
|
||||||
@@ -356,3 +297,324 @@ class AppPathFinder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Async Test
|
||||||
|
//class AppPathFinder {
|
||||||
|
// private var appInfo: AppInfo
|
||||||
|
// private var appState: AppState
|
||||||
|
// private var locations: Locations
|
||||||
|
// private var backgroundRun: Bool
|
||||||
|
// private var reverseAddon: Bool
|
||||||
|
// private var completion: () -> Void = {}
|
||||||
|
// private var collection: [URL] = []
|
||||||
|
// private let collectionAccessQueue = DispatchQueue(label: "com.alienator88.Pearcleaner.appPathFinder.collectionAccess")
|
||||||
|
// private var state = PathFinderState() // Actor instance
|
||||||
|
//
|
||||||
|
// init(appInfo: AppInfo = .empty, appState: AppState, locations: Locations, backgroundRun: Bool = false, reverseAddon: Bool = false, completion: @escaping () -> Void = {}) {
|
||||||
|
// self.appInfo = appInfo
|
||||||
|
// self.appState = appState
|
||||||
|
// self.locations = locations
|
||||||
|
// self.backgroundRun = backgroundRun
|
||||||
|
// self.reverseAddon = reverseAddon
|
||||||
|
// self.completion = completion
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func findPaths() async {
|
||||||
|
// await initialURLProcessing()
|
||||||
|
// await collectDirectories()
|
||||||
|
// await collectFiles()
|
||||||
|
// await finalizeCollection()
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private func initialURLProcessing() async {
|
||||||
|
// if let url = URL(string: self.appInfo.path.absoluteString), !url.path.contains(".Trash") {
|
||||||
|
// let modifiedUrl = url.path.contains("Wrapper") ? url.deletingLastPathComponent().deletingLastPathComponent() : url
|
||||||
|
// collectionAccessQueue.sync {
|
||||||
|
// self.collection.append(modifiedUrl)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private func collectDirectories() async {
|
||||||
|
// for location in self.locations.apps.paths {
|
||||||
|
// await processDirectoryLocation(location)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private func processDirectoryLocation(_ location: String) async {
|
||||||
|
// do {
|
||||||
|
// let contents = try FileManager.default.contentsOfDirectory(atPath: location)
|
||||||
|
// for item in contents {
|
||||||
|
// let itemURL = URL(fileURLWithPath: location).appendingPathComponent(item)
|
||||||
|
// let itemL = item.replacingOccurrences(of: ".", with: "").replacingOccurrences(of: " ", with: "").lowercased()
|
||||||
|
//
|
||||||
|
// var isDirectory: ObjCBool = false
|
||||||
|
// if FileManager.default.fileExists(atPath: itemURL.path, isDirectory: &isDirectory), isDirectory.boolValue {
|
||||||
|
// // Perform the check to skip the item if needed
|
||||||
|
// if shouldSkipItem(itemL, at: itemURL) {
|
||||||
|
// continue
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// collectionAccessQueue.sync {
|
||||||
|
// let alreadyIncluded = self.collection.contains { existingURL in
|
||||||
|
// itemURL.path.hasPrefix(existingURL.path)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if !alreadyIncluded && specificCondition(itemL: itemL, itemURL: itemURL) {
|
||||||
|
// self.collection.append(itemURL)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// } catch {
|
||||||
|
// print("Error processing directory location: \(location), error: \(error)")
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private func collectFiles() async {
|
||||||
|
// for location in self.locations.apps.paths {
|
||||||
|
// await processFileLocation(location)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private func processFileLocation(_ location: String) async {
|
||||||
|
// do {
|
||||||
|
// let contents = try FileManager.default.contentsOfDirectory(atPath: location)
|
||||||
|
// for item in contents {
|
||||||
|
// let itemURL = URL(fileURLWithPath: location).appendingPathComponent(item)
|
||||||
|
// let itemL = item.replacingOccurrences(of: ".", with: "").replacingOccurrences(of: " ", with: "").lowercased()
|
||||||
|
//
|
||||||
|
// if FileManager.default.fileExists(atPath: itemURL.path),
|
||||||
|
// !shouldSkipItem(itemL, at: itemURL),
|
||||||
|
// specificCondition(itemL: itemL, itemURL: itemURL) {
|
||||||
|
// collectionAccessQueue.sync {
|
||||||
|
// self.collection.append(itemURL)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// } catch {
|
||||||
|
// print("Error processing file location: \(location), error: \(error)")
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// private func shouldSkipItem(_ itemL: String, at itemURL: URL) -> Bool {
|
||||||
|
// var containsItem = false
|
||||||
|
// collectionAccessQueue.sync {
|
||||||
|
// containsItem = self.collection.contains(itemURL)
|
||||||
|
// }
|
||||||
|
// if containsItem || !isSupportedFileType(at: itemURL.path) {
|
||||||
|
// return true
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// for skipCondition in skipConditions {
|
||||||
|
// if itemL.hasPrefix(skipCondition.skipPrefix) {
|
||||||
|
// let isAllowed = skipCondition.allowPrefixes.contains(where: itemL.hasPrefix)
|
||||||
|
// if !isAllowed {
|
||||||
|
// return true // Skip because it starts with a base prefix but is not in the allowed list
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// return false
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// private func specificCondition(itemL: String, itemURL: URL) -> Bool {
|
||||||
|
// let bundleIdentifierL = self.appInfo.bundleIdentifier.pearFormat()
|
||||||
|
// let bundleComponents = self.appInfo.bundleIdentifier.components(separatedBy: ".").compactMap { $0 != "-" ? $0.lowercased() : nil }
|
||||||
|
// let bundle = bundleComponents.suffix(2).joined()
|
||||||
|
// let nameL = self.appInfo.appName.pearFormat()
|
||||||
|
// let nameP = self.appInfo.path.lastPathComponent.replacingOccurrences(of: ".app", with: "")
|
||||||
|
//
|
||||||
|
// for condition in conditions {
|
||||||
|
// if bundleIdentifierL.contains(condition.bundle_id) {
|
||||||
|
// // Exclude and include keywords
|
||||||
|
// let hasIncludeKeyword = condition.include.contains(where: itemL.contains)
|
||||||
|
// let hasExcludeKeyword = condition.exclude.contains(where: itemL.contains)
|
||||||
|
//
|
||||||
|
// if hasExcludeKeyword {
|
||||||
|
// return false
|
||||||
|
// }
|
||||||
|
// if hasIncludeKeyword {
|
||||||
|
// if !condition.exclude.contains(where: itemL.contains) {
|
||||||
|
// return true
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// if self.appInfo.webApp {
|
||||||
|
// return itemL.contains(bundleIdentifierL)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// return itemL.contains(bundleIdentifierL) || itemL.contains(bundle) || (nameL.count > 3 && itemL.contains(nameL)) || (nameP.count > 3 && itemL.contains(nameP))
|
||||||
|
//
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// func getGroupContainers(bundleURL: URL) async -> [URL] {
|
||||||
|
// await withCheckedContinuation { continuation in
|
||||||
|
// var groupContainers: [URL] = []
|
||||||
|
// // Assume creating a SecStaticCode is synchronous and quick.
|
||||||
|
// var staticCode: SecStaticCode?
|
||||||
|
// let status = SecStaticCodeCreateWithPath(bundleURL as CFURL, [], &staticCode)
|
||||||
|
//
|
||||||
|
// if status == errSecSuccess, let staticCode = staticCode {
|
||||||
|
// var signingInformation: CFDictionary?
|
||||||
|
//
|
||||||
|
// // This may involve a blocking call to fetch signing information.
|
||||||
|
// let status = SecCodeCopySigningInformation(staticCode, SecCSFlags(), &signingInformation)
|
||||||
|
//
|
||||||
|
// if status == errSecSuccess, let infoDict = signingInformation as? [String: Any],
|
||||||
|
// let entitlementsDict = infoDict["entitlements-dict"] as? [String: Any],
|
||||||
|
// let appGroups = entitlementsDict["com.apple.security.application-groups"] as? [String] {
|
||||||
|
//
|
||||||
|
// let fileManager = FileManager.default
|
||||||
|
// let home = NSHomeDirectory() // Assuming `home` is derived earlier or statically known.
|
||||||
|
//
|
||||||
|
// for groupID in appGroups {
|
||||||
|
// let groupURL = URL(fileURLWithPath: "\(home)/Library/Group Containers/\(groupID)")
|
||||||
|
// if fileManager.fileExists(atPath: groupURL.path) {
|
||||||
|
// groupContainers.append(groupURL)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Continue once all operations are complete.
|
||||||
|
// continuation.resume(returning: groupContainers)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// private func handleOutliers() -> [URL] {
|
||||||
|
// var outliers: [URL] = []
|
||||||
|
// let bundleIdentifier = self.appInfo.bundleIdentifier.pearFormat()
|
||||||
|
//
|
||||||
|
// // Find conditions that match the current app's bundle identifier
|
||||||
|
// let matchingConditions = conditions.filter { condition in
|
||||||
|
// bundleIdentifier.contains(condition.bundle_id)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// for condition in matchingConditions {
|
||||||
|
// if let forceIncludes = condition.includeForce {
|
||||||
|
// for path in forceIncludes {
|
||||||
|
// if let url = URL(string: path), FileManager.default.fileExists(atPath: url.path) {
|
||||||
|
// outliers.append(url)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// return outliers
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private func finalizeCollection() async {
|
||||||
|
// let groupContainers = await getGroupContainers(bundleURL: self.appInfo.path)
|
||||||
|
// let outliers = handleOutliers()
|
||||||
|
// var tempCollection: [URL] = []
|
||||||
|
//
|
||||||
|
// collectionAccessQueue.sync {
|
||||||
|
// tempCollection = self.collection
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// tempCollection.append(contentsOf: groupContainers)
|
||||||
|
// tempCollection.append(contentsOf: outliers)
|
||||||
|
//
|
||||||
|
// // Sort and standardize URLs to ensure consistent comparisons
|
||||||
|
// let sortedCollection = tempCollection.map { $0.standardizedFileURL }.sorted(by: { $0.path < $1.path })
|
||||||
|
// var filteredCollection: [URL] = []
|
||||||
|
// var previousUrl: URL?
|
||||||
|
//
|
||||||
|
// for url in sortedCollection {
|
||||||
|
// if let previous = previousUrl, url.path.hasPrefix(previous.path + "/") {
|
||||||
|
// // Current URL is a subdirectory of the previous one, so skip it
|
||||||
|
// continue
|
||||||
|
// }
|
||||||
|
// // This URL is not a subdirectory of the previous one, so keep it and set it as the previous URL
|
||||||
|
// filteredCollection.append(url)
|
||||||
|
// previousUrl = url
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// await handlePostProcessing(sortedCollection: filteredCollection)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private func handlePostProcessing(sortedCollection: [URL]) async {
|
||||||
|
// for path in sortedCollection {
|
||||||
|
// let size = totalSizeOnDisk(for: path)
|
||||||
|
// if let icon = getIconForFileOrFolderNS(atPath: path) { // Retrieve NSImage
|
||||||
|
// let iconData = serializeImage(icon) // Convert NSImage to Data
|
||||||
|
// await state.setFileDetails(for: path, size: size, icon: iconData) // Pass Data to actor
|
||||||
|
// } else {
|
||||||
|
// await state.setFileDetails(for: path, size: size, icon: nil)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// await updateAppState(with: sortedCollection)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private func updateAppState(with sortedCollection: [URL]) async {
|
||||||
|
// // Retrieve state from the actor
|
||||||
|
// let fileSize = await self.state.getFileSize()
|
||||||
|
// let fileIconData = await self.state.getFileIconData()
|
||||||
|
// let fileIcons = fileIconData.mapValues { deserializeImage($0) }
|
||||||
|
//
|
||||||
|
// // Assume updating of appInfo needs to happen on the main thread
|
||||||
|
// self.appInfo.fileSize = fileSize
|
||||||
|
// self.appInfo.fileIcon = fileIcons
|
||||||
|
//
|
||||||
|
// // Execute UI related updates on the main thread using MainActor
|
||||||
|
// await MainActor.run {
|
||||||
|
// if !self.backgroundRun {
|
||||||
|
// self.appState.appInfo = self.appInfo
|
||||||
|
// self.appState.selectedItems = Set(sortedCollection)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// self.completion() // Call the completion handler
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//actor PathFinderState {
|
||||||
|
// var fileSize: [URL: Int64] = [:]
|
||||||
|
// var fileIconData: [URL: Data?] = [:]
|
||||||
|
//
|
||||||
|
// func setFileDetails(for path: URL, size: Int64, icon: Data?) {
|
||||||
|
// fileSize[path] = size
|
||||||
|
// fileIconData[path] = icon
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func getFileSize() -> [URL: Int64] {
|
||||||
|
// return fileSize
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func getFileIconData() -> [URL: Data?] {
|
||||||
|
// return fileIconData
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//func serializeImage(_ image: NSImage?) -> Data? {
|
||||||
|
// guard let image = image else { return nil }
|
||||||
|
// guard let tiffData = image.tiffRepresentation else { return nil }
|
||||||
|
// let bitmapImage = NSBitmapImageRep(data: tiffData)
|
||||||
|
// return bitmapImage?.representation(using: .png, properties: [:])
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//func deserializeImage(_ data: Data?) -> NSImage? {
|
||||||
|
// guard let data = data else { return nil }
|
||||||
|
// return NSImage(data: data)
|
||||||
|
//}
|
||||||
|
|
||||||
|
|||||||
@@ -29,9 +29,6 @@ class AppState: ObservableObject
|
|||||||
@Published var progressBar: (String, Double) = ("Ready", 0.0)
|
@Published var progressBar: (String, Double) = ("Ready", 0.0)
|
||||||
@Published var reload: Bool = false
|
@Published var reload: Bool = false
|
||||||
@Published var showProgress: Bool = false
|
@Published var showProgress: Bool = false
|
||||||
@Published var popCount: Int = 0
|
|
||||||
@Published var instantProgress: Double = 0.0
|
|
||||||
@Published var instantTotal: Double = 0.0
|
|
||||||
@Published var finderExtensionEnabled: Bool = false
|
@Published var finderExtensionEnabled: Bool = false
|
||||||
|
|
||||||
|
|
||||||
@@ -48,12 +45,14 @@ class AppState: ObservableObject
|
|||||||
system: false,
|
system: false,
|
||||||
files: [],
|
files: [],
|
||||||
fileSize: [:],
|
fileSize: [:],
|
||||||
|
fileSizeLogical: [:],
|
||||||
fileIcon: [:]
|
fileIcon: [:]
|
||||||
)
|
)
|
||||||
|
|
||||||
self.zombieFile = ZombieFile(
|
self.zombieFile = ZombieFile(
|
||||||
id: UUID(),
|
id: UUID(),
|
||||||
fileSize: [:],
|
fileSize: [:],
|
||||||
|
fileSizeLogical: [:],
|
||||||
fileIcon: [:]
|
fileIcon: [:]
|
||||||
)
|
)
|
||||||
updateExtensionStatus()
|
updateExtensionStatus()
|
||||||
@@ -91,14 +90,19 @@ struct AppInfo: Identifiable, Equatable, Hashable {
|
|||||||
let system: Bool
|
let system: Bool
|
||||||
var files: [URL]
|
var files: [URL]
|
||||||
var fileSize: [URL:Int64]
|
var fileSize: [URL:Int64]
|
||||||
|
var fileSizeLogical: [URL:Int64]
|
||||||
var fileIcon: [URL:NSImage?]
|
var fileIcon: [URL:NSImage?]
|
||||||
var totalSize: Int64
|
var totalSize: Int64
|
||||||
{
|
{
|
||||||
return fileSize.values.reduce(0, +)
|
return fileSize.values.reduce(0, +)
|
||||||
}
|
}
|
||||||
|
var totalSizeLogical: Int64
|
||||||
|
{
|
||||||
|
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, files: [], fileSize: [:], fileIcon: [:])
|
static let empty = AppInfo(id: UUID(), path: URL(fileURLWithPath: ""), bundleIdentifier: "", appName: "", appVersion: "", appIcon: nil, webApp: false, wrapped: false, system: false, files: [], fileSize: [:], fileSizeLogical: [:], fileIcon: [:])
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,14 +111,19 @@ struct AppInfo: Identifiable, Equatable, Hashable {
|
|||||||
struct ZombieFile: Identifiable, Equatable, Hashable {
|
struct ZombieFile: Identifiable, Equatable, Hashable {
|
||||||
let id: UUID
|
let id: UUID
|
||||||
var fileSize: [URL:Int64]
|
var fileSize: [URL:Int64]
|
||||||
|
var fileSizeLogical: [URL:Int64]
|
||||||
var fileIcon: [URL:NSImage?]
|
var fileIcon: [URL:NSImage?]
|
||||||
var totalSize: Int64
|
var totalSize: Int64
|
||||||
{
|
{
|
||||||
return fileSize.values.reduce(0, +)
|
return fileSize.values.reduce(0, +)
|
||||||
}
|
}
|
||||||
|
var totalSizeLogical: Int64
|
||||||
|
{
|
||||||
|
return fileSizeLogical.values.reduce(0, +)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static let empty = ZombieFile(id: UUID(), fileSize: [:], fileIcon: [:])
|
static let empty = ZombieFile(id: UUID(), fileSize: [:], fileSizeLogical: [:], fileIcon: [:])
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,5 +210,3 @@ enum DisplayMode: Int, CaseIterable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
//
|
||||||
|
// Conditions.swift
|
||||||
|
// Pearcleaner
|
||||||
|
//
|
||||||
|
// Created by Alin Lupascu on 4/15/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
|
||||||
|
struct Condition: Decodable {
|
||||||
|
var bundle_id: String
|
||||||
|
var include: [String]
|
||||||
|
var exclude: [String]
|
||||||
|
var includeForce: [String]?
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SkipCondition {
|
||||||
|
var skipPrefix: String
|
||||||
|
var allowPrefixes: [String]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Conditions for some apps that need to include/exclude certain files/folders when names are more complicated
|
||||||
|
var conditions: [Condition] = [
|
||||||
|
Condition(
|
||||||
|
bundle_id: "comappledtxcode",
|
||||||
|
include: ["comappledt", "xcode", "simulator"],
|
||||||
|
exclude: ["comrobotsandpencilsxcodesapp", "comoneminutegamesxcodecleaner", "iohyperappxcodecleaner", "xcodesjson"],
|
||||||
|
includeForce: ["\(home)/Library/Containers/com.apple.iphonesimulator.ShareExtension"]
|
||||||
|
),
|
||||||
|
Condition(
|
||||||
|
bundle_id: "comrobotsandpencilsxcodesapp",
|
||||||
|
include: [],
|
||||||
|
exclude: ["comappledtxcode", "comoneminutegamesxcodecleaner", "iohyperappxcodecleaner"]
|
||||||
|
),
|
||||||
|
Condition(
|
||||||
|
bundle_id: "iohyperappxcodecleaner",
|
||||||
|
include: [],
|
||||||
|
exclude: ["comrobotsandpencilsxcodesapp", "comoneminutegamesxcodecleaner", "comappledtxcode", "xcodesjson"]
|
||||||
|
),
|
||||||
|
Condition(
|
||||||
|
bundle_id: "uszoomxos",
|
||||||
|
include: ["zoom"],
|
||||||
|
exclude: []
|
||||||
|
),
|
||||||
|
Condition(
|
||||||
|
bundle_id: "combravebrowser",
|
||||||
|
include: ["brave"],
|
||||||
|
exclude: []
|
||||||
|
),
|
||||||
|
Condition(
|
||||||
|
bundle_id: "comoktamobile",
|
||||||
|
include: ["okta"],
|
||||||
|
exclude: []
|
||||||
|
),
|
||||||
|
Condition(
|
||||||
|
bundle_id: "comgooglechrome",
|
||||||
|
include: ["google", "chrome"],
|
||||||
|
exclude: ["iterm", "chromefeaturestate"]
|
||||||
|
),
|
||||||
|
Condition(
|
||||||
|
bundle_id: "commicrosoftedgemac",
|
||||||
|
include: ["microsoft"],
|
||||||
|
exclude: ["vscode", "rdc", "appcenter", "office", "oneauth"]
|
||||||
|
),
|
||||||
|
Condition(
|
||||||
|
bundle_id: "orgmozillafirefox",
|
||||||
|
include: ["mozilla"],
|
||||||
|
exclude: []
|
||||||
|
),
|
||||||
|
Condition(
|
||||||
|
bundle_id: "comlogioptionsplus",
|
||||||
|
include: ["logi"],
|
||||||
|
exclude: ["login", "logic"],
|
||||||
|
includeForce: []
|
||||||
|
),
|
||||||
|
Condition(
|
||||||
|
bundle_id: "commicrosoftvscode",
|
||||||
|
include: ["vscode"],
|
||||||
|
exclude: [],
|
||||||
|
includeForce: ["\(home)/Library/Application Support/Code"]
|
||||||
|
),
|
||||||
|
Condition(
|
||||||
|
bundle_id: "comfacebookarchondeveloperid",
|
||||||
|
include: ["archonloginhelper"],
|
||||||
|
exclude: []
|
||||||
|
),
|
||||||
|
Condition(
|
||||||
|
bundle_id: "euexelbanstats",
|
||||||
|
include: [],
|
||||||
|
exclude: ["video"]
|
||||||
|
),
|
||||||
|
Condition(
|
||||||
|
bundle_id: "jetbrains",
|
||||||
|
include: ["jetbrains", "jcef"],
|
||||||
|
exclude: []
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Skip com.apple files/folders since most are system originated, allow some for apps
|
||||||
|
let skipConditions: [SkipCondition] = [
|
||||||
|
SkipCondition(
|
||||||
|
skipPrefix: "comapple",
|
||||||
|
allowPrefixes: ["comappleconfigurator", "comappledt", "comappleiwork", "comapplesfsymbols", "comappletestflight"]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
// Skip files/folders during reverse file search
|
||||||
|
let skipReverse = ["apple", "temporary", "btserver", "proapps", "scripteditor", "ilife", "livefsd", "siritoday", "addressbook", "animoji", "appstore", "askpermission", "callhistory", "clouddocs", "diskimages", "dock", "facetime", "fileprovider", "instruments", "knowledge", "mobilesync", "syncservices", "homeenergyd", "icloud", "icdd", "networkserviceproxy", "familycircle", "geoservices", "installation", "passkit", "sharedimagecache", "desktop", "mbuseragent", "swiftpm", "baseband", "coresimulator", "photoslegacyupgrade", "photosupgrade", "siritts", "ipod", "globalpreferences", "apmanalytics", "apmexperiment", "avatarcache", "byhost", "contextstoreagent", "mobilemeaccounts", "intentbuilderc", "loginwindow", "momc", "replayd", "sharedfilelistd", "clang", "audiocomponent", "csexattrcryptoservice", "livetranscriptionagent", "sandboxhelper", "statuskitagent", "betaenrollmentd", "contentlinkingd", "diagnosticextensionsd", "gamed", "heard", "homed", "itunescloudd", "lldb", "mds", "mediaanalysisd", "metrickitd", "mobiletimerd", "proactived", "ptpcamerad", "studentd", "talagent", "watchlistd", "apptranslocation", "xcrun", "ds_store", "caches", "crashreporter", "trash", "pearcleaner", "amsdatamigratortool", "arfilecache", "assistant", "chromium", "cloudkit", "webkit", "databases", "diagnostic", "cache", "gamekit", "homebrew", "logi", "microsoft", "mozilla", "sync", "google", "sentinel", "hexnode", "sentry", "tvappservices"]
|
||||||
|
|
||||||
|
|
||||||
|
// Function to load additional conditions from a GitHub JSON file
|
||||||
|
func loadConditionsFromGitHub() {
|
||||||
|
let url = URL(string: "https://api.github.com/repos/alienator88/Pearcleaner/contents/conditions.json")!
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
request.setValue("application/vnd.github.VERSION.raw", forHTTPHeaderField: "Accept")
|
||||||
|
request.setValue("2022-11-28", forHTTPHeaderField: "X-GitHub-Api-Version")
|
||||||
|
|
||||||
|
URLSession.shared.dataTask(with: request) { data, response, error in
|
||||||
|
if let data = data {
|
||||||
|
do {
|
||||||
|
// Assuming the JSON structure directly maps to an array of Condition
|
||||||
|
let jsonObject = try JSONSerialization.jsonObject(with: data, options: [])
|
||||||
|
if let conditionArray = jsonObject as? [[String: Any]] {
|
||||||
|
let jsonData = try JSONSerialization.data(withJSONObject: conditionArray, options: [])
|
||||||
|
let additionalConditions = try JSONDecoder().decode([Condition].self, from: jsonData)
|
||||||
|
|
||||||
|
if !additionalConditions.isEmpty {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
conditions += additionalConditions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
printOS("The data format is incorrect or empty for GitHub conditions processing.")
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
printOS("Failed to decode conditions JSON: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
printOS("Failed to fetch conditions data: \(error?.localizedDescription ?? "Unknown error")")
|
||||||
|
}
|
||||||
|
}.resume()
|
||||||
|
}
|
||||||
|
|
||||||
@@ -19,8 +19,6 @@ class Locations: ObservableObject {
|
|||||||
|
|
||||||
var apps: Category
|
var apps: Category
|
||||||
var reverse: Category
|
var reverse: Category
|
||||||
// var widgets: Category
|
|
||||||
// var plugins: Category
|
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
let (cacheDir, tempDir) = darwinCT()
|
let (cacheDir, tempDir) = darwinCT()
|
||||||
@@ -103,35 +101,5 @@ class Locations: ObservableObject {
|
|||||||
cacheDir,
|
cacheDir,
|
||||||
tempDir
|
tempDir
|
||||||
])
|
])
|
||||||
|
|
||||||
// self.widgets = Category(name: "Widgets", paths: [
|
|
||||||
// // User
|
|
||||||
// "~/Library/Widgets",
|
|
||||||
// // System
|
|
||||||
// "/Library/Widgets"
|
|
||||||
// ])
|
|
||||||
//
|
|
||||||
// self.plugins = Category(name: "Plugins", paths: [
|
|
||||||
// // User
|
|
||||||
// "~/Library/Contextual Menu Items",
|
|
||||||
// "~/Library/InputManagers",
|
|
||||||
// "~/Library/Internet Plug-Ins",
|
|
||||||
// "~/Library/Mail/Bundles",
|
|
||||||
// "~/Library/PreferencePanes",
|
|
||||||
// "~/Library/QuickLook",
|
|
||||||
// "~/Library/QuickTime",
|
|
||||||
// "~/Library/Screen Savers",
|
|
||||||
// "~/Library/Spotlight",
|
|
||||||
// // System
|
|
||||||
// "/Library/Contextual Menu Items",
|
|
||||||
// "/Library/InputManagers",
|
|
||||||
// "/Library/Internet Plug-Ins",
|
|
||||||
// "/Library/Mail/Bundles",
|
|
||||||
// "/Library/PreferencePanes",
|
|
||||||
// "/Library/QuickLook",
|
|
||||||
// "/Library/QuickTime",
|
|
||||||
// "/Library/Screen Savers",
|
|
||||||
// "/Library/Spotlight",
|
|
||||||
// ])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import SwiftUI
|
|||||||
|
|
||||||
// Get all apps from /Applications and ~/Applications
|
// Get all apps from /Applications and ~/Applications
|
||||||
func getSortedApps(paths: [String], appState: AppState) -> [AppInfo] {
|
func getSortedApps(paths: [String], appState: AppState) -> [AppInfo] {
|
||||||
@AppStorage("settings.general.instant") var instantSearch: Bool = true
|
|
||||||
let fileManager = FileManager.default
|
let fileManager = FileManager.default
|
||||||
var apps: [URL] = []
|
var apps: [URL] = []
|
||||||
|
|
||||||
@@ -42,11 +41,6 @@ func getSortedApps(paths: [String], appState: AppState) -> [AppInfo] {
|
|||||||
.compactMap { AppInfoFetcher.getAppInfo(atPath: $0) }
|
.compactMap { AppInfoFetcher.getAppInfo(atPath: $0) }
|
||||||
.sorted { $0.appName.replacingOccurrences(of: ".", with: "").lowercased() < $1.appName.replacingOccurrences(of: ".", with: "").lowercased() }
|
.sorted { $0.appName.replacingOccurrences(of: ".", with: "").lowercased() < $1.appName.replacingOccurrences(of: ".", with: "").lowercased() }
|
||||||
|
|
||||||
if instantSearch {
|
|
||||||
updateOnMain {
|
|
||||||
appState.instantTotal = Double(sortedApps.count)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return sortedApps
|
return sortedApps
|
||||||
}
|
}
|
||||||
@@ -116,15 +110,14 @@ func listAppSupportDirectories() -> [String] {
|
|||||||
|
|
||||||
|
|
||||||
// Load app paths on launch
|
// Load app paths on launch
|
||||||
func loadAllPaths(allApps: [AppInfo], appState: AppState, locations: Locations, reverseAddon: Bool = false, completion: @escaping () -> Void = {}) {
|
func reversePreloader(allApps: [AppInfo], appState: AppState, locations: Locations, reverseAddon: Bool = false, completion: @escaping () -> Void = {}) {
|
||||||
let dispatchGroup = DispatchGroup()
|
let dispatchGroup = DispatchGroup()
|
||||||
appState.appInfoStore.removeAll()
|
appState.appInfoStore.removeAll()
|
||||||
|
|
||||||
for app in allApps {
|
for app in allApps {
|
||||||
dispatchGroup.enter()
|
dispatchGroup.enter()
|
||||||
DispatchQueue.global(qos: .background).async {
|
DispatchQueue.global(qos: .background).async {
|
||||||
let pathFinder = AppPathFinder(appInfo: app, appState: appState, locations: locations, backgroundRun: true, reverseAddon: reverseAddon)
|
AppPathFinder(appInfo: app, appState: appState, locations: locations, backgroundRun: true, reverseAddon: reverseAddon).findPaths()
|
||||||
pathFinder.findPaths()
|
|
||||||
dispatchGroup.leave()
|
dispatchGroup.leave()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -134,28 +127,17 @@ func loadAllPaths(allApps: [AppInfo], appState: AppState, locations: Locations,
|
|||||||
func checkAllAppsProcessed(retryCount: Int = 0, maxRetry: Int = 120) {
|
func checkAllAppsProcessed(retryCount: Int = 0, maxRetry: Int = 120) {
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||||
if appState.appInfoStore.count == allApps.count {
|
if appState.appInfoStore.count == allApps.count {
|
||||||
if reverseAddon {
|
ReversePathsSearcher(appState: appState, locations: locations).reversePathsSearch() {
|
||||||
let reverse = ReversePathsSearcher(appState: appState, locations: locations)
|
|
||||||
reverse.reversePathsSearch() {
|
|
||||||
updateOnMain {
|
updateOnMain {
|
||||||
appState.showProgress = false
|
appState.showProgress = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// Reset progress values to 0
|
|
||||||
updateOnMain {
|
|
||||||
appState.instantProgress = 0
|
|
||||||
appState.instantTotal = 0
|
|
||||||
appState.showProgress = false
|
|
||||||
}
|
|
||||||
completion()
|
completion()
|
||||||
} else if retryCount < maxRetry {
|
} else if retryCount < maxRetry {
|
||||||
checkAllAppsProcessed(retryCount: retryCount + 1, maxRetry: maxRetry)
|
checkAllAppsProcessed(retryCount: retryCount + 1, maxRetry: maxRetry)
|
||||||
} else {
|
} else {
|
||||||
// Reset progress values to 0
|
// Reset progress values to 0
|
||||||
updateOnMain {
|
updateOnMain {
|
||||||
appState.instantProgress = 0
|
|
||||||
appState.instantTotal = 0
|
|
||||||
appState.showProgress = false
|
appState.showProgress = false
|
||||||
}
|
}
|
||||||
printOS("loadAllPaths - Retry limit reached. Not all paths were loaded.")
|
printOS("loadAllPaths - Retry limit reached. Not all paths were loaded.")
|
||||||
@@ -196,13 +178,13 @@ func showAppInFiles(appInfo: AppInfo, appState: AppState, locations: Locations,
|
|||||||
appState.showProgress = true
|
appState.showProgress = true
|
||||||
|
|
||||||
// Initialize the path finder and execute its search.
|
// Initialize the path finder and execute its search.
|
||||||
let pathFinder = AppPathFinder(appInfo: appInfo, appState: appState, locations: locations) {
|
AppPathFinder(appInfo: appInfo, appState: appState, locations: locations) {
|
||||||
updateOnMain {
|
updateOnMain {
|
||||||
// Update the progress indicator on the main thread once the search completes.
|
// Update the progress indicator on the main thread once the search completes.
|
||||||
appState.showProgress = false
|
appState.showProgress = false
|
||||||
}
|
}
|
||||||
}
|
}.findPaths()
|
||||||
pathFinder.findPaths()
|
|
||||||
appState.appInfo = appInfo
|
appState.appInfo = appInfo
|
||||||
|
|
||||||
// Animate the view change and popover display.
|
// Animate the view change and popover display.
|
||||||
|
|||||||
@@ -14,16 +14,17 @@ class ReversePathsSearcher {
|
|||||||
private let fileManager = FileManager.default
|
private let fileManager = FileManager.default
|
||||||
private var collection: [URL] = []
|
private var collection: [URL] = []
|
||||||
private var fileSize: [URL: Int64] = [:]
|
private var fileSize: [URL: Int64] = [:]
|
||||||
|
private var fileSizeLogical: [URL: Int64] = [:]
|
||||||
private var fileIcon: [URL: NSImage?] = [:]
|
private var fileIcon: [URL: NSImage?] = [:]
|
||||||
private var skipped: Set<String>
|
|
||||||
private let dispatchGroup = DispatchGroup()
|
private let dispatchGroup = DispatchGroup()
|
||||||
|
|
||||||
init(appState: AppState, locations: Locations) {
|
init(appState: AppState, locations: Locations) {
|
||||||
self.appState = appState
|
self.appState = appState
|
||||||
self.locations = locations
|
self.locations = locations
|
||||||
self.skipped = ["apple", "temporary", "btserver", "proapps", "scripteditor", "ilife", "livefsd", "siritoday", "addressbook", "animoji", "appstore", "askpermission", "callhistory", "clouddocs", "diskimages", "dock", "facetime", "fileprovider", "instruments", "knowledge", "mobilesync", "syncservices", "homeenergyd", "icloud", "icdd", "networkserviceproxy", "familycircle", "geoservices", "installation", "passkit", "sharedimagecache", "desktop", "mbuseragent", "swiftpm", "baseband", "coresimulator", "photoslegacyupgrade", "photosupgrade", "siritts", "ipod", "globalpreferences", "apmanalytics", "apmexperiment", "avatarcache", "byhost", "contextstoreagent", "mobilemeaccounts", "intentbuilderc", "loginwindow", "momc", "replayd", "sharedfilelistd", "clang", "audiocomponent", "csexattrcryptoservice", "livetranscriptionagent", "sandboxhelper", "statuskitagent", "betaenrollmentd", "contentlinkingd", "diagnosticextensionsd", "gamed", "heard", "homed", "itunescloudd", "lldb", "mds", "mediaanalysisd", "metrickitd", "mobiletimerd", "proactived", "ptpcamerad", "studentd", "talagent", "watchlistd", "apptranslocation", "xcrun", "ds_store", "caches", "crashreporter", "trash", "pearcleaner", "amsdatamigratortool", "arfilecache", "assistant", "chromium", "cloudkit", "webkit", "databases", "diagnostic", "cache", "gamekit", "homebrew", "logi", "microsoft", "mozilla", "sync", "google", "sentinel", "hexnode", "sentry", "tvappservices"]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
func reversePathsSearch(completion: @escaping () -> Void = {}) {
|
func reversePathsSearch(completion: @escaping () -> Void = {}) {
|
||||||
Task(priority: .high) {
|
Task(priority: .high) {
|
||||||
self.processLocations()
|
self.processLocations()
|
||||||
@@ -34,17 +35,19 @@ class ReversePathsSearcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func processLocations() {
|
private func processLocations() {
|
||||||
let allPaths = appState.appInfoStore.flatMap { $0.files.map { $0.path.pearFormat() } }
|
let allPaths = appState.appInfoStore.flatMap { $0.fileSize.keys.map { $0.path.pearFormat() } }
|
||||||
let allNames = appState.appInfoStore.map { $0.appName.pearFormat() }
|
let allNames = appState.appInfoStore.map { $0.appName.pearFormat() }
|
||||||
|
|
||||||
for location in locations.reverse.paths where fileManager.fileExists(atPath: location) {
|
for location in locations.reverse.paths where fileManager.fileExists(atPath: location) {
|
||||||
dispatchGroup.enter()
|
dispatchGroup.enter()
|
||||||
processLocation(location, allPaths: allPaths, allNames: allNames)
|
processLocation(location, allPaths: allPaths, allNames: allNames)
|
||||||
dispatchGroup.leave()
|
dispatchGroup.leave()
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func processLocation(_ location: String, allPaths: [String], allNames: [String]) {
|
private func processLocation(_ location: String, allPaths: [String], allNames: [String]) {
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let contents = try fileManager.contentsOfDirectory(atPath: location)
|
let contents = try fileManager.contentsOfDirectory(atPath: location)
|
||||||
contents.forEach { itemName in
|
contents.forEach { itemName in
|
||||||
@@ -61,8 +64,7 @@ class ReversePathsSearcher {
|
|||||||
let itemPath = itemURL.path.pearFormat()
|
let itemPath = itemURL.path.pearFormat()
|
||||||
let itemLastPathComponent = itemURL.lastPathComponent.pearFormat()
|
let itemLastPathComponent = itemURL.lastPathComponent.pearFormat()
|
||||||
|
|
||||||
guard !skipped.contains(where: { formattedItemName.contains($0) }),
|
guard !skipReverse.contains(where: { formattedItemName.contains($0) }),
|
||||||
// !allPaths.contains(itemPath),
|
|
||||||
!allPaths.contains(where: { $0 == itemPath || $0.hasSuffix("/\(itemLastPathComponent)") }),
|
!allPaths.contains(where: { $0 == itemPath || $0.hasSuffix("/\(itemLastPathComponent)") }),
|
||||||
!allNames.contains(formattedItemName),
|
!allNames.contains(formattedItemName),
|
||||||
isSupportedFileType(at: itemURL.path) else {
|
isSupportedFileType(at: itemURL.path) else {
|
||||||
@@ -74,7 +76,9 @@ class ReversePathsSearcher {
|
|||||||
|
|
||||||
private func calculateFileDetails() {
|
private func calculateFileDetails() {
|
||||||
collection.forEach { path in
|
collection.forEach { path in
|
||||||
fileSize[path] = totalSizeOnDisk(for: path)
|
let size = totalSizeOnDisk(for: path)
|
||||||
|
fileSize[path] = size.real
|
||||||
|
fileSizeLogical[path] = size.logical
|
||||||
fileIcon[path] = getIconForFileOrFolderNS(atPath: path)
|
fileIcon[path] = getIconForFileOrFolderNS(atPath: path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -83,6 +87,7 @@ class ReversePathsSearcher {
|
|||||||
dispatchGroup.notify(queue: .main) {
|
dispatchGroup.notify(queue: .main) {
|
||||||
var updatedZombieFile = ZombieFile.empty
|
var updatedZombieFile = ZombieFile.empty
|
||||||
updatedZombieFile.fileSize = self.fileSize
|
updatedZombieFile.fileSize = self.fileSize
|
||||||
|
updatedZombieFile.fileSizeLogical = self.fileSizeLogical
|
||||||
updatedZombieFile.fileIcon = self.fileIcon
|
updatedZombieFile.fileIcon = self.fileIcon
|
||||||
self.appState.zombieFile = updatedZombieFile
|
self.appState.zombieFile = updatedZombieFile
|
||||||
self.appState.showProgress = false
|
self.appState.showProgress = false
|
||||||
|
|||||||
@@ -441,56 +441,11 @@ struct SimpleSearchStyle: TextFieldStyle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
.disabled(appState.instantProgress != 0 && appState.instantProgress != appState.instantTotal)
|
|
||||||
.padding(6)
|
.padding(6)
|
||||||
.overlay(
|
.overlay(
|
||||||
Group {
|
|
||||||
GeometryReader { geometry in
|
|
||||||
ZStack(alignment: .leading) {
|
|
||||||
|
|
||||||
|
|
||||||
if appState.instantProgress != 0 && appState.instantProgress != appState.instantTotal {
|
|
||||||
let totalWidth = geometry.size.width
|
|
||||||
let progressFraction = CGFloat(appState.instantProgress / appState.instantTotal)
|
|
||||||
let progressWidth = totalWidth * progressFraction
|
|
||||||
|
|
||||||
RoundedRectangle(cornerRadius: 6)
|
|
||||||
.fill(Color.accentColor)
|
|
||||||
.padding(4)
|
|
||||||
.frame(width: progressWidth)
|
|
||||||
.animation(.linear, value: progressWidth)
|
|
||||||
.allowsHitTesting(false)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
RoundedRectangle(cornerRadius: 6)
|
RoundedRectangle(cornerRadius: 6)
|
||||||
.strokeBorder(Color("mode").opacity(0.2), lineWidth: 1)
|
.strokeBorder(Color("mode").opacity(0.2), lineWidth: 1)
|
||||||
.allowsHitTesting(false)
|
.allowsHitTesting(false)
|
||||||
}
|
|
||||||
}
|
|
||||||
// ZStack {
|
|
||||||
// RoundedRectangle(cornerRadius: 6)
|
|
||||||
// .strokeBorder(Color("mode").opacity(0.2), lineWidth: 1)
|
|
||||||
// .allowsHitTesting(false)
|
|
||||||
//
|
|
||||||
// if appState.instantTotal > 0 {
|
|
||||||
// let progressWidth = CGFloat(appState.instantProgress / appState.instantTotal)
|
|
||||||
// * NSScreen.main.bounds.width // Assuming full width for simplicity; adjust as needed
|
|
||||||
// RoundedRectangle(cornerRadius: 6)
|
|
||||||
// .fill(Color.accentColor.opacity(0.5)) // Adjust color and opacity as needed
|
|
||||||
// .frame(width: progressWidth)
|
|
||||||
// .animation(.linear, value: progressWidth)
|
|
||||||
// .allowsHitTesting(false)
|
|
||||||
// }
|
|
||||||
//// if appState.instantProgress != 0 && appState.instantProgress != appState.instantTotal {
|
|
||||||
//// ProgressView("", value: appState.instantProgress, total: appState.instantTotal)
|
|
||||||
//// .progressViewStyle(.linear)
|
|
||||||
//// .padding()
|
|
||||||
//// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
.onHover { hovering in
|
.onHover { hovering in
|
||||||
withAnimation(Animation.easeInOut(duration: 0.15)) {
|
withAnimation(Animation.easeInOut(duration: 0.15)) {
|
||||||
|
|||||||
@@ -28,10 +28,12 @@ class ThemeSettings: ObservableObject {
|
|||||||
if let components = UserDefaults.standard.object(forKey: colorKey) as? [CGFloat], components.count >= 4 {
|
if let components = UserDefaults.standard.object(forKey: colorKey) as? [CGFloat], components.count >= 4 {
|
||||||
themeColor = Color(.sRGB, red: components[0], green: components[1], blue: components[2], opacity: components[3])
|
themeColor = Color(.sRGB, red: components[0], green: components[1], blue: components[2], opacity: components[3])
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
themeColor = Color.clear
|
themeColor = Color.clear
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func saveThemeColor() {
|
func saveThemeColor() {
|
||||||
let nsColor = NSColor(themeColor)
|
let nsColor = NSColor(themeColor)
|
||||||
if let components = nsColor.cgColor.components {
|
if let components = nsColor.cgColor.components {
|
||||||
@@ -43,17 +45,61 @@ class ThemeSettings: ObservableObject {
|
|||||||
themeColor = dark ? Color(.sRGB, red: 0.149, green: 0.149, blue: 0.149, opacity: 1) : Color(.sRGB, red: 1.0, green: 1.0, blue: 1.0, opacity: 1)
|
themeColor = dark ? Color(.sRGB, red: 0.149, green: 0.149, blue: 0.149, opacity: 1) : Color(.sRGB, red: 1.0, green: 1.0, blue: 1.0, opacity: 1)
|
||||||
saveThemeColor()
|
saveThemeColor()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setPreset(preset: String, colorScheme: DisplayMode) {
|
||||||
|
themeColor = getColorForPreset(preset: preset, colorScheme: colorScheme)
|
||||||
|
saveThemeColor()
|
||||||
|
}
|
||||||
|
|
||||||
|
func getColorForPreset(preset: String, colorScheme: DisplayMode) -> Color {
|
||||||
|
let slate = colorScheme.colorScheme == .light ? Color(.sRGB, red: 0.499549, green: 0.545169, blue: 0.682028, opacity: 1) : Color(.sRGB, red: 0.188143, green: 0.208556, blue: 0.262679, opacity: 1)
|
||||||
|
let solarized = colorScheme.colorScheme == .light ? Color(.sRGB, red: 0.554372, green: 0.6557, blue: 0.734336, opacity: 1) : Color(.sRGB, red: 0.117257, green: 0.22506, blue: 0.249171, opacity: 1)
|
||||||
|
let dracula = colorScheme.colorScheme == .light ? Color(.sRGB, red: 0.567094, green: 0.562125, blue: 0.81285, opacity: 1) : Color(.sRGB, red: 0.268614, green: 0.264737, blue: 0.383503, opacity: 1)
|
||||||
|
|
||||||
|
switch preset {
|
||||||
|
case "slate":
|
||||||
|
return slate
|
||||||
|
case "solarized":
|
||||||
|
return solarized
|
||||||
|
case "dracula":
|
||||||
|
return dracula
|
||||||
|
default:
|
||||||
|
return themeColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Color {
|
extension Color {
|
||||||
|
/// Darkens the color by a percentage.
|
||||||
func darker(by percentage: CGFloat = 30.0) -> Color {
|
func darker(by percentage: CGFloat = 30.0) -> Color {
|
||||||
var hsb = (hue: CGFloat(0), saturation: CGFloat(0), brightness: CGFloat(0), alpha: CGFloat(0))
|
var hsb = (hue: CGFloat(0), saturation: CGFloat(0), brightness: CGFloat(0), alpha: CGFloat(0))
|
||||||
NSColor(self).getHue(&hsb.hue, saturation: &hsb.saturation, brightness: &hsb.brightness, alpha: &hsb.alpha)
|
NSColor(self).getHue(&hsb.hue, saturation: &hsb.saturation, brightness: &hsb.brightness, alpha: &hsb.alpha)
|
||||||
return Color(hue: hsb.hue, saturation: hsb.saturation, brightness: max(hsb.brightness - percentage/100, 0), opacity: hsb.alpha)
|
return Color(hue: hsb.hue, saturation: hsb.saturation, brightness: max(hsb.brightness - percentage/100, 0), opacity: hsb.alpha)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Lightens the color by a percentage.
|
||||||
func lighter(by percentage: CGFloat = 30.0) -> Color {
|
func lighter(by percentage: CGFloat = 30.0) -> Color {
|
||||||
var hsb = (hue: CGFloat(0), saturation: CGFloat(0), brightness: CGFloat(0), alpha: CGFloat(0))
|
var hsb = (hue: CGFloat(0), saturation: CGFloat(0), brightness: CGFloat(0), alpha: CGFloat(0))
|
||||||
NSColor(self).getHue(&hsb.hue, saturation: &hsb.saturation, brightness: &hsb.brightness, alpha: &hsb.alpha)
|
NSColor(self).getHue(&hsb.hue, saturation: &hsb.saturation, brightness: &hsb.brightness, alpha: &hsb.alpha)
|
||||||
return Color(hue: hsb.hue, saturation: hsb.saturation, brightness: min(hsb.brightness + percentage / 100, 1.0), opacity: hsb.alpha)
|
return Color(hue: hsb.hue, saturation: hsb.saturation, brightness: min(hsb.brightness + percentage / 100, 1.0), opacity: hsb.alpha)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension UserDefaults {
|
||||||
|
func color(forKey key: String) -> Color? {
|
||||||
|
guard let components = array(forKey: key) as? [CGFloat], components.count == 4 else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return Color(.sRGB, red: components[0], green: components[1], blue: components[2], opacity: components[3])
|
||||||
|
}
|
||||||
|
|
||||||
|
func setColor(_ color: Color, forKey key: String) {
|
||||||
|
let nsColor = NSColor(color)
|
||||||
|
if let components = nsColor.cgColor.components, components.count == 4 {
|
||||||
|
set(components, forKey: key)
|
||||||
|
} else {
|
||||||
|
removeObject(forKey: key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -174,20 +174,13 @@ func isCurrentUserAdmin(completion: @escaping (Bool) -> Void) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if appearance is dark mode
|
// Check if appearance is dark mode
|
||||||
func isDarkModeEnabled() -> Bool {
|
func isDarkMode() -> Bool {
|
||||||
let process = Process()
|
return NSApp.effectiveAppearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua
|
||||||
process.executableURL = URL(fileURLWithPath: "/bin/zsh")
|
|
||||||
process.arguments = ["-c", "defaults read -g AppleInterfaceStyle"]
|
|
||||||
let pipe = Pipe()
|
|
||||||
process.standardOutput = pipe
|
|
||||||
process.standardError = pipe
|
|
||||||
try? process.run()
|
|
||||||
let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
|
||||||
if !data.isEmpty{
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set app color mode
|
||||||
|
func setAppearance(mode: NSAppearance.Name) {
|
||||||
|
NSApp.appearance = NSAppearance(named: mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if Pearcleaner has any windows open
|
// Check if Pearcleaner has any windows open
|
||||||
@@ -216,6 +209,13 @@ func findAndShowWindows(named titles: [String]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Copy to clipboard
|
||||||
|
func copyToClipboard(text: String) {
|
||||||
|
let pasteboard = NSPasteboard.general
|
||||||
|
pasteboard.clearContents()
|
||||||
|
pasteboard.setString(text, forType: .string)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Check if appearance is dark mode
|
// Check if appearance is dark mode
|
||||||
//func getCasks() -> [String] {
|
//func getCasks() -> [String] {
|
||||||
@@ -458,86 +458,73 @@ func printOS(_ items: Any..., separator: String = " ", terminator: String = "\n"
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Get size of files
|
||||||
//func totalSizeOnDisk2(for paths: [URL]) -> Int64 {
|
func totalSizeOnDisk(for paths: [URL]) -> (real: Int64, logical: Int64) {
|
||||||
// var totalSize = 0
|
|
||||||
//
|
|
||||||
// let process = Process()
|
|
||||||
// process.launchPath = "/usr/bin/du"
|
|
||||||
// process.arguments = ["-sk"] + paths.map { (url: URL) -> String in
|
|
||||||
// return url.path
|
|
||||||
// }
|
|
||||||
// let pipe = Pipe()
|
|
||||||
// process.standardOutput = pipe
|
|
||||||
// process.standardError = pipe//FileHandle.nullDevice
|
|
||||||
//
|
|
||||||
// try? process.run()
|
|
||||||
// process.waitUntilExit()
|
|
||||||
//
|
|
||||||
// let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
|
||||||
// if let output = String(data: data, encoding: .utf8) {
|
|
||||||
// let lines = output.components(separatedBy: .newlines)
|
|
||||||
// for line in lines {
|
|
||||||
// let components = line.components(separatedBy: "\t")
|
|
||||||
// if let sizeString = components.first, let size = Int(sizeString) {
|
|
||||||
// totalSize += size
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return Int64(totalSize) * 1024 // Convert the size from kilobytes to bytes
|
|
||||||
//}
|
|
||||||
|
|
||||||
|
|
||||||
// Get total size of folders and files using DU cli command
|
|
||||||
func totalSizeOnDisk(for paths: [URL]) -> Int64 {
|
|
||||||
let fileManager = FileManager.default
|
let fileManager = FileManager.default
|
||||||
var totalSize: Int64 = 0
|
var totalAllocatedSize: Int64 = 0
|
||||||
|
var totalFileSize: Int64 = 0
|
||||||
|
|
||||||
for url in paths {
|
for url in paths {
|
||||||
var isDirectory: ObjCBool = false
|
var isDirectory: ObjCBool = false
|
||||||
if fileManager.fileExists(atPath: url.path, isDirectory: &isDirectory) {
|
if fileManager.fileExists(atPath: url.path, isDirectory: &isDirectory) {
|
||||||
|
let keys: [URLResourceKey] = [.totalFileAllocatedSizeKey, .fileSizeKey]
|
||||||
if isDirectory.boolValue {
|
if isDirectory.boolValue {
|
||||||
// It's a directory, recurse into it
|
// It's a directory, recurse into it
|
||||||
if let enumerator = fileManager.enumerator(at: url, includingPropertiesForKeys: [.totalFileAllocatedSizeKey], errorHandler: nil) {
|
if let enumerator = fileManager.enumerator(at: url, includingPropertiesForKeys: keys, errorHandler: nil) {
|
||||||
for case let fileURL as URL in enumerator {
|
for case let fileURL as URL in enumerator {
|
||||||
do {
|
do {
|
||||||
let fileAttributes = try fileURL.resourceValues(forKeys: [.totalFileAllocatedSizeKey])
|
let fileAttributes = try fileURL.resourceValues(forKeys: Set(keys))
|
||||||
if let fileSize = fileAttributes.totalFileAllocatedSize {
|
if let allocatedSize = fileAttributes.totalFileAllocatedSize {
|
||||||
totalSize += Int64(fileSize)
|
totalAllocatedSize += Int64(allocatedSize)
|
||||||
|
}
|
||||||
|
if let fileSize = fileAttributes.fileSize {
|
||||||
|
totalFileSize += Int64(fileSize)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
printOS("Error getting file size for \(fileURL): \(error)")
|
print("Error getting file attributes for \(fileURL): \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// It's a file
|
// It's a file
|
||||||
do {
|
do {
|
||||||
let fileAttributes = try url.resourceValues(forKeys: [.totalFileAllocatedSizeKey])
|
let fileAttributes = try url.resourceValues(forKeys: Set(keys))
|
||||||
if let fileSize = fileAttributes.totalFileAllocatedSize {
|
if let allocatedSize = fileAttributes.totalFileAllocatedSize {
|
||||||
totalSize += Int64(fileSize)
|
totalAllocatedSize += Int64(allocatedSize)
|
||||||
|
}
|
||||||
|
if let fileSize = fileAttributes.fileSize {
|
||||||
|
totalFileSize += Int64(fileSize)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
printOS("Error getting file size for \(url): \(error)")
|
print("Error getting file attributes for \(url): \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return totalSize
|
return (real: totalAllocatedSize, logical: totalFileSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
func totalSizeOnDisk(for path: URL) -> Int64 {
|
|
||||||
|
|
||||||
|
func totalSizeOnDisk(for path: URL) -> (real: Int64, logical: Int64) {
|
||||||
return totalSizeOnDisk(for: [path])
|
return totalSizeOnDisk(for: [path])
|
||||||
}
|
}
|
||||||
|
|
||||||
// ByteFormatter
|
// ByteFormatter
|
||||||
func formatByte(size: Int64) -> String {
|
func formatByte(size: Int64) -> (human: String, byte: String) {
|
||||||
let byteCountFormatter = ByteCountFormatter()
|
let byteCountFormatter = ByteCountFormatter()
|
||||||
byteCountFormatter.countStyle = .file
|
byteCountFormatter.countStyle = .file
|
||||||
byteCountFormatter.allowedUnits = [.useAll]
|
byteCountFormatter.allowedUnits = [.useAll]
|
||||||
return byteCountFormatter.string(fromByteCount: size)
|
let human = byteCountFormatter.string(fromByteCount: size)
|
||||||
|
|
||||||
|
let numberformatter = NumberFormatter()
|
||||||
|
numberformatter.numberStyle = .decimal
|
||||||
|
let formattedNumber = numberformatter.string(from: NSNumber(value: size)) ?? "\(size)"
|
||||||
|
let byte = "\(formattedNumber)"
|
||||||
|
|
||||||
|
return (human: human, byte: byte)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only process supported files
|
// Only process supported files
|
||||||
@@ -603,12 +590,9 @@ func uninstallPearcleaner(appState: AppState, locations: Locations) {
|
|||||||
|
|
||||||
// Get app info for Pearcleaner
|
// Get app info for Pearcleaner
|
||||||
let appInfo = AppInfoFetcher.getAppInfo(atPath: Bundle.main.bundleURL)
|
let appInfo = AppInfoFetcher.getAppInfo(atPath: Bundle.main.bundleURL)
|
||||||
// appState.appInfo = appInfo!
|
|
||||||
|
|
||||||
// Find application files for Pearcleaner
|
// Find application files for Pearcleaner
|
||||||
let pathFinder = AppPathFinder(appInfo: appInfo!, appState: appState, locations: locations)
|
AppPathFinder(appInfo: appInfo!, appState: appState, locations: locations).findPaths()
|
||||||
pathFinder.findPaths()
|
|
||||||
// findPathsForApp(appInfo: appInfo!,appState: appState, locations: locations)
|
|
||||||
|
|
||||||
// Kill Pearcleaner and tell Finder to trash the files
|
// Kill Pearcleaner and tell Finder to trash the files
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ struct PearcleanerApp: App {
|
|||||||
@AppStorage("displayMode") var displayMode: DisplayMode = .system
|
@AppStorage("displayMode") var displayMode: DisplayMode = .system
|
||||||
@AppStorage("settings.general.mini") private var mini: Bool = false
|
@AppStorage("settings.general.mini") private var mini: Bool = false
|
||||||
@AppStorage("settings.general.miniview") private var miniView: Bool = true
|
@AppStorage("settings.general.miniview") private var miniView: Bool = true
|
||||||
@AppStorage("settings.general.instant") private var instantSearch: Bool = true
|
|
||||||
@AppStorage("settings.general.features") private var features: String = ""
|
@AppStorage("settings.general.features") private var features: String = ""
|
||||||
@AppStorage("settings.general.brew") private var brew: Bool = false
|
@AppStorage("settings.general.brew") private var brew: Bool = false
|
||||||
@AppStorage("settings.menubar.enabled") private var menubarEnabled: Bool = false
|
@AppStorage("settings.menubar.enabled") private var menubarEnabled: Bool = false
|
||||||
@@ -102,13 +101,7 @@ struct PearcleanerApp: App {
|
|||||||
let sortedApps = getSortedApps(paths: fsm.folderPaths, appState: appState)
|
let sortedApps = getSortedApps(paths: fsm.folderPaths, appState: appState)
|
||||||
appState.sortedApps = sortedApps
|
appState.sortedApps = sortedApps
|
||||||
|
|
||||||
|
// Enable menubar item
|
||||||
// Find all app paths/information on load if instantSearch is enabled
|
|
||||||
if instantSearch {
|
|
||||||
loadAllPaths(allApps: sortedApps, appState: appState, locations: locations)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if menubarEnabled {
|
if menubarEnabled {
|
||||||
MenuBarExtraManager.shared.addMenuBarExtra(withView: {
|
MenuBarExtraManager.shared.addMenuBarExtra(withView: {
|
||||||
MenuBarMiniAppView(search: $search, showPopover: $showPopover)
|
MenuBarMiniAppView(search: $search, showPopover: $showPopover)
|
||||||
@@ -140,6 +133,9 @@ struct PearcleanerApp: App {
|
|||||||
hasLaunched = true
|
hasLaunched = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load extra conditions from GitHub
|
||||||
|
loadConditionsFromGitHub()
|
||||||
|
|
||||||
|
|
||||||
// TIMERS ////////////////////////////////////////////////////////////////////////////////////
|
// TIMERS ////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
@@ -183,6 +179,7 @@ struct PearcleanerApp: App {
|
|||||||
|
|
||||||
|
|
||||||
class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
|
class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
|
||||||
|
var observer: NSObjectProtocol?
|
||||||
|
|
||||||
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||||
let menubarEnabled = UserDefaults.standard.bool(forKey: "settings.menubar.enabled")
|
let menubarEnabled = UserDefaults.standard.bool(forKey: "settings.menubar.enabled")
|
||||||
@@ -191,11 +188,49 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
|
|||||||
|
|
||||||
func applicationDidFinishLaunching(_ notification: Notification) {
|
func applicationDidFinishLaunching(_ notification: Notification) {
|
||||||
let menubarEnabled = UserDefaults.standard.bool(forKey: "settings.menubar.enabled")
|
let menubarEnabled = UserDefaults.standard.bool(forKey: "settings.menubar.enabled")
|
||||||
|
|
||||||
|
if UserDefaults.standard.object(forKey: "themeColor") == nil {
|
||||||
|
self.appearanceChanged()
|
||||||
|
}
|
||||||
|
|
||||||
if menubarEnabled {
|
if menubarEnabled {
|
||||||
findAndHideWindows(named: ["Pearcleaner"])
|
findAndHideWindows(named: ["Pearcleaner"])
|
||||||
NSApplication.shared.setActivationPolicy(.accessory)
|
NSApplication.shared.setActivationPolicy(.accessory)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start observing the appearance change
|
||||||
|
observer = DistributedNotificationCenter.default().addObserver(forName: NSNotification.Name(rawValue: "AppleInterfaceThemeChangedNotification"), object: nil, queue: OperationQueue.main) { [weak self] _ in
|
||||||
|
let themeMode = UserDefaults.standard.string(forKey: "settings.general.selectedTheme")
|
||||||
|
if themeMode == "Auto" {
|
||||||
|
self?.appearanceChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func appearanceChanged() {
|
||||||
|
let dm = UserDefaults.standard.integer(forKey: "displayMode")
|
||||||
|
var displayMode = DisplayMode(rawValue: dm)
|
||||||
|
let dark = isDarkMode()
|
||||||
|
if dark {
|
||||||
|
displayMode?.colorScheme = .dark
|
||||||
|
} else {
|
||||||
|
displayMode?.colorScheme = .light
|
||||||
|
}
|
||||||
|
|
||||||
|
UserDefaults.standard.set(displayMode?.rawValue, forKey: "displayMode")
|
||||||
|
|
||||||
|
ThemeSettings.shared.themeColor = isDarkMode() ? Color(.sRGB, red: 0.149, green: 0.149, blue: 0.149, opacity: 1) : Color(.sRGB, red: 1.0, green: 1.0, blue: 1.0, opacity: 1)
|
||||||
|
ThemeSettings.shared.saveThemeColor()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func applicationWillTerminate(_ notification: Notification) {
|
||||||
|
// Stop observing the appearance change
|
||||||
|
if let observer = observer {
|
||||||
|
DistributedNotificationCenter.default().removeObserver(observer)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -220,4 +255,3 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,16 +23,15 @@ struct GeneralSettingsTab: View {
|
|||||||
@AppStorage("displayMode") var displayMode: DisplayMode = .system
|
@AppStorage("displayMode") var displayMode: DisplayMode = .system
|
||||||
@AppStorage("settings.sentinel.enable") private var sentinel: Bool = false
|
@AppStorage("settings.sentinel.enable") private var sentinel: Bool = false
|
||||||
@AppStorage("settings.general.brew") private var brew: Bool = false
|
@AppStorage("settings.general.brew") private var brew: Bool = false
|
||||||
@AppStorage("settings.general.instant") private var instantSearch: Bool = true
|
|
||||||
@AppStorage("settings.general.selectedTheme") var selectedTheme: String = "Auto"
|
|
||||||
@AppStorage("settings.general.selectedSort") var selectedSortAlpha: Bool = true
|
@AppStorage("settings.general.selectedSort") var selectedSortAlpha: Bool = true
|
||||||
// @State private var finderExtensionEnabled: Bool = false
|
@AppStorage("settings.general.sizeType") var sizeType: String = "Real"
|
||||||
@State private var diskStatus: Bool = false
|
@State private var diskStatus: Bool = false
|
||||||
@State private var accessStatus: Bool = false
|
@State private var accessStatus: Bool = false
|
||||||
private let themes = ["Auto", "Dark", "Light"]
|
|
||||||
@Binding var showPopover: Bool
|
@Binding var showPopover: Bool
|
||||||
@Binding var search: String
|
@Binding var search: String
|
||||||
|
|
||||||
|
@State var selectedIndex: Int?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
VStack {
|
VStack {
|
||||||
@@ -44,30 +43,6 @@ struct GeneralSettingsTab: View {
|
|||||||
.padding(.leading)
|
.padding(.leading)
|
||||||
|
|
||||||
|
|
||||||
HStack(spacing: 0) {
|
|
||||||
Image(systemName: instantSearch ? "bolt" : "bolt.slash")
|
|
||||||
.resizable()
|
|
||||||
.scaledToFit()
|
|
||||||
.frame(width: 20, height: 20)
|
|
||||||
.padding(.trailing)
|
|
||||||
.foregroundStyle(Color("mode").opacity(0.5))
|
|
||||||
VStack(alignment: .leading, spacing: 5) {
|
|
||||||
Text("\(instantSearch ? "Instant search is enabled" : "Instant search is disabled")")
|
|
||||||
.font(.callout)
|
|
||||||
.foregroundStyle(Color("mode").opacity(0.5))
|
|
||||||
}
|
|
||||||
|
|
||||||
InfoButton(text: "When instant search is enabled, all application files are gathered and cached for later use on startup instead of on each app click. There might be a slight delay of a few seconds when launching Pearcleaner depending on the amount of apps you have installed.", color: nil, label: "")
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
Toggle(isOn: $instantSearch, label: {
|
|
||||||
})
|
|
||||||
.toggleStyle(.switch)
|
|
||||||
}
|
|
||||||
.padding(5)
|
|
||||||
.padding(.leading)
|
|
||||||
|
|
||||||
|
|
||||||
HStack(spacing: 0) {
|
HStack(spacing: 0) {
|
||||||
Image(systemName: brew ? "mug.fill" : "mug")
|
Image(systemName: brew ? "mug.fill" : "mug")
|
||||||
.resizable()
|
.resizable()
|
||||||
@@ -104,14 +79,88 @@ struct GeneralSettingsTab: View {
|
|||||||
.font(.callout)
|
.font(.callout)
|
||||||
.foregroundStyle(Color("mode").opacity(0.5))
|
.foregroundStyle(Color("mode").opacity(0.5))
|
||||||
}
|
}
|
||||||
InfoButton(text: "When searching for app files or leftover files, the list will be sorted based on this choice", color: nil, label: "")
|
InfoButton(text: "When searching for app files or leftover files, the list will be sorted either alphabetically or by size(large to small)", color: nil, label: "")
|
||||||
Spacer()
|
Spacer()
|
||||||
Picker("", selection: $selectedSortAlpha) {
|
SegmentedPicker(
|
||||||
Text("Alphabetical").tag(true)
|
["Alpha", "Size"],
|
||||||
Text("Size").tag(false)
|
selectedIndex: Binding(
|
||||||
|
get: { selectedSortAlpha ? 0 : 1 },
|
||||||
|
set: { newIndex in
|
||||||
|
withAnimation(.easeInOut(duration: 0.3)) {
|
||||||
|
selectedSortAlpha = (newIndex == 0)
|
||||||
}
|
}
|
||||||
.pickerStyle(SegmentedPickerStyle())
|
}),
|
||||||
.frame(width: 200)
|
selectionAlignment: .bottom,
|
||||||
|
content: { item, isSelected in
|
||||||
|
Text(item)
|
||||||
|
.font(.callout)
|
||||||
|
.foregroundColor(isSelected ? Color("mode") : Color("mode").opacity(0.5))
|
||||||
|
.padding(.horizontal)
|
||||||
|
.padding(.bottom, 5)
|
||||||
|
.frame(width: 75)
|
||||||
|
|
||||||
|
},
|
||||||
|
selection: {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
Spacer()
|
||||||
|
Color("pear").frame(height: 1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
.padding(5)
|
||||||
|
.padding(.leading)
|
||||||
|
|
||||||
|
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Image(systemName: "plus.forwardslash.minus")
|
||||||
|
.resizable()
|
||||||
|
.scaledToFit()
|
||||||
|
.frame(width: 20, height: 20)
|
||||||
|
.padding(.trailing)
|
||||||
|
.foregroundStyle(Color("mode").opacity(0.5))
|
||||||
|
VStack(alignment: .leading, spacing: 5) {
|
||||||
|
Text("File size display")
|
||||||
|
.font(.callout)
|
||||||
|
.foregroundStyle(Color("mode").opacity(0.5))
|
||||||
|
}
|
||||||
|
InfoButton(text: "Real size type will show how much actual allocated space the file has on disk. Logical type shows the binary size. The filesystem can compress and deduplicate sectors on disk, so real size is sometimes smaller(or bigger) than logical size. Finder size is similar to if you right click > Get Info on a file in Finder, which will show both the logical and real sizes together.", color: nil, label: "")
|
||||||
|
Spacer()
|
||||||
|
SegmentedPicker(
|
||||||
|
["Real", "Logical", "Finder"],
|
||||||
|
selectedIndex: Binding(
|
||||||
|
get: {
|
||||||
|
switch sizeType {
|
||||||
|
case "Real": return 0
|
||||||
|
case "Logical": return 1
|
||||||
|
case "Finder": return 2
|
||||||
|
default: return 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
set: { newIndex in
|
||||||
|
withAnimation(.easeInOut(duration: 0.3)) {
|
||||||
|
switch newIndex {
|
||||||
|
case 0: sizeType = "Real"
|
||||||
|
case 1: sizeType = "Logical"
|
||||||
|
case 2: sizeType = "Finder"
|
||||||
|
default: sizeType = "Real"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
selectionAlignment: .bottom,
|
||||||
|
content: { item, isSelected in
|
||||||
|
Text(item)
|
||||||
|
.font(.callout)
|
||||||
|
.foregroundColor(isSelected ? Color("mode") : Color("mode").opacity(0.5) )
|
||||||
|
.padding(.horizontal)
|
||||||
|
.padding(.bottom, 5)
|
||||||
|
.frame(width: 75)
|
||||||
|
},
|
||||||
|
selection: {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
Spacer()
|
||||||
|
Color("pear").frame(height: 1)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
.padding(5)
|
.padding(5)
|
||||||
.padding(.leading)
|
.padding(.leading)
|
||||||
@@ -275,7 +324,7 @@ struct GeneralSettingsTab: View {
|
|||||||
|
|
||||||
}
|
}
|
||||||
.padding(20)
|
.padding(20)
|
||||||
.frame(width: 500, height: 560)
|
.frame(width: 500, height: 600)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,18 +31,9 @@ struct InterfaceSettingsTab: View {
|
|||||||
@Binding var showPopover: Bool
|
@Binding var showPopover: Bool
|
||||||
@Binding var search: String
|
@Binding var search: String
|
||||||
let icons = ["externaldrive", "trash", "folder", "pear-1", "pear-1.5", "pear-2", "pear-3", "pear-4"]
|
let icons = ["externaldrive", "trash", "folder", "pear-1", "pear-1.5", "pear-2", "pear-3", "pear-4"]
|
||||||
private let themes = ["Auto", "Dark", "Light"]
|
|
||||||
|
|
||||||
// @State private var slate = Color(.sRGB, red: 0.188143, green: 0.208556, blue: 0.262679, opacity: 1)
|
|
||||||
// @State private var solarized = Color(.sRGB, red: 0.117257, green: 0.22506, blue: 0.249171, opacity: 1)
|
|
||||||
// @State private var dracula = Color(.sRGB, red: 0.268614, green: 0.264737, blue: 0.383503, opacity: 1)
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
|
||||||
let slate = displayMode.colorScheme == .light ? Color(.sRGB, red: 0.499549, green: 0.545169, blue: 0.682028, opacity: 1) : Color(.sRGB, red: 0.188143, green: 0.208556, blue: 0.262679, opacity: 1)
|
|
||||||
let solarized = displayMode.colorScheme == .light ? Color(.sRGB, red: 0.554372, green: 0.6557, blue: 0.734336, opacity: 1) : Color(.sRGB, red: 0.117257, green: 0.22506, blue: 0.249171, opacity: 1)
|
|
||||||
let dracula = displayMode.colorScheme == .light ? Color(.sRGB, red: 0.567094, green: 0.562125, blue: 0.81285, opacity: 1) : Color(.sRGB, red: 0.268614, green: 0.264737, blue: 0.383503, opacity: 1)
|
|
||||||
|
|
||||||
Form {
|
Form {
|
||||||
VStack {
|
VStack {
|
||||||
|
|
||||||
@@ -92,23 +83,21 @@ struct InterfaceSettingsTab: View {
|
|||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
Button("") {
|
Button("") {
|
||||||
themeSettings.themeColor = slate
|
themeSettings.setPreset(preset: "slate", colorScheme: displayMode)
|
||||||
themeSettings.saveThemeColor()
|
|
||||||
}
|
}
|
||||||
.buttonStyle(PresetColor(fillColor: slate, label: "Slate"))
|
.buttonStyle(PresetColor(fillColor: themeSettings.getColorForPreset(preset: "slate", colorScheme: displayMode), label: "Slate"))
|
||||||
|
|
||||||
Button("") {
|
Button("") {
|
||||||
themeSettings.themeColor = dracula
|
themeSettings.setPreset(preset: "dracula", colorScheme: displayMode)
|
||||||
themeSettings.saveThemeColor()
|
|
||||||
}
|
}
|
||||||
.buttonStyle(PresetColor(fillColor: dracula, label: "Dracula"))
|
.buttonStyle(PresetColor(fillColor: themeSettings.getColorForPreset(preset: "dracula", colorScheme: displayMode), label: "Dracula"))
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
|
|
||||||
Button("") {
|
Button("") {
|
||||||
themeSettings.themeColor = solarized
|
themeSettings.setPreset(preset: "solarized", colorScheme: displayMode)
|
||||||
themeSettings.saveThemeColor()
|
themeSettings.saveThemeColor()
|
||||||
}
|
}
|
||||||
.buttonStyle(PresetColor(fillColor: solarized, label: "Solarized"))
|
.buttonStyle(PresetColor(fillColor: themeSettings.getColorForPreset(preset: "solarized", colorScheme: displayMode), label: "Solarized"))
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
@@ -151,18 +140,50 @@ struct InterfaceSettingsTab: View {
|
|||||||
}
|
}
|
||||||
InfoButton(text: "Changing the color mode will reset the base color to defaults", color: nil, label: "")
|
InfoButton(text: "Changing the color mode will reset the base color to defaults", color: nil, label: "")
|
||||||
Spacer()
|
Spacer()
|
||||||
Picker("", selection: $selectedTheme) {
|
SegmentedPicker(
|
||||||
ForEach(themes, id: \.self) { theme in
|
["Auto", "Dark", "Light"],
|
||||||
Text(theme)
|
selectedIndex: Binding(
|
||||||
|
get: {
|
||||||
|
switch selectedTheme {
|
||||||
|
case "Dark": return 1
|
||||||
|
case "Light": return 2
|
||||||
|
default: return 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
set: { newIndex in
|
||||||
|
withAnimation(.easeInOut(duration: 0.3)) {
|
||||||
|
switch newIndex {
|
||||||
|
case 1: selectedTheme = "Dark"
|
||||||
|
case 2: selectedTheme = "Light"
|
||||||
|
default: selectedTheme = "Auto"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.pickerStyle(SegmentedPickerStyle())
|
}),
|
||||||
.frame(width: 200)
|
selectionAlignment: .bottom,
|
||||||
|
content: { item, isSelected in
|
||||||
|
Text(item)
|
||||||
|
.font(.callout)
|
||||||
|
.foregroundColor(isSelected ? Color("mode") : Color("mode").opacity(0.5) )
|
||||||
|
.padding(.horizontal)
|
||||||
|
.padding(.bottom, 5)
|
||||||
|
.frame(width: 65)
|
||||||
|
},
|
||||||
|
selection: {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
Spacer()
|
||||||
|
Color("pear").frame(height: 1)
|
||||||
|
}
|
||||||
|
})
|
||||||
.onChange(of: selectedTheme) { newTheme in
|
.onChange(of: selectedTheme) { newTheme in
|
||||||
switch newTheme {
|
switch newTheme {
|
||||||
case "Auto":
|
case "Auto":
|
||||||
displayMode.colorScheme = nil
|
if isDarkMode() {
|
||||||
themeSettings.resetToDefault(dark: isDarkModeEnabled())
|
displayMode.colorScheme = .dark
|
||||||
|
} else {
|
||||||
|
displayMode.colorScheme = .light
|
||||||
|
}
|
||||||
|
// displayMode.colorScheme = nil
|
||||||
|
themeSettings.resetToDefault(dark: isDarkMode())
|
||||||
// Refresh foreground colors
|
// Refresh foreground colors
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||||
self.selectedTab = .interface
|
self.selectedTab = .interface
|
||||||
@@ -172,7 +193,7 @@ struct InterfaceSettingsTab: View {
|
|||||||
}
|
}
|
||||||
case "Dark":
|
case "Dark":
|
||||||
displayMode.colorScheme = .dark
|
displayMode.colorScheme = .dark
|
||||||
themeSettings.resetToDefault(dark: true)
|
// themeSettings.resetToDefault(dark: true)
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||||
self.selectedTab = .interface
|
self.selectedTab = .interface
|
||||||
}
|
}
|
||||||
@@ -181,7 +202,7 @@ struct InterfaceSettingsTab: View {
|
|||||||
}
|
}
|
||||||
case "Light":
|
case "Light":
|
||||||
displayMode.colorScheme = .light
|
displayMode.colorScheme = .light
|
||||||
themeSettings.resetToDefault(dark: false)
|
// themeSettings.resetToDefault(dark: false)
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||||
self.selectedTab = .interface
|
self.selectedTab = .interface
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ struct AppListItems: View {
|
|||||||
|
|
||||||
if (isHovered || isSelected) && !mini {
|
if (isHovered || isSelected) && !mini {
|
||||||
Triangle()
|
Triangle()
|
||||||
.fill(isSelected ? themeSettings.themeColor : Color("mode").opacity(0.1))
|
.fill(themeSettings.themeColor)
|
||||||
.frame(width: isSelected ? 16 : 8, height: 30)
|
.frame(width: isSelected ? 16 : 8, height: 30)
|
||||||
.padding(.leading, 5)
|
.padding(.leading, 5)
|
||||||
.offset(x: 22)
|
.offset(x: 22)
|
||||||
@@ -115,7 +115,6 @@ struct AppListItems: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// .grayscale(opacityForItem(appInfo.path))
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,24 +14,36 @@ struct FilesView: View {
|
|||||||
@State private var showPop: Bool = false
|
@State private var showPop: Bool = false
|
||||||
@AppStorage("settings.general.mini") private var mini: Bool = false
|
@AppStorage("settings.general.mini") private var mini: Bool = false
|
||||||
@AppStorage("settings.sentinel.enable") private var sentinel: Bool = false
|
@AppStorage("settings.sentinel.enable") private var sentinel: Bool = false
|
||||||
@AppStorage("settings.general.instant") var instantSearch: Bool = true
|
|
||||||
@AppStorage("settings.general.brew") private var brew: Bool = false
|
@AppStorage("settings.general.brew") private var brew: Bool = false
|
||||||
@AppStorage("settings.menubar.enabled") private var menubarEnabled: Bool = false
|
@AppStorage("settings.menubar.enabled") private var menubarEnabled: Bool = false
|
||||||
@AppStorage("settings.general.selectedSort") var selectedSortAlpha: Bool = true
|
@AppStorage("settings.general.selectedSort") var selectedSortAlpha: Bool = true
|
||||||
|
@AppStorage("settings.general.sizeType") var sizeType: String = "Real"
|
||||||
|
@State private var localKey = UUID()
|
||||||
@Environment(\.colorScheme) var colorScheme
|
@Environment(\.colorScheme) var colorScheme
|
||||||
@Binding var showPopover: Bool
|
@Binding var showPopover: Bool
|
||||||
@Binding var search: String
|
@Binding var search: String
|
||||||
var regularWin: Bool
|
var regularWin: Bool
|
||||||
// @State private var selectedOption = "Alpha"
|
|
||||||
@State private var elapsedTime = 0
|
@State private var elapsedTime = 0
|
||||||
@State private var timer: Timer? = nil
|
@State private var timer: Timer? = nil
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
|
||||||
|
|
||||||
let totalSelectedSize = appState.selectedItems.reduce(Int64(0)) { (result, url) in
|
var totalSelectedSize: (real: String, logical: String, finder: String) {
|
||||||
result + (appState.appInfo.fileSize[url] ?? 0)
|
var totalReal: Int64 = 0
|
||||||
|
var totalLogical: Int64 = 0
|
||||||
|
for url in appState.selectedItems {
|
||||||
|
let realSize = appState.appInfo.fileSize[url] ?? 0
|
||||||
|
let logicalSize = appState.appInfo.fileSizeLogical[url] ?? 0
|
||||||
|
totalReal += realSize
|
||||||
|
totalLogical += logicalSize
|
||||||
}
|
}
|
||||||
|
return (real: formatByte(size: totalReal).human, logical: formatByte(size: totalLogical).human, finder: "\(formatByte(size: totalLogical).byte) (\(formatByte(size: totalReal).human))")
|
||||||
|
}
|
||||||
|
|
||||||
|
let displaySizeTotal = sizeType == "Real" ? formatByte(size: appState.appInfo.totalSize).human :
|
||||||
|
sizeType == "Logical" ? formatByte(size: appState.appInfo.totalSizeLogical).human :
|
||||||
|
"\(formatByte(size: appState.appInfo.totalSizeLogical).byte) (\(formatByte(size: appState.appInfo.totalSize).human))"
|
||||||
|
|
||||||
VStack(alignment: .center) {
|
VStack(alignment: .center) {
|
||||||
if appState.showProgress {
|
if appState.showProgress {
|
||||||
@@ -73,22 +85,6 @@ struct FilesView: View {
|
|||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
if instantSearch {
|
|
||||||
Button("Rescan") {
|
|
||||||
updateOnMain {
|
|
||||||
appState.showProgress.toggle()
|
|
||||||
let pathFinder = AppPathFinder(appInfo: appState.appInfo, appState: appState, locations: locations) {
|
|
||||||
updateOnMain {
|
|
||||||
appState.showProgress = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pathFinder.findPaths()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.buttonStyle(SimpleButtonStyle(icon: "arrow.counterclockwise.circle.fill", help: "Rescan files"))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Button("Close") {
|
Button("Close") {
|
||||||
updateOnMain {
|
updateOnMain {
|
||||||
appState.appInfo = AppInfo.empty
|
appState.appInfo = AppInfo.empty
|
||||||
@@ -144,7 +140,8 @@ struct FilesView: View {
|
|||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
VStack(alignment: .trailing, spacing: 5) {
|
VStack(alignment: .trailing, spacing: 5) {
|
||||||
Text("\(formatByte(size: appState.appInfo.totalSize))").font(.title).fontWeight(.bold)
|
|
||||||
|
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((Color("mode").opacity(0.5)))
|
.font(.callout).foregroundStyle((Color("mode").opacity(0.5)))
|
||||||
}
|
}
|
||||||
@@ -161,10 +158,10 @@ struct FilesView: View {
|
|||||||
// Item selection and sorting toolbar
|
// Item selection and sorting toolbar
|
||||||
HStack() {
|
HStack() {
|
||||||
Toggle("", isOn: Binding(
|
Toggle("", isOn: Binding(
|
||||||
get: { self.appState.selectedItems.count == self.appState.appInfo.files.count },
|
get: { self.appState.selectedItems.count == self.appState.appInfo.fileSize.keys.count },
|
||||||
set: { newValue in
|
set: { newValue in
|
||||||
updateOnMain {
|
updateOnMain {
|
||||||
self.appState.selectedItems = newValue ? Set(self.appState.appInfo.files) : []
|
self.appState.selectedItems = newValue ? Set(self.appState.appInfo.fileSize.keys) : []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
))
|
))
|
||||||
@@ -212,7 +209,6 @@ struct FilesView: View {
|
|||||||
|
|
||||||
Button("") {
|
Button("") {
|
||||||
selectedSortAlpha.toggle()
|
selectedSortAlpha.toggle()
|
||||||
// selectedOption = selectedOption == "Alpha" ? "Size" : "Alpha"
|
|
||||||
}
|
}
|
||||||
.buttonStyle(SimpleButtonStyle(icon: selectedSortAlpha ? "textformat.abc" : "textformat.123", help: selectedSortAlpha ? "Sorted alphabetically" : "Sorted by size"))
|
.buttonStyle(SimpleButtonStyle(icon: selectedSortAlpha ? "textformat.abc" : "textformat.123", help: selectedSortAlpha ? "Sorted alphabetically" : "Sorted by size"))
|
||||||
|
|
||||||
@@ -228,10 +224,9 @@ struct FilesView: View {
|
|||||||
|
|
||||||
ScrollView() {
|
ScrollView() {
|
||||||
LazyVStack {
|
LazyVStack {
|
||||||
let sortedFilesSize = appState.appInfo.files.sorted(by: { appState.appInfo.fileSize[$0, default: 0] > appState.appInfo.fileSize[$1, default: 0] })
|
let sortedFilesSize = appState.appInfo.fileSize.keys.sorted(by: { appState.appInfo.fileSize[$0, default: 0] > appState.appInfo.fileSize[$1, default: 0] })
|
||||||
|
|
||||||
// let sortedFilesAlpha = appState.appInfo.files
|
let sortedFilesAlpha = appState.appInfo.fileSize.keys.sorted { firstURL, secondURL in
|
||||||
let sortedFilesAlpha = appState.appInfo.files.sorted { firstURL, secondURL in
|
|
||||||
let isFirstPathApp = firstURL.pathExtension == "app"
|
let isFirstPathApp = firstURL.pathExtension == "app"
|
||||||
let isSecondPathApp = secondURL.pathExtension == "app"
|
let isSecondPathApp = secondURL.pathExtension == "app"
|
||||||
if isFirstPathApp, !isSecondPathApp {
|
if isFirstPathApp, !isSecondPathApp {
|
||||||
@@ -247,10 +242,10 @@ struct FilesView: View {
|
|||||||
let sort = selectedSortAlpha ? sortedFilesAlpha : sortedFilesSize
|
let sort = selectedSortAlpha ? sortedFilesAlpha : sortedFilesSize
|
||||||
|
|
||||||
ForEach(Array(sort.enumerated()), id: \.element) { index, path in
|
ForEach(Array(sort.enumerated()), id: \.element) { index, path in
|
||||||
if let fileSize = appState.appInfo.fileSize[path], let fileIcon = appState.appInfo.fileIcon[path] {
|
if let fileSize = appState.appInfo.fileSize[path], let fileSizeL = appState.appInfo.fileSizeLogical[path], let fileIcon = appState.appInfo.fileIcon[path] {
|
||||||
let iconImage = fileIcon.map(Image.init(nsImage:))
|
let iconImage = fileIcon.map(Image.init(nsImage:))
|
||||||
VStack {
|
VStack {
|
||||||
FileDetailsItem(size: fileSize, icon: iconImage, path: path)
|
FileDetailsItem(size: fileSize, sizeL: fileSizeL, icon: iconImage, path: path)
|
||||||
if index < sort.count - 1 {
|
if index < sort.count - 1 {
|
||||||
Divider().padding(.leading, 40).opacity(0.5)
|
Divider().padding(.leading, 40).opacity(0.5)
|
||||||
}
|
}
|
||||||
@@ -260,7 +255,12 @@ struct FilesView: View {
|
|||||||
|
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
|
.onChange(of: sizeType) { _ in
|
||||||
|
localKey = UUID()
|
||||||
}
|
}
|
||||||
|
.id(localKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
@@ -269,7 +269,7 @@ struct FilesView: View {
|
|||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
Button("\(formatByte(size: totalSelectedSize))") {
|
Button("\(sizeType == "Logical" ? totalSelectedSize.logical : sizeType == "Finder" ? totalSelectedSize.finder : totalSelectedSize.real)") {
|
||||||
Task {
|
Task {
|
||||||
updateOnMain {
|
updateOnMain {
|
||||||
search = ""
|
search = ""
|
||||||
@@ -305,7 +305,6 @@ struct FilesView: View {
|
|||||||
removeApp(appState: appState, withId: appState.appInfo.id)
|
removeApp(appState: appState, withId: appState.appInfo.id)
|
||||||
} else {
|
} else {
|
||||||
// Add deleted appInfo object to trashed array
|
// Add deleted appInfo object to trashed array
|
||||||
appState.appInfo.files = []
|
|
||||||
appState.appInfo.fileSize = [:]
|
appState.appInfo.fileSize = [:]
|
||||||
appState.trashedFiles.append(appState.appInfo)
|
appState.trashedFiles.append(appState.appInfo)
|
||||||
|
|
||||||
@@ -346,7 +345,10 @@ struct FilesView: View {
|
|||||||
|
|
||||||
struct FileDetailsItem: View {
|
struct FileDetailsItem: View {
|
||||||
@EnvironmentObject var appState: AppState
|
@EnvironmentObject var appState: AppState
|
||||||
|
@State private var isHovered = false
|
||||||
|
@AppStorage("settings.general.sizeType") var sizeType: String = "Real"
|
||||||
let size: Int64?
|
let size: Int64?
|
||||||
|
let sizeL: Int64?
|
||||||
let icon: Image?
|
let icon: Image?
|
||||||
let path: URL
|
let path: URL
|
||||||
|
|
||||||
@@ -372,6 +374,8 @@ struct FileDetailsItem: View {
|
|||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
.frame(width: 30, height: 30)
|
.frame(width: 30, height: 30)
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||||
|
.brightness(isHovered ? 0.2 : 0)
|
||||||
|
.shadow(color: Color("mode"), radius: isHovered ? 2 : 0)
|
||||||
|
|
||||||
}
|
}
|
||||||
VStack(alignment: .leading, spacing: 5) {
|
VStack(alignment: .leading, spacing: 5) {
|
||||||
@@ -396,19 +400,35 @@ struct FileDetailsItem: View {
|
|||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
Text(formatByte(size:size!))
|
let displaySize = sizeType == "Real" ? formatByte(size: size!).human :
|
||||||
|
sizeType == "Logical" ? formatByte(size: sizeL!).human :
|
||||||
|
"\(formatByte(size: sizeL!).byte) (\(formatByte(size: size!).human))"
|
||||||
|
|
||||||
|
Text("\(displaySize)")
|
||||||
|
|
||||||
|
}
|
||||||
Button("") {
|
.onHover { hovering in
|
||||||
|
withAnimation(Animation.easeIn(duration: 0.2)) {
|
||||||
|
self.isHovered = hovering
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onTapGesture {
|
||||||
NSWorkspace.shared.selectFile(path.path, inFileViewerRootedAtPath: path.deletingLastPathComponent().path)
|
NSWorkspace.shared.selectFile(path.path, inFileViewerRootedAtPath: path.deletingLastPathComponent().path)
|
||||||
}
|
}
|
||||||
.buttonStyle(SimpleButtonStyle(icon: "folder.fill", help: "Show in Finder"))
|
.contextMenu {
|
||||||
|
if path.pathExtension == "app" {
|
||||||
|
Button("Open \(path.deletingPathExtension().lastPathComponent)") {
|
||||||
|
NSWorkspace.shared.open(path)
|
||||||
|
}
|
||||||
|
Divider()
|
||||||
|
}
|
||||||
|
Button("Copy Path") {
|
||||||
|
copyToClipboard(text: path.path)
|
||||||
|
}
|
||||||
|
Button("View in Finder") {
|
||||||
|
NSWorkspace.shared.selectFile(path.path, inFileViewerRootedAtPath: path.deletingLastPathComponent().path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
.background(
|
|
||||||
RoundedRectangle(cornerRadius: 10)
|
|
||||||
.fill(Color.white.opacity(0))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ struct MenuBarMiniAppView: 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
|
||||||
@AppStorage("settings.general.mini") private var mini: Bool = false
|
@AppStorage("settings.general.mini") private var mini: Bool = false
|
||||||
@AppStorage("settings.general.instant") private var instantSearch: Bool = true
|
|
||||||
@AppStorage("settings.general.glass") private var glass: Bool = true
|
@AppStorage("settings.general.glass") private var glass: Bool = true
|
||||||
@AppStorage("settings.general.popover") private var popoverStay: Bool = true
|
@AppStorage("settings.general.popover") private var popoverStay: Bool = true
|
||||||
@Binding var showPopover: Bool
|
@Binding var showPopover: Bool
|
||||||
@@ -40,15 +39,7 @@ struct MenuBarMiniAppView: View {
|
|||||||
if appState.reload {
|
if appState.reload {
|
||||||
VStack {
|
VStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
ProgressView(instantSearch ? "Refreshing app list and caching files" : "Refreshing app list")
|
ProgressView("Refreshing app list")
|
||||||
if instantSearch {
|
|
||||||
Image(systemName: "bolt.fill")
|
|
||||||
.resizable()
|
|
||||||
.scaledToFit()
|
|
||||||
.foregroundStyle(Color("mode").opacity(0.5))
|
|
||||||
.frame(width: 30, height: 30)
|
|
||||||
.padding()
|
|
||||||
}
|
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
.padding(.vertical)
|
.padding(.vertical)
|
||||||
@@ -70,13 +61,7 @@ struct MenuBarMiniAppView: View {
|
|||||||
appState.currentView = .zombie
|
appState.currentView = .zombie
|
||||||
appState.showProgress.toggle()
|
appState.showProgress.toggle()
|
||||||
showPopover.toggle()
|
showPopover.toggle()
|
||||||
if instantSearch {
|
reversePreloader(allApps: appState.sortedApps, appState: appState, locations: locations, reverseAddon: true)
|
||||||
let reverse = ReversePathsSearcher(appState: appState, locations: locations)
|
|
||||||
reverse.reversePathsSearch()
|
|
||||||
// reversePathsSearch(appState: appState, locations: locations)
|
|
||||||
} else {
|
|
||||||
loadAllPaths(allApps: appState.sortedApps, appState: appState, locations: locations, reverseAddon: true)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
appState.currentView = .zombie
|
appState.currentView = .zombie
|
||||||
showPopover.toggle()
|
showPopover.toggle()
|
||||||
|
|||||||
@@ -155,7 +155,6 @@ struct MiniAppView: 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
|
||||||
@AppStorage("settings.general.mini") private var mini: Bool = false
|
@AppStorage("settings.general.mini") private var mini: Bool = false
|
||||||
@AppStorage("settings.general.instant") private var instantSearch: Bool = true
|
|
||||||
@AppStorage("settings.general.glass") private var glass: Bool = true
|
@AppStorage("settings.general.glass") private var glass: Bool = true
|
||||||
@Binding var showPopover: Bool
|
@Binding var showPopover: Bool
|
||||||
|
|
||||||
@@ -175,15 +174,7 @@ struct MiniAppView: View {
|
|||||||
if appState.reload {
|
if appState.reload {
|
||||||
VStack {
|
VStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
ProgressView(instantSearch ? "Refreshing app list and caching files" : "Refreshing app list")
|
ProgressView("Refreshing app list")
|
||||||
if instantSearch {
|
|
||||||
Image(systemName: "bolt.fill")
|
|
||||||
.resizable()
|
|
||||||
.scaledToFit()
|
|
||||||
.foregroundStyle(Color("mode").opacity(0.5))
|
|
||||||
.frame(width: 30, height: 30)
|
|
||||||
.padding()
|
|
||||||
}
|
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
.padding(.vertical)
|
.padding(.vertical)
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
//
|
||||||
|
// SegmentedPicker.swift
|
||||||
|
// Pearcleaner
|
||||||
|
//
|
||||||
|
// Created by Alin Lupascu on 4/17/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
public struct SegmentedPicker<Element, Content, Selection>: View
|
||||||
|
where
|
||||||
|
Content: View,
|
||||||
|
Selection: View {
|
||||||
|
|
||||||
|
public typealias Data = [Element]
|
||||||
|
|
||||||
|
@State private var frames: [CGRect]
|
||||||
|
@Binding private var selectedIndex: Data.Index?
|
||||||
|
|
||||||
|
private let data: Data
|
||||||
|
private let selection: () -> Selection
|
||||||
|
private let content: (Data.Element, Bool) -> Content
|
||||||
|
private let selectionAlignment: VerticalAlignment
|
||||||
|
|
||||||
|
public init(_ data: Data,
|
||||||
|
selectedIndex: Binding<Data.Index?>,
|
||||||
|
selectionAlignment: VerticalAlignment = .center,
|
||||||
|
@ViewBuilder content: @escaping (Data.Element, Bool) -> Content,
|
||||||
|
@ViewBuilder selection: @escaping () -> Selection) {
|
||||||
|
|
||||||
|
self.data = data
|
||||||
|
self.content = content
|
||||||
|
self.selection = selection
|
||||||
|
self._selectedIndex = selectedIndex
|
||||||
|
self._frames = State(wrappedValue: Array(repeating: .zero,
|
||||||
|
count: data.count))
|
||||||
|
self.selectionAlignment = selectionAlignment
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
ZStack(alignment: Alignment(horizontal: .horizontalCenterAlignment,
|
||||||
|
vertical: selectionAlignment)) {
|
||||||
|
|
||||||
|
if let selectedIndex = selectedIndex {
|
||||||
|
selection()
|
||||||
|
.frame(width: frames[selectedIndex].width,
|
||||||
|
height: frames[selectedIndex].height)
|
||||||
|
.alignmentGuide(.horizontalCenterAlignment) { dimensions in
|
||||||
|
dimensions[HorizontalAlignment.center]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
ForEach(data.indices, id: \.self) { index in
|
||||||
|
Button(action: { selectedIndex = index },
|
||||||
|
label: { content(data[index], selectedIndex == index) }
|
||||||
|
)
|
||||||
|
.buttonStyle(PlainButtonStyle())
|
||||||
|
.background(GeometryReader { proxy in
|
||||||
|
Color.clear.onAppear { frames[index] = proxy.frame(in: .global) }
|
||||||
|
})
|
||||||
|
.alignmentGuide(.horizontalCenterAlignment,
|
||||||
|
isActive: selectedIndex == index) { dimensions in
|
||||||
|
dimensions[HorizontalAlignment.center]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
extension HorizontalAlignment {
|
||||||
|
private enum CenterAlignmentID: AlignmentID {
|
||||||
|
static func defaultValue(in dimension: ViewDimensions) -> CGFloat {
|
||||||
|
return dimension[HorizontalAlignment.center]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static var horizontalCenterAlignment: HorizontalAlignment {
|
||||||
|
HorizontalAlignment(CenterAlignmentID.self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
extension View {
|
||||||
|
@ViewBuilder
|
||||||
|
@inlinable func alignmentGuide(_ alignment: HorizontalAlignment,
|
||||||
|
isActive: Bool,
|
||||||
|
computeValue: @escaping (ViewDimensions) -> CGFloat) -> some View {
|
||||||
|
if isActive {
|
||||||
|
alignmentGuide(alignment, computeValue: computeValue)
|
||||||
|
} else {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
@inlinable func alignmentGuide(_ alignment: VerticalAlignment,
|
||||||
|
isActive: Bool,
|
||||||
|
computeValue: @escaping (ViewDimensions) -> CGFloat) -> some View {
|
||||||
|
|
||||||
|
if isActive {
|
||||||
|
alignmentGuide(alignment, computeValue: computeValue)
|
||||||
|
} else {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,13 +13,10 @@ struct RegularMode: View {
|
|||||||
@EnvironmentObject var themeSettings: ThemeSettings
|
@EnvironmentObject var themeSettings: ThemeSettings
|
||||||
@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.general.instant") private var instantSearch: 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
|
||||||
@Binding var showPopover: Bool
|
@Binding var showPopover: Bool
|
||||||
// @State private var isHovering = false
|
|
||||||
// @State private var isHovering2 = false
|
|
||||||
|
|
||||||
|
|
||||||
var filteredApps: [AppInfo] {
|
var filteredApps: [AppInfo] {
|
||||||
@@ -30,7 +27,6 @@ struct RegularMode: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// let slate = Color(.sRGB, red: 0.188143, green: 0.208556, blue: 0.262679, opacity: 1)
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
|
||||||
@@ -41,13 +37,7 @@ struct RegularMode: View {
|
|||||||
if appState.reload {
|
if appState.reload {
|
||||||
VStack {
|
VStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
ProgressView(instantSearch ? "Refreshing app list and caching files" : "Refreshing app list")
|
ProgressView("Refreshing app list")
|
||||||
if instantSearch {
|
|
||||||
Image(systemName: "bolt")
|
|
||||||
.resizable()
|
|
||||||
.scaledToFit()
|
|
||||||
.frame(width: 30, height: 30)
|
|
||||||
}
|
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
.frame(width: sidebarWidth)
|
.frame(width: sidebarWidth)
|
||||||
@@ -159,7 +149,6 @@ struct Header: View {
|
|||||||
@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.instant") private var instantSearch: Bool = true
|
|
||||||
@AppStorage("settings.general.glass") private var glass: Bool = true
|
@AppStorage("settings.general.glass") private var glass: Bool = true
|
||||||
|
|
||||||
|
|
||||||
@@ -198,15 +187,8 @@ struct Header: View {
|
|||||||
let sortedApps = getSortedApps(paths: fsm.folderPaths, appState: appState)
|
let sortedApps = getSortedApps(paths: fsm.folderPaths, appState: appState)
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||||
appState.sortedApps = sortedApps
|
appState.sortedApps = sortedApps
|
||||||
if instantSearch {
|
|
||||||
loadAllPaths(allApps: sortedApps, appState: appState, locations: locations) {
|
|
||||||
appState.reload.toggle()
|
appState.reload.toggle()
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
appState.reload.toggle()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(minHeight: 20)
|
.frame(minHeight: 20)
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ struct TopBar: View {
|
|||||||
@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
|
||||||
@AppStorage("settings.general.mini") private var mini: Bool = false
|
@AppStorage("settings.general.mini") private var mini: Bool = false
|
||||||
@AppStorage("settings.general.instant") private var instantSearch: Bool = true
|
|
||||||
@EnvironmentObject var appState: AppState
|
@EnvironmentObject var appState: AppState
|
||||||
@Binding var showPopover: Bool
|
@Binding var showPopover: Bool
|
||||||
@EnvironmentObject var locations: Locations
|
@EnvironmentObject var locations: Locations
|
||||||
@@ -47,12 +46,7 @@ struct TopBar: View {
|
|||||||
updateOnMain {
|
updateOnMain {
|
||||||
appState.zombieFile = .empty
|
appState.zombieFile = .empty
|
||||||
appState.showProgress.toggle()
|
appState.showProgress.toggle()
|
||||||
if instantSearch {
|
reversePreloader(allApps: appState.sortedApps, appState: appState, locations: locations, reverseAddon: true)
|
||||||
let reverse = ReversePathsSearcher(appState: appState, locations: locations)
|
|
||||||
reverse.reversePathsSearch()
|
|
||||||
} else {
|
|
||||||
loadAllPaths(allApps: appState.sortedApps, appState: appState, locations: locations, reverseAddon: true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.buttonStyle(SimpleButtonStyle(icon: "arrow.counterclockwise.circle.fill", help: "Rescan files"))
|
.buttonStyle(SimpleButtonStyle(icon: "arrow.counterclockwise.circle.fill", help: "Rescan files"))
|
||||||
@@ -68,13 +62,7 @@ struct TopBar: View {
|
|||||||
appState.currentView = .zombie
|
appState.currentView = .zombie
|
||||||
appState.showProgress.toggle()
|
appState.showProgress.toggle()
|
||||||
showPopover.toggle()
|
showPopover.toggle()
|
||||||
if instantSearch {
|
reversePreloader(allApps: appState.sortedApps, appState: appState, locations: locations, reverseAddon: true)
|
||||||
let reverse = ReversePathsSearcher(appState: appState, locations: locations)
|
|
||||||
reverse.reversePathsSearch()
|
|
||||||
// reversePathsSearch(appState: appState, locations: locations)
|
|
||||||
} else {
|
|
||||||
loadAllPaths(allApps: appState.sortedApps, appState: appState, locations: locations, reverseAddon: true)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
appState.currentView = .zombie
|
appState.currentView = .zombie
|
||||||
showPopover.toggle()
|
showPopover.toggle()
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ struct TopBarMini: View {
|
|||||||
@AppStorage("displayMode") var displayMode: DisplayMode = .system
|
@AppStorage("displayMode") var displayMode: DisplayMode = .system
|
||||||
@AppStorage("settings.sentinel.enable") private var sentinel: Bool = false
|
@AppStorage("settings.sentinel.enable") private var sentinel: Bool = false
|
||||||
@AppStorage("settings.general.mini") private var mini: Bool = false
|
@AppStorage("settings.general.mini") private var mini: Bool = false
|
||||||
@AppStorage("settings.general.instant") private var instantSearch: Bool = true
|
|
||||||
@Binding var search: String
|
@Binding var search: String
|
||||||
@Binding var showPopover: Bool
|
@Binding var showPopover: Bool
|
||||||
@EnvironmentObject var appState: AppState
|
@EnvironmentObject var appState: AppState
|
||||||
@@ -36,13 +35,7 @@ struct TopBarMini: View {
|
|||||||
appState.currentView = .zombie
|
appState.currentView = .zombie
|
||||||
appState.showProgress.toggle()
|
appState.showProgress.toggle()
|
||||||
showPopover.toggle()
|
showPopover.toggle()
|
||||||
if instantSearch {
|
reversePreloader(allApps: appState.sortedApps, appState: appState, locations: locations, reverseAddon: true)
|
||||||
let reverse = ReversePathsSearcher(appState: appState, locations: locations)
|
|
||||||
reverse.reversePathsSearch()
|
|
||||||
// reversePathsSearch(appState: appState, locations: locations)
|
|
||||||
} else {
|
|
||||||
loadAllPaths(allApps: appState.sortedApps, appState: appState, locations: locations, reverseAddon: true)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
appState.currentView = .zombie
|
appState.currentView = .zombie
|
||||||
showPopover.toggle()
|
showPopover.toggle()
|
||||||
@@ -79,12 +72,7 @@ struct TopBarMini: View {
|
|||||||
appState.currentView = .zombie
|
appState.currentView = .zombie
|
||||||
appState.showProgress.toggle()
|
appState.showProgress.toggle()
|
||||||
showPopover.toggle()
|
showPopover.toggle()
|
||||||
if instantSearch {
|
reversePreloader(allApps: appState.sortedApps, appState: appState, locations: locations, reverseAddon: true)
|
||||||
let reverse = ReversePathsSearcher(appState: appState, locations: locations)
|
|
||||||
reverse.reversePathsSearch()
|
|
||||||
} else {
|
|
||||||
loadAllPaths(allApps: appState.sortedApps, appState: appState, locations: locations, reverseAddon: true)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
appState.currentView = .zombie
|
appState.currentView = .zombie
|
||||||
showPopover.toggle()
|
showPopover.toggle()
|
||||||
@@ -133,7 +121,7 @@ struct SearchBarMiniBottom: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack {
|
HStack {
|
||||||
TextField("\(appState.instantTotal > appState.instantProgress ? " Searching the file system" : " Search")", text: $search)
|
TextField(" Search", text: $search)
|
||||||
.textFieldStyle(SimpleSearchStyle(trash: true, text: $search))
|
.textFieldStyle(SimpleSearchStyle(trash: true, text: $search))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,9 +14,10 @@ struct ZombieView: View {
|
|||||||
@State private var showPop: Bool = false
|
@State private var showPop: Bool = false
|
||||||
@AppStorage("settings.general.mini") private var mini: Bool = false
|
@AppStorage("settings.general.mini") private var mini: Bool = false
|
||||||
@AppStorage("settings.sentinel.enable") private var sentinel: Bool = false
|
@AppStorage("settings.sentinel.enable") private var sentinel: Bool = false
|
||||||
@AppStorage("settings.general.instant") private var instantSearch: Bool = true
|
|
||||||
@AppStorage("settings.menubar.enabled") private var menubarEnabled: Bool = false
|
@AppStorage("settings.menubar.enabled") private var menubarEnabled: Bool = false
|
||||||
@AppStorage("settings.general.selectedSort") var selectedSortAlpha: Bool = true
|
@AppStorage("settings.general.selectedSort") var selectedSortAlpha: Bool = true
|
||||||
|
@AppStorage("settings.general.sizeType") var sizeType: String = "Real"
|
||||||
|
@State private var localKey = UUID()
|
||||||
@Environment(\.colorScheme) var colorScheme
|
@Environment(\.colorScheme) var colorScheme
|
||||||
@Binding var showPopover: Bool
|
@Binding var showPopover: Bool
|
||||||
@Binding var search: String
|
@Binding var search: String
|
||||||
@@ -27,28 +28,45 @@ struct ZombieView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
|
||||||
let totalSelectedZombieSize = appState.selectedZombieItems.reduce(Int64(0)) { (result, url) in
|
let totalSelectedZombieSize: (real: String, logical: String, finder: String) = {
|
||||||
result + (appState.zombieFile.fileSize[url] ?? 0)
|
var totalReal: Int64 = 0
|
||||||
}
|
var totalLogical: Int64 = 0
|
||||||
|
|
||||||
let filteredAndSortedFiles: ([URL], Int64) = {
|
for url in appState.selectedZombieItems {
|
||||||
let filteredFiles = appState.zombieFile.fileSize.filter { (url, _) in
|
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)
|
searchZ.isEmpty || url.lastPathComponent.localizedCaseInsensitiveContains(searchZ)
|
||||||
}
|
}
|
||||||
|
let filteredFilesLogical = fileSizeLogical.filter { (url, _) in
|
||||||
let sortedFilteredFiles = filteredFiles.sorted(by: {
|
searchZ.isEmpty || url.lastPathComponent.localizedCaseInsensitiveContains(searchZ)
|
||||||
|
}
|
||||||
|
let filesToSort = (sizeType == "Real" || sizeType == "Finder" ? filteredFilesReal : filteredFilesLogical)
|
||||||
|
let sortedFilteredFiles = filesToSort.sorted(by: {
|
||||||
if selectedSortAlpha {
|
if selectedSortAlpha {
|
||||||
return $0.key.lastPathComponent.pearFormat() < $1.key.lastPathComponent.pearFormat()
|
return $0.key.lastPathComponent.pearFormat() < $1.key.lastPathComponent.pearFormat()
|
||||||
} else {
|
} else {
|
||||||
return $0.value > $1.value
|
return $0.value > $1.value
|
||||||
}
|
}
|
||||||
}).map { $0.key }
|
}).map { $0.key }
|
||||||
|
let totalSize = filteredFilesReal.values.reduce(0, +)
|
||||||
let totalSize = filteredFiles.values.reduce(0, +)
|
let totalSizeL = filteredFilesLogical.values.reduce(0, +)
|
||||||
|
return (sortedFilteredFiles, totalSize, totalSizeL)
|
||||||
return (sortedFilteredFiles, totalSize)
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
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 {
|
||||||
@@ -62,6 +80,7 @@ struct ZombieView: View {
|
|||||||
.progressViewStyle(.linear)
|
.progressViewStyle(.linear)
|
||||||
.frame(width: 400, height: 10)
|
.frame(width: 400, height: 10)
|
||||||
|
|
||||||
|
|
||||||
Text("\(elapsedTime)")
|
Text("\(elapsedTime)")
|
||||||
.font(.title).monospacedDigit()
|
.font(.title).monospacedDigit()
|
||||||
.foregroundStyle(Color("mode").opacity(0.5))
|
.foregroundStyle(Color("mode").opacity(0.5))
|
||||||
@@ -97,13 +116,7 @@ struct ZombieView: View {
|
|||||||
updateOnMain {
|
updateOnMain {
|
||||||
appState.zombieFile = .empty
|
appState.zombieFile = .empty
|
||||||
appState.showProgress.toggle()
|
appState.showProgress.toggle()
|
||||||
if instantSearch {
|
reversePreloader(allApps: appState.sortedApps, appState: appState, locations: locations, reverseAddon: true)
|
||||||
let reverse = ReversePathsSearcher(appState: appState, locations: locations)
|
|
||||||
reverse.reversePathsSearch()
|
|
||||||
// reversePathsSearch(appState: appState, locations: locations)
|
|
||||||
} else {
|
|
||||||
loadAllPaths(allApps: appState.sortedApps, appState: appState, locations: locations, reverseAddon: true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.buttonStyle(SimpleButtonStyle(icon: "arrow.counterclockwise.circle.fill", help: "Rescan files"))
|
.buttonStyle(SimpleButtonStyle(icon: "arrow.counterclockwise.circle.fill", help: "Rescan files"))
|
||||||
@@ -142,14 +155,14 @@ struct ZombieView: View {
|
|||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
}
|
}
|
||||||
Text("Files and folders remaining from previously installed applications")
|
Text("Remaining files and folders from previous installs")
|
||||||
.font(.callout).foregroundStyle(Color("mode").opacity(0.5))
|
.font(.callout).foregroundStyle(Color("mode").opacity(0.5))
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
VStack(alignment: .trailing, spacing: 5) {
|
VStack(alignment: .trailing, spacing: 5) {
|
||||||
Text("\(formatByte(size: filteredAndSortedFiles.1))").font(.title).fontWeight(.bold)
|
Text("\(displaySizeTotal)").font(.title).fontWeight(.bold).help("Total size on disk")
|
||||||
|
|
||||||
Text("\(appState.zombieFile.fileSize.count == 1 ? "\(appState.selectedZombieItems.count) / \(appState.zombieFile.fileSize.count) item" : "\(appState.selectedZombieItems.count) / \(appState.zombieFile.fileSize.count) items")")
|
Text("\(appState.zombieFile.fileSize.count == 1 ? "\(appState.selectedZombieItems.count) / \(appState.zombieFile.fileSize.count) item" : "\(appState.selectedZombieItems.count) / \(appState.zombieFile.fileSize.count) items")")
|
||||||
.font(.callout).foregroundStyle(Color("mode").opacity(0.5))
|
.font(.callout).foregroundStyle(Color("mode").opacity(0.5))
|
||||||
@@ -195,10 +208,10 @@ struct ZombieView: View {
|
|||||||
ScrollView() {
|
ScrollView() {
|
||||||
LazyVStack {
|
LazyVStack {
|
||||||
ForEach(Array(filteredAndSortedFiles.0.enumerated()), id: \.element) { index, file in
|
ForEach(Array(filteredAndSortedFiles.0.enumerated()), id: \.element) { index, file in
|
||||||
if let fileSize = appState.zombieFile.fileSize[file], let fileIcon = appState.zombieFile.fileIcon[file] {
|
if let fileSize = appState.zombieFile.fileSize[file], let fileSizeL = appState.zombieFile.fileSizeLogical[file], let fileIcon = appState.zombieFile.fileIcon[file] {
|
||||||
let iconImage = fileIcon.map(Image.init(nsImage:))
|
let iconImage = fileIcon.map(Image.init(nsImage:))
|
||||||
VStack {
|
VStack {
|
||||||
ZombieFileDetailsItem(size: fileSize, icon: iconImage, path: file)
|
ZombieFileDetailsItem(size: fileSize, sizeL: fileSizeL, icon: iconImage, path: file)
|
||||||
.padding(.trailing)
|
.padding(.trailing)
|
||||||
|
|
||||||
if index < filteredAndSortedFiles.0.count - 1 {
|
if index < filteredAndSortedFiles.0.count - 1 {
|
||||||
@@ -210,7 +223,12 @@ struct ZombieView: View {
|
|||||||
|
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
|
.onChange(of: sizeType) { _ in
|
||||||
|
localKey = UUID()
|
||||||
}
|
}
|
||||||
|
.id(localKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
@@ -218,7 +236,7 @@ struct ZombieView: View {
|
|||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
Button("\(formatByte(size: totalSelectedZombieSize))") {
|
Button("\(sizeType == "Logical" ? totalSelectedZombieSize.logical : sizeType == "Finder" ? totalSelectedZombieSize.finder : totalSelectedZombieSize.real)") {
|
||||||
Task {
|
Task {
|
||||||
updateOnMain {
|
updateOnMain {
|
||||||
appState.zombieFile = .empty
|
appState.zombieFile = .empty
|
||||||
@@ -271,7 +289,10 @@ struct ZombieView: View {
|
|||||||
|
|
||||||
struct ZombieFileDetailsItem: View {
|
struct ZombieFileDetailsItem: View {
|
||||||
@EnvironmentObject var appState: AppState
|
@EnvironmentObject var appState: AppState
|
||||||
|
@State private var isHovered = false
|
||||||
|
@AppStorage("settings.general.sizeType") var sizeType: String = "Real"
|
||||||
let size: Int64?
|
let size: Int64?
|
||||||
|
let sizeL: Int64?
|
||||||
let icon: Image?
|
let icon: Image?
|
||||||
let path: URL
|
let path: URL
|
||||||
|
|
||||||
@@ -295,6 +316,8 @@ struct ZombieFileDetailsItem: View {
|
|||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
.frame(width: 30, height: 30)
|
.frame(width: 30, height: 30)
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||||
|
.brightness(isHovered ? 0.2 : 0)
|
||||||
|
.shadow(color: Color("mode"), radius: isHovered ? 2 : 0)
|
||||||
|
|
||||||
}
|
}
|
||||||
VStack(alignment: .leading, spacing: 5) {
|
VStack(alignment: .leading, spacing: 5) {
|
||||||
@@ -313,20 +336,35 @@ struct ZombieFileDetailsItem: View {
|
|||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
Text(formatByte(size:size!))
|
let displaySize = sizeType == "Real" ? formatByte(size: size!).human :
|
||||||
|
sizeType == "Logical" ? formatByte(size: sizeL!).human :
|
||||||
|
"\(formatByte(size: sizeL!).byte) (\(formatByte(size: size!).human))"
|
||||||
|
|
||||||
|
Text("\(displaySize)")
|
||||||
|
|
||||||
|
}
|
||||||
Button("") {
|
.onHover { hovering in
|
||||||
|
withAnimation(Animation.easeIn(duration: 0.2)) {
|
||||||
|
self.isHovered = hovering
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onTapGesture {
|
||||||
|
NSWorkspace.shared.selectFile(path.path, inFileViewerRootedAtPath: path.deletingLastPathComponent().path)
|
||||||
|
}
|
||||||
|
.contextMenu {
|
||||||
|
if path.pathExtension == "app" {
|
||||||
|
Button("Open \(path.deletingPathExtension().lastPathComponent)") {
|
||||||
|
NSWorkspace.shared.open(path)
|
||||||
|
}
|
||||||
|
Divider()
|
||||||
|
}
|
||||||
|
Button("Copy Path") {
|
||||||
|
copyToClipboard(text: path.path)
|
||||||
|
}
|
||||||
|
Button("View in Finder") {
|
||||||
NSWorkspace.shared.selectFile(path.path, inFileViewerRootedAtPath: path.deletingLastPathComponent().path)
|
NSWorkspace.shared.selectFile(path.path, inFileViewerRootedAtPath: path.deletingLastPathComponent().path)
|
||||||
}
|
}
|
||||||
.buttonStyle(SimpleButtonStyle(icon: "folder.fill", help: "Show in Finder"))
|
|
||||||
|
|
||||||
}
|
}
|
||||||
.background(
|
|
||||||
RoundedRectangle(cornerRadius: 10)
|
|
||||||
.fill(Color.white.opacity(0))
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user