mirror of
https://github.com/exituser/Pearcleaner.git
synced 2026-09-17 08:29:07 +00:00
3.6.2
This commit is contained in:
@@ -568,8 +568,8 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
APP_BUILD = 40;
|
APP_BUILD = 41;
|
||||||
APP_VERSION = 3.6.1;
|
APP_VERSION = 3.6.2;
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
@@ -638,8 +638,8 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
APP_BUILD = 40;
|
APP_BUILD = 41;
|
||||||
APP_VERSION = 3.6.1;
|
APP_VERSION = 3.6.2;
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ class DeeplinkManager {
|
|||||||
if url.pathExtension == "app" {
|
if url.pathExtension == "app" {
|
||||||
handleAppBundle(url: url, appState: appState, locations: locations)
|
handleAppBundle(url: url, appState: appState, locations: locations)
|
||||||
} else if url.scheme == DeepLinkConstants.scheme,
|
} else if url.scheme == DeepLinkConstants.scheme,
|
||||||
|
// This handles SentinelMonitor FileWatcher
|
||||||
url.host == DeepLinkConstants.host,
|
url.host == DeepLinkConstants.host,
|
||||||
let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
|
let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
|
||||||
let queryItems = components.queryItems {
|
let queryItems = components.queryItems {
|
||||||
|
|||||||
@@ -200,14 +200,14 @@ func showAppInFiles(appInfo: AppInfo, appState: AppState, locations: Locations,
|
|||||||
|
|
||||||
// Move files to trash using applescript/Finder so it asks for user password if needed
|
// Move files to trash using applescript/Finder so it asks for user password if needed
|
||||||
func moveFilesToTrash(appState: AppState, at fileURLs: [URL], completion: @escaping (Bool) -> Void = {_ in }) {
|
func moveFilesToTrash(appState: AppState, at fileURLs: [URL], completion: @escaping (Bool) -> Void = {_ in }) {
|
||||||
@AppStorage("settings.sentinel.enable") var sentinel: Bool = false
|
// Stop Sentinel FileWatcher momentarily to ignore .app bundle being sent to Trash
|
||||||
if sentinel {
|
sendStopNotificationFW()
|
||||||
launchctl(load: false)
|
|
||||||
}
|
|
||||||
updateOnBackground {
|
updateOnBackground {
|
||||||
let posixFiles = fileURLs.map { "POSIX file \"\($0.path)\", " }.joined().dropLast(3)
|
let posixFiles = fileURLs.map { item in
|
||||||
|
return "POSIX file \"\(item.path)\"" + (item == fileURLs.last ? "" : ", ")}.joined()
|
||||||
let scriptSource = """
|
let scriptSource = """
|
||||||
tell application \"Finder\" to delete { \(posixFiles)" }
|
tell application \"Finder\" to delete { \(posixFiles) }
|
||||||
"""
|
"""
|
||||||
|
|
||||||
var error: NSDictionary?
|
var error: NSDictionary?
|
||||||
|
|||||||
@@ -66,10 +66,10 @@ func checkForUpdate(appState: AppState, manual: Bool = false) {
|
|||||||
|
|
||||||
func downloadUpdate(appState: AppState) {
|
func downloadUpdate(appState: AppState) {
|
||||||
updateOnMain {
|
updateOnMain {
|
||||||
appState.progressBar.0 = "Getting update file links ready"
|
appState.progressBar.0 = "UPDATER: Getting update link"
|
||||||
appState.progressBar.1 = 0.1
|
appState.progressBar.1 = 0.1
|
||||||
}
|
}
|
||||||
|
let fileManager = FileManager.default
|
||||||
guard let latestRelease = appState.releases.first else { return }
|
guard let latestRelease = appState.releases.first else { return }
|
||||||
guard let asset = latestRelease.assets.first else { return }
|
guard let asset = latestRelease.assets.first else { return }
|
||||||
guard let url = URL(string: asset.url) else { return }
|
guard let url = URL(string: asset.url) else { return }
|
||||||
@@ -78,38 +78,37 @@ func downloadUpdate(appState: AppState) {
|
|||||||
|
|
||||||
let downloadTask = URLSession.shared.downloadTask(with: request) { localURL, urlResponse, error in
|
let downloadTask = URLSession.shared.downloadTask(with: request) { localURL, urlResponse, error in
|
||||||
updateOnMain {
|
updateOnMain {
|
||||||
appState.progressBar.0 = "Downloading update file"
|
appState.progressBar.0 = "UPDATER: Starting download of update file"
|
||||||
appState.progressBar.1 = 0.2
|
appState.progressBar.1 = 0.2
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let localURL = localURL else { return }
|
guard let localURL = localURL else { return }
|
||||||
|
|
||||||
let fileManager = FileManager.default
|
let destinationURL = FileManager.default.temporaryDirectory.appendingPathComponent("\(asset.name)")
|
||||||
let destinationURL = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!.appendingPathComponent("Pearcleaner").appendingPathComponent("\(asset.name)")
|
|
||||||
|
|
||||||
do {
|
do {
|
||||||
if fileManager.fileExists(atPath: destinationURL.path) {
|
if fileManager.fileExists(atPath: destinationURL.path) {
|
||||||
try? fileManager.removeItem(at: destinationURL)
|
try? fileManager.removeItem(at: destinationURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
updateOnMain {
|
updateOnMain {
|
||||||
appState.progressBar.0 = "Moving update file to Application Support"
|
appState.progressBar.0 = "UPDATER: File downloaded to temp directory"
|
||||||
|
appState.progressBar.1 = 0.3
|
||||||
|
}
|
||||||
|
try fileManager.moveItem(at: localURL, to: destinationURL)
|
||||||
|
|
||||||
|
updateOnMain {
|
||||||
|
appState.progressBar.0 = "UPDATER: File renamed using asset name"
|
||||||
appState.progressBar.1 = 0.4
|
appState.progressBar.1 = 0.4
|
||||||
}
|
}
|
||||||
|
|
||||||
try fileManager.moveItem(at: localURL, to: destinationURL)
|
|
||||||
|
|
||||||
UnzipAndReplace(DownloadedFileURL: destinationURL.path, appState: appState)
|
UnzipAndReplace(DownloadedFileURL: destinationURL.path, appState: appState)
|
||||||
|
|
||||||
updateOnMain {
|
|
||||||
appState.progressBar.0 = "Done, please restart!"
|
|
||||||
appState.progressBar.1 = 1.0
|
|
||||||
appState.updateAvailable = false
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
printOS("Error moving downloaded file: \(error.localizedDescription)")
|
printOS("Error moving downloaded file: \(error.localizedDescription)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadTask.resume()
|
downloadTask.resume()
|
||||||
@@ -122,7 +121,7 @@ func UnzipAndReplace(DownloadedFileURL fileURL: String, appState: AppState) {
|
|||||||
|
|
||||||
do {
|
do {
|
||||||
updateOnMain {
|
updateOnMain {
|
||||||
appState.progressBar.0 = "Deleting existing application"
|
appState.progressBar.0 = "UPDATER: Removing currently installed application bundle"
|
||||||
appState.progressBar.1 = 0.5
|
appState.progressBar.1 = 0.5
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,11 +129,10 @@ func UnzipAndReplace(DownloadedFileURL fileURL: String, appState: AppState) {
|
|||||||
try fileManager.removeItem(atPath: appBundle)
|
try fileManager.removeItem(atPath: appBundle)
|
||||||
|
|
||||||
updateOnMain {
|
updateOnMain {
|
||||||
appState.progressBar.0 = "Unziping new update file to original Pearcleaner location"
|
appState.progressBar.0 = "UPDATER: Unziping file to original install location"
|
||||||
appState.progressBar.1 = 0.6
|
appState.progressBar.1 = 0.6
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Unzip the downloaded update file to your app's bundle path
|
// Unzip the downloaded update file to your app's bundle path
|
||||||
let process = Process()
|
let process = Process()
|
||||||
process.executableURL = URL(fileURLWithPath: "/usr/bin/ditto")
|
process.executableURL = URL(fileURLWithPath: "/usr/bin/ditto")
|
||||||
@@ -146,13 +144,18 @@ func UnzipAndReplace(DownloadedFileURL fileURL: String, appState: AppState) {
|
|||||||
process.waitUntilExit()
|
process.waitUntilExit()
|
||||||
|
|
||||||
updateOnMain {
|
updateOnMain {
|
||||||
appState.progressBar.0 = "Deleting update file"
|
appState.progressBar.0 = "UPDATER: Removing file from temp directory"
|
||||||
appState.progressBar.1 = 0.8
|
appState.progressBar.1 = 0.8
|
||||||
}
|
}
|
||||||
|
|
||||||
// After unzipping, remove the update file
|
// After unzipping, remove the update file
|
||||||
try fileManager.removeItem(atPath: fileURL)
|
try fileManager.removeItem(atPath: fileURL)
|
||||||
|
|
||||||
|
updateOnMain {
|
||||||
|
appState.progressBar.0 = "UPDATER: Completed, please restart!"
|
||||||
|
appState.progressBar.1 = 1.0
|
||||||
|
appState.updateAvailable = false
|
||||||
|
}
|
||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
printOS("Error updating the app: \(error)")
|
printOS("Error updating the app: \(error)")
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ func findAndHideWindows(named titles: [String]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func findAndSetWindowFrame(named titles: [String], windowSettings: WindowSettings) {
|
func findAndSetWindowFrame(named titles: [String], windowSettings: WindowSettings) {
|
||||||
|
windowSettings.registerDefaultWindowSettings() {
|
||||||
for title in titles {
|
for title in titles {
|
||||||
if let window = NSApp.windows.first(where: { $0.title == title }) {
|
if let window = NSApp.windows.first(where: { $0.title == title }) {
|
||||||
window.isRestorable = false
|
window.isRestorable = false
|
||||||
@@ -129,6 +130,7 @@ func findAndSetWindowFrame(named titles: [String], windowSettings: WindowSetting
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
func findAndShowWindows(named titles: [String]) {
|
func findAndShowWindows(named titles: [String]) {
|
||||||
@@ -279,16 +281,17 @@ func killApp(appId: String, completion: @escaping () -> Void = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Remove app from cache
|
// Remove app from cache
|
||||||
func removeApp(appState: AppState, withId id: UUID) {
|
func removeApp(appState: AppState, withPath path: URL) {
|
||||||
@AppStorage("settings.general.brew") var brew: Bool = false
|
@AppStorage("settings.general.brew") var brew: Bool = false
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
|
||||||
// Remove from sortedApps if found
|
// Remove from sortedApps if found
|
||||||
if let index = appState.sortedApps.firstIndex(where: { $0.id == id }) {
|
if let index = appState.sortedApps.firstIndex(where: { $0.path == path }) {
|
||||||
appState.sortedApps.remove(at: index)
|
appState.sortedApps.remove(at: index)
|
||||||
return // Exit the function if the app was found and removed
|
// return // Exit the function if the app was found and removed
|
||||||
}
|
}
|
||||||
// Remove from appInfoStore if found
|
// Remove from appInfoStore if found
|
||||||
if let index = appState.appInfoStore.firstIndex(where: { $0.id == id }) {
|
if let index = appState.appInfoStore.firstIndex(where: { $0.path == path }) {
|
||||||
appState.appInfoStore.remove(at: index)
|
appState.appInfoStore.remove(at: index)
|
||||||
}
|
}
|
||||||
// Brew cleanup if enabled
|
// Brew cleanup if enabled
|
||||||
@@ -410,13 +413,13 @@ extension String {
|
|||||||
|
|
||||||
|
|
||||||
// --- Trash Relationship ---
|
// --- Trash Relationship ---
|
||||||
extension FileManager {
|
//extension FileManager {
|
||||||
public func isInTrash(_ file: URL) -> Bool {
|
// public func isInTrash(_ file: URL) -> Bool {
|
||||||
var relationship: URLRelationship = .other
|
// var relationship: URLRelationship = .other
|
||||||
try? getRelationship(&relationship, of: .trashDirectory, in: .userDomainMask, toItemAt: file)
|
// try? getRelationship(&relationship, of: .trashDirectory, in: .userDomainMask, toItemAt: file)
|
||||||
return relationship == .contains
|
// return relationship == .contains
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
|
||||||
// --- Extend print command to also output to the Console ---
|
// --- Extend print command to also output to the Console ---
|
||||||
func printOS(_ items: Any..., separator: String = " ", terminator: String = "\n") {
|
func printOS(_ items: Any..., separator: String = " ", terminator: String = "\n") {
|
||||||
@@ -518,37 +521,6 @@ func isSupportedFileType(at path: String) -> Bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Alerts
|
|
||||||
func presentAlert(appState: AppState) -> Alert {
|
|
||||||
|
|
||||||
switch appState.alertType {
|
|
||||||
case .update:
|
|
||||||
return Alert(title: Text("Update Available 🥳"), message: Text("You may choose to install the update now, otherwise you may check again later from Settings"), primaryButton: .default(Text("Install")) {
|
|
||||||
downloadUpdate(appState: appState)
|
|
||||||
appState.alertType = .off
|
|
||||||
}, secondaryButton: .cancel())
|
|
||||||
case .no_update:
|
|
||||||
return Alert(title: Text("No Updates 😌"), message: Text("Pearcleaner is on the latest release available"), primaryButton: .cancel(Text("Okay")), secondaryButton: .default(Text("Force Update")) {
|
|
||||||
downloadUpdate(appState: appState)
|
|
||||||
appState.alertType = .off
|
|
||||||
})
|
|
||||||
case .diskAccess:
|
|
||||||
return Alert(title: Text("Permissions"), message: Text("Pearcleaner requires Full Disk and Accessibility permissions. Drag the app into the Full Disk and Accessibility pane to enable or toggle On if already present."), primaryButton: .default(Text("Allow in Settings")) {
|
|
||||||
if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles") {
|
|
||||||
NSWorkspace.shared.open(url)
|
|
||||||
}
|
|
||||||
appState.alertType = .off
|
|
||||||
}, secondaryButton: .cancel(Text("Later")))
|
|
||||||
case .restartApp:
|
|
||||||
return Alert(title: Text("Update Completed!"), message: Text("The application has been updated to the latest version, would you like to restart now?"), primaryButton: .default(Text("Restart")) {
|
|
||||||
appState.alertType = .off
|
|
||||||
relaunchApp()
|
|
||||||
}, secondaryButton: .cancel(Text("Later")))
|
|
||||||
case .off:
|
|
||||||
return Alert(title: Text(""))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// --- Pearcleaner Uninstall --
|
// --- Pearcleaner Uninstall --
|
||||||
@@ -564,16 +536,15 @@ func uninstallPearcleaner(appState: AppState, locations: Locations) {
|
|||||||
AppPathFinder(appInfo: appInfo!, appState: appState, locations: locations, completion: {
|
AppPathFinder(appInfo: appInfo!, appState: appState, locations: locations, completion: {
|
||||||
// Kill Pearcleaner and tell Finder to trash the files
|
// Kill Pearcleaner and tell Finder to trash the files
|
||||||
let selectedItemsArray = Array(appState.selectedItems).filter { !$0.path.contains(".Trash") }
|
let selectedItemsArray = Array(appState.selectedItems).filter { !$0.path.contains(".Trash") }
|
||||||
let posixFiles = selectedItemsArray.map { "POSIX file \"\($0.path)\", " }.joined().dropLast(3)
|
let posixFiles = selectedItemsArray.map { item in
|
||||||
|
return "POSIX file \"\(item.path)\"" + (item == selectedItemsArray.last ? "" : ", ")}.joined()
|
||||||
let scriptSource = """
|
let scriptSource = """
|
||||||
tell application \"Finder\" to delete { \(posixFiles)" }
|
tell application \"Finder\" to delete { \(posixFiles) }
|
||||||
"""
|
"""
|
||||||
let task = Process()
|
let task = Process()
|
||||||
task.launchPath = "/bin/sh"
|
task.launchPath = "/bin/sh"
|
||||||
task.arguments = ["-c", "sleep 1; osascript -e '\(scriptSource)'"]
|
task.arguments = ["-c", "sleep 1; osascript -e '\(scriptSource)'"]
|
||||||
task.launch()
|
task.launch()
|
||||||
|
|
||||||
NSApp.terminate(nil)
|
|
||||||
exit(0)
|
exit(0)
|
||||||
}).findPaths()
|
}).findPaths()
|
||||||
}
|
}
|
||||||
@@ -653,7 +624,13 @@ func launchctl(load: Bool, completion: @escaping () -> Void = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func sendStartNotificationFW() {
|
||||||
|
DistributedNotificationCenter.default().postNotificationName(Notification.Name("Pearcleaner.StartFileWatcher"), object: nil, userInfo: nil, deliverImmediately: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendStopNotificationFW() {
|
||||||
|
DistributedNotificationCenter.default().postNotificationName(Notification.Name("Pearcleaner.StopFileWatcher"), object: nil, userInfo: nil, deliverImmediately: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
func getCurrentTimestamp() -> String {
|
func getCurrentTimestamp() -> String {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import AppKit
|
import AppKit
|
||||||
//import ServiceManagement
|
|
||||||
|
|
||||||
@main
|
@main
|
||||||
struct PearcleanerApp: App {
|
struct PearcleanerApp: App {
|
||||||
@@ -111,7 +110,8 @@ struct PearcleanerApp: App {
|
|||||||
|
|
||||||
|
|
||||||
// Make sure App Support folder exists in the future if needed for storage
|
// Make sure App Support folder exists in the future if needed for storage
|
||||||
ensureApplicationSupportFolderExists(appState: appState)
|
//MARK: This is not needed any longer as the update file is stored in /tmp directory
|
||||||
|
// ensureApplicationSupportFolderExists(appState: appState)
|
||||||
|
|
||||||
// Check for updates after app launch
|
// Check for updates after app launch
|
||||||
checkAllPermissions(appState: appState) { results in
|
checkAllPermissions(appState: appState) { results in
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ struct GeneralSettingsTab: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.buttonStyle(SimpleButtonStyle(icon: "arrow.triangle.2.circlepath", help: "Refresh permissions"))
|
.buttonStyle(SimpleButtonStyle(icon: "arrow.triangle.2.circlepath", label: "Refresh", help: "Refresh permissions"))
|
||||||
.padding(.trailing, 5)
|
.padding(.trailing, 5)
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -412,11 +412,7 @@ struct GeneralSettingsTab: View {
|
|||||||
private func resetUserDefaults() {
|
private func resetUserDefaults() {
|
||||||
isResetting = true
|
isResetting = true
|
||||||
DispatchQueue.global(qos: .background).async {
|
DispatchQueue.global(qos: .background).async {
|
||||||
let defaults = UserDefaults.standard
|
UserDefaults.standard.dictionaryRepresentation().keys.forEach(UserDefaults.standard.removeObject(forKey:))
|
||||||
let dictionary = defaults.dictionaryRepresentation()
|
|
||||||
dictionary.keys.forEach { key in
|
|
||||||
defaults.removeObject(forKey: key)
|
|
||||||
}
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
isResetting = false
|
isResetting = false
|
||||||
}
|
}
|
||||||
@@ -424,3 +420,6 @@ struct GeneralSettingsTab: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,11 @@ struct AppSearchView: View {
|
|||||||
|
|
||||||
HStack(spacing: 10) {
|
HStack(spacing: 10) {
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
Image(systemName: "ant.fill")
|
||||||
|
.foregroundStyle(.orange)
|
||||||
|
.help("DEBUG")
|
||||||
|
#endif
|
||||||
|
|
||||||
SearchBar(search: $search, darker: (mini || menubarEnabled) ? false : true, glass: glass)
|
SearchBar(search: $search, darker: (mini || menubarEnabled) ? false : true, glass: glass)
|
||||||
|
|
||||||
|
|||||||
@@ -257,22 +257,30 @@ struct FilesView: View {
|
|||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
HStack() {
|
HStack(alignment: .center) {
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
if appState.appInfo.fileSize.keys.count == 0 {
|
||||||
|
Text("Sentinel Monitor found no other files to remove")
|
||||||
|
.font(.title2)
|
||||||
|
.opacity(0.5)
|
||||||
|
.padding(.top)
|
||||||
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
Button("\(sizeType == "Logical" ? totalSelectedSize.logical : sizeType == "Finder" ? totalSelectedSize.finder : totalSelectedSize.real)") {
|
Button("\(sizeType == "Logical" ? totalSelectedSize.logical : sizeType == "Finder" ? totalSelectedSize.finder : totalSelectedSize.real)") {
|
||||||
Task {
|
Task {
|
||||||
|
|
||||||
|
|
||||||
let selectedItemsArray = Array(appState.selectedItems)
|
let selectedItemsArray = Array(appState.selectedItems)
|
||||||
|
|
||||||
killApp(appId: appState.appInfo.bundleIdentifier) {
|
killApp(appId: appState.appInfo.bundleIdentifier) {
|
||||||
|
|
||||||
moveFilesToTrash(appState: appState, at: selectedItemsArray) { success in
|
moveFilesToTrash(appState: appState, at: selectedItemsArray) { success in
|
||||||
if sentinel {
|
|
||||||
launchctl(load: true)
|
// Send Sentinel FileWatcher start notification
|
||||||
}
|
sendStartNotificationFW()
|
||||||
|
|
||||||
guard success else {
|
guard success else {
|
||||||
return
|
return
|
||||||
@@ -296,7 +304,7 @@ struct FilesView: View {
|
|||||||
if (appState.appInfo.wrapped && selectedItemsArray.contains(where: { $0.absoluteString == appState.appInfo.path.deletingLastPathComponent().deletingLastPathComponent().absoluteString })) ||
|
if (appState.appInfo.wrapped && selectedItemsArray.contains(where: { $0.absoluteString == appState.appInfo.path.deletingLastPathComponent().deletingLastPathComponent().absoluteString })) ||
|
||||||
(!appState.appInfo.wrapped && selectedItemsArray.contains(where: { $0.absoluteString == appState.appInfo.path.absoluteString })) {
|
(!appState.appInfo.wrapped && selectedItemsArray.contains(where: { $0.absoluteString == appState.appInfo.path.absoluteString })) {
|
||||||
// Match found, remove the app
|
// Match found, remove the app
|
||||||
removeApp(appState: appState, withId: appState.appInfo.id)
|
removeApp(appState: appState, withPath: appState.appInfo.path)
|
||||||
} else {
|
} else {
|
||||||
// Add deleted appInfo object to trashed array
|
// Add deleted appInfo object to trashed array
|
||||||
appState.trashedFiles.append(appState.appInfo)
|
appState.trashedFiles.append(appState.appInfo)
|
||||||
|
|||||||
@@ -111,7 +111,6 @@ struct AppDetailsEmptyView: View {
|
|||||||
PearDropView()
|
PearDropView()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
Text("Drop an app here")
|
Text("Drop an app here")
|
||||||
|
|||||||
@@ -17,33 +17,64 @@ class WindowSettings {
|
|||||||
@AppStorage("settings.general.mini") private var mini: Bool = false
|
@AppStorage("settings.general.mini") private var mini: Bool = false
|
||||||
var windows: [NSWindow] = []
|
var windows: [NSWindow] = []
|
||||||
|
|
||||||
func saveWindowSettings(frame: NSRect) {
|
func registerDefaultWindowSettings(completion: @escaping () -> Void = {}) {
|
||||||
|
|
||||||
|
let defaults = UserDefaults.standard
|
||||||
|
|
||||||
|
// Get primary screen
|
||||||
|
let screenFrame = NSScreen.main?.visibleFrame ?? NSRect(x: 0, y: 0, width: 800, height: 600)
|
||||||
|
|
||||||
|
// Calculate default window sizes and x/y coordinates
|
||||||
|
let defaultWidth = Float(900) // Default width for regular window
|
||||||
|
let defaultHeight = Float(600) // Default height for regular window
|
||||||
|
let defaultWidthMini = Float(300) // Default width for mini window
|
||||||
|
let defaultHeightMini = Float(370) // Default height for mini window
|
||||||
|
let defaultX = Float((screenFrame.width - CGFloat(defaultWidth)) / 2 + screenFrame.origin.x) // Default X coordinate
|
||||||
|
let defaultY = Float((screenFrame.height - CGFloat(defaultHeight)) / 2 + screenFrame.origin.y) // Default Y coordinate
|
||||||
|
|
||||||
|
// Set defaults only if they are not already set
|
||||||
|
if defaults.object(forKey: windowWidthKey) == nil {
|
||||||
|
defaults.set(defaultWidth, forKey: windowWidthKey)
|
||||||
|
}
|
||||||
|
if defaults.object(forKey: windowHeightKey) == nil {
|
||||||
|
defaults.set(defaultHeight, forKey: windowHeightKey)
|
||||||
|
}
|
||||||
|
if defaults.object(forKey: windowWidthKeyMini) == nil {
|
||||||
|
defaults.set(defaultWidthMini, forKey: windowWidthKeyMini)
|
||||||
|
}
|
||||||
|
if defaults.object(forKey: windowHeightKeyMini) == nil {
|
||||||
|
defaults.set(defaultHeightMini, forKey: windowHeightKeyMini)
|
||||||
|
}
|
||||||
|
if defaults.object(forKey: windowXKey) == nil {
|
||||||
|
defaults.set(defaultX, forKey: windowXKey)
|
||||||
|
}
|
||||||
|
if defaults.object(forKey: windowYKey) == nil {
|
||||||
|
defaults.set(defaultY, forKey: windowYKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
completion()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save user window settings
|
||||||
|
func saveWindowSettings(frame: NSRect) {
|
||||||
UserDefaults.standard.set(Float(frame.size.width), forKey: mini ? windowWidthKeyMini : windowWidthKey)
|
UserDefaults.standard.set(Float(frame.size.width), forKey: mini ? windowWidthKeyMini : windowWidthKey)
|
||||||
UserDefaults.standard.set(Float(frame.size.height), forKey: mini ? windowHeightKeyMini : windowHeightKey)
|
UserDefaults.standard.set(Float(frame.size.height), forKey: mini ? windowHeightKeyMini : windowHeightKey)
|
||||||
UserDefaults.standard.set(Float(frame.origin.x), forKey: windowXKey)
|
UserDefaults.standard.set(Float(frame.origin.x), forKey: windowXKey)
|
||||||
UserDefaults.standard.set(Float(frame.origin.y), forKey: windowYKey)
|
UserDefaults.standard.set(Float(frame.origin.y), forKey: windowYKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load default window settings or user defined settings
|
||||||
func loadWindowSettings() -> NSRect {
|
func loadWindowSettings() -> NSRect {
|
||||||
|
|
||||||
// Retrieve window size
|
|
||||||
let width = CGFloat(UserDefaults.standard.float(forKey: mini ? windowWidthKeyMini : windowWidthKey))
|
let width = CGFloat(UserDefaults.standard.float(forKey: mini ? windowWidthKeyMini : windowWidthKey))
|
||||||
let height = CGFloat(UserDefaults.standard.float(forKey: mini ? windowHeightKeyMini : windowHeightKey))
|
let height = CGFloat(UserDefaults.standard.float(forKey: mini ? windowHeightKeyMini : windowHeightKey))
|
||||||
|
let x = CGFloat(UserDefaults.standard.float(forKey: windowXKey))
|
||||||
|
let y = CGFloat(UserDefaults.standard.float(forKey: windowYKey))
|
||||||
|
|
||||||
// Set default middle position if not set in UserDefaults
|
|
||||||
var x = CGFloat(UserDefaults.standard.float(forKey: windowXKey))
|
|
||||||
var y = CGFloat(UserDefaults.standard.float(forKey: windowYKey))
|
|
||||||
|
|
||||||
|
|
||||||
if UserDefaults.standard.object(forKey: windowXKey) == nil || UserDefaults.standard.object(forKey: windowYKey) == nil {
|
|
||||||
let screenFrame = NSScreen.main?.visibleFrame ?? NSRect(x: 0, y: 0, width: 800, height: 600)
|
|
||||||
x = (screenFrame.width - width) / 2 + screenFrame.origin.x
|
|
||||||
y = (screenFrame.height - height) / 2 + screenFrame.origin.y
|
|
||||||
}
|
|
||||||
return NSRect(x: x, y: y, width: width, height: height)
|
return NSRect(x: x, y: y, width: width, height: height)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Launch new app windows on demand
|
||||||
func newWindow<V: View>(withView view: @escaping () -> V, completion: @escaping () -> Void = {}) {
|
func newWindow<V: View>(withView view: @escaping () -> V, completion: @escaping () -> Void = {}) {
|
||||||
findAndHideWindows(named: ["Pearcleaner"])
|
findAndHideWindows(named: ["Pearcleaner"])
|
||||||
let contentView = view
|
let contentView = view
|
||||||
|
|||||||
+38
-216
@@ -5,28 +5,48 @@
|
|||||||
// Created by Alin Lupascu on 11/9/23.
|
// Created by Alin Lupascu on 11/9/23.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import AppKit
|
import AppKit
|
||||||
import FileWatcher
|
import FileWatcher
|
||||||
|
|
||||||
// Start Trash monitoring
|
main()
|
||||||
fileWatcher()
|
|
||||||
|
|
||||||
// Keep alive indefinitely
|
var globalFileWatcher: FileWatcher?
|
||||||
while true {
|
|
||||||
sleep(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func fileWatcher() {
|
func startGlobalFileWatcher() {
|
||||||
let home = FileManager.default.homeDirectoryForCurrentUser.path
|
let home = FileManager.default.homeDirectoryForCurrentUser.path
|
||||||
let filewatcher = FileWatcher(["\(home)/.Trash"])
|
globalFileWatcher = FileWatcher(["\(home)/.Trash"])
|
||||||
filewatcher.queue = DispatchQueue.global()
|
globalFileWatcher?.queue = DispatchQueue.global()
|
||||||
filewatcher.callback = { event in
|
globalFileWatcher?.callback = { event in
|
||||||
checkApp(file: event.path)
|
checkApp(file: event.path)
|
||||||
}
|
}
|
||||||
filewatcher.start()
|
globalFileWatcher?.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func stopGlobalFileWatcher() {
|
||||||
|
globalFileWatcher?.stop()
|
||||||
|
globalFileWatcher = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupNotificationListener() {
|
||||||
|
let notificationCenter = DistributedNotificationCenter.default()
|
||||||
|
notificationCenter.addObserver(forName: Notification.Name("Pearcleaner.StartFileWatcher"), object: nil, queue: nil) { notification in
|
||||||
|
print("Received start notification")
|
||||||
|
startGlobalFileWatcher()
|
||||||
|
}
|
||||||
|
notificationCenter.addObserver(forName: Notification.Name("Pearcleaner.StopFileWatcher"), object: nil, queue: nil) { notification in
|
||||||
|
print("Received stop notification")
|
||||||
|
stopGlobalFileWatcher()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
setupNotificationListener()
|
||||||
|
startGlobalFileWatcher()
|
||||||
|
RunLoop.main.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
func checkApp(file: String) {
|
func checkApp(file: String) {
|
||||||
let app = URL(fileURLWithPath: file)
|
let app = URL(fileURLWithPath: file)
|
||||||
let appExt = app.pathExtension
|
let appExt = app.pathExtension
|
||||||
@@ -45,6 +65,11 @@ func checkApp(file: String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// --- Trash Relationship ---
|
// --- Trash Relationship ---
|
||||||
extension FileManager {
|
extension FileManager {
|
||||||
public func isInTrash(_ file: URL) -> Bool {
|
public func isInTrash(_ file: URL) -> Bool {
|
||||||
@@ -64,7 +89,7 @@ extension FileManager {
|
|||||||
|
|
||||||
|
|
||||||
// For testing and outputing logging to file from cmd line tool
|
// For testing and outputing logging to file from cmd line tool
|
||||||
func writeLog(string: String) {
|
func writeLogMon(string: String) {
|
||||||
let fileManager = FileManager.default
|
let fileManager = FileManager.default
|
||||||
let home = fileManager.homeDirectoryForCurrentUser.path
|
let home = fileManager.homeDirectoryForCurrentUser.path
|
||||||
let logFilePath = "\(home)/Downloads/monitor.txt"
|
let logFilePath = "\(home)/Downloads/monitor.txt"
|
||||||
@@ -88,206 +113,3 @@ func writeLog(string: String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// let trashContents = getTrashContents()
|
|
||||||
// if !trashContents.isEmpty && trashContents.contains(event.path) {
|
|
||||||
// NSWorkspace.shared.open(URL(string: "pear://com.alienator88.Pearcleaner?path=\(event.path)")!)
|
|
||||||
// }
|
|
||||||
|
|
||||||
//func getTrashContents() -> [String] {
|
|
||||||
// let fileManager = FileManager.default
|
|
||||||
// let trashURLs = fileManager.urls(for: .trashDirectory, in: .userDomainMask)
|
|
||||||
// do {
|
|
||||||
// let trashContents = try fileManager.contentsOfDirectory(at: trashURLs.first!, includingPropertiesForKeys: nil, options: [])
|
|
||||||
// let appFiles = trashContents.filter { $0.pathExtension == "app" }
|
|
||||||
// writeLog(string: appFiles.first!.path)
|
|
||||||
//
|
|
||||||
// return appFiles.map { $0.path }
|
|
||||||
// } catch {
|
|
||||||
// printOS("Failed to get contents of trash directory: \(error)")
|
|
||||||
// return []
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
|
|
||||||
//func hasAccessToTrashFolder() -> Bool {
|
|
||||||
// let fileManager = FileManager.default
|
|
||||||
// let trashURLs = fileManager.urls(for: .trashDirectory, in: .userDomainMask)
|
|
||||||
//
|
|
||||||
// if let trashURL = trashURLs.first {
|
|
||||||
// return fileManager.isReadableFile(atPath: trashURL.path) && fileManager.isWritableFile(atPath: trashURL.path)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return false
|
|
||||||
//}
|
|
||||||
|
|
||||||
//if hasAccessToTrashFolder() {
|
|
||||||
// writeLog(string: "Your tool has access to the Trash folder.")
|
|
||||||
// printOS("Your tool has access to the Trash folder.")
|
|
||||||
//} else {
|
|
||||||
// if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles") {
|
|
||||||
// NSWorkspace.shared.open(url)
|
|
||||||
// }
|
|
||||||
// writeLog(string: "Your tool does not have access to the Trash folder.")
|
|
||||||
// printOS("Your tool does not have access to the Trash folder.")
|
|
||||||
//}
|
|
||||||
|
|
||||||
|
|
||||||
//var dirWatcher: DirMonitor?
|
|
||||||
//func fileW2() {
|
|
||||||
// let home = FileManager.default.homeDirectoryForCurrentUser.path
|
|
||||||
// let url = URL(fileURLWithPath: "\(home)/.Trash")
|
|
||||||
// dirWatcher = DirMonitor(dir: url, queue: .global())
|
|
||||||
// if dirWatcher?.start() == true {
|
|
||||||
// NSLog("Started directory monitoring")
|
|
||||||
// } else {
|
|
||||||
// NSLog("Failed to start directory monitoring")
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
//
|
|
||||||
//class DirMonitor {
|
|
||||||
//
|
|
||||||
// init(dir: URL, queue: DispatchQueue) {
|
|
||||||
// self.dir = dir
|
|
||||||
// self.queue = queue
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// deinit {
|
|
||||||
// // The stream has a reference to us via its `info` pointer. If the
|
|
||||||
// // client releases their reference to us without calling `stop`, that
|
|
||||||
// // results in a dangling pointer. We detect this as a programming error.
|
|
||||||
// // There are other approaches to take here (for example, messing around
|
|
||||||
// // with weak, or forming a retain cycle that’s broken on `stop`), but
|
|
||||||
// // this approach:
|
|
||||||
// //
|
|
||||||
// // * Has clear rules
|
|
||||||
// // * Is easy to implement
|
|
||||||
// // * Generate a sensible debug message if the client gets things wrong
|
|
||||||
// precondition(self.stream == nil, "released a running monitor")
|
|
||||||
// // I added this log line as part of my testing of the deallocation path.
|
|
||||||
// NSLog("did deinit")
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// let dir: URL
|
|
||||||
// let queue: DispatchQueue
|
|
||||||
//
|
|
||||||
// private var stream: FSEventStreamRef? = nil
|
|
||||||
//
|
|
||||||
// func start() -> Bool {
|
|
||||||
// precondition(self.stream == nil, "started a running monitor")
|
|
||||||
//
|
|
||||||
// // Set up our context.
|
|
||||||
// //
|
|
||||||
// // `FSEventStreamCallback` is a C function, so we pass `self` to the
|
|
||||||
// // `info` pointer so that it get call our `handleUnsafeEvents(…)`
|
|
||||||
// // method. This involves the standard `Unmanaged` dance:
|
|
||||||
// //
|
|
||||||
// // * Here we set `info` to an unretained pointer to `self`.
|
|
||||||
// // * Inside the function we extract that pointer as `obj` and then use
|
|
||||||
// // that to call `handleUnsafeEvents(…)`.
|
|
||||||
//
|
|
||||||
// var context = FSEventStreamContext()
|
|
||||||
// context.info = Unmanaged.passUnretained(self).toOpaque()
|
|
||||||
//
|
|
||||||
// // Create the stream.
|
|
||||||
// //
|
|
||||||
// // In this example I wanted to show how to deal with raw string paths,
|
|
||||||
// // so I’m not taking advantage of `kFSEventStreamCreateFlagUseCFTypes`
|
|
||||||
// // or the even cooler `kFSEventStreamCreateFlagUseExtendedData`.
|
|
||||||
//
|
|
||||||
// guard let stream = FSEventStreamCreate(nil, { (stream, info, numEvents, eventPaths, eventFlags, eventIds) in
|
|
||||||
// let obj = Unmanaged<DirMonitor>.fromOpaque(info!).takeUnretainedValue()
|
|
||||||
// obj.handleUnsafeEvents(numEvents: numEvents, eventPaths: eventPaths, eventFlags: eventFlags, eventIDs: eventIds)
|
|
||||||
// },
|
|
||||||
// &context,
|
|
||||||
// [self.dir.path as NSString] as NSArray,
|
|
||||||
// UInt64(kFSEventStreamEventIdSinceNow),
|
|
||||||
// 1.0,
|
|
||||||
// FSEventStreamCreateFlags(kFSEventStreamCreateFlagNone)
|
|
||||||
// ) else {
|
|
||||||
// return false
|
|
||||||
// }
|
|
||||||
// self.stream = stream
|
|
||||||
//
|
|
||||||
// // Now that we have a stream, schedule it on our target queue.
|
|
||||||
//
|
|
||||||
// FSEventStreamSetDispatchQueue(stream, queue)
|
|
||||||
// guard FSEventStreamStart(stream) else {
|
|
||||||
// FSEventStreamInvalidate(stream)
|
|
||||||
// self.stream = nil
|
|
||||||
// return false
|
|
||||||
// }
|
|
||||||
// return true
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// private func handleUnsafeEvents(numEvents: Int, eventPaths: UnsafeMutableRawPointer, eventFlags: UnsafePointer<FSEventStreamEventFlags>, eventIDs: UnsafePointer<FSEventStreamEventId>) {
|
|
||||||
// // This takes the low-level goo from the C callback, converts it to
|
|
||||||
// // something that makes sense for Swift, and then passes that to
|
|
||||||
// // `handle(events:…)`.
|
|
||||||
// //
|
|
||||||
// // Note that we don’t need to do any rebinding here because this data is
|
|
||||||
// // coming C as the right type.
|
|
||||||
// let pathsBase = eventPaths.assumingMemoryBound(to: UnsafePointer<CChar>.self)
|
|
||||||
// let pathsBuffer = UnsafeBufferPointer(start: pathsBase, count: numEvents)
|
|
||||||
// let flagsBuffer = UnsafeBufferPointer(start: eventFlags, count: numEvents)
|
|
||||||
// let eventIDsBuffer = UnsafeBufferPointer(start: eventIDs, count: numEvents)
|
|
||||||
// // As `zip(_:_:)` only handles two sequences, I map over the index.
|
|
||||||
// let events = (0..<numEvents).map { i -> (url: URL, flags: FSEventStreamEventFlags, eventIDs: FSEventStreamEventId) in
|
|
||||||
// let path = pathsBuffer[i]
|
|
||||||
// // We set `isDirectory` to true because we only generate directory
|
|
||||||
// // events (that is, we don’t pass
|
|
||||||
// // `kFSEventStreamCreateFlagFileEvents` to `FSEventStreamCreate`.
|
|
||||||
// // This is generally the best way to use FSEvents, but if you decide
|
|
||||||
// // to take advantage of `kFSEventStreamCreateFlagFileEvents` then
|
|
||||||
// // you’ll need to code to `isDirectory` correctly.
|
|
||||||
// let url: URL = URL(fileURLWithFileSystemRepresentation: path, isDirectory: true, relativeTo: nil)
|
|
||||||
// return (url, flagsBuffer[i], eventIDsBuffer[i])
|
|
||||||
// }
|
|
||||||
// self.handle(events: events)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// private func handle(events: [(url: URL, flags: FSEventStreamEventFlags, eventIDs: FSEventStreamEventId)]) {
|
|
||||||
// // In this example we just print the events with get, prefixed by a
|
|
||||||
// // count so that we can see the batching in action.
|
|
||||||
// NSLog("%d", events.count)
|
|
||||||
// for (url, flags, eventID) in events {
|
|
||||||
// NSLog("%16x %8x %@", eventID, flags, url.path)
|
|
||||||
// }
|
|
||||||
// for (url, flags, _) in events {
|
|
||||||
// if flags & FSEventStreamEventFlags(kFSEventStreamEventFlagItemRemoved) != 0 {
|
|
||||||
// NSLog("Removed: \(url.path)")
|
|
||||||
// }
|
|
||||||
// if flags & FSEventStreamEventFlags(kFSEventStreamEventFlagItemCreated) != 0 {
|
|
||||||
// NSLog("Created: \(url.path)")
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// getTrashContents()
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// func stop() {
|
|
||||||
// guard let stream = self.stream else {
|
|
||||||
// return // We accept redundant calls to `stop`.
|
|
||||||
// }
|
|
||||||
// FSEventStreamStop(stream)
|
|
||||||
// FSEventStreamInvalidate(stream)
|
|
||||||
// self.stream = nil
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// func getTrashContents() {
|
|
||||||
// let fileManager = FileManager.default
|
|
||||||
// let trashURLs = fileManager.urls(for: .trashDirectory, in: .userDomainMask)
|
|
||||||
// let trashContents = try? fileManager.contentsOfDirectory(at: trashURLs.first!, includingPropertiesForKeys: nil, options: [])
|
|
||||||
// printOS(trashContents as Any)
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user