mirror of
https://github.com/exituser/Pearcleaner.git
synced 2026-09-17 02:38:54 +00:00
v3.8.7
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_32x32.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.0 KiB |
@@ -27,8 +27,11 @@ class FinderOpen: FIFinderSync {
|
||||
selectedItemURLs.count == 1, selectedItemURLs.first?.pathExtension == "app" {
|
||||
// Add menu item if the selected item is a .app file
|
||||
let menuItem = NSMenuItem(title: "Pearcleaner Uninstall", action: #selector(openInMyApp), keyEquivalent: "")
|
||||
// menuItem.image = NSImage(named: NSImage.trashEmptyName)
|
||||
// menu.addItem(withTitle: "Pearcleaner Uninstall", action: #selector(openInMyApp), keyEquivalent: "")
|
||||
if let appIcon = NSImage(named: "Icon") {
|
||||
menuItem.image = appIcon
|
||||
} else {
|
||||
menuItem.image = NSImage(named: NSImage.trashFullName)
|
||||
}
|
||||
menu.addItem(menuItem)
|
||||
}
|
||||
}
|
||||
@@ -53,79 +56,3 @@ class FinderOpen: FIFinderSync {
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
//class FinderSync: FIFinderSync {
|
||||
//
|
||||
// var myFolderURL = URL(fileURLWithPath: "/Users/Shared/MySyncExtension Documents")
|
||||
//
|
||||
// override init() {
|
||||
// super.init()
|
||||
//
|
||||
// NSLog("FinderSync() launched from %@", Bundle.main.bundlePath as NSString)
|
||||
//
|
||||
// // Set up the directory we are syncing.
|
||||
// FIFinderSyncController.default().directoryURLs = [self.myFolderURL]
|
||||
//
|
||||
// // Set up images for our badge identifiers. For demonstration purposes, this uses off-the-shelf images.
|
||||
// FIFinderSyncController.default().setBadgeImage(NSImage(named: NSImage.colorPanelName)!, label: "Status One" , forBadgeIdentifier: "One")
|
||||
// FIFinderSyncController.default().setBadgeImage(NSImage(named: NSImage.cautionName)!, label: "Status Two", forBadgeIdentifier: "Two")
|
||||
// }
|
||||
//
|
||||
// // MARK: - Primary Finder Sync protocol methods
|
||||
//
|
||||
// override func beginObservingDirectory(at url: URL) {
|
||||
// // The user is now seeing the container's contents.
|
||||
// // If they see it in more than one view at a time, we're only told once.
|
||||
// NSLog("beginObservingDirectoryAtURL: %@", url.path as NSString)
|
||||
// }
|
||||
//
|
||||
//
|
||||
// override func endObservingDirectory(at url: URL) {
|
||||
// // The user is no longer seeing the container's contents.
|
||||
// NSLog("endObservingDirectoryAtURL: %@", url.path as NSString)
|
||||
// }
|
||||
//
|
||||
// override func requestBadgeIdentifier(for url: URL) {
|
||||
// NSLog("requestBadgeIdentifierForURL: %@", url.path as NSString)
|
||||
//
|
||||
// // For demonstration purposes, this picks one of our two badges, or no badge at all, based on the filename.
|
||||
// let whichBadge = abs(url.path.hash) % 3
|
||||
// let badgeIdentifier = ["", "One", "Two"][whichBadge]
|
||||
// FIFinderSyncController.default().setBadgeIdentifier(badgeIdentifier, for: url)
|
||||
// }
|
||||
//
|
||||
// // MARK: - Menu and toolbar item support
|
||||
//
|
||||
// override var toolbarItemName: String {
|
||||
// return "FinderSy"
|
||||
// }
|
||||
//
|
||||
// override var toolbarItemToolTip: String {
|
||||
// return "FinderSy: Click the toolbar item for a menu."
|
||||
// }
|
||||
//
|
||||
// override var toolbarItemImage: NSImage {
|
||||
// return NSImage(named: NSImage.cautionName)!
|
||||
// }
|
||||
//
|
||||
// override func menu(for menuKind: FIMenuKind) -> NSMenu {
|
||||
// // Produce a menu for the extension.
|
||||
// let menu = NSMenu(title: "")
|
||||
// menu.addItem(withTitle: "Example Menu Item", action: #selector(sampleAction(_:)), keyEquivalent: "")
|
||||
// return menu
|
||||
// }
|
||||
//
|
||||
// @IBAction func sampleAction(_ sender: AnyObject?) {
|
||||
// let target = FIFinderSyncController.default().targetedURL()
|
||||
// let items = FIFinderSyncController.default().selectedItemURLs()
|
||||
//
|
||||
// let item = sender as! NSMenuItem
|
||||
// NSLog("sampleAction: menu item: %@, target = %@, items = ", item.title as NSString, target!.path as NSString)
|
||||
// for obj in items! {
|
||||
// NSLog(" %@", obj.path as NSString)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//}
|
||||
|
||||
|
||||
@@ -11,13 +11,7 @@
|
||||
C71848442B8D2D600046CB13 /* ZombieView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C71848432B8D2D600046CB13 /* ZombieView.swift */; };
|
||||
C72893052AFD42E600C8C1CD /* DeepLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = C72893042AFD42E600C8C1CD /* DeepLink.swift */; };
|
||||
C72893122AFD51EA00C8C1CD /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = C72893112AFD51EA00C8C1CD /* main.swift */; };
|
||||
C72F4B6C2C5DF4ED00B04F60 /* SunburstChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = C72F4B6B2C5DF4ED00B04F60 /* SunburstChart.swift */; };
|
||||
C74412C72BBF249000DDFCA8 /* AppPathsFetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C74412C62BBF249000DDFCA8 /* AppPathsFetch.swift */; };
|
||||
C74D76B72C5C4ABC00748DD1 /* TreeMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = C74D76B62C5C4ABC00748DD1 /* TreeMap.swift */; };
|
||||
C74D76BD2C5D2DF300748DD1 /* FileSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C74D76B92C5D2DF300748DD1 /* FileSearch.swift */; };
|
||||
C74D76BE2C5D2DF300748DD1 /* PearDiskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C74D76BA2C5D2DF300748DD1 /* PearDiskView.swift */; };
|
||||
C74D76BF2C5D2DF300748DD1 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = C74D76BB2C5D2DF300748DD1 /* Data.swift */; };
|
||||
C74D76C02C5D2DF300748DD1 /* FolderDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = C74D76BC2C5D2DF300748DD1 /* FolderDetail.swift */; };
|
||||
C74FDC632BCD833D00B8960F /* Conditions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C74FDC622BCD833D00B8960F /* Conditions.swift */; };
|
||||
C7575D392B0182DE006A600A /* PearcleanerSentinel in CopyFiles */ = {isa = PBXBuildFile; fileRef = C728930F2AFD51EA00C8C1CD /* PearcleanerSentinel */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
|
||||
C76D08482AF83C3F00D07867 /* Update.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76D08472AF83C3F00D07867 /* Update.swift */; };
|
||||
@@ -39,6 +33,7 @@
|
||||
C79947962C34A794007279CC /* Tips.swift in Sources */ = {isa = PBXBuildFile; fileRef = C79947952C34A794007279CC /* Tips.swift */; };
|
||||
C7A27E812AFD7C4600166168 /* com.alienator88.PearcleanerSentinel.plist in Resources */ = {isa = PBXBuildFile; fileRef = C7A27E802AFD7C4600166168 /* com.alienator88.PearcleanerSentinel.plist */; };
|
||||
C7A2AE5D2B9644F300161540 /* FileWatcher in Frameworks */ = {isa = PBXBuildFile; productRef = C7A2AE5C2B9644F300161540 /* FileWatcher */; };
|
||||
C7A6DBF12C9DD27200CFA042 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C7A6DBF02C9DD27200CFA042 /* Assets.xcassets */; };
|
||||
C7CF47242B3B3F1700979C5F /* WindowSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7CF47232B3B3F1700979C5F /* WindowSettings.swift */; };
|
||||
C7D31D4A2AFEB26700C7ED9E /* AppListItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7D31D492AFEB26700C7ED9E /* AppListItems.swift */; };
|
||||
C7D31D512AFF00F300C7ED9E /* Locations.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7D31D502AFF00F300C7ED9E /* Locations.swift */; };
|
||||
@@ -102,13 +97,7 @@
|
||||
C72893042AFD42E600C8C1CD /* DeepLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLink.swift; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
C72F4B6B2C5DF4ED00B04F60 /* SunburstChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SunburstChart.swift; sourceTree = "<group>"; };
|
||||
C74412C62BBF249000DDFCA8 /* AppPathsFetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPathsFetch.swift; sourceTree = "<group>"; };
|
||||
C74D76B62C5C4ABC00748DD1 /* TreeMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TreeMap.swift; sourceTree = "<group>"; };
|
||||
C74D76B92C5D2DF300748DD1 /* FileSearch.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileSearch.swift; sourceTree = "<group>"; };
|
||||
C74D76BA2C5D2DF300748DD1 /* PearDiskView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PearDiskView.swift; sourceTree = "<group>"; };
|
||||
C74D76BB2C5D2DF300748DD1 /* Data.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Data.swift; sourceTree = "<group>"; };
|
||||
C74D76BC2C5D2DF300748DD1 /* FolderDetail.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FolderDetail.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>"; };
|
||||
C76D08492AF83C6E00D07867 /* General.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = General.swift; sourceTree = "<group>"; };
|
||||
@@ -133,6 +122,7 @@
|
||||
C79947952C34A794007279CC /* Tips.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tips.swift; sourceTree = "<group>"; };
|
||||
C7A27E7F2AFD70DD00166168 /* PearcleanerSentinel.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = PearcleanerSentinel.entitlements; sourceTree = "<group>"; };
|
||||
C7A27E802AFD7C4600166168 /* com.alienator88.PearcleanerSentinel.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = com.alienator88.PearcleanerSentinel.plist; sourceTree = "<group>"; };
|
||||
C7A6DBF02C9DD27200CFA042 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
C7CF47232B3B3F1700979C5F /* WindowSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowSettings.swift; sourceTree = "<group>"; };
|
||||
C7D31D492AFEB26700C7ED9E /* AppListItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppListItems.swift; sourceTree = "<group>"; };
|
||||
C7D31D502AFF00F300C7ED9E /* Locations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Locations.swift; sourceTree = "<group>"; };
|
||||
@@ -211,19 +201,6 @@
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
C74D76B82C5D2D7900748DD1 /* PearDisk */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C74D76BA2C5D2DF300748DD1 /* PearDiskView.swift */,
|
||||
C74D76BB2C5D2DF300748DD1 /* Data.swift */,
|
||||
C74D76B92C5D2DF300748DD1 /* FileSearch.swift */,
|
||||
C74D76B62C5C4ABC00748DD1 /* TreeMap.swift */,
|
||||
C74D76BC2C5D2DF300748DD1 /* FolderDetail.swift */,
|
||||
C72F4B6B2C5DF4ED00B04F60 /* SunburstChart.swift */,
|
||||
);
|
||||
path = PearDisk;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
C76D08462AF83C2F00D07867 /* Settings */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -243,8 +220,8 @@
|
||||
children = (
|
||||
C7A27E802AFD7C4600166168 /* com.alienator88.PearcleanerSentinel.plist */,
|
||||
C77B90142AF18E7C009CC655 /* Info.plist */,
|
||||
C77B90072AF18E2F009CC655 /* Assets.xcassets */,
|
||||
C77B90242AF2D796009CC655 /* Pearcleaner.entitlements */,
|
||||
C77B90072AF18E2F009CC655 /* Assets.xcassets */,
|
||||
);
|
||||
path = Resources;
|
||||
sourceTree = "<group>";
|
||||
@@ -277,7 +254,6 @@
|
||||
C786F1A72C29E599001B3800 /* Builds */,
|
||||
C76D084F2AF84E0100D07867 /* Resources */,
|
||||
C76D08462AF83C2F00D07867 /* Settings */,
|
||||
C74D76B82C5D2D7900748DD1 /* PearDisk */,
|
||||
C77B901F2AF1B390009CC655 /* Views */,
|
||||
C72893072AFD442600C8C1CD /* Logic */,
|
||||
);
|
||||
@@ -305,6 +281,7 @@
|
||||
C78121652BC892A000BE06BD /* FinderOpen.swift */,
|
||||
C78121672BC892A000BE06BD /* Info.plist */,
|
||||
C78121682BC892A000BE06BD /* FinderOpen.entitlements */,
|
||||
C7A6DBF02C9DD27200CFA042 /* Assets.xcassets */,
|
||||
);
|
||||
path = FinderOpen;
|
||||
sourceTree = "<group>";
|
||||
@@ -432,6 +409,7 @@
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
C7A6DBF12C9DD27200CFA042 /* Assets.xcassets in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -451,21 +429,16 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
C7DD49EA2BAB4D8100CCBA16 /* AppInfoFetch.swift in Sources */,
|
||||
C74D76BD2C5D2DF300748DD1 /* FileSearch.swift in Sources */,
|
||||
C79947962C34A794007279CC /* Tips.swift in Sources */,
|
||||
C76D08482AF83C3F00D07867 /* Update.swift in Sources */,
|
||||
C7FB173B2B96321300B96F9A /* AppsListView.swift in Sources */,
|
||||
C77B90162AF19377009CC655 /* AppState.swift in Sources */,
|
||||
C72F4B6C2C5DF4ED00B04F60 /* SunburstChart.swift in Sources */,
|
||||
C74D76B72C5C4ABC00748DD1 /* TreeMap.swift in Sources */,
|
||||
C74D76C02C5D2DF300748DD1 /* FolderDetail.swift in Sources */,
|
||||
C7DD49EE2BAB7F6000CCBA16 /* ReversePathsFetch.swift in Sources */,
|
||||
C7EA06332C1CBE2000F872FC /* ConditionBuilderView.swift in Sources */,
|
||||
C7DD49F02BABA14400CCBA16 /* Folders.swift in Sources */,
|
||||
C7D31D512AFF00F300C7ED9E /* Locations.swift in Sources */,
|
||||
C77B901A2AF1938F009CC655 /* Logic.swift in Sources */,
|
||||
C77B901C2AF193A3009CC655 /* Styles.swift in Sources */,
|
||||
C74D76BF2C5D2DF300748DD1 /* Data.swift in Sources */,
|
||||
C7045A282B03E71D00376976 /* MiniMode.swift in Sources */,
|
||||
C7ED86B12BA8A35500F13DE4 /* Interface.swift in Sources */,
|
||||
C72893052AFD42E600C8C1CD /* DeepLink.swift in Sources */,
|
||||
@@ -475,7 +448,6 @@
|
||||
C77B90042AF18E2E009CC655 /* PearcleanerApp.swift in Sources */,
|
||||
C76D08512AF850F300D07867 /* About.swift in Sources */,
|
||||
C77B90182AF19382009CC655 /* AppCommands.swift in Sources */,
|
||||
C74D76BE2C5D2DF300748DD1 /* PearDiskView.swift in Sources */,
|
||||
C7D31D4A2AFEB26700C7ED9E /* AppListItems.swift in Sources */,
|
||||
C77B901E2AF1A9DB009CC655 /* SettingsWindow.swift in Sources */,
|
||||
C74412C72BBF249000DDFCA8 /* AppPathsFetch.swift in Sources */,
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import FinderSync
|
||||
//import FinderSync
|
||||
|
||||
let home = FileManager.default.homeDirectoryForCurrentUser.path
|
||||
|
||||
@@ -79,10 +79,28 @@ class AppState: ObservableObject {
|
||||
}
|
||||
|
||||
@objc func updateExtensionStatus() {
|
||||
let extensionStatus = FIFinderSyncController.isExtensionEnabled
|
||||
DispatchQueue.main.async {
|
||||
self.finderExtensionEnabled = extensionStatus
|
||||
let task = Process()
|
||||
let pipe = Pipe()
|
||||
|
||||
task.launchPath = "/bin/zsh"
|
||||
task.arguments = ["-c", "pluginkit -m -i com.alienator88.Pearcleaner"]
|
||||
task.standardOutput = pipe
|
||||
task.launch()
|
||||
|
||||
let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
||||
task.waitUntilExit()
|
||||
|
||||
if let output = String(data: data, encoding: .utf8) {
|
||||
// Check if the output starts with a '+' indicating it's enabled
|
||||
let extensionStatus = output.contains("+")
|
||||
DispatchQueue.main.async {
|
||||
self.finderExtensionEnabled = extensionStatus
|
||||
}
|
||||
}
|
||||
// let extensionStatus = FIFinderSyncController.isExtensionEnabled
|
||||
// DispatchQueue.main.async {
|
||||
// self.finderExtensionEnabled = extensionStatus
|
||||
// }
|
||||
}
|
||||
|
||||
func triggerUninstallAlert() {
|
||||
|
||||
@@ -27,12 +27,14 @@ func reloadAppsList(appState: AppState, fsm: FolderSettingsManager) {
|
||||
|
||||
func resizeWindowAuto(windowSettings: WindowSettings, title: String) {
|
||||
if let window = NSApplication.shared.windows.first(where: { $0.title == title }) {
|
||||
print(windowSettings.loadWindowSettings())
|
||||
let newSize = NSSize(width: windowSettings.loadWindowSettings().width, height: windowSettings.loadWindowSettings().height)
|
||||
window.setContentSize(newSize)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Check if Pearcleaner has any windows open
|
||||
func hasWindowOpen() -> Bool {
|
||||
for window in NSApp.windows where window.title == "Pearcleaner" {
|
||||
@@ -260,6 +262,18 @@ func manageSymlink(install: Bool) {
|
||||
}
|
||||
|
||||
|
||||
// FinderExtension Sequoia Fix
|
||||
func manageFinderPlugin(install: Bool) {
|
||||
let task = Process()
|
||||
task.launchPath = "/usr/bin/pluginkit"
|
||||
|
||||
task.arguments = ["-e", "\(install ? "use" : "ignore")", "-i", "com.alienator88.Pearcleaner"]
|
||||
|
||||
task.launch()
|
||||
task.waitUntilExit()
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Brew cleanup
|
||||
func caskCleanup(app: String) {
|
||||
|
||||
@@ -13,28 +13,20 @@ import Combine
|
||||
class WindowSettings {
|
||||
private let windowWidthKey = "windowWidthKey"
|
||||
private let windowHeightKey = "windowHeightKey"
|
||||
private let windowWidthKeyMini = "windowWidthKeyMini"
|
||||
private let windowHeightKeyMini = "windowHeightKeyMini"
|
||||
private let windowXKey = "windowXKey"
|
||||
private let windowYKey = "windowYKey"
|
||||
@AppStorage("settings.general.mini") private var mini: Bool = false
|
||||
var windows: [NSWindow] = []
|
||||
|
||||
// Register default sizes if the AppStorage keys are invalid
|
||||
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 defaultHeight = Float(628) // Default height for regular 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)
|
||||
@@ -42,60 +34,69 @@ class WindowSettings {
|
||||
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.height), forKey: mini ? windowHeightKeyMini : windowHeightKey)
|
||||
UserDefaults.standard.set(Float(frame.size.width), forKey: windowWidthKey)
|
||||
UserDefaults.standard.set(Float(frame.size.height), forKey: windowHeightKey)
|
||||
UserDefaults.standard.set(Float(frame.origin.x), forKey: windowXKey)
|
||||
UserDefaults.standard.set(Float(frame.origin.y), forKey: windowYKey)
|
||||
}
|
||||
|
||||
// Load default window settings or user defined settings
|
||||
func loadWindowSettings() -> NSRect {
|
||||
let width = CGFloat(UserDefaults.standard.float(forKey: mini ? windowWidthKeyMini : windowWidthKey))
|
||||
let height = CGFloat(UserDefaults.standard.float(forKey: mini ? windowHeightKeyMini : windowHeightKey))
|
||||
let width = CGFloat(UserDefaults.standard.float(forKey: windowWidthKey))
|
||||
let height = CGFloat(UserDefaults.standard.float(forKey: windowHeightKey))
|
||||
let x = CGFloat(UserDefaults.standard.float(forKey: windowXKey))
|
||||
let y = CGFloat(UserDefaults.standard.float(forKey: windowYKey))
|
||||
|
||||
return NSRect(x: x, y: y, width: width, height: height)
|
||||
}
|
||||
|
||||
// Reset to default sizes when toggling the window state
|
||||
func resetWindowSettings(mini: Bool) -> NSRect {
|
||||
let screenFrame = NSScreen.main?.visibleFrame ?? NSRect(x: 0, y: 0, width: 800, height: 600)
|
||||
// Define sizes based on whether mini mode is active
|
||||
let defaultWidth: CGFloat = mini ? 300 : 900 // Mini or Regular width
|
||||
let defaultHeight: CGFloat = mini ? 373 : 628 // Mini or Regular height
|
||||
// Calculate the X and Y coordinates based on the size
|
||||
let defaultX = (screenFrame.width - defaultWidth) / 2 + screenFrame.origin.x
|
||||
let defaultY = (screenFrame.height - defaultHeight) / 2 + screenFrame.origin.y
|
||||
// Store these values in UserDefaults (if needed)
|
||||
UserDefaults.standard.set(Float(defaultWidth), forKey: windowWidthKey)
|
||||
UserDefaults.standard.set(Float(defaultHeight), forKey: windowHeightKey)
|
||||
UserDefaults.standard.set(Float(defaultX), forKey: windowXKey)
|
||||
UserDefaults.standard.set(Float(defaultY), forKey: windowYKey)
|
||||
// Return the frame for the window with appropriate size and position
|
||||
return NSRect(x: defaultX, y: defaultY, width: defaultWidth, height: defaultHeight)
|
||||
}
|
||||
|
||||
// Launch new app windows on demand
|
||||
func newWindow<V: View>(withView view: @escaping () -> V) {
|
||||
func newWindow<V: View>(mini: Bool, withView view: @escaping () -> V) {
|
||||
// Close existing window
|
||||
findAndHideWindows(named: ["Pearcleaner"])
|
||||
// Create new window using defaults
|
||||
let contentView = view
|
||||
let frame = self.loadWindowSettings()
|
||||
let frame = self.resetWindowSettings(mini: mini)
|
||||
let newWindow = NSWindow(
|
||||
contentRect: frame,
|
||||
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
|
||||
backing: .buffered, defer: false)
|
||||
newWindow.contentView = NSHostingView(rootView: contentView())
|
||||
newWindow.titlebarAppearsTransparent = true
|
||||
newWindow.isMovableByWindowBackground = true
|
||||
newWindow.center()
|
||||
newWindow.title = "Pearcleaner"
|
||||
newWindow.isRestorable = false
|
||||
newWindow.titleVisibility = .hidden
|
||||
newWindow.setFrameAutosaveName("Pearcleaner")
|
||||
newWindow.contentView = NSHostingView(rootView: contentView())
|
||||
self.windows.append(newWindow)
|
||||
newWindow.titleVisibility = .hidden
|
||||
newWindow.makeKeyAndOrderFront(nil)
|
||||
resizeWindowAuto(windowSettings: self, title: "Pearcleaner")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,184 +0,0 @@
|
||||
////
|
||||
//// Item.swift
|
||||
//// PearDisk
|
||||
////
|
||||
//// Created by Alin Lupascu on 7/17/24.
|
||||
////
|
||||
//
|
||||
//import Foundation
|
||||
//import SwiftData
|
||||
//
|
||||
//// Data Model
|
||||
//@Model
|
||||
//final class Item: Identifiable {
|
||||
// @Attribute(.unique) let url: URL
|
||||
// let name: String
|
||||
// var size: Int64
|
||||
// let isDirectory: Bool
|
||||
// let parentURL: URL?
|
||||
// let timestamp: Date
|
||||
//
|
||||
// init(url: URL, name: String, size: Int64, isDirectory: Bool, parentURL: URL? = nil) {
|
||||
// self.url = url
|
||||
// self.name = name
|
||||
// self.size = size
|
||||
// self.isDirectory = isDirectory
|
||||
// self.parentURL = parentURL
|
||||
// self.timestamp = Date()
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//
|
||||
//// Wipe peardisk.sqlite file
|
||||
//func wipeSwiftDataStorage() async {
|
||||
// let fileManager = FileManager.default
|
||||
// guard let urlApp = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first else {
|
||||
// print("Could not find the Application Support directory.")
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// let peardiskURL = urlApp.appendingPathComponent("PearDisk").appendingPathComponent("Database")
|
||||
//
|
||||
// do {
|
||||
//
|
||||
// if fileManager.fileExists(atPath: peardiskURL.path) {
|
||||
// try fileManager.removeItem(at: peardiskURL)
|
||||
// print("\(peardiskURL.lastPathComponent) has been deleted.")
|
||||
// }
|
||||
// } catch {
|
||||
// print("Error deleting SwiftData storage: \(error)")
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//func wipeSwiftDataStorage2() async {
|
||||
// guard let container = try? ModelContainer(for: Item.self) else {
|
||||
// print("Failed to create ModelContainer.")
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// let urlApp = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first
|
||||
//
|
||||
// if let urlApp = urlApp {
|
||||
// let url = urlApp.appendingPathComponent("PearDisk").appendingPathComponent("peardisk.sqlite")
|
||||
// if FileManager.default.fileExists(atPath: url.path) {
|
||||
// print("Peardisk.sqlite: \(url.path(percentEncoded: false))")
|
||||
// }
|
||||
// } else {
|
||||
// fatalError("Could not find the Application Support directory.")
|
||||
// }
|
||||
//
|
||||
//
|
||||
// await MainActor.run {
|
||||
// let context = container.mainContext
|
||||
//
|
||||
// do {
|
||||
// try context.delete(model: Item.self)
|
||||
// try context.save()
|
||||
// print("Peardisk.sqlite has been successfully wiped.")
|
||||
// } catch {
|
||||
// print("Error wiping SwiftData storage: \(error)")
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//// Print size of peardisk.sqlite file
|
||||
//func printSwiftDataCacheSize() async {
|
||||
// // Get the Application Support directory URL for your app
|
||||
// guard let applicationSupportDirectory = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first else {
|
||||
// print("Could not find the Application Support directory.")
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// // Create the custom directory URL
|
||||
// let customDirectoryURL = applicationSupportDirectory.appendingPathComponent("PearDisk").appendingPathComponent("Database")
|
||||
// let storeURL = customDirectoryURL.appendingPathComponent("peardisk").appendingPathExtension("sqlite")
|
||||
//
|
||||
// // Check if the store URL exists
|
||||
// guard FileManager.default.fileExists(atPath: storeURL.path) else {
|
||||
// print("Store file does not exist.")
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// // Initialize the ModelContainer with the custom store URL
|
||||
// let modelConfiguration = ModelConfiguration(url: storeURL)
|
||||
//
|
||||
// do {
|
||||
// let container = try ModelContainer(for: Item.self, configurations: modelConfiguration)
|
||||
//
|
||||
// await MainActor.run {
|
||||
// let context = container.mainContext
|
||||
// let fetchDescriptor = FetchDescriptor<Item>()
|
||||
//
|
||||
// do {
|
||||
// let count = try context.fetchCount(fetchDescriptor)
|
||||
// print("Number of Item objects: \(count)")
|
||||
//
|
||||
// // Get the size of the store file directly
|
||||
// let fileManager = FileManager.default
|
||||
// do {
|
||||
// let attributes = try fileManager.attributesOfItem(atPath: storeURL.path)
|
||||
// if let size = attributes[.size] as? Int64 {
|
||||
// let sizeMB = Double(size) / (1024 * 1024)
|
||||
// print("Total size of SwiftData storage: \(String(format: "%.2f", sizeMB)) MB")
|
||||
// } else {
|
||||
// print("Could not retrieve file size.")
|
||||
// }
|
||||
// } catch {
|
||||
// print("Could not access file at \(storeURL.path): \(error)")
|
||||
// }
|
||||
// } catch {
|
||||
// print("Error fetching data: \(error)")
|
||||
// }
|
||||
// }
|
||||
// } catch {
|
||||
// print("Could not create ModelContainer: \(error)")
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//
|
||||
//
|
||||
//// Setup Data Model
|
||||
//func createSharedModelContainer() -> ModelContainer {
|
||||
// do {
|
||||
// // Get the Application Support directory URL for your app
|
||||
// guard let applicationSupportDirectory = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first else {
|
||||
// fatalError("Could not find the Application Support directory.")
|
||||
// }
|
||||
//
|
||||
// // Create the custom directory URL
|
||||
// let customDirectoryURL = applicationSupportDirectory.appendingPathComponent("PearDisk", isDirectory: true).appendingPathComponent("Database", isDirectory: true)
|
||||
//
|
||||
// // Create the directory if it doesn't exist
|
||||
// do {
|
||||
// try FileManager.default.createDirectory(at: customDirectoryURL, withIntermediateDirectories: true, attributes: nil)
|
||||
// } catch {
|
||||
// fatalError("Could not create directory: \(error)")
|
||||
// }
|
||||
//
|
||||
// // Create the store URL with the desired filename inside the custom directory
|
||||
// let storeURL = customDirectoryURL.appendingPathComponent("peardisk").appendingPathExtension("sqlite")
|
||||
//
|
||||
// // Ensure the directory is accessible for reading and writing
|
||||
// let fileManager = FileManager.default
|
||||
// var isDirectory: ObjCBool = true
|
||||
// if !fileManager.fileExists(atPath: customDirectoryURL.path, isDirectory: &isDirectory) {
|
||||
// fatalError("Directory does not exist.")
|
||||
// }
|
||||
//
|
||||
// if !fileManager.isWritableFile(atPath: customDirectoryURL.path) {
|
||||
// fatalError("Directory is not writable.")
|
||||
// }
|
||||
//
|
||||
// if !fileManager.isReadableFile(atPath: customDirectoryURL.path) {
|
||||
// fatalError("Directory is not readable.")
|
||||
// }
|
||||
//
|
||||
// let modelConfiguration = ModelConfiguration(url: storeURL)
|
||||
// return try ModelContainer(for: Item.self, configurations: modelConfiguration)
|
||||
// } catch {
|
||||
// fatalError("Could not create ModelContainer: \(error)")
|
||||
// }
|
||||
//}
|
||||
@@ -1,87 +0,0 @@
|
||||
//
|
||||
// FileSearch.swift
|
||||
// Pearcleaner
|
||||
//
|
||||
// Created by Alin Lupascu on 7/20/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftData
|
||||
import AlinFoundation
|
||||
|
||||
enum FastFileSearchError: Error {
|
||||
case invalidPath
|
||||
case failedToReadDirectory
|
||||
case failedToGetFileInfo
|
||||
}
|
||||
|
||||
class FastFileSearch {
|
||||
private let queue = DispatchQueue(label: "com.alienator88.Pearcleaner.disk", qos: .userInitiated, attributes: .concurrent)
|
||||
|
||||
func search(url: URL, completion: @escaping (Result<[Item], Error>) -> Void) {
|
||||
queue.async {
|
||||
do {
|
||||
let items = try self.searchRecursive(url: url)
|
||||
let sortedItems = items.sorted { $0.size > $1.size }
|
||||
DispatchQueue.main.async {
|
||||
completion(.success(sortedItems))
|
||||
}
|
||||
} catch {
|
||||
DispatchQueue.main.async {
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func searchRecursive(url: URL) throws -> [Item] {
|
||||
var items: [Item] = []
|
||||
|
||||
guard let path = url.path.cString(using: .utf8) else {
|
||||
throw FastFileSearchError.invalidPath
|
||||
}
|
||||
|
||||
guard let dir = opendir(path) else {
|
||||
throw FastFileSearchError.failedToReadDirectory
|
||||
}
|
||||
defer { closedir(dir) }
|
||||
|
||||
while let entry = readdir(dir) {
|
||||
let name = withUnsafePointer(to: entry.pointee.d_name) { ptr in
|
||||
ptr.withMemoryRebound(to: CChar.self, capacity: Int(entry.pointee.d_namlen)) {
|
||||
String(cString: $0)
|
||||
}
|
||||
}
|
||||
|
||||
let exclude = [".", "..", ".DS_Store", ".localized"]
|
||||
if exclude.contains(name) { continue }
|
||||
|
||||
let fullURL = url.appendingPathComponent(name)
|
||||
let fullPath = fullURL.path
|
||||
|
||||
var st = stat()
|
||||
guard lstat(fullPath, &st) == 0 else {
|
||||
print("Failed to get file info for \(fullPath)")
|
||||
continue
|
||||
}
|
||||
|
||||
let isDirectory = (st.st_mode & S_IFMT) == S_IFDIR
|
||||
if isDirectory {
|
||||
// Recursively search subdirectories
|
||||
do {
|
||||
let subItems = try searchRecursive(url: fullURL)
|
||||
items.append(contentsOf: subItems)
|
||||
} catch {
|
||||
print("Error processing directory \(fullPath): \(error)")
|
||||
}
|
||||
} else {
|
||||
// Add non-directory files to the items array
|
||||
let size = Int64(st.st_size)
|
||||
let item = Item(url: fullURL, name: name, size: size)
|
||||
items.append(item)
|
||||
}
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
}
|
||||
@@ -1,203 +0,0 @@
|
||||
//
|
||||
// FolderDetailView.swift
|
||||
// Pearcleaner
|
||||
//
|
||||
// Created by Alin Lupascu on 7/20/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
//MARK: Detail View
|
||||
struct FolderDetailView: View {
|
||||
let rootURL: URL
|
||||
@Binding var currentPath: URL?
|
||||
@State private var currentItem: Item?
|
||||
@State private var childItems: [Item] = []
|
||||
@State private var isLoading = false
|
||||
@State private var errorMessage: String?
|
||||
@State private var forwardStack: [URL] = []
|
||||
@State private var hoveredItem: Item?
|
||||
|
||||
private var fastSearch: FastFileSearch
|
||||
|
||||
init(rootURL: URL, currentPath: Binding<URL?>) {
|
||||
self.rootURL = rootURL
|
||||
self._currentPath = currentPath
|
||||
self.fastSearch = FastFileSearch()
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
|
||||
Spacer()
|
||||
|
||||
if isLoading {
|
||||
ProgressView("Loading...")
|
||||
} else if let errorMessage = errorMessage {
|
||||
Text(errorMessage)
|
||||
.foregroundColor(.red)
|
||||
.padding()
|
||||
} else if let currentItem = currentItem {
|
||||
if !childItems.isEmpty {
|
||||
|
||||
TreeMapChart(items: childItems, onItemSelected: { selectedItem in
|
||||
NSWorkspace.shared.selectFile(selectedItem.url.path, inFileViewerRootedAtPath: selectedItem.url.deletingLastPathComponent().path)
|
||||
}, hoveredItem: $hoveredItem)
|
||||
|
||||
HStack {
|
||||
Text("Total size: \(ByteCountFormatter.string(fromByteCount: currentItem.size, countStyle: .file))")
|
||||
Spacer()
|
||||
|
||||
if let hoveredItem = hoveredItem {
|
||||
Text("\(hoveredItem.name) (\(formatSize(hoveredItem.size)))")
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
VStack(alignment: .trailing) {
|
||||
HStack {
|
||||
RoundedRectangle(cornerRadius: 2)
|
||||
.fill(LinearGradient(
|
||||
gradient: Gradient(colors: [Color.cyan, Color.blue]),
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
))
|
||||
.frame(width: 10, height: 10)
|
||||
Text("Directory")
|
||||
.padding(.trailing)
|
||||
RoundedRectangle(cornerRadius: 2)
|
||||
.fill(LinearGradient(
|
||||
gradient: Gradient(colors: [Color.pink, Color.orange]),
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
))
|
||||
.frame(width: 10, height: 10)
|
||||
Text("File")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
.padding(5)
|
||||
|
||||
} else {
|
||||
Text("This folder is empty or there was an error loading its contents.")
|
||||
.padding(5)
|
||||
}
|
||||
} else {
|
||||
Text("No data available")
|
||||
.padding(5)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
.onChange(of: currentPath) { newValue in
|
||||
if let newPath = newValue {
|
||||
loadContents(url: newPath)
|
||||
}
|
||||
}
|
||||
// Might need this in another app where I won't use a Navigation View
|
||||
// .onAppear {
|
||||
// loadContents(url: rootURL)
|
||||
// }
|
||||
.toolbar {
|
||||
|
||||
// ToolbarItem(placement: .navigation) {
|
||||
// Button {
|
||||
// goBack()
|
||||
// } label: {
|
||||
// Image(systemName: "arrow.left")
|
||||
// }
|
||||
// .disabled(currentPath == rootURL)
|
||||
// }
|
||||
|
||||
// ToolbarItem(placement: .navigation) {
|
||||
// Button {
|
||||
// goForward()
|
||||
// } label: {
|
||||
// Image(systemName: "arrow.right")
|
||||
// }
|
||||
// .disabled(forwardStack.isEmpty)
|
||||
// }
|
||||
|
||||
ToolbarItem(placement: .principal) {
|
||||
if let currentPath = currentPath {
|
||||
Text(currentPath.path)
|
||||
.font(.headline)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.middle)
|
||||
.padding(5)
|
||||
.padding(.horizontal, 2)
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.fill(.primary)
|
||||
.opacity(0.05)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
ToolbarItemGroup(placement: .automatic) {
|
||||
Spacer()
|
||||
|
||||
Button {
|
||||
printItemsCount()
|
||||
} label: {
|
||||
Image(systemName: "number.square")
|
||||
}
|
||||
|
||||
Button {
|
||||
refreshCurrentFolder()
|
||||
} label: {
|
||||
Image(systemName: "arrow.counterclockwise")
|
||||
}
|
||||
// Button {
|
||||
// Task {
|
||||
// await wipeSwiftDataStorage()
|
||||
// }
|
||||
// } label: {
|
||||
// Image(systemName: "xmark.bin")
|
||||
// }
|
||||
// Button {
|
||||
// Task {
|
||||
// await printSwiftDataCacheSize()
|
||||
// }
|
||||
// } label: {
|
||||
// Image(systemName: "number.square")
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func loadContents(url: URL, wipeCache: Bool = false) {
|
||||
isLoading = true
|
||||
errorMessage = nil
|
||||
|
||||
fastSearch.search(url: url) { result in
|
||||
switch result {
|
||||
case .success(let items):
|
||||
self.childItems = items
|
||||
self.currentPath = url
|
||||
self.currentItem = Item(url: url, name: url.lastPathComponent, size: items.reduce(0) { $0 + $1.size })
|
||||
print("Items loaded")
|
||||
case .failure(let error):
|
||||
self.errorMessage = "Error loading contents: \(error.localizedDescription)"
|
||||
print("Error loading contents: \(error)")
|
||||
}
|
||||
self.isLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
private func refreshCurrentFolder() {
|
||||
if let currentPath = currentPath {
|
||||
loadContents(url: currentPath, wipeCache: true)
|
||||
}
|
||||
}
|
||||
|
||||
private func printItemsCount() {
|
||||
print("Number of items: \(childItems.count)")
|
||||
}
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
//
|
||||
// PearDiskView.swift
|
||||
// Pearcleaner
|
||||
//
|
||||
// Created by Alin Lupascu on 7/17/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import AlinFoundation
|
||||
|
||||
//MARK: Main View
|
||||
struct PearDiskView: View {
|
||||
@EnvironmentObject var themeManager: ThemeManager
|
||||
@State private var selectedVolume: URL?
|
||||
@State private var currentPath: URL?
|
||||
|
||||
var body: some View {
|
||||
NavigationSplitView {
|
||||
SidebarView(selectedVolume: $selectedVolume)
|
||||
} detail: {
|
||||
if let volume = selectedVolume {
|
||||
FolderDetailView(rootURL: volume, currentPath: $currentPath)
|
||||
} else {
|
||||
Text("Select a volume")
|
||||
}
|
||||
}
|
||||
.onChange(of: selectedVolume) { path in
|
||||
currentPath = path
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//MARK: Sidebar
|
||||
struct SidebarView: View {
|
||||
@EnvironmentObject var themeManager: ThemeManager
|
||||
@Binding var selectedVolume: URL?
|
||||
@State private var volumes: [URL] = []
|
||||
|
||||
var body: some View {
|
||||
List(volumes, id: \.self) { volume in
|
||||
Text(volume.lastPathComponent)
|
||||
.onTapGesture {
|
||||
selectedVolume = volume
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
loadVolumes()
|
||||
}
|
||||
.background(backgroundView(themeManager: themeManager))
|
||||
}
|
||||
|
||||
private func loadVolumes() {
|
||||
let fileManager = FileManager.default
|
||||
|
||||
// Define the base paths for the directories you want to scan
|
||||
let basePaths = [
|
||||
URL(fileURLWithPath: "/Volumes"),
|
||||
URL(fileURLWithPath: "/Users"),
|
||||
// URL(fileURLWithPath: "/Users/alin"),
|
||||
URL(fileURLWithPath: "/"),
|
||||
]
|
||||
|
||||
for basePath in basePaths {
|
||||
do {
|
||||
let urls = try fileManager.contentsOfDirectory(at: basePath, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles])
|
||||
|
||||
// Iterate over each URL in the directory
|
||||
for url in urls {
|
||||
if url.isSymlink() {
|
||||
// If the item is a symbolic link, resolve it
|
||||
let resolvedUrl = url.resolvingSymlinksInPath()
|
||||
volumes.append(resolvedUrl)
|
||||
} else {
|
||||
volumes.append(url)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
print("Error loading volumes: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
extension URL {
|
||||
public func isSymlink() -> Bool {
|
||||
do {
|
||||
let _ = try self.checkResourceIsReachable()
|
||||
let resourceValues = try self.resourceValues(forKeys: [.isSymbolicLinkKey])
|
||||
return resourceValues.isSymbolicLink == true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
////
|
||||
//// SunburstChart.swift
|
||||
//// Pearcleaner
|
||||
////
|
||||
//// Created by Alin Lupascu on 8/2/24.
|
||||
////
|
||||
//
|
||||
//
|
||||
//import SwiftUI
|
||||
//import Charts
|
||||
//
|
||||
//final class Item: Identifiable {
|
||||
// let id = UUID()
|
||||
// let url: URL
|
||||
// let name: String
|
||||
// var size: Int64
|
||||
// let isDirectory: Bool?
|
||||
//
|
||||
// init(url: URL, name: String, size: Int64, isDirectory: Bool = false) {
|
||||
// self.url = url
|
||||
// self.name = name
|
||||
// self.size = size
|
||||
// self.isDirectory = isDirectory
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//struct SunburstChart: View {
|
||||
// let chartWidth: CGFloat = 175
|
||||
// let items: [Item]
|
||||
//
|
||||
// @State private var selectedTotal1: Double?
|
||||
// @State private var selectedSegment1: Item?
|
||||
//
|
||||
// @State private var selectedTotal2: Double?
|
||||
// @State private var selectedSegment2: Item?
|
||||
//
|
||||
// init(items: [Item]) {
|
||||
// self.items = items
|
||||
// }
|
||||
//
|
||||
// var body: some View {
|
||||
// VStack {
|
||||
// Text(selectedText)
|
||||
// .font(.title)
|
||||
// Text(selectedSizeText)
|
||||
// .font(.title3)
|
||||
//
|
||||
// ZStack {
|
||||
// Chart(getFiles()) { item in
|
||||
// SectorMark(
|
||||
// angle: .value("Size", Double(item.size)),
|
||||
// innerRadius: .ratio(0.6),
|
||||
// outerRadius: .ratio(0.9),
|
||||
// angularInset: 1.5
|
||||
// )
|
||||
// .foregroundStyle(Color.blue.opacity(selectOpacity(item: item, selectedItem: selectedSegment2, ring: 2)))
|
||||
// }
|
||||
// .frame(width: chartWidth * 2, height: chartWidth * 2)
|
||||
// .chartAngleSelection(value: $selectedTotal2)
|
||||
// .onChange(of: selectedTotal2) { newValue in
|
||||
// updateSelection(newValue: newValue, items: getFiles(), updateSelected: { selectedSegment2 = $0 })
|
||||
// selectedSegment1 = nil
|
||||
// }
|
||||
//
|
||||
// Chart(getDirectories()) { item in
|
||||
// SectorMark(
|
||||
// angle: .value("Size", Double(item.size)),
|
||||
// innerRadius: .ratio(0.5),
|
||||
// angularInset: 1.5
|
||||
// )
|
||||
// .foregroundStyle(Color.green.opacity(selectOpacity(item: item, selectedItem: selectedSegment1)))
|
||||
// }
|
||||
// .frame(width: chartWidth, height: chartWidth)
|
||||
// .chartAngleSelection(value: $selectedTotal1)
|
||||
// .onChange(of: selectedTotal1) { newValue in
|
||||
// updateSelection(newValue: newValue, items: getDirectories(), updateSelected: { selectedSegment1 = $0 })
|
||||
// selectedSegment2 = nil
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private var selectedText: String {
|
||||
// if let selectedItem = selectedSegment1 ?? selectedSegment2 {
|
||||
// return selectedItem.name
|
||||
// } else {
|
||||
// return "File System Overview"
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private var selectedSizeText: String {
|
||||
// if let selectedItem = selectedSegment1 ?? selectedSegment2 {
|
||||
// return ByteCountFormatter.string(fromByteCount: selectedItem.size, countStyle: .file)
|
||||
// } else {
|
||||
// return "Select a segment..."
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private func getDirectories() -> [Item] {
|
||||
// return items.filter { $0.isDirectory == true }
|
||||
// }
|
||||
//
|
||||
// private func getFiles() -> [Item] {
|
||||
// return items.filter { $0.isDirectory == false }
|
||||
// }
|
||||
//
|
||||
// private func selectOpacity(item: Item, selectedItem: Item?, ring: Int = 1) -> Double {
|
||||
// if let selectedItem = selectedItem, selectedItem.id == item.id {
|
||||
// return 1.0
|
||||
// }
|
||||
// return ring == 1 ? 0.8 : 0.55
|
||||
// }
|
||||
//
|
||||
// private func updateSelection(newValue: Double?, items: [Item], updateSelected: (Item?) -> Void) {
|
||||
// guard let newValue = newValue else {
|
||||
// updateSelected(nil)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// var accumulatedSize: Double = 0
|
||||
// let selectedItem = items.first { item in
|
||||
// accumulatedSize += Double(item.size)
|
||||
// return newValue <= accumulatedSize
|
||||
// }
|
||||
//
|
||||
// updateSelected(selectedItem)
|
||||
// }
|
||||
//}
|
||||
@@ -1,260 +0,0 @@
|
||||
//
|
||||
// TreeMap.swift
|
||||
// Pearcleaner
|
||||
//
|
||||
// Created by Alin Lupascu on 8/1/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import AlinFoundation
|
||||
|
||||
|
||||
|
||||
struct TreeMapChart: View {
|
||||
let items: [Item]
|
||||
var onItemSelected: (Item) -> Void
|
||||
@Binding var hoveredItem: Item?
|
||||
@AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true
|
||||
|
||||
var displayedItems: [Item] {
|
||||
items//.filter { $0.size >= 1_048_576 } // Filter to keep only items with size >= 1MB
|
||||
.sorted(by: { $0.size > $1.size }) // Then sort the remaining items by size
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
|
||||
GeometryReader { proxy in
|
||||
|
||||
let ld = calculateLayout(
|
||||
w: proxy.size.width,
|
||||
h: proxy.size.height,
|
||||
data: displayedItems.map { Double($0.size) },
|
||||
from: 0
|
||||
)
|
||||
|
||||
TreeMapView(ld: ld, items: displayedItems, onItemSelected: onItemSelected, hoveredItem: $hoveredItem)
|
||||
|
||||
}
|
||||
.ignoresSafeArea()
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||
}
|
||||
}
|
||||
|
||||
struct TreeMapView: View {
|
||||
let ld: LayoutData
|
||||
let items: [Item]
|
||||
var onItemSelected: (Item) -> Void
|
||||
@Binding var hoveredItem: Item?
|
||||
@State private var hoveredIndex: Int?
|
||||
@AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if ld.direction == .h {
|
||||
HStack(spacing: 0) {
|
||||
VStack(spacing: 0) {
|
||||
content
|
||||
}
|
||||
if let child = ld.child {
|
||||
TreeMapView(ld: child, items: items, onItemSelected: onItemSelected, hoveredItem: $hoveredItem)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
VStack(spacing: 0) {
|
||||
HStack(spacing: 0) {
|
||||
content
|
||||
}
|
||||
|
||||
if let child = ld.child {
|
||||
TreeMapView(ld: child, items: items, onItemSelected: onItemSelected, hoveredItem: $hoveredItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var content: some View {
|
||||
ForEach(0..<ld.content.count, id: \.self) { i in
|
||||
let file = items[ld.content[i].index]
|
||||
// let maxDimension = max(ld.content[i].w, ld.content[i].h) / 5
|
||||
|
||||
ZStack {
|
||||
Rectangle()
|
||||
// .foregroundStyle(randomColor())
|
||||
// .foregroundStyle(colorForItem(file))
|
||||
.fill(gradientForItem(file))
|
||||
.brightness(hoveredIndex == i ? 0.1 : 0)
|
||||
.frame(width: ld.content[i].w, height: ld.content[i].h)
|
||||
.border(.black, width: 0.5)
|
||||
.onTapGesture {
|
||||
onItemSelected(file)
|
||||
}
|
||||
.overlay {
|
||||
|
||||
if hoveredIndex == i {
|
||||
VStack {
|
||||
Text(file.name)
|
||||
.minimumScaleFactor(0.5)
|
||||
.lineLimit(2)
|
||||
Text("\(formatSize(file.size))")
|
||||
.minimumScaleFactor(0.5)
|
||||
.lineLimit(1)
|
||||
}
|
||||
.padding(5)
|
||||
.padding(.horizontal, 2)
|
||||
.background(.background.opacity(0.5))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||
.zIndex(1)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
.onHover { isHovering in
|
||||
withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) {
|
||||
hoveredIndex = isHovering ? i : nil
|
||||
hoveredItem = isHovering ? file : nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// private func colorForItem(_ item: Item) -> Color {
|
||||
// item.isDirectory ? .blue : .gray
|
||||
// }
|
||||
|
||||
private func gradientForItem(_ item: Item) -> LinearGradient {
|
||||
LinearGradient(
|
||||
gradient: Gradient(colors: [Color.cyan, Color.blue]),
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
// item.isDirectory ? LinearGradient(
|
||||
// gradient: Gradient(colors: [Color.cyan, Color.blue]),
|
||||
// startPoint: .topLeading,
|
||||
// endPoint: .bottomTrailing
|
||||
// ) : LinearGradient(
|
||||
// gradient: Gradient(colors: [Color.pink, Color.orange]),
|
||||
// startPoint: .topLeading,
|
||||
// endPoint: .bottomTrailing
|
||||
// )
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
func formatSize(_ size: Int64) -> String {
|
||||
let formatter = ByteCountFormatter()
|
||||
formatter.allowedUnits = [.useAll]
|
||||
formatter.countStyle = .file
|
||||
return formatter.string(fromByteCount: size)
|
||||
}
|
||||
|
||||
func randomColor() -> Color {
|
||||
Color(hue: Double.random(in: 0.0...1.0),
|
||||
saturation: Double.random(in: 0.08...0.18),
|
||||
brightness: Double.random(in: 0.90...1.0))
|
||||
}
|
||||
|
||||
final class Item: Identifiable {
|
||||
let url: URL
|
||||
let name: String
|
||||
var size: Int64
|
||||
let isDirectory: Bool?
|
||||
|
||||
init(url: URL, name: String, size: Int64, isDirectory: Bool = false) {
|
||||
self.url = url
|
||||
self.name = name
|
||||
self.size = size
|
||||
self.isDirectory = isDirectory
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: Layout Generator ============================================================================================================
|
||||
|
||||
// Layout
|
||||
enum Direction {
|
||||
case h
|
||||
case v
|
||||
}
|
||||
|
||||
class LayoutData {
|
||||
var direction: Direction = .h
|
||||
var content: [(index: Int, w: Double, h: Double)] = []
|
||||
var child: LayoutData? = nil
|
||||
}
|
||||
|
||||
func calculateLayout(w: Double, h: Double, data: [Double], from: Int) -> LayoutData {
|
||||
|
||||
let returnData = LayoutData()
|
||||
if data.isEmpty {
|
||||
print("TreeMap layout data is empty")
|
||||
return returnData
|
||||
}
|
||||
let dataToArea = w * h / data[from...].reduce(0.0, +)
|
||||
if w < h {
|
||||
returnData.direction = .v
|
||||
} else {
|
||||
returnData.direction = .h
|
||||
}
|
||||
let mainLength = min(w, h)
|
||||
var currentIndex = from
|
||||
var area = data[currentIndex] * dataToArea
|
||||
var crossLength = area / mainLength
|
||||
|
||||
var cellRatio = mainLength / crossLength
|
||||
cellRatio = max(cellRatio, 1.0 / cellRatio)
|
||||
|
||||
while currentIndex + 1 < data.count {
|
||||
let newIndex = currentIndex + 1
|
||||
let newArea = area + data[newIndex] * dataToArea
|
||||
let newCrossLength = newArea / mainLength
|
||||
var newCellRatio = data[newIndex] * dataToArea / newCrossLength / newCrossLength
|
||||
newCellRatio = max(newCellRatio, 1.0 / newCellRatio)
|
||||
|
||||
if newCellRatio < cellRatio {
|
||||
currentIndex = newIndex
|
||||
area = newArea
|
||||
crossLength = newCrossLength
|
||||
cellRatio = newCellRatio
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
switch returnData.direction {
|
||||
case .h:
|
||||
for i in from...currentIndex {
|
||||
returnData.content.append((
|
||||
index: i,
|
||||
w: crossLength,
|
||||
h: data[i] * dataToArea / crossLength))
|
||||
}
|
||||
case .v:
|
||||
for i in from...currentIndex {
|
||||
returnData.content.append((
|
||||
index: i,
|
||||
w: data[i] * dataToArea / crossLength,
|
||||
h: crossLength))
|
||||
}
|
||||
}
|
||||
|
||||
if currentIndex != data.count - 1 {
|
||||
switch returnData.direction {
|
||||
case .h:
|
||||
returnData.child = calculateLayout(
|
||||
w: w - crossLength,
|
||||
h: h,
|
||||
data: data,
|
||||
from: currentIndex + 1)
|
||||
case .v:
|
||||
returnData.child = calculateLayout(
|
||||
w: w,
|
||||
h: h - crossLength,
|
||||
data: data,
|
||||
from: currentIndex + 1)
|
||||
}
|
||||
}
|
||||
return returnData
|
||||
}
|
||||
@@ -80,11 +80,11 @@ struct PearcleanerApp: App {
|
||||
return true
|
||||
}
|
||||
// Save window size on window dimension change
|
||||
.onChange(of: NSApplication.shared.windows.first?.frame) { newFrame in
|
||||
if let newFrame = newFrame {
|
||||
windowSettings.saveWindowSettings(frame: newFrame)
|
||||
}
|
||||
}
|
||||
// .onChange(of: NSApplication.shared.windows.first?.frame) { newFrame in
|
||||
// if let newFrame = newFrame {
|
||||
// windowSettings.saveWindowSettings(frame: newFrame)
|
||||
// }
|
||||
// }
|
||||
.alert(isPresented: $appState.showUninstallAlert) {
|
||||
Alert(
|
||||
title: Text("Warning!"),
|
||||
@@ -128,7 +128,7 @@ struct PearcleanerApp: App {
|
||||
.preferredColorScheme(themeManager.displayMode.colorScheme)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
#if !DEBUG
|
||||
Task {
|
||||
@@ -141,7 +141,6 @@ struct PearcleanerApp: App {
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
.windowStyle(.hiddenTitleBar)
|
||||
.windowResizability(.contentMinSize)
|
||||
.commands {
|
||||
@@ -171,6 +170,8 @@ struct PearcleanerApp: App {
|
||||
class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
|
||||
var windowSettings = WindowSettings()
|
||||
var themeManager = ThemeManager.shared
|
||||
var windowCloseObserver: NSObjectProtocol?
|
||||
var windowFrameObserver: NSObjectProtocol?
|
||||
|
||||
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||
let menubarEnabled = UserDefaults.standard.bool(forKey: "settings.menubar.enabled")
|
||||
@@ -190,16 +191,37 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
|
||||
NSApplication.shared.setActivationPolicy(.accessory)
|
||||
}
|
||||
|
||||
windowFrameObserver = NotificationCenter.default.addObserver(forName: nil, object: nil, queue: nil) { notification in
|
||||
if let window = notification.object as? NSWindow, window.title == "Pearcleaner" {
|
||||
if notification.name == NSWindow.didEndLiveResizeNotification || notification.name == NSWindow.didMoveNotification {
|
||||
self.windowSettings.saveWindowSettings(frame: window.frame)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
windowCloseObserver = NotificationCenter.default.addObserver(forName: NSWindow.willCloseNotification, object: nil, queue: nil) { notification in
|
||||
if let window = notification.object as? NSWindow, window.title == "Pearcleaner" {
|
||||
self.windowSettings.saveWindowSettings(frame: window.frame)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
func applicationWillTerminate(_ notification: Notification) {
|
||||
// Perform actions on app termination here
|
||||
// Remove the observers
|
||||
if let observer = windowCloseObserver {
|
||||
NotificationCenter.default.removeObserver(observer)
|
||||
}
|
||||
|
||||
if let observer = windowFrameObserver {
|
||||
NotificationCenter.default.removeObserver(observer)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
|
||||
let windowSettings = WindowSettings()
|
||||
|
||||
|
||||
@@ -160,6 +160,7 @@ struct Sponsor: Identifiable {
|
||||
let url: URL
|
||||
|
||||
static let sponsors: [Sponsor] = [
|
||||
Sponsor(name: "chris3ware", url: URL(string: "https://github.com/chris3ware")!),
|
||||
Sponsor(name: "fpuhan", url: URL(string: "https://github.com/fpuhan")!),
|
||||
Sponsor(name: "HungThinhIT", url: URL(string: "https://github.com/HungThinhIT")!),
|
||||
Sponsor(name: "DharsanB", url: URL(string: "https://github.com/dharsanb")!),
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import FinderSync
|
||||
//import FinderSync
|
||||
import AlinFoundation
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
@@ -92,7 +92,7 @@ struct GeneralSettingsTab: View {
|
||||
.foregroundStyle(.primary.opacity(0.5))
|
||||
}
|
||||
|
||||
InfoButton(text: "When deleting files using the Trash button, you can prevent accidental deletions by showing an alert before proceeding with the action")
|
||||
InfoButton(text: "When deleting files using the Trash button, you can prevent accidental deletions by showing an alert before proceeding with the action. Unrelated to the password prompt alert when deleting files in system folders.")
|
||||
|
||||
Spacer()
|
||||
Toggle(isOn: $confirmAlert, label: {
|
||||
@@ -250,10 +250,21 @@ struct GeneralSettingsTab: View {
|
||||
|
||||
Spacer()
|
||||
|
||||
Button("Extensions") {
|
||||
FIFinderSyncController.showExtensionManagementInterface()
|
||||
// Button("Extensions") {
|
||||
// FIFinderSyncController.showExtensionManagementInterface()
|
||||
// }
|
||||
// .buttonStyle(SimpleButtonStyle(icon: "folder", help: "Show Extensions Pane"))
|
||||
|
||||
Toggle(isOn: $appState.finderExtensionEnabled, label: {
|
||||
})
|
||||
.toggleStyle(.switch)
|
||||
.onChange(of: appState.finderExtensionEnabled) { newValue in
|
||||
if newValue {
|
||||
manageFinderPlugin(install: true)
|
||||
} else {
|
||||
manageFinderPlugin(install: false)
|
||||
}
|
||||
}
|
||||
.buttonStyle(SimpleButtonStyle(icon: "folder", help: "Show Extensions Pane"))
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -164,10 +164,11 @@ struct InterfaceSettingsTab: View {
|
||||
.padding(.trailing)
|
||||
.foregroundStyle(.primary.opacity(0.5))
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
Text("\(mini ? "Mini window mode" : "Full size window mode")")
|
||||
Text("\(mini ? "Mini window mode enabled" : "Mini window mode disabled")")
|
||||
.font(.callout)
|
||||
.foregroundStyle(.primary.opacity(0.5))
|
||||
}
|
||||
InfoButton(text: "Toggling between modes will reset the window frames to their default size/position")
|
||||
Spacer()
|
||||
Toggle(isOn: $mini, label: {
|
||||
})
|
||||
@@ -178,7 +179,7 @@ struct InterfaceSettingsTab: View {
|
||||
if mini {
|
||||
appState.currentView = miniView ? .apps : .empty
|
||||
showPopover = false
|
||||
windowSettings.newWindow(withView: {
|
||||
windowSettings.newWindow(mini: true, withView: {
|
||||
MiniMode(search: $search, showPopover: $showPopover)
|
||||
.environmentObject(appState)
|
||||
.environmentObject(locations)
|
||||
@@ -194,7 +195,7 @@ struct InterfaceSettingsTab: View {
|
||||
} else {
|
||||
appState.currentView = .files
|
||||
}
|
||||
windowSettings.newWindow(withView: {
|
||||
windowSettings.newWindow(mini: false, withView: {
|
||||
RegularMode(search: $search, showPopover: $showPopover)
|
||||
.environmentObject(appState)
|
||||
.environmentObject(locations)
|
||||
@@ -313,7 +314,7 @@ struct InterfaceSettingsTab: View {
|
||||
NSApplication.shared.setActivationPolicy(.regular)
|
||||
if !hasWindowOpen() {
|
||||
if mini {
|
||||
windowSettings.newWindow(withView: {
|
||||
windowSettings.newWindow(mini: true, withView: {
|
||||
MiniMode(search: $search, showPopover: $showPopover)
|
||||
.environmentObject(appState)
|
||||
.environmentObject(locations)
|
||||
@@ -324,7 +325,7 @@ struct InterfaceSettingsTab: View {
|
||||
.preferredColorScheme(themeManager.displayMode.colorScheme)
|
||||
})
|
||||
} else {
|
||||
windowSettings.newWindow(withView: {
|
||||
windowSettings.newWindow(mini: false, withView: {
|
||||
RegularMode(search: $search, showPopover: $showPopover)
|
||||
.environmentObject(appState)
|
||||
.environmentObject(locations)
|
||||
|
||||
@@ -23,78 +23,63 @@ struct RegularMode: View {
|
||||
@Binding var showPopover: Bool
|
||||
@State private var showMenu = false
|
||||
@State var isMenuBar: Bool = false
|
||||
@State private var isDrillView: Bool = false
|
||||
|
||||
var body: some View {
|
||||
|
||||
if isDrillView {
|
||||
VStack {
|
||||
Toggle("Drill", isOn: $isDrillView).padding()
|
||||
PearDiskView()
|
||||
.environmentObject(appState)
|
||||
HStack(alignment: .center, spacing: 0) {
|
||||
|
||||
// App List
|
||||
HStack(spacing: 0){
|
||||
|
||||
if appState.reload {
|
||||
VStack {
|
||||
Spacer()
|
||||
ProgressView() {
|
||||
Text("Gathering app details")
|
||||
.font(.callout)
|
||||
.foregroundStyle(.primary.opacity(0.5))
|
||||
.padding(5)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.frame(width: sidebarWidth)
|
||||
.padding(.vertical)
|
||||
} else {
|
||||
AppSearchView(glass: glass, menubarEnabled: menubarEnabled, mini: mini, search: $search, showPopover: $showPopover, isMenuBar: $isMenuBar)
|
||||
.frame(width: sidebarWidth)
|
||||
}
|
||||
|
||||
}
|
||||
.background(backgroundView(themeManager: themeManager, darker: true, glass: glass))
|
||||
.transition(.opacity)
|
||||
|
||||
SlideableDivider(dimension: $sidebarWidth)
|
||||
.background(backgroundView(themeManager: themeManager))
|
||||
.zIndex(3)
|
||||
|
||||
|
||||
// Details View
|
||||
HStack(spacing: 0) {
|
||||
Spacer()
|
||||
Group {
|
||||
if appState.currentView == .empty || appState.currentView == .apps {
|
||||
AppDetailsEmptyView(showPopover: $showPopover)
|
||||
} else if appState.currentView == .files {
|
||||
FilesView(showPopover: $showPopover, search: $search)
|
||||
.id(appState.appInfo.id)
|
||||
} else if appState.currentView == .zombie {
|
||||
ZombieView(showPopover: $showPopover, search: $search)
|
||||
.id(appState.appInfo.id)
|
||||
}
|
||||
}
|
||||
.transition(.opacity)
|
||||
Spacer()
|
||||
}
|
||||
.zIndex(2)
|
||||
.background(backgroundView(themeManager: themeManager))
|
||||
.frame(minWidth: 900, minHeight: 600)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
} else {
|
||||
HStack(alignment: .center, spacing: 0) {
|
||||
// Toggle("", isOn: $isDrillView)
|
||||
|
||||
// App List
|
||||
HStack(spacing: 0){
|
||||
|
||||
if appState.reload {
|
||||
VStack {
|
||||
Spacer()
|
||||
ProgressView() {
|
||||
Text("Gathering app details")
|
||||
.font(.callout)
|
||||
.foregroundStyle(.primary.opacity(0.5))
|
||||
.padding(5)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.frame(width: sidebarWidth)
|
||||
.padding(.vertical)
|
||||
} else {
|
||||
AppSearchView(glass: glass, menubarEnabled: menubarEnabled, mini: mini, search: $search, showPopover: $showPopover, isMenuBar: $isMenuBar)
|
||||
.frame(width: sidebarWidth)
|
||||
}
|
||||
|
||||
}
|
||||
.background(backgroundView(themeManager: themeManager, darker: true, glass: glass))
|
||||
.transition(.opacity)
|
||||
|
||||
SlideableDivider(dimension: $sidebarWidth)
|
||||
.background(backgroundView(themeManager: themeManager))
|
||||
.zIndex(3)
|
||||
|
||||
|
||||
// Details View
|
||||
HStack(spacing: 0) {
|
||||
Spacer()
|
||||
Group {
|
||||
if appState.currentView == .empty || appState.currentView == .apps {
|
||||
AppDetailsEmptyView(showPopover: $showPopover)
|
||||
} else if appState.currentView == .files {
|
||||
FilesView(showPopover: $showPopover, search: $search)
|
||||
.id(appState.appInfo.id)
|
||||
} else if appState.currentView == .zombie {
|
||||
ZombieView(showPopover: $showPopover, search: $search)
|
||||
.id(appState.appInfo.id)
|
||||
}
|
||||
}
|
||||
.transition(.opacity)
|
||||
Spacer()
|
||||
}
|
||||
.zIndex(2)
|
||||
.background(backgroundView(themeManager: themeManager))
|
||||
}
|
||||
.frame(minWidth: 900, minHeight: 600)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
}
|
||||
.frame(minWidth: 900, minHeight: 600)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
|
||||
|
||||
}
|
||||
@@ -109,43 +94,44 @@ struct AppDetailsEmptyView: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@EnvironmentObject var appState: AppState
|
||||
@EnvironmentObject var locations: Locations
|
||||
// @AppStorage("settings.general.animateLogo") private var animateLogo: Bool = true
|
||||
// @AppStorage("settings.general.animateLogo") private var animateLogo: Bool = true
|
||||
@Binding var showPopover: Bool
|
||||
// @State private var animationStart = false
|
||||
// @State private var animationStart = false
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .center) {
|
||||
|
||||
Spacer()
|
||||
|
||||
PearDropView()
|
||||
// if #available(macOS 14, *) {
|
||||
// if animateLogo && animationStart {
|
||||
// PearDropView()
|
||||
// .phaseAnimator([false, true]) { wwdc24, chromaRotate in
|
||||
// wwdc24
|
||||
// .hueRotation(.degrees(chromaRotate ? 420 : 0))
|
||||
// } animation: { chromaRotate in
|
||||
// .easeInOut(duration: 6)
|
||||
// }
|
||||
// } else {
|
||||
// PearDropView()
|
||||
// }
|
||||
//
|
||||
// } else {
|
||||
// PearDropView()
|
||||
// }
|
||||
// if #available(macOS 14, *) {
|
||||
// if animateLogo && animationStart {
|
||||
// PearDropView()
|
||||
// .phaseAnimator([false, true]) { wwdc24, chromaRotate in
|
||||
// wwdc24
|
||||
// .hueRotation(.degrees(chromaRotate ? 420 : 0))
|
||||
// } animation: { chromaRotate in
|
||||
// .easeInOut(duration: 6)
|
||||
// }
|
||||
// } else {
|
||||
// PearDropView()
|
||||
// }
|
||||
//
|
||||
// } else {
|
||||
// PearDropView()
|
||||
// }
|
||||
|
||||
Spacer()
|
||||
|
||||
Text("Drop an app here")
|
||||
.font(.title3)
|
||||
.padding(.bottom, 25)
|
||||
.opacity(0.5)
|
||||
// Text("Drop an app here")
|
||||
// .font(.title3)
|
||||
// .padding(.bottom, 25)
|
||||
// .opacity(0.5)
|
||||
}
|
||||
// .onAppear {
|
||||
// DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
// animationStart = true
|
||||
// }
|
||||
// }
|
||||
// .onAppear {
|
||||
// DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
// animationStart = true
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user