This commit is contained in:
Alin
2024-03-18 18:05:13 -06:00
parent 404bdf0776
commit 2318949caf
28 changed files with 865 additions and 452 deletions
+2 -6
View File
@@ -105,18 +105,14 @@ struct ZombieFile: Identifiable, Equatable, Hashable {
enum CurrentTabView:Int
{
case general
case menubar
// case permissions
// case sentinel
case interface
case update
case about
var title: String {
switch self {
case .general: return "General"
case .menubar: return "MenuBar"
// case .permissions: return "Permissions"
// case .sentinel: return "Sentinel"
case .interface: return "Interface"
case .update: return "Update"
case .about: return "About"
}
+106 -106
View File
@@ -5,109 +5,109 @@
// Created by Alin Lupascu on 2/23/24.
//
import Foundation
import Swift
// https://github.com/x13a/authorization-swift
public struct Authorization {
public enum Error: Swift.Error {
case create(OSStatus)
case copyRights(OSStatus)
case exec(OSStatus)
}
public static func executeWithPrivileges(
_ command: String
) -> Result<FileHandle, Error> {
let RTLD_DEFAULT = UnsafeMutableRawPointer(bitPattern: -2)
var fn: @convention(c) (
AuthorizationRef,
UnsafePointer<CChar>, // path
AuthorizationFlags,
UnsafePointer<UnsafePointer<CChar>?>, // args
UnsafeMutablePointer<UnsafeMutablePointer<FILE>>?
) -> OSStatus
fn = unsafeBitCast(
dlsym(RTLD_DEFAULT, "AuthorizationExecuteWithPrivileges"),
to: type(of: fn)
)
var authorizationRef: AuthorizationRef? = nil
var err = AuthorizationCreate(nil, nil, [], &authorizationRef)
guard err == errAuthorizationSuccess else {
return .failure(.create(err))
}
defer { AuthorizationFree(authorizationRef!, [.destroyRights]) }
var components = command.components(separatedBy: " ")
var path = components.remove(at: 0).cString(using: .utf8)!
let name = kAuthorizationRightExecute.cString(using: .utf8)!
var items: AuthorizationItem = name.withUnsafeBufferPointer { nameBuf in
path.withUnsafeBufferPointer { pathBuf in
let pathPtr =
UnsafeMutableRawPointer(mutating: pathBuf.baseAddress!)
return AuthorizationItem(
name: nameBuf.baseAddress!,
valueLength: path.count,
value: pathPtr,
flags: 0
)
}
}
var rights: AuthorizationRights =
withUnsafeMutablePointer(to: &items) { items in
return AuthorizationRights(count: 1, items: items)
}
let flags: AuthorizationFlags = [
.interactionAllowed,
.preAuthorize,
.extendRights,
]
err = AuthorizationCopyRights(
authorizationRef!,
&rights,
nil,
flags,
nil
)
guard err == errAuthorizationSuccess else {
return .failure(.copyRights(err))
}
let rest = components.map { $0.cString(using: .utf8)! }
var args = Array<UnsafePointer<CChar>?>(
repeating: nil,
count: rest.count + 1
)
for (idx, arg) in rest.enumerated() {
args[idx] = UnsafePointer<CChar>?(arg)
}
var file = FILE()
let fh: FileHandle?
(err, fh) = withUnsafeMutablePointer(to: &file) { file in
var pipe = file
let err = fn(authorizationRef!, &path, [], &args, &pipe)
guard err == errAuthorizationSuccess else {
return (err, nil)
}
let fh = FileHandle(
fileDescriptor: fileno(pipe),
closeOnDealloc: true
)
return (err, fh)
}
guard err == errAuthorizationSuccess else {
return .failure(.exec(err))
}
return .success(fh!)
}
}
//import Foundation
//import Swift
//
//// https://github.com/x13a/authorization-swift
//
//public struct Authorization {
//
// public enum Error: Swift.Error {
// case create(OSStatus)
// case copyRights(OSStatus)
// case exec(OSStatus)
// }
//
// public static func executeWithPrivileges(
// _ command: String
// ) -> Result<FileHandle, Error> {
//
// let RTLD_DEFAULT = UnsafeMutableRawPointer(bitPattern: -2)
// var fn: @convention(c) (
// AuthorizationRef,
// UnsafePointer<CChar>, // path
// AuthorizationFlags,
// UnsafePointer<UnsafePointer<CChar>?>, // args
// UnsafeMutablePointer<UnsafeMutablePointer<FILE>>?
// ) -> OSStatus
// fn = unsafeBitCast(
// dlsym(RTLD_DEFAULT, "AuthorizationExecuteWithPrivileges"),
// to: type(of: fn)
// )
//
// var authorizationRef: AuthorizationRef? = nil
// var err = AuthorizationCreate(nil, nil, [], &authorizationRef)
// guard err == errAuthorizationSuccess else {
// return .failure(.create(err))
// }
// defer { AuthorizationFree(authorizationRef!, [.destroyRights]) }
//
// var components = command.components(separatedBy: " ")
// var path = components.remove(at: 0).cString(using: .utf8)!
// let name = kAuthorizationRightExecute.cString(using: .utf8)!
//
// var items: AuthorizationItem = name.withUnsafeBufferPointer { nameBuf in
// path.withUnsafeBufferPointer { pathBuf in
// let pathPtr =
// UnsafeMutableRawPointer(mutating: pathBuf.baseAddress!)
// return AuthorizationItem(
// name: nameBuf.baseAddress!,
// valueLength: path.count,
// value: pathPtr,
// flags: 0
// )
// }
// }
//
// var rights: AuthorizationRights =
// withUnsafeMutablePointer(to: &items) { items in
// return AuthorizationRights(count: 1, items: items)
// }
//
// let flags: AuthorizationFlags = [
// .interactionAllowed,
// .preAuthorize,
// .extendRights,
// ]
//
// err = AuthorizationCopyRights(
// authorizationRef!,
// &rights,
// nil,
// flags,
// nil
// )
// guard err == errAuthorizationSuccess else {
// return .failure(.copyRights(err))
// }
//
// let rest = components.map { $0.cString(using: .utf8)! }
// var args = Array<UnsafePointer<CChar>?>(
// repeating: nil,
// count: rest.count + 1
// )
// for (idx, arg) in rest.enumerated() {
// args[idx] = UnsafePointer<CChar>?(arg)
// }
//
// var file = FILE()
// let fh: FileHandle?
//
// (err, fh) = withUnsafeMutablePointer(to: &file) { file in
// var pipe = file
// let err = fn(authorizationRef!, &path, [], &args, &pipe)
// guard err == errAuthorizationSuccess else {
// return (err, nil)
// }
// let fh = FileHandle(
// fileDescriptor: fileno(pipe),
// closeOnDealloc: true
// )
// return (err, fh)
// }
// guard err == errAuthorizationSuccess else {
// return .failure(.exec(err))
// }
// return .success(fh!)
// }
//}
-24
View File
@@ -640,30 +640,6 @@ func reversePathsSearch(appState: AppState, locations: Locations, completion: @e
// Move files to trash using applescript/Finder so it asks for user password if needed
func moveFilesToTrash(at fileURLs: [URL], completion: @escaping () -> Void = {}) {
@AppStorage("settings.sentinel.enable") var sentinel: Bool = false
// var filesFinder = fileURLs
// var filesSudo: [URL] = []
//
// for file in fileURLs {
// if isSocketFile(at: file) {
// if let index = filesFinder.firstIndex(of: file) {
// filesFinder.remove(at: index)
// filesSudo.insert(file, at: 0)
// }
// }
// }
//
// if !filesSudo.isEmpty {
// // Remove socket files with rm
// let filesSudoPaths = filesSudo.map { $0.path }
// do {
// let fileHandler = try Authorization.executeWithPrivileges("/bin/rm -f \(filesSudoPaths.joined(separator: " "))").get()
// printOS(String(bytes: fileHandler.readDataToEndOfFile(), encoding: .utf8)!)
// } catch {
// printOS("Failed to remove socket file/s with privileges: \(error)")
// }
// }
updateOnBackground {
let posixFiles = fileURLs.map { "POSIX file \"\($0.path)\", " }.joined().dropLast(3)
+100
View File
@@ -0,0 +1,100 @@
//
// Trash.swift
// Pearcleaner
//
// Created by Alin Lupascu on 3/18/24.
//
import Foundation
//class TrashManager {
// private var fileLocations: [URL: URL] = [:] // Maps original location to trash location
//
// // Function to move files to Trash and track their original locations
// func delete(fileURLs: [URL]) {
// let fileManager = FileManager.default
// let trashURL = try! fileManager.url(for: .trashDirectory, in: .allDomainsMask, appropriateFor: nil, create: false)
//
// for originalURL in fileURLs {
// let destinationURL = trashURL.appendingPathComponent(originalURL.lastPathComponent)
//
// do {
// // The resultingItemURL is the new location of the item in the trash.
// var resultingItemURL: NSURL?
// try fileManager.trashItem(at: originalURL, resultingItemURL: &resultingItemURL)
// if let trashPath = resultingItemURL as URL? {
// // Track the original URL and its corresponding location in the Trash
// fileLocations[originalURL] = trashPath
// }
// } catch {
// print("Error moving file to Trash: \(error)")
// }
// }
// }
//
// // Function to undo the deletion of files, moving them back from Trash to their original locations
// func undoDelete() {
// let fileManager = FileManager.default
//
// for (originalURL, trashURL) in fileLocations {
// do {
// // Attempt to move the item back to its original location
// try fileManager.moveItem(at: trashURL, to: originalURL)
// } catch {
// print("Error moving file back from Trash: \(error)")
// }
// }
//
// // Clear the tracking dictionary after undoing the deletions
// fileLocations.removeAll()
// }
//}
//import Foundation
//
//func moveToTrash(fileURLs: [URL], completion: @escaping () -> Void = {}) {
// for fileURL in fileURLs {
// do {
// try _ = fileURL.checkResourceIsReachable()
// } catch {
// printOS(error.localizedDescription)
// }
// }
// let target: NSAppleEventDescriptor = .init(bundleIdentifier: "com.apple.finder")
// let event: NSAppleEventDescriptor = .init(eventClass: kAECoreSuite,
// eventID: AEEventID(kAEDelete),
// targetDescriptor: target,
// returnID: AEReturnID(kAutoGenerateReturnID),
// transactionID: AETransactionID(kAnyTransactionID))
// let fileList: NSAppleEventDescriptor = fileURLs.enumerated().reduce(into: .init(listDescriptor: ())) {
// (result: inout NSAppleEventDescriptor, element: (offset: Int, element: URL)) in
// if let nativePath: NSAppleEventDescriptor = .init(
// descriptorType: typeFileURL,
// data: element.element.absoluteString.data(using: .utf8)
// ) {
// result.insert(nativePath, at: element.offset + 1)
// }
// }
//
// event.setParam(fileList, forKeyword: keyDirectObject)
//
// do {
// try event.sendEvent(options: .noReply, timeout: TimeInterval(kAEDefaultTimeout))
// } catch let error as NSError {
// if case -600 = error.code {
// printOS("Finder is not running.")
// } else {
// printOS(error.description)
// }
// }
//
// completion()
//}
+25
View File
@@ -156,6 +156,31 @@ func isDarkModeEnabled() -> Bool {
}
}
// Check if Pearcleaner has any windows open
func hasWindowOpen() -> Bool {
for window in NSApp.windows where window.title == "Pearcleaner" {
return true
}
return false
}
// Find and hide/show main app window when using menubar item
func findAndHideWindows(named titles: [String]) {
for title in titles {
if let window = NSApp.windows.first(where: { $0.title == title }) {
window.orderOut(nil)
}
}
}
func findAndShowWindows(named titles: [String]) {
for title in titles {
if let window = NSApp.windows.first(where: { $0.title == title }) {
window.makeKeyAndOrderFront(nil)
}
}
}
// Check if appearance is dark mode
//func getCasks() -> [String] {
+42 -10
View File
@@ -27,6 +27,7 @@ struct PearcleanerApp: App {
@AppStorage("settings.general.brew") private var brew: Bool = false
@AppStorage("settings.menubar.enabled") private var menubarEnabled: Bool = false
@AppStorage("settings.menubar.mainWin") private var mainWinEnabled: Bool = false
@AppStorage("settings.interface.selectedMenubarIcon") var selectedMenubarIcon: String = "trash"
@State private var search = ""
@State private var showPopover: Bool = false
@@ -41,10 +42,8 @@ struct PearcleanerApp: App {
ZStack() {
if !mini {
RegularMode(search: $search, showPopover: $showPopover)
.environmentObject(locations)
} else {
MiniMode(search: $search, showPopover: $showPopover)
.environmentObject(locations)
}
if showFeature {
@@ -57,8 +56,8 @@ struct PearcleanerApp: App {
}
.environmentObject(appState)
.environmentObject(locations)
.preferredColorScheme(displayMode.colorScheme)
// .alert(isPresented: $appState.showAlert) { presentAlert(appState: appState) }
.handlesExternalEvents(preferring: Set(arrayLiteral: "pear"), allowing: Set(arrayLiteral: "*"))
.onOpenURL(perform: { url in
let deeplinkManager = DeeplinkManager(showPopover: $showPopover)
@@ -107,15 +106,14 @@ struct PearcleanerApp: App {
}
if menubarEnabled {
MenuBarExtraManager.shared.addMenuBarExtra {
MenuBarExtraManager.shared.addMenuBarExtra(withView: {
MenuBarMiniAppView(search: $search, showPopover: $showPopover)
.environmentObject(locations)
.environmentObject(appState)
}
}, icon: selectedMenubarIcon)
}
#if !DEBUG
Task {
@@ -175,7 +173,7 @@ struct PearcleanerApp: App {
class AppDelegate: NSObject, NSApplicationDelegate {
class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
let menubarEnabled = UserDefaults.standard.bool(forKey: "settings.menubar.enabled")
@@ -183,15 +181,49 @@ class AppDelegate: NSObject, NSApplicationDelegate {
}
#if !DEBUG
func windowShouldClose(_ sender: NSWindow) -> Bool {
let menubarEnabled = UserDefaults.standard.bool(forKey: "settings.menubar.enabled")
// let alert = NSAlert.init()
// alert.addButton(withTitle: "Return")
// alert.addButton(withTitle: "Quit")
// alert.informativeText = "Quit or return to application?"
// let response = alert.runModal()
// if response == NSApplication.ModalResponse.alertFirstButtonReturn {
// return false
// } else {
// NSApplication.shared.terminate(self)
// return true
// }
if menubarEnabled {
findAndHideWindows(named: ["Pearcleaner"])
return false
} else {
return true
}
}
#endif
func applicationDidFinishLaunching(_ notification: Notification) {
let menubarEnabled = UserDefaults.standard.bool(forKey: "settings.menubar.enabled")
// let dockEnabled = UserDefaults.standard.bool(forKey: "settings.dock.enabled")
#if !DEBUG
if menubarEnabled {
NSApp.windows.first?.close()
#if !DEBUG
findAndHideWindows(named: ["Pearcleaner"])
// NSApp.windows.first?.close()
NSApplication.shared.setActivationPolicy(.accessory)
}
#endif
// if dockEnabled {
// NSApplication.shared.setActivationPolicy(.regular)
// } else {
// NSApplication.shared.setActivationPolicy(.accessory)
// }
}
// Link window to delegate
let mainWindow = NSApp.windows[0]
mainWindow.delegate = self
}
@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "pear-1.5.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 561 B

@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "pear-1.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 565 B

@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "pear-2.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "pear-3.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "pear-4.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

+1 -1
View File
@@ -152,7 +152,7 @@ struct AboutSettingsTab: View {
Text("Made with ❤️ by Alin Lupascu (dev@itsalin.com)").font(.footnote).padding(.bottom)
}
.padding(20)
.frame(width: 500, height: 650)
.frame(width: 500, height: 600)
}
}
+2 -163
View File
@@ -35,32 +35,11 @@ struct GeneralSettingsTab: View {
VStack {
HStack() {
Text("Appearance").font(.title2)
Text("Functionality").font(.title2)
Spacer()
}
.padding(.leading)
HStack(spacing: 0) {
Image(systemName: glass ? "cube.transparent" : "cube.transparent.fill")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(.gray)
VStack(alignment: .leading, spacing: 5) {
Text("\(glass ? "Transparent material enabled" : "Transparent material disabled")")
.font(.callout)
.foregroundStyle(.gray)
}
// InfoButton(text: "When transparent material is enabled, sticky section headers (User/System) in app list are disabled to keep from showing app name text overlayed under the section header text with no background to separate the two.", color: nil)
Spacer()
Toggle(isOn: $glass, label: {
})
.toggleStyle(.switch)
}
.padding(5)
.padding(.leading)
HStack(spacing: 0) {
Image(systemName: instantSearch ? "bolt" : "bolt.slash")
@@ -110,146 +89,6 @@ struct GeneralSettingsTab: View {
.padding(.leading)
HStack(spacing: 0) {
Image(systemName: displayMode.colorScheme == .dark ? "moon.fill" : "sun.max.fill")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(.gray)
VStack(alignment: .leading, spacing: 5) {
Text("Set application color mode")
.font(.callout)
.foregroundStyle(.gray)
}
Spacer()
Picker("", selection: $selectedTheme) {
ForEach(themes, id: \.self) { theme in
Text(theme)
}
}
.pickerStyle(SegmentedPickerStyle())
.frame(width: 200)
.onChange(of: selectedTheme) { newTheme in
switch newTheme {
case "Auto":
displayMode.colorScheme = nil
if isDarkModeEnabled() {
displayMode.colorScheme = .dark
} else {
displayMode.colorScheme = .light
}
case "Dark":
displayMode.colorScheme = .dark
case "Light":
displayMode.colorScheme = .light
default:
break
}
}
}
.padding(5)
.padding(.leading)
// === Mini =================================================================================================
Divider()
.padding()
HStack() {
Text("Mini Configuration").font(.title2)
Spacer()
}
.padding(.leading)
HStack(spacing: 0) {
Image(systemName: mini ? "square.resize.up" : "square.resize.down")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(.gray)
VStack(alignment: .leading, spacing: 5) {
Text("\(mini ? "Mini window mode selected" : "Full size window mode selected")")
.font(.callout)
.foregroundStyle(.gray)
}
Spacer()
Toggle(isOn: $mini, label: {
})
.toggleStyle(.switch)
.onChange(of: mini) { newVal in
if mini {
appState.currentView = miniView ? .apps : .empty
showPopover = false
resizeWindowAuto(windowSettings: windowSettings)
} else {
resizeWindowAuto(windowSettings: windowSettings)
if appState.appInfo.appName.isEmpty {
appState.currentView = .empty
} else {
appState.currentView = .files
}
}
}
}
.padding(5)
.padding(.leading)
HStack(spacing: 0) {
Image(systemName: miniView ? "square.grid.3x3.square" : "plus.square.dashed")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(.gray)
VStack(alignment: .leading, spacing: 5) {
Text("\(miniView ? "Show apps list on startup" : "Show drop target on startup")")
.font(.callout)
.foregroundStyle(.gray)
}
InfoButton(text: "In mini window mode, you can have Pearcleaner startup to the Apps List view or the Drop Target view.", color: nil, label: "")
Spacer()
Toggle(isOn: $miniView, label: {
})
.toggleStyle(.switch)
.onChange(of: miniView) { newVal in
appState.currentView = newVal ? .apps : .empty
}
}
.padding(5)
.padding(.leading)
HStack(spacing: 0) {
Image(systemName: popoverStay ? "pin" : "pin.slash")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(.gray)
VStack(alignment: .leading, spacing: 5) {
Text("\(popoverStay ? "Popover window will stay on top" : "Popover window will not stay on top")")
.font(.callout)
.foregroundStyle(.gray)
}
InfoButton(text: "In mini window mode, if you pin the Files popover on top, clicking away from the window will not dismiss it. Otherwise, it will dismiss by clicking anywhere outside the popover.", color: nil, label: "")
Spacer()
Toggle(isOn: $popoverStay, label: {
})
.toggleStyle(.switch)
}
.padding(5)
.padding(.leading)
// === Perms ================================================================================================
@@ -369,7 +208,7 @@ struct GeneralSettingsTab: View {
}
.padding(20)
.frame(width: 500, height: 690)
.frame(width: 500, height: 420)
}
+396
View File
@@ -0,0 +1,396 @@
//
// Interface.swift
// Pearcleaner
//
// Created by Alin Lupascu on 3/18/24.
//
import Foundation
import SwiftUI
import ServiceManagement
struct InterfaceSettingsTab: View {
@EnvironmentObject var appState: AppState
@EnvironmentObject var locations: Locations
@State private var windowSettings = WindowSettings()
@AppStorage("settings.menubar.enabled") private var menubarEnabled: Bool = false
// @AppStorage("settings.dock.enabled") private var dockEnabled: Bool = false
@AppStorage("settings.general.mini") private var mini: Bool = false
@AppStorage("displayMode") var displayMode: DisplayMode = .system
@AppStorage("settings.general.glass") private var glass: Bool = true
@AppStorage("settings.general.dark") var isDark: Bool = true
@AppStorage("settings.general.popover") private var popoverStay: Bool = true
@AppStorage("settings.general.miniview") private var miniView: Bool = true
@AppStorage("settings.general.selectedTheme") var selectedTheme: String = "Auto"
@AppStorage("settings.interface.selectedMenubarIcon") var selectedMenubarIcon: String = "pear-4"
private let themes = ["Auto", "Dark", "Light"]
@State private var isLaunchAtLoginEnabled: Bool = false
let icons = ["externaldrive", "trash", "folder", "pear-1", "pear-1.5", "pear-2", "pear-3", "pear-4"]
@Binding var showPopover: Bool
@Binding var search: String
var body: some View {
Form {
VStack {
HStack() {
Text("Appearance").font(.title2)
Spacer()
}
.padding(.leading)
HStack(spacing: 0) {
Image(systemName: glass ? "cube.transparent" : "cube.transparent.fill")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(.gray)
VStack(alignment: .leading, spacing: 5) {
Text("\(glass ? "Transparent material enabled" : "Transparent material disabled")")
.font(.callout)
.foregroundStyle(.gray)
}
Spacer()
Toggle(isOn: $glass, label: {
})
.toggleStyle(.switch)
}
.padding(5)
.padding(.leading)
HStack(spacing: 0) {
Image(systemName: displayMode.colorScheme == .dark ? "moon.fill" : "sun.max.fill")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(.gray)
VStack(alignment: .leading, spacing: 5) {
Text("Application color mode")
.font(.callout)
.foregroundStyle(.gray)
}
Spacer()
Picker("", selection: $selectedTheme) {
ForEach(themes, id: \.self) { theme in
Text(theme)
}
}
.pickerStyle(SegmentedPickerStyle())
.frame(width: 200)
.onChange(of: selectedTheme) { newTheme in
switch newTheme {
case "Auto":
displayMode.colorScheme = nil
if isDarkModeEnabled() {
displayMode.colorScheme = .dark
} else {
displayMode.colorScheme = .light
}
case "Dark":
displayMode.colorScheme = .dark
case "Light":
displayMode.colorScheme = .light
default:
break
}
}
}
.padding(5)
.padding(.leading)
// === Mini =================================================================================================
Divider()
.padding()
HStack() {
Text("Mini Configuration").font(.title2)
Spacer()
}
.padding(.leading)
HStack(spacing: 0) {
Image(systemName: mini ? "square.resize.up" : "square.resize.down")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(.gray)
VStack(alignment: .leading, spacing: 5) {
Text("\(mini ? "Mini window mode" : "Full size window mode")")
.font(.callout)
.foregroundStyle(.gray)
}
Spacer()
Toggle(isOn: $mini, label: {
})
.toggleStyle(.switch)
.onChange(of: mini) { newVal in
if mini {
appState.currentView = miniView ? .apps : .empty
showPopover = false
resizeWindowAuto(windowSettings: windowSettings)
} else {
resizeWindowAuto(windowSettings: windowSettings)
if appState.appInfo.appName.isEmpty {
appState.currentView = .empty
} else {
appState.currentView = .files
}
}
}
}
.padding(5)
.padding(.leading)
HStack(spacing: 0) {
Image(systemName: miniView ? "square.grid.3x3.square" : "plus.square.dashed")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(.gray)
VStack(alignment: .leading, spacing: 5) {
Text("\(miniView ? "Show apps list on startup" : "Show drop target on startup")")
.font(.callout)
.foregroundStyle(.gray)
}
InfoButton(text: "In mini window mode, you can have Pearcleaner startup to the Apps List view or the Drop Target view.", color: nil, label: "")
Spacer()
Toggle(isOn: $miniView, label: {
})
.toggleStyle(.switch)
.onChange(of: miniView) { newVal in
appState.currentView = newVal ? .apps : .empty
}
}
.padding(5)
.padding(.leading)
HStack(spacing: 0) {
Image(systemName: popoverStay ? "pin" : "pin.slash")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(.gray)
VStack(alignment: .leading, spacing: 5) {
Text("\(popoverStay ? "Popover window will stay on top" : "Popover window will not stay on top")")
.font(.callout)
.foregroundStyle(.gray)
}
InfoButton(text: "In mini window mode, if you pin the Files popover on top, clicking away from the window will not dismiss it. Otherwise, it will dismiss by clicking anywhere outside the popover.", color: nil, label: "")
Spacer()
Toggle(isOn: $popoverStay, label: {
})
.toggleStyle(.switch)
}
.padding(5)
.padding(.leading)
// === MenuBar===============================================================================================
Divider()
.padding()
HStack() {
Text("Menubar").font(.title2)
Spacer()
}
.padding(.leading)
HStack(spacing: 0) {
Image(systemName: "menubar.rectangle")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(.gray)
VStack(alignment: .leading, spacing: 5) {
Text("\(menubarEnabled ? "Menubar icon enabled" : "Menubar icon disabled")")
.font(.callout)
.foregroundStyle(.gray)
}
InfoButton(text: "When menubar icon is enabled, the main app window and dock icon will be hidden. You can still pop-out the main app window from the menubar icon temporarily if you'd like.", color: nil, label: "")
Spacer()
Toggle(isOn: $menubarEnabled, label: {
})
.toggleStyle(.switch)
.onChange(of: menubarEnabled) { newVal in
if newVal {
MenuBarExtraManager.shared.addMenuBarExtra(withView: {
MenuBarMiniAppView(search: $search, showPopover: $showPopover)
.environmentObject(locations)
.environmentObject(appState)
}, icon: selectedMenubarIcon)
NSApplication.shared.setActivationPolicy(.accessory)
findAndShowWindows(named: ["Pearcleaner", "Interface"])
} else {
MenuBarExtraManager.shared.removeMenuBarExtra()
// dockEnabled = true
NSApplication.shared.setActivationPolicy(.regular)
if !hasWindowOpen() {
if mini {
windowSettings.newWindow {
MiniMode(search: $search, showPopover: $showPopover)
.environmentObject(locations)
.environmentObject(appState)
}
} else {
windowSettings.newWindow {
RegularMode(search: $search, showPopover: $showPopover)
.environmentObject(locations)
.environmentObject(appState)
}
}
}
}
}
}
.padding(5)
.padding(.leading)
HStack(spacing: 0) {
Image(systemName: isLaunchAtLoginEnabled ? "person" : "person.slash")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(.gray)
VStack(alignment: .leading, spacing: 5) {
Text("\(isLaunchAtLoginEnabled ? "Launch at login enabled" : "Launch at login disabled")")
.font(.callout)
.foregroundStyle(.gray)
}
InfoButton(text: "This setting will affect Pearcleaner whether you're running in menubar mode or regular mode. If you disable menubar icon, you might want to disable this as well so Pearcleaner doesn't start on login.", color: nil, label: "")
Spacer()
Toggle(isOn: $isLaunchAtLoginEnabled, label: {
})
.toggleStyle(.switch)
.onAppear {
isLaunchAtLoginEnabled = SMAppService.mainApp.status == .enabled
}
.onChange(of: isLaunchAtLoginEnabled) { newValue in
do {
if newValue {
if SMAppService.mainApp.status == .enabled {
try? SMAppService.mainApp.unregister()
}
try SMAppService.mainApp.register()
} else {
try SMAppService.mainApp.unregister()
}
} catch {
printOS("Failed to \(newValue ? "enable" : "disable") launch at login: \(error.localizedDescription)")
}
}
}
.padding(5)
.padding(.leading)
HStack(spacing: 0) {
Image(systemName: "paintbrush")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(.gray)
VStack(alignment: .leading, spacing: 5) {
Text("Menubar icon")
.font(.callout)
.foregroundStyle(.gray)
}
Spacer()
Picker("", selection: $selectedMenubarIcon) {
ForEach(icons, id: \.self) { icon in
HStack {
if icon.contains("pear") {
Image(icon)
.resizable()
.scaledToFit()
} else {
Image(systemName: icon)
.resizable()
.scaledToFit()
}
}
.tag(icon)
}
}
.frame(width: 60)
.onChange(of: selectedMenubarIcon) { newValue in
MenuBarExtraManager.shared.swapMenuBarIcon(icon: newValue)
}
}
.padding(5)
.padding(.leading)
// HStack(spacing: 0) {
// Image(systemName: "dock.rectangle")
// .resizable()
// .scaledToFit()
// .frame(width: 20, height: 20)
// .padding(.trailing)
// .foregroundStyle(.gray)
// VStack(alignment: .leading, spacing: 5) {
// Text("\(dockEnabled ? "Show dock icon" : "Hide dock icon")")
// .font(.callout)
// .foregroundStyle(.gray)
// }
// InfoButton(text: "This setting only affects Pearcleaner when the menubar icon is enabled, otherwise the dock icon will always show", color: nil, label: "")
// Spacer()
// Toggle(isOn: $dockEnabled, label: {
// })
// .toggleStyle(.switch)
// .onChange(of: dockEnabled) { newValue in
// if newValue {
// NSApplication.shared.setActivationPolicy(.regular)
// } else {
// if menubarEnabled {
// NSApplication.shared.setActivationPolicy(.accessory)
// }
//
// }
// }
//
// }
// .padding(5)
// .padding(.leading)
Spacer()
}
}
.padding(20)
.frame(width: 500, height: 520)
}
}
-112
View File
@@ -1,112 +0,0 @@
//
// MenuBar.swift
// Pearcleaner
//
// Created by Alin Lupascu on 3/16/24.
//
import Foundation
import SwiftUI
import ServiceManagement
struct MenuBarSettingsTab: View {
@EnvironmentObject var appState: AppState
@EnvironmentObject var locations: Locations
@AppStorage("settings.menubar.enabled") private var menubarEnabled: Bool = false
@State private var isLaunchAtLoginEnabled: Bool = false
@Binding var showPopover: Bool
@Binding var search: String
var body: some View {
Form {
VStack {
HStack() {
Text("Configuration").font(.title2)
Spacer()
}
.padding(.leading)
HStack(spacing: 0) {
Image(systemName: "menubar.rectangle")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(.gray)
VStack(alignment: .leading, spacing: 5) {
Text("\(menubarEnabled ? "Menubar icon enabled" : "Menubar icon disabled")")
.font(.callout)
.foregroundStyle(.gray)
}
Spacer()
Toggle(isOn: $menubarEnabled, label: {
})
.toggleStyle(.switch)
.onChange(of: menubarEnabled) { newVal in
if newVal {
MenuBarExtraManager.shared.addMenuBarExtra {
MenuBarMiniAppView(search: $search, showPopover: $showPopover)
.environmentObject(locations)
.environmentObject(appState)
}
} else {
MenuBarExtraManager.shared.removeMenuBarExtra()
}
}
}
.padding(5)
.padding(.leading)
HStack(spacing: 0) {
Image(systemName: isLaunchAtLoginEnabled ? "person.crop.circle.badge.checkmark" : "person.crop.circle")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(.gray)
VStack(alignment: .leading, spacing: 5) {
Text("\(isLaunchAtLoginEnabled ? "Launch at login enabled" : "Launch at login disabled")")
.font(.callout)
.foregroundStyle(.gray)
}
Spacer()
Toggle(isOn: $isLaunchAtLoginEnabled, label: {
})
.toggleStyle(.switch)
.onAppear {
isLaunchAtLoginEnabled = SMAppService.mainApp.status == .enabled
}
.onChange(of: isLaunchAtLoginEnabled) { newValue in
do {
if newValue {
if SMAppService.mainApp.status == .enabled {
try? SMAppService.mainApp.unregister()
}
try SMAppService.mainApp.register()
} else {
try SMAppService.mainApp.unregister()
}
} catch {
printOS("Failed to \(newValue ? "enable" : "disable") launch at login: \(error.localizedDescription)")
}
}
}
.padding(5)
.padding(.leading)
Spacer()
}
}
.padding(20)
.frame(width: 500, height: 690)
}
}
+3 -3
View File
@@ -23,11 +23,11 @@ struct SettingsView: View {
}
.tag(CurrentTabView.general)
MenuBarSettingsTab(showPopover: $showPopover, search: $search)
InterfaceSettingsTab(showPopover: $showPopover, search: $search)
.tabItem {
Label(CurrentTabView.menubar.title, systemImage: "menubar.rectangle")
Label(CurrentTabView.interface.title, systemImage: "macwindow")
}
.tag(CurrentTabView.menubar)
.tag(CurrentTabView.interface)
UpdateSettingsTab(showFeature: $showFeature)
.tabItem {
+1 -1
View File
@@ -107,7 +107,7 @@ struct UpdateSettingsTab: View {
}
.padding(20)
.frame(width: 500, height: 650)
.frame(width: 500, height: 520)
}
}
+43 -7
View File
@@ -12,7 +12,7 @@ struct MenuBarMiniAppView: View {
@Environment(\.colorScheme) var colorScheme
@EnvironmentObject var appState: AppState
@EnvironmentObject var locations: Locations
@State private var windowSettings = WindowSettings()
// @State private var windowSettings = WindowSettings()
@State private var animateGradient: Bool = false
@Binding var search: String
@State private var showSys: Bool = true
@@ -56,6 +56,7 @@ struct MenuBarMiniAppView: View {
AppsListView(search: $search, showPopover: $showPopover, filteredApps: filteredApps).padding(0)
HStack(spacing: 10) {
if #available(macOS 14.0, *) {
SettingsLink()
.buttonStyle(SimpleButtonStyle(icon: "gear", help: "Settings", color: Color("mode")))
@@ -66,16 +67,51 @@ struct MenuBarMiniAppView: View {
.buttonStyle(SimpleButtonStyle(icon: "gear", help: "Settings", color: Color("mode")))
}
Button("") {
withAnimation(.easeInOut(duration: 0.5)) {
showPopover = false
updateOnMain {
appState.appInfo = .empty
appState.selectedZombieItems = []
if appState.zombieFile.fileSize.keys.count == 0 {
appState.currentView = .zombie
appState.showProgress.toggle()
showPopover.toggle()
if instantSearch {
reversePathsSearch(appState: appState, locations: locations)
} else {
loadAllPaths(allApps: appState.sortedApps, appState: appState, locations: locations, reverseAddon: true)
}
} else {
appState.currentView = .zombie
showPopover.toggle()
}
}
}
}
.buttonStyle(SimpleButtonStyle(icon: "clock.arrow.circlepath", help: "Leftover Files", color: Color("mode")))
SearchBarMiniBottom(search: $search)
Button("Main") {
windowSettings.newWindow {
MiniMode(search: $search, showPopover: $showPopover)
.environmentObject(locations)
.environmentObject(appState)
}
findAndShowWindows(named: ["Pearcleaner"])
// if mini {
// windowSettings.newWindow {
// MiniMode(search: $search, showPopover: $showPopover)
// .environmentObject(locations)
// .environmentObject(appState)
// }
// } else {
// windowSettings.newWindow {
// RegularMode(search: $search, showPopover: $showPopover)
// .environmentObject(locations)
// .environmentObject(appState)
// }
// }
}
.buttonStyle(SimpleButtonStyle(icon: "macwindow", help: "Show Main Window", color: Color("mode")))
.buttonStyle(SimpleButtonStyle(icon: "macwindow.on.rectangle", help: "Pop Out Window", color: Color("mode")))
Button("Kill") {
NSApp.terminate(nil)
+1
View File
@@ -65,6 +65,7 @@ struct MiniMode: View {
// MARK: Background for whole app
// .background(Color("bg").opacity(1))
// .background(VisualEffect(material: .sidebar, blendingMode: .behindWindow).edgesIgnoringSafeArea(.all))
}
}
@@ -13,7 +13,7 @@ class MenuBarExtraManager {
private var statusItem: NSStatusItem?
private var popover = NSPopover()
func addMenuBarExtra<V: View>(withView view: @escaping () -> V) {
func addMenuBarExtra<V: View>(withView view: @escaping () -> V, icon: String) {
guard statusItem == nil else { return }
// Initialize the status item
@@ -21,7 +21,12 @@ class MenuBarExtraManager {
// Set up the status item's button
if let button = statusItem?.button {
button.image = NSImage(systemSymbolName: "menubar.rectangle", accessibilityDescription: "Pearcleaner")
if NSImage(systemSymbolName: icon, accessibilityDescription: nil) != nil {
button.image = NSImage(systemSymbolName: icon, accessibilityDescription: "Pearcleaner")
} else {
button.image = NSImage(named: icon)
}
button.action = #selector(togglePopover(_:))
button.target = self
}
@@ -40,14 +45,23 @@ class MenuBarExtraManager {
}
}
func getStatus() -> Bool {
if let item = statusItem {
return true
func swapMenuBarIcon(icon: String) {
guard let button = statusItem?.button else { return }
if let image = NSImage(systemSymbolName: icon, accessibilityDescription: nil) {
button.image = image
} else {
return false
button.image = NSImage(named: icon)
}
}
// func getStatus() -> Bool {
// if let item = statusItem {
// return true
// } else {
// return false
// }
// }
@objc func togglePopover(_ sender: AnyObject?) {
if let button = statusItem?.button {
if popover.isShown {
@@ -15,7 +15,7 @@ class WindowSettings {
private let windowXKey = "windowXKey"
private let windowYKey = "windowYKey"
@AppStorage("settings.general.mini") private var mini: Bool = false
var window: NSWindow?
var windows: [NSWindow] = []
func saveWindowSettings(frame: NSRect) {
@@ -37,7 +37,6 @@ class WindowSettings {
func newWindow<V: View>(withView view: @escaping () -> V) {
let contentView = view
let frame = self.loadWindowSettings()
// Create the window and set the content view
let newWindow = NSWindow(
contentRect: frame,
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
@@ -47,7 +46,8 @@ class WindowSettings {
newWindow.center()
newWindow.setFrameAutosaveName("Main Window")
newWindow.contentView = NSHostingView(rootView: contentView())
self.window = newWindow
// self.window = newWindow
self.windows.append(newWindow)
newWindow.makeKeyAndOrderFront(nil)
}
}