Road to v4.0.0

This commit is contained in:
Alin
2024-09-27 20:11:44 -06:00
parent c248e4c8ce
commit abdf1cfc3a
17 changed files with 1295 additions and 1024 deletions
+6
View File
@@ -14,6 +14,7 @@
C74412C72BBF249000DDFCA8 /* AppPathsFetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C74412C62BBF249000DDFCA8 /* AppPathsFetch.swift */; }; C74412C72BBF249000DDFCA8 /* AppPathsFetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C74412C62BBF249000DDFCA8 /* AppPathsFetch.swift */; };
C74FDC632BCD833D00B8960F /* Conditions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C74FDC622BCD833D00B8960F /* Conditions.swift */; }; C74FDC632BCD833D00B8960F /* Conditions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C74FDC622BCD833D00B8960F /* Conditions.swift */; };
C7575D392B0182DE006A600A /* PearcleanerSentinel in CopyFiles */ = {isa = PBXBuildFile; fileRef = C728930F2AFD51EA00C8C1CD /* PearcleanerSentinel */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; C7575D392B0182DE006A600A /* PearcleanerSentinel in CopyFiles */ = {isa = PBXBuildFile; fileRef = C728930F2AFD51EA00C8C1CD /* PearcleanerSentinel */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
C764680F2CA7790200DC72DF /* PearGroupBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = C764680E2CA778FB00DC72DF /* PearGroupBox.swift */; };
C76D08482AF83C3F00D07867 /* Update.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76D08472AF83C3F00D07867 /* Update.swift */; }; C76D08482AF83C3F00D07867 /* Update.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76D08472AF83C3F00D07867 /* Update.swift */; };
C76D084A2AF83C6E00D07867 /* General.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76D08492AF83C6E00D07867 /* General.swift */; }; C76D084A2AF83C6E00D07867 /* General.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76D08492AF83C6E00D07867 /* General.swift */; };
C76D08512AF850F300D07867 /* About.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76D08502AF850F300D07867 /* About.swift */; }; C76D08512AF850F300D07867 /* About.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76D08502AF850F300D07867 /* About.swift */; };
@@ -99,6 +100,7 @@
C72893112AFD51EA00C8C1CD /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; }; C72893112AFD51EA00C8C1CD /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
C74412C62BBF249000DDFCA8 /* AppPathsFetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPathsFetch.swift; sourceTree = "<group>"; }; C74412C62BBF249000DDFCA8 /* AppPathsFetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPathsFetch.swift; sourceTree = "<group>"; };
C74FDC622BCD833D00B8960F /* Conditions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Conditions.swift; sourceTree = "<group>"; }; C74FDC622BCD833D00B8960F /* Conditions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Conditions.swift; sourceTree = "<group>"; };
C764680E2CA778FB00DC72DF /* PearGroupBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PearGroupBox.swift; sourceTree = "<group>"; };
C76D08472AF83C3F00D07867 /* Update.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Update.swift; sourceTree = "<group>"; }; C76D08472AF83C3F00D07867 /* Update.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Update.swift; sourceTree = "<group>"; };
C76D08492AF83C6E00D07867 /* General.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = General.swift; sourceTree = "<group>"; }; C76D08492AF83C6E00D07867 /* General.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = General.swift; sourceTree = "<group>"; };
C76D08502AF850F300D07867 /* About.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = About.swift; sourceTree = "<group>"; }; C76D08502AF850F300D07867 /* About.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = About.swift; sourceTree = "<group>"; };
@@ -177,6 +179,7 @@
C7F539372AF60865007DF1B2 /* Utilities.swift */, C7F539372AF60865007DF1B2 /* Utilities.swift */,
C72893042AFD42E600C8C1CD /* DeepLink.swift */, C72893042AFD42E600C8C1CD /* DeepLink.swift */,
C77B901B2AF193A3009CC655 /* Styles.swift */, C77B901B2AF193A3009CC655 /* Styles.swift */,
C764680E2CA778FB00DC72DF /* PearGroupBox.swift */,
C7D31D502AFF00F300C7ED9E /* Locations.swift */, C7D31D502AFF00F300C7ED9E /* Locations.swift */,
C74FDC622BCD833D00B8960F /* Conditions.swift */, C74FDC622BCD833D00B8960F /* Conditions.swift */,
C7DE672A2BA6343D00EB1633 /* MenuBarItem.swift */, C7DE672A2BA6343D00EB1633 /* MenuBarItem.swift */,
@@ -429,6 +432,7 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
C7DD49EA2BAB4D8100CCBA16 /* AppInfoFetch.swift in Sources */, C7DD49EA2BAB4D8100CCBA16 /* AppInfoFetch.swift in Sources */,
C764680F2CA7790200DC72DF /* PearGroupBox.swift in Sources */,
C79947962C34A794007279CC /* Tips.swift in Sources */, C79947962C34A794007279CC /* Tips.swift in Sources */,
C76D08482AF83C3F00D07867 /* Update.swift in Sources */, C76D08482AF83C3F00D07867 /* Update.swift in Sources */,
C7FB173B2B96321300B96F9A /* AppsListView.swift in Sources */, C7FB173B2B96321300B96F9A /* AppsListView.swift in Sources */,
@@ -683,6 +687,7 @@
INFOPLIST_KEY_NSAppleEventsUsageDescription = "Pearcleaner needs to send apple events to FInder to delete files"; INFOPLIST_KEY_NSAppleEventsUsageDescription = "Pearcleaner needs to send apple events to FInder to delete files";
INFOPLIST_KEY_NSHumanReadableCopyright = ""; INFOPLIST_KEY_NSHumanReadableCopyright = "";
INFOPLIST_KEY_NSSystemAdministrationUsageDescription = "Pearcleaner requires full disk access to be able to delete files in locations with no permission"; INFOPLIST_KEY_NSSystemAdministrationUsageDescription = "Pearcleaner requires full disk access to be able to delete files in locations with no permission";
INFOPLIST_KEY_NSSystemExtensionUsageDescription = "";
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
@@ -722,6 +727,7 @@
INFOPLIST_KEY_NSAppleEventsUsageDescription = "Pearcleaner needs to send apple events to FInder to delete files"; INFOPLIST_KEY_NSAppleEventsUsageDescription = "Pearcleaner needs to send apple events to FInder to delete files";
INFOPLIST_KEY_NSHumanReadableCopyright = ""; INFOPLIST_KEY_NSHumanReadableCopyright = "";
INFOPLIST_KEY_NSSystemAdministrationUsageDescription = "Pearcleaner requires full disk access to be able to delete files in locations with no permission"; INFOPLIST_KEY_NSSystemAdministrationUsageDescription = "Pearcleaner requires full disk access to be able to delete files in locations with no permission";
INFOPLIST_KEY_NSSystemExtensionUsageDescription = "";
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
@@ -7,7 +7,7 @@
"location" : "https://github.com/alienator88/AlinFoundation", "location" : "https://github.com/alienator88/AlinFoundation",
"state" : { "state" : {
"branch" : "main", "branch" : "main",
"revision" : "eb160d04a32e1e2fe54bfa3fac1f6a7535d7b2fb" "revision" : "579f5dd4730f085ad0163442a6c81d1c5fb65e90"
} }
}, },
{ {
+1 -1
View File
@@ -31,7 +31,7 @@ struct AppCommands: Commands {
CommandGroup(replacing: .appInfo) { CommandGroup(replacing: .appInfo) {
Button { Button {
updater.checkForUpdates() updater.checkForUpdatesForce(showSheet: true)
} label: { } label: {
Text("Check for Updates") Text("Check for Updates")
} }
+136
View File
@@ -0,0 +1,136 @@
//
// IceGroupBox.swift
// Pearcleaner
//
// Created by Jordan Baird (Ice), modified for Pearcleaner usage by Alin Lupascu on 9/27/24.
//
import SwiftUI
struct PearGroupBox<Header: View, Content: View, Footer: View>: View {
private let header: Header
private let content: Content
private let footer: Footer
private let padding: CGFloat
/// Example usage:
/// ```
/// PearGroupBox(
/// header: { Text("Header View") },
/// content: { Text("Content View") },
/// footer: { Text("Footer View") }
/// )
/// ```
init(
padding: CGFloat = 10,
@ViewBuilder header: () -> Header,
@ViewBuilder content: () -> Content,
@ViewBuilder footer: () -> Footer
) {
self.padding = padding
self.header = header()
self.content = content()
self.footer = footer()
}
/// Example usage with no header:
/// ```
/// PearGroupBox(
/// content: { Text("Content View") },
/// footer: { Text("Footer View") }
/// )
/// ```
init(
padding: CGFloat = 10,
@ViewBuilder content: () -> Content,
@ViewBuilder footer: () -> Footer
) where Header == EmptyView {
self.init(padding: padding) {
EmptyView()
} content: {
content()
} footer: {
footer()
}
}
/// Example usage with no footer:
/// ```
/// PearGroupBox(
/// header: { Text("Header View") },
/// content: { Text("Content View") }
/// )
/// ```
init(
padding: CGFloat = 10,
@ViewBuilder header: () -> Header,
@ViewBuilder content: () -> Content
) where Footer == EmptyView {
self.init(padding: padding) {
header()
} content: {
content()
} footer: {
EmptyView()
}
}
/// Example usage with content only:
/// ```
/// PearGroupBox {
/// Text("Content View Only")
/// }
/// ```
init(
padding: CGFloat = 10,
@ViewBuilder content: () -> Content
) where Header == EmptyView, Footer == EmptyView {
self.init(padding: padding) {
EmptyView()
} content: {
content()
} footer: {
EmptyView()
}
}
/// Example usage with a title and content only:
/// ```
/// PearGroupBox("Header Title") {
/// Text("Content View")
/// }
/// ```
init(
_ title: LocalizedStringKey,
padding: CGFloat = 10,
@ViewBuilder content: () -> Content
) where Header == Text, Footer == EmptyView {
self.init(padding: padding) {
Text(title)
.font(.headline)
} content: {
content()
}
}
var body: some View {
VStack(alignment: .leading) {
header
content
.padding(padding)
.background {
backgroundShape
.fill(.quinary)
.overlay {
backgroundShape
.stroke(.quaternary)
}
}
footer
}
}
@ViewBuilder
private var backgroundShape: some Shape {
RoundedRectangle(cornerRadius: 7, style: .circular)
}
}
+28 -6
View File
@@ -282,12 +282,37 @@ struct SimpleButtonBrightStyle: ButtonStyle {
struct PearDropView: View { struct PearDropView: View {
var multiplier: CGFloat = 1.0 // Multiplier parameter with a default value
var body: some View { var body: some View {
VStack(alignment: .center, spacing: 0) { VStack(alignment: .center, spacing: 0) {
HStack(spacing: 0) { HStack(spacing: 0) {
LinearGradient(gradient: Gradient(colors: [.green, .orange]), startPoint: .leading, endPoint: .trailing) LinearGradient(gradient: Gradient(colors: [.green, .orange]), startPoint: .leading, endPoint: .trailing)
.frame(width: 220) .frame(width: 220 * multiplier) // Adjust the width based on the multiplier
LinearGradient(gradient: Gradient(colors: [.orange, .primary.opacity(0.5)]), startPoint: .leading, endPoint: .trailing)
.frame(width: 10 * multiplier) // Adjust the width based on the multiplier
LinearGradient(gradient: Gradient(colors: [.primary.opacity(0.5), .primary.opacity(0.5)]), startPoint: .leading, endPoint: .trailing)
.frame(width: 300 * multiplier) // Adjust the width based on the multiplier
}
.mask(
Image("logo_text_small")
.resizable()
.scaledToFit()
.frame(width: 500 * multiplier) // Scale the mask image width
.padding()
)
}
.frame(height: 120 * multiplier) // Adjust the height of the VStack based on the multiplier
}
}
struct PearDropViewSmall: View {
var body: some View {
VStack(alignment: .center, spacing: 0) {
HStack(spacing: 0) {
LinearGradient(gradient: Gradient(colors: [.green, .orange]), startPoint: .leading, endPoint: .trailing)
.frame(width: 260)
LinearGradient(gradient: Gradient(colors: [.orange, .primary.opacity(0.5)]), startPoint: .leading, endPoint: .trailing) LinearGradient(gradient: Gradient(colors: [.orange, .primary.opacity(0.5)]), startPoint: .leading, endPoint: .trailing)
.frame(width: 10) .frame(width: 10)
LinearGradient(gradient: Gradient(colors: [.primary.opacity(0.5), .primary.opacity(0.5)]), startPoint: .leading, endPoint: .trailing) LinearGradient(gradient: Gradient(colors: [.primary.opacity(0.5), .primary.opacity(0.5)]), startPoint: .leading, endPoint: .trailing)
@@ -297,13 +322,10 @@ struct PearDropView: View {
Image("logo_text_small") Image("logo_text_small")
.resizable() .resizable()
.scaledToFit() .scaledToFit()
.frame(width: 500) .frame(width: 180)
.padding()
) )
} }
.frame(height: 120) .frame(height: 50)
} }
} }
+39
View File
@@ -703,6 +703,45 @@ extension URL {
} }
} }
// Removes the sidebar toggle button from the toolbar, if running on macOS 14.0 or newer.
extension View {
@ViewBuilder
func removeSidebarToggle() -> some View {
if #available(macOS 14.0, *) {
self.toolbar(removing: .sidebarToggle)
} else {
self
}
}
}
// Drag window by background
extension View {
// Helper function to apply the movable background window
func movableByWindowBackground() -> some View {
self.background(MovableWindowAccessor())
}
}
// Custom NSWindow accessor to modify window properties
struct MovableWindowAccessor: NSViewRepresentable {
func makeNSView(context: Context) -> NSView {
let nsView = NSView()
DispatchQueue.main.async {
if let window = nsView.window {
// Enable dragging by the window's background
window.isMovableByWindowBackground = true
}
}
return nsView
}
func updateNSView(_ nsView: NSView, context: Context) {}
}
// Return image for different folders // Return image for different folders
func folderImages(for path: String) -> AnyView? { func folderImages(for path: String) -> AnyView? {
if path.contains("/Library/Containers/") || path.contains("/Library/Group Containers/") { if path.contains("/Library/Containers/") || path.contains("/Library/Group Containers/") {
+11 -9
View File
@@ -151,16 +151,18 @@ struct PearcleanerApp: App {
Settings { Settings {
SettingsView(showPopover: $showPopover, search: $search) SettingsView(showPopover: $showPopover, search: $search)
.environmentObject(appState) .environmentObject(appState)
.environmentObject(locations) .environmentObject(locations)
.environmentObject(fsm) .environmentObject(fsm)
.environmentObject(themeManager) .environmentObject(themeManager)
.environmentObject(updater) .environmentObject(updater)
.environmentObject(permissionManager) .environmentObject(permissionManager)
.toolbarBackground(.clear) .preferredColorScheme(themeManager.displayMode.colorScheme)
.preferredColorScheme(themeManager.displayMode.colorScheme) .toolbarBackground(.clear)
.movableByWindowBackground()
} }
} }
} }
-4
View File
@@ -2,10 +2,6 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>CFBundleShortVersionString</key>
<string>$(APP_VERSION)</string>
<key>CFBundleVersion</key>
<string>$(APP_BUILD)</string>
<key>CFBundleDocumentTypes</key> <key>CFBundleDocumentTypes</key>
<array> <array>
<dict> <dict>
+90 -107
View File
@@ -19,135 +19,119 @@ struct AboutSettingsTab: View {
let credits = Credit.credits let credits = Credit.credits
VStack(alignment: .center) { VStack(alignment: .center) {
Image(nsImage: NSApp.applicationIconImage)
Text(Bundle.main.name)
.font(.title)
VStack(alignment: .center, spacing: 10) { .bold()
Image(nsImage: NSApp.applicationIconImage) HStack {
// .padding(.top, 20) Text("Version \(Bundle.main.version)")
Text(Bundle.main.name) Text("(Build \(Bundle.main.buildVersion))").font(.footnote)
.font(.title)
.bold()
HStack {
Text("Version \(Bundle.main.version)")
Text("(Build \(Bundle.main.buildVersion))").font(.footnote)
}
HStack{
Image(systemName: "star")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
VStack(alignment: .leading){
Text("GitHub")
Text("Submit a bug or feature request via the repo")
.font(.callout)
.foregroundStyle(.primary.opacity(0.5))
}
Spacer()
Button(""){
NSWorkspace.shared.open(URL(string: "https://github.com/alienator88/Pearcleaner/issues/new/choose")!)
}
.buttonStyle(SimpleButtonStyle(icon: "link", help: "View"))
}
.padding(5)
.padding(.leading)
.padding(.top, 5)
} }
Divider() Text("Made with ❤️ by Alin Lupascu").font(.footnote)
HStack{ VStack(spacing: 20) {
Image(systemName: "dollarsign.circle") // GitHub
.resizable() PearGroupBox(header: { Text("GitHub Issues").font(.title2) }, content: {
.scaledToFit() HStack{
.frame(width: 20, height: 20) Image(systemName: "star")
.padding(.trailing) .resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
DisclosureGroup(isExpanded: $disclose, content: { VStack(alignment: .leading){
ScrollView { Text("Submit a bug or feature request via the repo")
LazyVStack(alignment: .leading, spacing: 0) { .font(.callout)
ForEach(sponsors) { sponsor in .foregroundStyle(.primary)
HStack() {
Text(sponsor.name) }
Spacer() Spacer()
Button(""){ Button(""){
NSWorkspace.shared.open(sponsor.url) NSWorkspace.shared.open(URL(string: "https://github.com/alienator88/Pearcleaner/issues/new/choose")!)
}
.buttonStyle(SimpleButtonStyle(icon: "link", help: "View"))
}
})
// GitHub Sponsors
PearGroupBox(header: { Text("GitHub Sponsors").font(.title2) }, content: {
HStack{
Image(systemName: "dollarsign.circle")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
Text("View project contributors")
DisclosureGroup("", isExpanded: $disclose, content: {
ScrollView {
LazyVStack(alignment: .leading, spacing: 0) {
ForEach(sponsors) { sponsor in
HStack() {
Text(sponsor.name)
Spacer()
Button(""){
NSWorkspace.shared.open(sponsor.url)
}
.buttonStyle(SimpleButtonStyle(icon: "link", help: "View", padding: 5))
.padding(.trailing)
}
} }
.buttonStyle(SimpleButtonStyle(icon: "link", help: "View", padding: 5))
} }
.frame(maxWidth: .infinity, maxHeight: .infinity)
} }
} .frame(height: 100)
.frame(maxWidth: .infinity, maxHeight: .infinity) .padding(5)
})
} }
.frame(height: 100)
.padding(5)
}, label: {
Text("GitHub Sponsors")
.font(.title2)
}) })
// Credits
PearGroupBox(header: { Text("Credits").font(.title2) }, content: {
HStack{
Image(systemName: "info.circle")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
Text("View project resources")
} DisclosureGroup("", isExpanded: $discloseCredits, content: {
.padding(5) VStack(alignment: .leading) {
.padding(.leading)
Divider() ForEach(credits) { credit in
HStack{
VStack(alignment: .leading){
Text(credit.name)
Text(credit.description)
.font(.callout)
.foregroundStyle(.primary.opacity(0.5))
HStack{ }
Image(systemName: "info.circle") Spacer()
.resizable() Button(""){
.scaledToFit() NSWorkspace.shared.open(credit.url)
.frame(width: 20, height: 20) }
.padding(.trailing) .buttonStyle(SimpleButtonStyle(icon: "link", help: "View"))
.padding(.trailing)
DisclosureGroup(isExpanded: $discloseCredits, content: {
VStack(alignment: .leading) {
ForEach(credits) { credit in
HStack{
VStack(alignment: .leading){
Text(credit.name)
Text(credit.description)
.font(.callout)
.foregroundStyle(.primary.opacity(0.5))
}
} }
Spacer()
Button(""){
NSWorkspace.shared.open(credit.url)
}
.buttonStyle(SimpleButtonStyle(icon: "link", help: "View"))
} }
} .padding(5)
})
} }
.padding(5)
}, label: {
Text("Credits")
.font(.title2)
}) })
} }
.padding(5)
.padding(.leading)
Spacer()
Text("Made with ❤️ by Alin Lupascu").font(.footnote)//.padding(.bottom)
} }
.padding(20)
.frame(width: 500, height: 500)
} }
} }
@@ -173,7 +157,6 @@ struct Sponsor: Identifiable {
//MARK: Credits //MARK: Credits
struct Credit: Identifiable { struct Credit: Identifiable {
let id = UUID() let id = UUID()
// let icon: String
let name: String let name: String
let description: String let description: String
let url: URL let url: URL
+135 -148
View File
@@ -22,191 +22,178 @@ struct FolderSettingsTab: View {
var body: some View { var body: some View {
Form { VStack(spacing: 20) {
VStack(spacing: 0) {
HStack(spacing: 0) { // === Application Folders============================================================================================
Text("Application Folders").font(.title2) PearGroupBox(header: {
InfoButton(text: "Locations that will be searched for .app files. Click a non-default path to remove it. Add new folders using the + button or drag/drop over the list. Non-default paths can't be removed.") HStack(alignment: .center, spacing: 0) {
Text("Search these folders for applications").font(.title2)
InfoButton(text: "Locations that will be searched for .app files. Click a non-default path to remove it. Default paths can't be removed.")
.padding(.leading, 5) .padding(.leading, 5)
Spacer() Spacer()
Button("") {
selectFolder()
}
.buttonStyle(SimpleButtonStyle(icon: "plus.circle", help: "Add folder", size: 16, rotate: true))
} }
.padding(.bottom, 5) }, content: {
VStack {
ScrollView {
VStack(spacing: 5) {
ForEach(fsm.folderPaths.indices, id: \.self) { index in
HStack {
Text(fsm.folderPaths[index])
ScrollView { .font(.callout)
VStack(spacing: 5) { .lineLimit(1)
ForEach(fsm.folderPaths.indices, id: \.self) { index in .truncationMode(.tail)
HStack { .opacity(fsm.defaultPaths.contains(fsm.folderPaths[index]) ? 0.5 : 1)
.padding(5)
Text(fsm.folderPaths[index]) Spacer()
.font(.callout)
.lineLimit(1)
.truncationMode(.tail)
.opacity(fsm.defaultPaths.contains(fsm.folderPaths[index]) ? 0.5 : 1)
.padding(5)
Spacer()
}
.disabled(fsm.defaultPaths.contains(fsm.folderPaths[index]))
.onHover { hovering in
withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) {
isHovered = hovering
} }
if isHovered && !fsm.defaultPaths.contains(fsm.folderPaths[index]) { .disabled(fsm.defaultPaths.contains(fsm.folderPaths[index]))
NSCursor.disappearingItem.push() .onHover { hovering in
} else { withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) {
NSCursor.pop() isHovered = hovering
}
if isHovered && !fsm.defaultPaths.contains(fsm.folderPaths[index]) {
NSCursor.disappearingItem.push()
} else {
NSCursor.pop()
}
} }
} .onTapGesture {
.onTapGesture { if !fsm.defaultPaths.contains(fsm.folderPaths[index]) {
if !fsm.defaultPaths.contains(fsm.folderPaths[index]) { fsm.removePath(at: index)
fsm.removePath(at: index) }
}
if index != fsm.folderPaths.indices.last {
Divider().opacity(0.5)
} }
} }
if index != fsm.folderPaths.indices.last {
Divider().opacity(0.5)
}
} }
} }
.scrollIndicators(.automatic)
} .padding()
.scrollIndicators(.automatic) .frame(height: 200)
.padding() .clipShape(RoundedRectangle(cornerRadius: 10))
.frame(height: 200) .onDrop(of: ["public.file-url"], isTargeted: nil) { providers -> Bool in
// .background(.primary.opacity(0.05)) providers.forEach { provider in
.background(backgroundView(themeManager: themeManager, darker: true, glass: glass)) provider.loadDataRepresentation(forTypeIdentifier: "public.file-url") { (data, error) in
.clipShape(RoundedRectangle(cornerRadius: 10)) guard let data = data, error == nil,
.onDrop(of: ["public.file-url"], isTargeted: nil) { providers -> Bool in let url = URL(dataRepresentation: data, relativeTo: nil),
providers.forEach { provider in url.hasDirectoryPath else {
provider.loadDataRepresentation(forTypeIdentifier: "public.file-url") { (data, error) in printOS("FSM: Failed to load URL or the item is not a folder")
guard let data = data, error == nil, return
let url = URL(dataRepresentation: data, relativeTo: nil), }
url.hasDirectoryPath else { updateOnMain {
printOS("FSM: Failed to load URL or the item is not a folder") fsm.addPath(url.path)
return }
}
updateOnMain {
fsm.addPath(url.path)
} }
} }
return true
}
HStack {
Spacer()
Text("Drop folders above or click to add").opacity(0.5)
Button("") {
selectFolder()
}
.buttonStyle(SimpleButtonStyle(icon: "plus.circle", help: "Add folder", size: 16, rotate: true))
Spacer()
} }
return true
} }
})
HStack { // === Leftover Folders============================================================================================
Spacer()
Text("Drop folders here").opacity(0.5)
Spacer()
}
.padding(.top, 5)
Divider()
.padding(.vertical)
// === LEFTOVER FILES ================================================================================================
PearGroupBox(header: {
HStack(spacing: 0) { HStack(spacing: 0) {
Text("Leftover Files Exclusions").font(.title2) Text("Exclude these files and folders from leftover search").font(.title2)
InfoButton(text: "Add files or folders that will be ignored when searching for leftover files. Click a path to remove it from the list. Add new files/folders using the + button or drag/drop over the list.") InfoButton(text: "Add files or folders that will be ignored when searching for leftover files. Click a path to remove it from the list.")
.padding(.leading, 5) .padding(.leading, 5)
Spacer() Spacer()
Button("") {
selectFilesFoldersZ()
}
.buttonStyle(SimpleButtonStyle(icon: "plus.circle", help: "Add file/folder", size: 16, rotate: true))
} }
.padding(.bottom, 5) }, content: {
VStack {
ScrollView {
ScrollView { VStack(spacing: 5) {
VStack(spacing: 5) { if fsm.fileFolderPathsZ.count == 0 {
if fsm.fileFolderPathsZ.count == 0 { HStack {
HStack { Text("No files or folders added")
Text("No files or folders added") .font(.callout)
.font(.callout) .opacity(0.5)
.opacity(0.5) .padding(5)
.padding(5) Spacer()
Spacer()
}
.disabled(true)
}
ForEach(fsm.fileFolderPathsZ.indices, id: \.self) { index in
HStack {
Text(fsm.fileFolderPathsZ[index])
.font(.callout)
.lineLimit(1)
.truncationMode(.tail)
.padding(5)
Spacer()
}
.onHover { hovering in
withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) {
isHovered = hovering
} }
if isHovered { .disabled(true)
NSCursor.disappearingItem.push() }
} else { ForEach(fsm.fileFolderPathsZ.indices, id: \.self) { index in
NSCursor.pop() HStack {
Text(fsm.fileFolderPathsZ[index])
.font(.callout)
.lineLimit(1)
.truncationMode(.tail)
.padding(5)
Spacer()
}
.onHover { hovering in
withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) {
isHovered = hovering
}
if isHovered {
NSCursor.disappearingItem.push()
} else {
NSCursor.pop()
}
}
.onTapGesture {
fsm.removePathZ(at: index)
}
if index != fsm.fileFolderPathsZ.indices.last {
Divider().opacity(0.5)
} }
} }
.onTapGesture {
fsm.removePathZ(at: index)
}
if index != fsm.fileFolderPathsZ.indices.last {
Divider().opacity(0.5)
}
} }
} }
.scrollIndicators(.automatic)
} .padding()
.scrollIndicators(.automatic) .frame(height: 200)
.padding() .clipShape(RoundedRectangle(cornerRadius: 10))
.frame(height: 200) .onDrop(of: ["public.file-url"], isTargeted: nil) { providers -> Bool in
.background(backgroundView(themeManager: themeManager, darker: true, glass: glass)) providers.forEach { provider in
// .background(.primary.opacity(0.05)) provider.loadDataRepresentation(forTypeIdentifier: "public.file-url") { (data, error) in
.clipShape(RoundedRectangle(cornerRadius: 10)) guard let data = data, error == nil,
.onDrop(of: ["public.file-url"], isTargeted: nil) { providers -> Bool in let url = URL(dataRepresentation: data, relativeTo: nil) else {
providers.forEach { provider in printOS("FSM: Failed to load URL")
provider.loadDataRepresentation(forTypeIdentifier: "public.file-url") { (data, error) in return
guard let data = data, error == nil, }
let url = URL(dataRepresentation: data, relativeTo: nil) else { updateOnMain {
printOS("FSM: Failed to load URL") fsm.addPathZ(url.path)
return }
}
updateOnMain {
fsm.addPathZ(url.path)
} }
} }
return true
} }
return true
}
HStack { HStack {
Spacer() Spacer()
Text("Drop files/folders here").opacity(0.5) Text("Drop files or folders above or click to add").opacity(0.5)
Spacer() Button("") {
selectFilesFoldersZ()
}
.buttonStyle(SimpleButtonStyle(icon: "plus.circle", help: "Add file/folder", size: 16, rotate: true))
Spacer()
}
} }
.padding(.top, 5) })
}
Spacer()
} }
.padding(20)
.frame(width: 500, height: 580)
} }
+227 -318
View File
@@ -21,352 +21,261 @@ struct GeneralSettingsTab: View {
@AppStorage("settings.general.confirmAlert") private var confirmAlert: Bool = false @AppStorage("settings.general.confirmAlert") private var confirmAlert: Bool = false
@AppStorage("settings.general.selectedSort") var selectedSortAlpha: Bool = true @AppStorage("settings.general.selectedSort") var selectedSortAlpha: Bool = true
@AppStorage("settings.general.sizeType") var sizeType: String = "Real" @AppStorage("settings.general.sizeType") var sizeType: String = "Real"
// @AppStorage("settings.general.brewTerminal") var brewTerm: String = "Terminal"
@State private var isResetting = false
@State private var isCLISymlinked = false @State private var isCLISymlinked = false
var body: some View { var body: some View {
VStack() { VStack(spacing: 20) {
HStack() { // === Functionality ================================================================================================
Text("Functionality").font(.title2) PearGroupBox(
Spacer() header: { Text("Functionality").font(.title2) },
} content: {
.padding(.leading) VStack {
HStack(spacing: 0) {
Image(systemName: brew ? "mug.fill" : "mug")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(.primary.opacity(1))
VStack(alignment: .leading, spacing: 0) {
HStack(spacing: 0) {
Text("Homebrew cleanup after uninstall")
.font(.callout)
.foregroundStyle(.primary.opacity(1))
InfoButton(text: "When homebrew cleanup is enabled, Pearcleaner will check if the app you are removing was installed via homebrew and launch Terminal.app to execute a brew uninstall and cleanup command to let homebrew know that the app is removed. This way your homebrew list will be synced up correctly and caching will be removed. Terminal.app is required since some apps need sudo permissions to remove services and files placed in system folders. Since other terminal apps don't support applescript and/or the 'do script' command, I opted to use the default macOS Terminal app for this.\n\nNOTE: If you undo the file delete with CMD+Z, the files will be put back but homebrew will not be aware of it. To get the homebrew list back in sync you'd need to run:\n brew install APPNAME --force")
}
}
HStack(spacing: 0) {
Image(systemName: brew ? "mug.fill" : "mug") Spacer()
.resizable() Toggle(isOn: $brew, label: {
.scaledToFit() })
.frame(width: 20, height: 20) .toggleStyle(.switch)
.padding(.trailing) }
.foregroundStyle(.primary.opacity(0.5)) .padding(5)
VStack(alignment: .leading, spacing: 0) {
HStack(spacing: 0) {
Image(systemName: confirmAlert ? "exclamationmark.triangle.fill" : "exclamationmark.triangle")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(.primary)
VStack(alignment: .leading, spacing: 5) {
Text("Uninstall confirmation alerts")
.font(.callout)
.foregroundStyle(.primary)
}
InfoButton(text: "When deleting files using the Trash button, you can prevent accidental deletions by showing an alert before proceeding with the action.")
Spacer()
Toggle(isOn: $confirmAlert, label: {
})
.toggleStyle(.switch)
}
.padding(5)
HStack(spacing: 0) {
Image(systemName: oneShotMode ? "scope" : "circlebadge")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(.primary)
VStack(alignment: .leading, spacing: 5) {
Text("Close after uninstall")
.font(.callout)
.foregroundStyle(.primary)
}
InfoButton(text: "When this mode is enabled, clicking the Uninstall button to remove an app will also close Pearcleaner right after.\n This only affects Pearcleaner when it is opened via external means, like Sentinel Trash Monitor, Finder extension or a Deep Link.\nThis allows for single use of the app for a quick uninstall. When Pearcleaner is opened normally, this setting is ignored and will work as usual.")
Spacer()
Toggle(isOn: $oneShotMode, label: {
})
.toggleStyle(.switch)
}
.padding(5)
HStack(spacing: 0) {
Image(systemName: selectedSortAlpha ? "textformat.abc" : "textformat.123")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(.primary)
VStack(alignment: .leading, spacing: 5) {
Text("File list sorting order")
.font(.callout)
.foregroundStyle(.primary)
}
Spacer()
Picker("", selection: $selectedSortAlpha) {
Text("Alphabetical")
.tag(true)
Text("File Size")
.tag(false)
}
.buttonStyle(.borderless)
}
.padding(5)
HStack(spacing: 0) {
Image(systemName: "plus.forwardslash.minus")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(.primary)
VStack(alignment: .leading, spacing: 5) {
Text("File size display options")
.font(.callout)
.foregroundStyle(.primary)
}
InfoButton(text: "Real size type will show how much actual allocated space the file has on disk.\n\nLogical type shows the binary size. The filesystem can compress and deduplicate sectors on disk, so real size is sometimes smaller(or bigger) than logical size.\n\nFinder size is similar to if you right click > Get Info on a file in Finder, which will show both the logical and real sizes together.")
Spacer()
Picker("", selection: $sizeType) {
Text("Real")
.tag("Real")
Text("Logical")
.tag("Logical")
Text("Finder")
.tag("Finder")
}
.buttonStyle(.borderless)
}
.padding(5)
}
}
)
// === Sentinel =====================================================================================================
PearGroupBox(
header: { Text("Sentinel Monitor").font(.title2) },
content: {
HStack(spacing: 0) { HStack(spacing: 0) {
Text("\(brew ? "Homebrew cleanup is enabled" : "Homebrew cleanup is disabled")") Image(systemName: sentinel ? "eye.circle" : "eye.slash.circle")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(.primary)
// .saturation(themeManager.displayMode.colorScheme == .dark ? 0.8 : 1)
Text("Detect when apps are moved to Trash")
.font(.callout) .font(.callout)
.foregroundStyle(.primary.opacity(0.5)) .foregroundStyle(.primary)
InfoButton(text: "When homebrew cleanup is enabled, Pearcleaner will check if the app you are removing was installed via homebrew and launch Terminal.app to execute a brew uninstall and cleanup command to let homebrew know that the app is removed. This way your homebrew list will be synced up correctly and caching will be removed. Terminal.app is required since some apps need sudo permissions to remove services and files placed in system folders. Since other terminal apps don't support applescript and/or the 'do script' command, I opted to use the default macOS Terminal app for this.\n\nNOTE: If you undo the file delete with CMD+Z, the files will be put back but homebrew will not be aware of it. To get the homebrew list back in sync you'd need to run:\n brew install APPNAME --force") InfoButton(text: "When applications are moved to Trash, Pearcleaner will launch and find related files and folders for deletion.")
Spacer()
Toggle(isOn: $sentinel, label: {
})
.toggleStyle(.switch)
.onChange(of: sentinel) { newValue in
if newValue {
launchctl(load: true)
} else {
launchctl(load: false)
}
}
} }
.padding(5)
// HStack(spacing: 0) { })
// Text("Cleanup application: \(brewTerm)")
// .font(.footnote)
// .foregroundStyle(.primary.opacity(0.4))
//
// Button("") {
// selectBrewTerm()
// }
// .buttonStyle(SimpleButtonStyle(icon: "gear", help: "Set terminal application, right click to Reset", size: 14))
// .contextMenu{
// Button("Reset") {
// brewTerm = "Terminal"
// }
// }
// }
}
Spacer()
Toggle(isOn: $brew, label: {
})
.toggleStyle(.switch)
}
.padding(5)
.padding(.leading)
HStack(spacing: 0) {
Image(systemName: confirmAlert ? "exclamationmark.triangle.fill" : "exclamationmark.triangle")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(.primary.opacity(0.5))
VStack(alignment: .leading, spacing: 5) {
Text("\(confirmAlert ? "Confirmation alerts enabled" : "Confirmation alerts disabled")")
.font(.callout)
.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. Unrelated to the password prompt alert when deleting files in system folders.")
Spacer()
Toggle(isOn: $confirmAlert, label: {
})
.toggleStyle(.switch)
}
.padding(5)
.padding(.leading)
HStack(spacing: 0) {
Image(systemName: oneShotMode ? "scope" : "circlebadge")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(.primary.opacity(0.5))
VStack(alignment: .leading, spacing: 5) {
Text("\(oneShotMode ? "One-Shot Mode is enabled" : "One-Shot Mode is disabled")")
.font(.callout)
.foregroundStyle(.primary.opacity(0.5))
}
InfoButton(text: "When one-shot mode is enabled, clicking the Uninstall button to remove an app will also close Pearcleaner right after. This only affects Pearcleaner when it is opened via external means, like Sentinel Trash Monitor or the Finder extension. This allows for single use of the app, AKA one-shot mode. When Pearcleaner is opened normally, this setting is ignored and will work as usual.")
Spacer()
Toggle(isOn: $oneShotMode, label: {
})
.toggleStyle(.switch)
}
.padding(5)
.padding(.leading)
HStack(spacing: 0) {
Image(systemName: selectedSortAlpha ? "textformat.abc" : "textformat.123")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(.primary.opacity(0.5))
VStack(alignment: .leading, spacing: 5) {
Text("File list sorting mode")
.font(.callout)
.foregroundStyle(.primary.opacity(0.5))
}
// InfoButton(text: "When searching for app files or leftover files, the list will be sorted either alphabetically or by size(large to small)")
Spacer()
Picker("", selection: $selectedSortAlpha) {
Text("Alphabetical")
.tag(true)
Text("File Size")
.tag(false)
}
.buttonStyle(.borderless)
}
.padding(5)
.padding(.leading)
HStack(spacing: 0) {
Image(systemName: "plus.forwardslash.minus")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(.primary.opacity(0.5))
VStack(alignment: .leading, spacing: 5) {
Text("File size display mode")
.font(.callout)
.foregroundStyle(.primary.opacity(0.5))
}
InfoButton(text: "Real size type will show how much actual allocated space the file has on disk. Logical type shows the binary size. The filesystem can compress and deduplicate sectors on disk, so real size is sometimes smaller(or bigger) than logical size. Finder size is similar to if you right click > Get Info on a file in Finder, which will show both the logical and real sizes together.")
Spacer()
Picker("", selection: $sizeType) {
Text("Real")
.tag("Real")
Text("Logical")
.tag("Logical")
Text("Finder")
.tag("Finder")
}
.buttonStyle(.borderless)
}
.padding(5)
.padding(.leading)
// === Sentinel =============================================================================================
Divider()
.padding()
HStack() {
Text("Sentinel Monitor").font(.title2)
Spacer()
}
.padding(.leading)
HStack(spacing: 0) {
Image(systemName: sentinel ? "eye.circle" : "eye.slash.circle")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(sentinel ? .green : .red)
.saturation(themeManager.displayMode.colorScheme == .dark ? 0.8 : 1)
Text(sentinel ? "Detecting when apps are moved to Trash" : "**NOT** detecting when apps are moved to Trash")
.font(.callout)
.foregroundStyle(.primary.opacity(0.5))
Spacer()
Toggle(isOn: $sentinel, label: {
})
.toggleStyle(.switch)
.onChange(of: sentinel) { newValue in
if newValue {
launchctl(load: true)
} else {
launchctl(load: false)
}
}
}
.padding(5)
.padding(.leading)
// === Finder Extension ============================================================================================= // === Finder Extension =============================================================================================
PearGroupBox(
header: { Text("Finder Extension").font(.title2) },
content: {
HStack(spacing: 0) {
Image(systemName: appState.finderExtensionEnabled ? "puzzlepiece.extension.fill" : "puzzlepiece.extension")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(.primary)
// .saturation(themeManager.displayMode.colorScheme == .dark ? 0.8 : 1)
Text("Enable context menu extension for Finder")
.font(.callout)
.foregroundStyle(.primary)
InfoButton(text: "Enabling this extension will allow you to right click apps in Finder to quickly uninstall them with Pearcleaner")
Divider() Spacer()
.padding()
HStack() { // Button("Extensions") {
Text("Finder Extension").font(.title2) // FIFinderSyncController.showExtensionManagementInterface()
Spacer() // }
} // .buttonStyle(SimpleButtonStyle(icon: "folder", help: "Show Extensions Pane"))
.padding(.leading)
HStack(spacing: 0) { Toggle(isOn: $appState.finderExtensionEnabled, label: {
Image(systemName: appState.finderExtensionEnabled ? "puzzlepiece.extension.fill" : "puzzlepiece.extension") })
.resizable() .toggleStyle(.switch)
.scaledToFit() .onChange(of: appState.finderExtensionEnabled) { newValue in
.frame(width: 20, height: 20) if newValue {
.padding(.trailing) manageFinderPlugin(install: true)
.foregroundStyle(appState.finderExtensionEnabled ? .green : .red) } else {
.saturation(themeManager.displayMode.colorScheme == .dark ? 0.8 : 1) manageFinderPlugin(install: false)
Text(appState.finderExtensionEnabled ? "Context menu extension for Finder is enabled" : "Context menu extension for Finder is disabled") }
.font(.callout) }
.foregroundStyle(.primary.opacity(0.5))
InfoButton(text: "Enabling the extension will allow you to right click apps in Finder and quickly uninstall them with Pearcleaner")
Spacer()
// 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)
} }
} .padding(5)
})
// === CLI ==========================================================================================================
PearGroupBox(
header: { Text("Command Line").font(.title2) },
content: {
HStack(spacing: 0) {
Image(systemName: "terminal")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(.primary)
// .saturation(themeManager.displayMode.colorScheme == .dark ? 0.8 : 1)
Text("Pearcleaner CLI support")
.font(.callout)
.foregroundStyle(.primary)
InfoButton(text: "Enabling the CLI will allow you to execute Pearcleaner actions from the Terminal. This will add pearcleaner command into /usr/local/bin so it's available directly from your PATH environment variable. Try it after enabling:\n\n> pearcleaner --help")
Spacer()
} Toggle(isOn: $isCLISymlinked, label: {
.padding(5) })
.padding(.leading) .toggleStyle(.switch)
.onChange(of: isCLISymlinked) { newValue in
if newValue {
manageSymlink(install: true)
} else {
manageSymlink(install: false)
}
}
// === CLI =============================================================================================
Divider()
.padding()
HStack() {
Text("Command Line").font(.title2)
Spacer()
}
.padding(.leading)
HStack(spacing: 0) {
Image(systemName: "terminal")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(isCLISymlinked ? .green : .red)
.saturation(themeManager.displayMode.colorScheme == .dark ? 0.8 : 1)
Text(isCLISymlinked ? "Pearcleaner CLI is installed" : "Pearcleaner CLI is **not** installed")
.font(.callout)
.foregroundStyle(.primary.opacity(0.5))
InfoButton(text: "Enabling the CLI will allow you to execute Pearcleaner actions from the Terminal. This will add pearcleaner command into /usr/local/bin so it's available directly from your PATH environment variable. Try it after enabling:\n\n > pearcleaner --help")
Spacer()
Toggle(isOn: $isCLISymlinked, label: {
})
.toggleStyle(.switch)
.onChange(of: isCLISymlinked) { newValue in
if newValue {
manageSymlink(install: true)
} else {
manageSymlink(install: false)
} }
} .padding(5)
})
}
.padding(5)
.padding(.leading)
// === Reset Settings =============================================================================================
HStack() {
Spacer()
Button("") {
resetUserDefaults()
}
.buttonStyle(ResetSettingsButtonStyle(isResetting: $isResetting, label: "Reset Settings", help: "Reset all app settings to default"))
.disabled(isResetting)
Spacer()
}
.padding(.vertical, 10)
Spacer()
} }
.onAppear { .onAppear {
appState.updateExtensionStatus() appState.updateExtensionStatus()
isCLISymlinked = checkCLISymlink() isCLISymlinked = checkCLISymlink()
} }
.padding(20)
.frame(width: 500, height: 640)
} }
private func resetUserDefaults() {
isResetting = true
DispatchQueue.global(qos: .background).async {
UserDefaults.standard.dictionaryRepresentation().keys.forEach(UserDefaults.standard.removeObject(forKey:))
DispatchQueue.main.async {
isResetting = false
}
}
}
// private func selectBrewTerm() {
// let dialog = NSOpenPanel()
// dialog.title = "Select a Terminal application"
// dialog.allowedContentTypes = [UTType("com.apple.application-bundle")!] // Limit to .app bundles
// dialog.allowsMultipleSelection = false
// dialog.canChooseFiles = true
// dialog.canChooseDirectories = false
//
// if dialog.runModal() == .OK, let url = dialog.url {
// let appNameWithExtension = url.lastPathComponent
// brewTerm = appNameWithExtension.replacingOccurrences(of: ".app", with: "")
// }
// }
} }
+311 -341
View File
@@ -46,274 +46,124 @@ struct InterfaceSettingsTab: View {
var body: some View { var body: some View {
VStack { VStack(spacing: 20) {
HStack() { // === Appearance =================================================================================================
Text("Appearance").font(.title2) PearGroupBox(header: { Text("Appearance").font(.title2) },
Spacer() content: {
} VStack {
.padding(.leading) HStack(spacing: 0) {
Image(systemName: glass ? "cube.transparent" : "cube.transparent.fill")
HStack(spacing: 0) { .resizable()
Image(systemName: glass ? "cube.transparent" : "cube.transparent.fill") .scaledToFit()
.resizable() .frame(width: 20, height: 20)
.scaledToFit() .padding(.trailing)
.frame(width: 20, height: 20) .foregroundStyle(.primary)
.padding(.trailing) VStack(alignment: .leading, spacing: 5) {
.foregroundStyle(.primary.opacity(0.5)) Text("Transparent material")
VStack(alignment: .leading, spacing: 5) { .font(.callout)
Text("\(glass ? "Transparent material enabled" : "Transparent material disabled")") .foregroundStyle(.primary)
.font(.callout)
.foregroundStyle(.primary.opacity(0.5))
}
Spacer()
Toggle(isOn: $glass, label: {
})
.toggleStyle(.switch)
.onChange(of: glass) { newVal in
MenuBarExtraManager.shared.restartMenuBarExtra()
}
}
.padding(5)
.padding(.leading)
HStack(spacing: 0) {
Image(systemName: animationEnabled ? "play" : "play.slash")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(.primary.opacity(0.5))
VStack(alignment: .leading, spacing: 5) {
Text("\(animationEnabled ? "Animations enabled" : "Animations disabled")")
.font(.callout)
.foregroundStyle(.primary.opacity(0.5))
}
Spacer()
Toggle(isOn: $animationEnabled, label: {
})
.toggleStyle(.switch)
}
.padding(5)
.padding(.leading)
HStack(spacing: 0) {
Image(systemName: minimalEnabled ? "list.dash.header.rectangle" : "list.bullet.rectangle")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(.primary.opacity(0.5))
VStack(alignment: .leading, spacing: 5) {
Text("\(minimalEnabled ? "Minimalist app list rows enabled" : "Minimalist app list rows disabled")")
.font(.callout)
.foregroundStyle(.primary.opacity(0.5))
}
Spacer()
Toggle(isOn: $minimalEnabled, label: {
})
.toggleStyle(.switch)
}
.padding(5)
.padding(.leading)
HStack(spacing: 0) {
Image(systemName: "paintbrush")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(.primary.opacity(0.5))
VStack(alignment: .leading, spacing: 5) {
Text("Theme Mode")
.font(.callout)
.foregroundStyle(.primary.opacity(0.5))
}
Spacer()
ThemeSettingsView(opacity: 1)
.onChange(of: themeManager.displayMode) { _ in
MenuBarExtraManager.shared.restartMenuBarExtra()
}
}
.padding(5)
.padding(.leading)
// === Mini =================================================================================================
Divider()
.padding()
HStack() {
Text("Mini Configuration").font(.title2)
Spacer()
}
.padding(.leading)
HStack(spacing: 0) {
Image(systemName: isVersionOrHigher(version: 14) ? "square.resize.up" : "macwindow.on.rectangle")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(.primary.opacity(0.5))
VStack(alignment: .leading, spacing: 5) {
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: {
})
.toggleStyle(.switch)
.disabled(menubarEnabled)
.help(menubarEnabled ? "Disabled when menubar icon is enabled" : "")
.onChange(of: mini) { newVal in
if mini {
appState.currentView = miniView ? .apps : .empty
showPopover = false
windowSettings.newWindow(mini: true, withView: {
MiniMode(search: $search, showPopover: $showPopover)
.environmentObject(appState)
.environmentObject(locations)
.environmentObject(fsm)
.environmentObject(themeManager)
.environmentObject(updater)
.environmentObject(permissionManager)
.preferredColorScheme(themeManager.displayMode.colorScheme)
})
} else {
if appState.appInfo.appName.isEmpty {
appState.currentView = .empty
} else {
appState.currentView = .files
} }
windowSettings.newWindow(mini: false, withView: { Spacer()
RegularMode(search: $search, showPopover: $showPopover) Toggle(isOn: $glass, label: {
.environmentObject(appState)
.environmentObject(locations)
.environmentObject(fsm)
.environmentObject(themeManager)
.environmentObject(updater)
.environmentObject(permissionManager)
.preferredColorScheme(themeManager.displayMode.colorScheme)
}
)
}
}
}
.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(.primary.opacity(0.5))
VStack(alignment: .leading, spacing: 5) {
Text("\(miniView ? "Show apps list on startup" : "Show drop target on startup")")
.font(.callout)
.foregroundStyle(.primary.opacity(0.5))
}
InfoButton(text: "In mini window mode, you can have Pearcleaner startup to the Apps List view or the Drop Target view.")
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(.primary.opacity(0.5))
VStack(alignment: .leading, spacing: 5) {
Text("\(popoverStay ? "Popover window will stay on top" : "Popover window will not stay on top")")
.font(.callout)
.foregroundStyle(.primary.opacity(0.5))
}
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.")
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(.primary.opacity(0.5))
VStack(alignment: .leading, spacing: 5) {
Text("\(menubarEnabled ? "Menubar icon enabled" : "Menubar icon disabled")")
.font(.callout)
.foregroundStyle(.primary.opacity(0.5))
}
InfoButton(text: "When menubar icon is enabled, the main app window and dock icon will be disabled since the app will be put in accessory mode.")
Spacer()
Toggle(isOn: $menubarEnabled, label: {
})
.toggleStyle(.switch)
.onChange(of: menubarEnabled) { newVal in
if newVal {
MenuBarExtraManager.shared.addMenuBarExtra(withView: {
MiniAppView(search: $search, showPopover: $showPopover, isMenuBar: true)
.environmentObject(appState)
.environmentObject(locations)
.environmentObject(fsm)
.environmentObject(themeManager)
.environmentObject(updater)
.environmentObject(permissionManager)
.preferredColorScheme(themeManager.displayMode.colorScheme)
}) })
NSApplication.shared.setActivationPolicy(.accessory) .toggleStyle(.switch)
findAndHideWindows(named: ["Pearcleaner"]) .onChange(of: glass) { newVal in
} else { MenuBarExtraManager.shared.restartMenuBarExtra()
MenuBarExtraManager.shared.removeMenuBarExtra() }
NSApplication.shared.setActivationPolicy(.regular) }
if !hasWindowOpen() { .padding(5)
HStack(spacing: 0) {
Image(systemName: animationEnabled ? "play" : "play.slash")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(.primary)
VStack(alignment: .leading, spacing: 5) {
Text("\(animationEnabled ? "Animations enabled" : "Animations disabled")")
.font(.callout)
.foregroundStyle(.primary)
}
Spacer()
Toggle(isOn: $animationEnabled, label: {
})
.toggleStyle(.switch)
}
.padding(5)
HStack(spacing: 0) {
Image(systemName: minimalEnabled ? "list.dash.header.rectangle" : "list.bullet.rectangle")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(.primary)
VStack(alignment: .leading, spacing: 5) {
Text("\(minimalEnabled ? "Minimalist app list rows enabled" : "Minimalist app list rows disabled")")
.font(.callout)
.foregroundStyle(.primary)
}
Spacer()
Toggle(isOn: $minimalEnabled, label: {
})
.toggleStyle(.switch)
}
.padding(5)
HStack(spacing: 0) {
Image(systemName: "paintbrush")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(.primary)
VStack(alignment: .leading, spacing: 5) {
Text("Theme Mode")
.font(.callout)
.foregroundStyle(.primary)
}
Spacer()
ThemeSettingsView(opacity: 1)
.onChange(of: themeManager.displayMode) { _ in
MenuBarExtraManager.shared.restartMenuBarExtra()
}
}
.padding(5)
}
})
// === Mini =======================================================================================================
PearGroupBox(header: { Text("Mini Configuration").font(.title2) },
content: {
VStack {
HStack(spacing: 0) {
Image(systemName: isVersionOrHigher(version: 14) ? "square.resize.up" : "macwindow.on.rectangle")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(.primary)
VStack(alignment: .leading, spacing: 5) {
Text("Mini window mode")
.font(.callout)
.foregroundStyle(.primary)
}
InfoButton(text: "Toggling between modes will reset the window frames to their default size/position")
Spacer()
Toggle(isOn: $mini, label: {
})
.toggleStyle(.switch)
.disabled(menubarEnabled)
.help(menubarEnabled ? "Disabled when menubar icon is enabled" : "")
.onChange(of: mini) { newVal in
if mini { if mini {
appState.currentView = miniView ? .apps : .empty
showPopover = false
windowSettings.newWindow(mini: true, withView: { windowSettings.newWindow(mini: true, withView: {
MiniMode(search: $search, showPopover: $showPopover) MiniMode(search: $search, showPopover: $showPopover)
.environmentObject(appState) .environmentObject(appState)
@@ -325,6 +175,11 @@ struct InterfaceSettingsTab: View {
.preferredColorScheme(themeManager.displayMode.colorScheme) .preferredColorScheme(themeManager.displayMode.colorScheme)
}) })
} else { } else {
if appState.appInfo.appName.isEmpty {
appState.currentView = .empty
} else {
appState.currentView = .files
}
windowSettings.newWindow(mini: false, withView: { windowSettings.newWindow(mini: false, withView: {
RegularMode(search: $search, showPopover: $showPopover) RegularMode(search: $search, showPopover: $showPopover)
.environmentObject(appState) .environmentObject(appState)
@@ -334,98 +189,213 @@ struct InterfaceSettingsTab: View {
.environmentObject(updater) .environmentObject(updater)
.environmentObject(permissionManager) .environmentObject(permissionManager)
.preferredColorScheme(themeManager.displayMode.colorScheme) .preferredColorScheme(themeManager.displayMode.colorScheme)
}
)
}
}
}
.padding(5)
HStack(spacing: 0) {
Image(systemName: miniView ? "square.grid.3x3.square" : "plus.square.dashed")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(.primary)
VStack(alignment: .leading, spacing: 5) {
Text("Show apps list on startup")
.font(.callout)
.foregroundStyle(.primary)
}
InfoButton(text: "In mini window mode, you can have Pearcleaner startup to the Apps List view or the Drop Target view.")
Spacer()
Toggle(isOn: $miniView, label: {
})
.toggleStyle(.switch)
.onChange(of: miniView) { newVal in
appState.currentView = newVal ? .apps : .empty
}
}
.padding(5)
HStack(spacing: 0) {
Image(systemName: popoverStay ? "pin" : "pin.slash")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(.primary)
VStack(alignment: .leading, spacing: 5) {
Text("Pin popover window on top")
.font(.callout)
.foregroundStyle(.primary)
}
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 app.")
Spacer()
Toggle(isOn: $popoverStay, label: {
})
.toggleStyle(.switch)
}
.padding(5)
}
})
// === Menubar ====================================================================================================
PearGroupBox(header: { Text("Menubar").font(.title2) },
content: {
VStack {
HStack(spacing: 0) {
Image(systemName: "menubar.rectangle")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(.primary)
VStack(alignment: .leading, spacing: 5) {
Text("Menubar icon mode")
.font(.callout)
.foregroundStyle(.primary)
}
InfoButton(text: "When menubar icon is enabled, the main app window and dock icon will be disabled since the app will be put in accessory mode.")
Spacer()
Toggle(isOn: $menubarEnabled, label: {
})
.toggleStyle(.switch)
.onChange(of: menubarEnabled) { newVal in
if newVal {
MenuBarExtraManager.shared.addMenuBarExtra(withView: {
MiniAppView(search: $search, showPopover: $showPopover, isMenuBar: true)
.environmentObject(appState)
.environmentObject(locations)
.environmentObject(fsm)
.environmentObject(themeManager)
.environmentObject(updater)
.environmentObject(permissionManager)
.preferredColorScheme(themeManager.displayMode.colorScheme)
}) })
NSApplication.shared.setActivationPolicy(.accessory)
findAndHideWindows(named: ["Pearcleaner"])
} else {
MenuBarExtraManager.shared.removeMenuBarExtra()
NSApplication.shared.setActivationPolicy(.regular)
if !hasWindowOpen() {
if mini {
windowSettings.newWindow(mini: true, withView: {
MiniMode(search: $search, showPopover: $showPopover)
.environmentObject(appState)
.environmentObject(locations)
.environmentObject(fsm)
.environmentObject(themeManager)
.environmentObject(updater)
.environmentObject(permissionManager)
.preferredColorScheme(themeManager.displayMode.colorScheme)
})
} else {
windowSettings.newWindow(mini: false, withView: {
RegularMode(search: $search, showPopover: $showPopover)
.environmentObject(appState)
.environmentObject(locations)
.environmentObject(fsm)
.environmentObject(themeManager)
.environmentObject(updater)
.environmentObject(permissionManager)
.preferredColorScheme(themeManager.displayMode.colorScheme)
})
}
}
} }
} }
} }
} .padding(5)
}
.padding(5)
.padding(.leading)
HStack(spacing: 0) { HStack(spacing: 0) {
Image(systemName: isLaunchAtLoginEnabled ? "person.fill" : "person") Image(systemName: isLaunchAtLoginEnabled ? "person.fill" : "person")
.resizable() .resizable()
.scaledToFit() .scaledToFit()
.frame(width: 20, height: 20) .frame(width: 20, height: 20)
.padding(.trailing) .padding(.trailing)
.foregroundStyle(.primary.opacity(0.5)) .foregroundStyle(.primary)
VStack(alignment: .leading, spacing: 5) { VStack(alignment: .leading, spacing: 5) {
Text("\(isLaunchAtLoginEnabled ? "Launch at login enabled" : "Launch at login disabled")") Text("Launch Pearcleaner at login")
.font(.callout) .font(.callout)
.foregroundStyle(.primary.opacity(0.5)) .foregroundStyle(.primary)
} }
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.") 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.")
Spacer() Spacer()
Toggle(isOn: $isLaunchAtLoginEnabled, label: { Toggle(isOn: $isLaunchAtLoginEnabled, label: {
}) })
.toggleStyle(.switch) .toggleStyle(.switch)
.onAppear { .onAppear {
isLaunchAtLoginEnabled = SMAppService.mainApp.status == .enabled isLaunchAtLoginEnabled = SMAppService.mainApp.status == .enabled
} }
.onChange(of: isLaunchAtLoginEnabled) { newValue in .onChange(of: isLaunchAtLoginEnabled) { newValue in
do { do {
if newValue { if newValue {
if SMAppService.mainApp.status == .enabled { if SMAppService.mainApp.status == .enabled {
try? SMAppService.mainApp.unregister() 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)")
} }
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(.primary.opacity(0.5))
VStack(alignment: .leading, spacing: 5) {
Text("Menubar icon")
.font(.callout)
.foregroundStyle(.primary.opacity(0.5))
}
Spacer()
Picker("", selection: $selectedMenubarIcon) {
ForEach(icons, id: \.self) { icon in
HStack {
Image(systemName: icon)
.resizable()
.scaledToFit()
}
.tag(icon)
} }
} .padding(5)
.frame(width: 60)
.onChange(of: selectedMenubarIcon) { newValue in
MenuBarExtraManager.shared.swapMenuBarIcon(icon: newValue)
}
.buttonStyle(.borderless)
}
.padding(5)
.padding(.leading)
Spacer() HStack(spacing: 0) {
Image(systemName: "paintbrush")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(.primary)
VStack(alignment: .leading, spacing: 5) {
Text("Menubar icon preference")
.font(.callout)
.foregroundStyle(.primary)
}
Spacer()
Picker("", selection: $selectedMenubarIcon) {
ForEach(icons, id: \.self) { icon in
HStack {
Image(systemName: icon)
.resizable()
.scaledToFit()
}
.tag(icon)
}
}
.frame(width: 60)
.onChange(of: selectedMenubarIcon) { newValue in
MenuBarExtraManager.shared.swapMenuBarIcon(icon: newValue)
}
.buttonStyle(.borderless)
}
.padding(5)
}
})
} }
.padding(20)
.frame(width: 500, height: 600)
} }
+268 -41
View File
@@ -17,53 +17,143 @@ struct SettingsView: View {
@Binding var search: String @Binding var search: String
@AppStorage("settings.general.glass") private var glass: Bool = false @AppStorage("settings.general.glass") private var glass: Bool = false
@AppStorage("settings.general.selectedTab") private var selectedTab: CurrentTabView = .general @AppStorage("settings.general.selectedTab") private var selectedTab: CurrentTabView = .general
@State private var isResetting = false
var body: some View { var body: some View {
TabView(selection: $selectedTab) {
GeneralSettingsTab()
.tabItem {
Label(CurrentTabView.general.title, systemImage: "gear")
}
.tag(CurrentTabView.general)
InterfaceSettingsTab(showPopover: $showPopover, search: $search) NavigationStack {
.tabItem { HStack(spacing: 0) {
Label(CurrentTabView.interface.title, systemImage: "macwindow") sidebarView
} detailView
.tag(CurrentTabView.interface) }
.environmentObject(themeManager)
FolderSettingsTab()
.tabItem {
Label(CurrentTabView.folders.title, systemImage: "folder")
}
.tag(CurrentTabView.folders)
.environmentObject(themeManager)
UpdateSettingsTab()
.tabItem {
Label(CurrentTabView.update.title, systemImage: "cloud")
}
.tag(CurrentTabView.update)
.environmentObject(themeManager)
TipsSettingsTab()
.tabItem {
Label(CurrentTabView.tips.title, systemImage: "star")
}
.tag(CurrentTabView.tips)
AboutSettingsTab()
.tabItem {
Label(CurrentTabView.about.title, systemImage: "info.circle")
}
.tag(CurrentTabView.about)
} }
.background(backgroundView(themeManager: themeManager, glass: glass)) .navigationTitle("")
.frame(width: 750, height: 620)
}
/// Sidebar view for navigation items
@ViewBuilder
private var sidebarView: some View {
VStack(alignment: .center, spacing: 4) {
PearDropViewSmall()
.padding(.leading, 4)
Divider()
.padding(.bottom, 8)
SidebarItemView(title: "General", systemImage: "gear", isSelected: selectedTab == .general) {
selectedTab = .general
}
SidebarItemView(title: "Interface", systemImage: "macwindow", isSelected: selectedTab == .interface) {
selectedTab = .interface
}
SidebarItemView(title: "Folders", systemImage: "folder", isSelected: selectedTab == .folders) {
selectedTab = .folders
}
SidebarItemView(title: "Update", systemImage: "cloud", isSelected: selectedTab == .update) {
selectedTab = .update
}
SidebarItemView(title: "Tips", systemImage: "star", isSelected: selectedTab == .tips) {
selectedTab = .tips
}
SidebarItemView(title: "About", systemImage: "info.circle", isSelected: selectedTab == .about) {
selectedTab = .about
}
Spacer()
Text("v\(Bundle.main.version)").foregroundStyle(.secondary)
.padding(.bottom, 4)
Button("") {
resetUserDefaults()
}
.buttonStyle(ResetSettingsButtonStyle(isResetting: $isResetting, label: "Reset Settings", help: "Reset all settings to default"))
.disabled(isResetting)
}
.padding(.bottom)
.padding(.horizontal, 8)
.frame(width: 200)
.background(backgroundView(themeManager: themeManager, darker: true, glass: glass))
}
/// Detail view content based on the selected tab
@ViewBuilder
private var detailView: some View {
ScrollView() {
// The actual detail views wrapped inside the VStack
switch selectedTab {
case .general:
GeneralSettingsTab()
.environmentObject(appState)
case .interface:
InterfaceSettingsTab(showPopover: $showPopover, search: $search)
.environmentObject(themeManager)
case .folders:
FolderSettingsTab()
.environmentObject(themeManager)
case .update:
UpdateSettingsTab()
.environmentObject(updater)
case .tips:
TipsSettingsTab()
case .about:
AboutSettingsTab()
}
}
.scrollIndicators(.automatic)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding()
.offset(y: -22)
.background(backgroundView(themeManager: themeManager, glass: false))
} }
private func resetUserDefaults() {
isResetting = true
DispatchQueue.global(qos: .background).async {
UserDefaults.standard.dictionaryRepresentation().keys.forEach(UserDefaults.standard.removeObject(forKey:))
DispatchQueue.main.async {
isResetting = false
}
}
}
}
struct SidebarItemView: View {
var title: String
var systemImage: String
var isSelected: Bool
var onTap: () -> Void
var body: some View {
HStack(spacing: 14) {
Image(systemName: systemImage)
.resizable()
.aspectRatio(contentMode: .fit)
.foregroundColor(isSelected ? .accentColor : .primary)
.frame(width: 20, height: 20)
Text(title)
.font(.system(size: 14, weight: .regular))
.foregroundColor(isSelected ? .accentColor : .primary)
Spacer()
}
.padding(.vertical, 8)
.padding(.leading, 8)
.frame(maxWidth: .infinity, alignment: .leading)
.background(isSelected ? .primary.opacity(0.2) : Color.clear)
.cornerRadius(6)
.contentShape(Rectangle())
.onTapGesture {
onTap()
}
}
} }
@@ -72,5 +162,142 @@ struct SettingsView: View {
// NavigationStack {
// HStack(spacing: 0) {
// // Custom Sidebar with buttons for each tab
// List(selection: $selectedTab) {
// SidebarItemView(title: CurrentTabView.general.title,
// systemImage: "gear",
// isSelected: selectedTab == .general) {
// selectedTab = .general
// }
//
// SidebarItemView(title: CurrentTabView.interface.title,
// systemImage: "macwindow",
// isSelected: selectedTab == .interface) {
// selectedTab = .interface
// }
//
// SidebarItemView(title: CurrentTabView.folders.title,
// systemImage: "folder",
// isSelected: selectedTab == .folders) {
// selectedTab = .folders
// }
//
// SidebarItemView(title: CurrentTabView.update.title,
// systemImage: "cloud",
// isSelected: selectedTab == .update) {
// selectedTab = .update
// }
//
// SidebarItemView(title: CurrentTabView.tips.title,
// systemImage: "star",
// isSelected: selectedTab == .tips) {
// selectedTab = .tips
// }
//
// SidebarItemView(title: CurrentTabView.about.title,
// systemImage: "info.circle",
// isSelected: selectedTab == .about) {
// selectedTab = .about
// }
//
// Spacer() // Pushes items to the top
// }
// .removeSidebarToggle()
// .frame(width: 200) // Fixed width for sidebar
// .padding(.top, 20) // Top padding to space out items
// .background(backgroundView(themeManager: themeManager, glass: true))
//
// ZStack(alignment: .top) {
// // Custom Toolbar View at the top
// HStack {
// Text(selectedTab.title)
// .font(.headline)
// .padding(.leading)
//
// Spacer()
//
// // Add more toolbar buttons as needed
// Button(action: {
// print("Toolbar Button Tapped")
// }) {
// Image(systemName: "gearshape")
// .padding()
// }
// }
// .frame(height: 40) // Set fixed height for toolbar
// .background(backgroundView(themeManager: themeManager, glass: true))
// .zIndex(1) // Ensure this is on top
// .offset(y: -30)
//
// // The actual detail view content
// Group {
// switch selectedTab {
// case .general:
// GeneralSettingsTab()
// case .interface:
// InterfaceSettingsTab(showPopover: $showPopover, search: $search)
// .environmentObject(themeManager)
// case .folders:
// FolderSettingsTab()
// .environmentObject(themeManager)
// case .update:
// UpdateSettingsTab()
// .environmentObject(themeManager)
// case .tips:
// TipsSettingsTab()
// case .about:
// AboutSettingsTab()
// }
// }
// .padding(.top, 40) // Add padding below the custom toolbar
// .frame(maxWidth: .infinity, maxHeight: .infinity) // Fill remaining space
// .background(backgroundView(themeManager: themeManager, glass: false))
// }
//
// }
// .navigationTitle("")
// }
// TabView(selection: $selectedTab) {
// GeneralSettingsTab()
// .tabItem {
// Label(CurrentTabView.general.title, systemImage: "gear")
// }
// .tag(CurrentTabView.general)
//
// InterfaceSettingsTab(showPopover: $showPopover, search: $search)
// .tabItem {
// Label(CurrentTabView.interface.title, systemImage: "macwindow")
// }
// .tag(CurrentTabView.interface)
// .environmentObject(themeManager)
//
// FolderSettingsTab()
// .tabItem {
// Label(CurrentTabView.folders.title, systemImage: "folder")
// }
// .tag(CurrentTabView.folders)
// .environmentObject(themeManager)
//
// UpdateSettingsTab()
// .tabItem {
// Label(CurrentTabView.update.title, systemImage: "cloud")
// }
// .tag(CurrentTabView.update)
// .environmentObject(themeManager)
//
// TipsSettingsTab()
// .tabItem {
// Label(CurrentTabView.tips.title, systemImage: "star")
// }
// .tag(CurrentTabView.tips)
//
// AboutSettingsTab()
// .tabItem {
// Label(CurrentTabView.about.title, systemImage: "info.circle")
// }
// .tag(CurrentTabView.about)
// }
// .background(backgroundView(themeManager: themeManager, glass: glass))
+26 -30
View File
@@ -18,39 +18,35 @@ struct TipsSettingsTab: View {
var body: some View { var body: some View {
VStack { VStack {
ScrollView { PearGroupBox(header: { Text("Tips And Tricks").font(.title2) }, content: {
VStack(alignment: .leading, spacing: 15) { ScrollView {
Text("1. Clicking an app will perform a search for files and folders, which will be displayed in the Files view") VStack(alignment: .leading, spacing: 15) {
Text("2. The Files view will show app details and the found files along with a toolbar above the file list which shows related badges for the app, allows checking/unchecking all files and sorting alphabetically or by size") Text("1. Clicking an app will perform a search for files and folders, which will be displayed in the Files view")
Text("3. Clicking the Trash button on the Files view will remove the selected files and folders only") Text("2. The Files view will show app details and the found files along with a toolbar above the file list which shows related badges for the app, allows checking/unchecking all files and sorting alphabetically or by size")
Text("4. After deletion, you can hit CMD + Z to undo while Pearcleaner or Finder is focused") Text("3. Clicking the Trash button on the Files view will remove the selected files and folders only")
Text("5. While on the Files view for an app, you can open the Condition Builder with CMD + B or from the Tools menubar item Instructions on using Condition Builder are available on the popup sheet") Text("4. After deletion, you can hit CMD + Z to undo while Pearcleaner or Finder is focused")
Text("6. On the Files view again, you can hit CMD + E to export a list of the found files or from the Tools menubar item") Text("5. While on the Files view for an app, you can open the Condition Builder with CMD + B or from the Tools menubar item Instructions on using Condition Builder are available on the popup sheet")
Text("7. CMD + U will check for updates manually") Text("6. On the Files view again, you can hit CMD + E to export a list of the found files or from the Tools menubar item")
Text("8. CMD + R will refresh the app list. This can also be done from the refresh button in the search bar") Text("7. CMD + U will check for updates manually")
Text("9. In the search bar menu button, you will also find Leftover Files which will find files leftover by previously uninstalled applications") Text("8. CMD + R will refresh the app list. This can also be done from the refresh button in the search bar")
Text("10. You can enable a menubar icon from Settings") Text("9. In the search bar menu button, you will also find Leftover Files which will find files leftover by previously uninstalled applications")
Text("11. You can enable UI mini mode from Settings") Text("10. You can enable a menubar icon from Settings")
Text("12. You can enable a Finder extension from Settings which allows you to uninstall apps by right clicking them in Finder") Text("11. You can enable mini mode version of the UI from Settings")
Text("13. You can enable Homebrew cleanup from Settings") Text("12. You can enable a Finder extension from Settings which allows you to uninstall apps by right clicking them in Finder")
Text("14. You can change the app theme colors from Settings") Text("13. You can enable Homebrew cleanup from Settings")
Text("15. When there's new features available, a new features badge will show at the top of the app list. Same for app updates or missing permissions") Text("14. You can change the app theme colors from Settings")
Text("16. You can disable animations in the Settings Interface tab plus a few more other options like confirmation dialog when removing files") Text("15. When there's new features available, an announcement badge will show at the top of the app list. Same for app app updates or missing permissions")
Text("16. You can disable animations in the Settings Interface tab plus a few more other options like confirmation dialog when removing files")
}
.padding()
} }
.padding() .scrollIndicators(.visible)
} .frame(height: 530)
.scrollIndicators(.visible) .frame(maxWidth: .infinity)
.frame(height: 500) })
.frame(maxWidth: .infinity)
.background(backgroundView(themeManager: themeManager, darker: true, glass: glass))
.clipShape(RoundedRectangle(cornerRadius: 8))
.padding(.bottom)
Spacer()
} }
.padding(20)
.frame(width: 500, height: 550)
} }
} }
+14 -16
View File
@@ -17,23 +17,26 @@ struct UpdateSettingsTab: View {
@AppStorage("settings.general.glass") private var glass: Bool = false @AppStorage("settings.general.glass") private var glass: Bool = false
var body: some View { var body: some View {
VStack { VStack(spacing: 20) {
FrequencyView(updater: updater) // === Frequency ============================================================================================
.padding(5) PearGroupBox(header: { Text("Update Frequency").font(.title2) }, content: {
.padding(.horizontal) FrequencyView(updater: updater)
})
ReleasesView(updater: updater) // === Release Notes ========================================================================================
.frame(height: 400) PearGroupBox(header: { Text("Release Notes").font(.title2) }, content: {
.frame(maxWidth: .infinity) ReleasesView(updater: updater)
.background(backgroundView(themeManager: themeManager, darker: true, glass: glass)) .frame(height: 380)
.clipShape(RoundedRectangle(cornerRadius: 8)) .frame(maxWidth: .infinity)
})
// === Buttons ==============================================================================================
HStack(alignment: .center, spacing: 20) { HStack(alignment: .center, spacing: 20) {
Button(""){ Button(""){
updater.checkForUpdates(showSheet: false) updater.checkForUpdatesForce(showSheet: false)
} }
.buttonStyle(SimpleButtonStyle(icon: "arrow.uturn.left.circle", label: "Refresh", help: "Refresh updater")) .buttonStyle(SimpleButtonStyle(icon: "arrow.uturn.left.circle", label: "Refresh", help: "Refresh updater"))
@@ -50,13 +53,8 @@ struct UpdateSettingsTab: View {
.buttonStyle(SimpleButtonStyle(icon: "link", label: "Releases", help: "View releases on GitHub")) .buttonStyle(SimpleButtonStyle(icon: "link", label: "Releases", help: "View releases on GitHub"))
} }
.padding(.top, 5)
Spacer()
} }
.padding(20)
.frame(width: 500, height: 540)
} }
+1
View File
@@ -159,6 +159,7 @@ struct AppListItems: View {
DispatchQueue.global(qos: .userInitiated).async { DispatchQueue.global(qos: .userInitiated).async {
let size = totalSizeOnDisk(for: appInfo.path).logical let size = totalSizeOnDisk(for: appInfo.path).logical
DispatchQueue.main.async { DispatchQueue.main.async {
print("Sizing up")
self.bundleSize = size self.bundleSize = size
// Update the appInfo in the appState array // Update the appInfo in the appState array
if let index = appState.sortedApps.firstIndex(where: { $0.path == appInfo.path }) { if let index = appState.sortedApps.firstIndex(where: { $0.path == appInfo.path }) {
+1 -2
View File
@@ -123,8 +123,7 @@ struct AppSearchView: View {
Button("Settings") { Button("Settings") {
if #available(macOS 13.0, *) { if #available(macOS 13.0, *) {
NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil) NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil)
} } else {
else {
NSApp.sendAction(Selector(("showPreferencesWindow:")), to: nil, from: nil) NSApp.sendAction(Selector(("showPreferencesWindow:")), to: nil, from: nil)
} }
showMenu = false showMenu = false