This commit is contained in:
Alin
2024-07-15 17:58:21 -06:00
parent 15c5ad33ae
commit cf5143f17d
40 changed files with 1035 additions and 2614 deletions
+16 -17
View File
@@ -3,13 +3,12 @@
archiveVersion = 1;
classes = {
};
objectVersion = 56;
objectVersion = 60;
objects = {
/* Begin PBXBuildFile section */
C7045A282B03E71D00376976 /* MiniMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7045A272B03E71D00376976 /* MiniMode.swift */; };
C7045A322B068AD700376976 /* NewWin.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7045A312B068AD700376976 /* NewWin.swift */; };
C7045A342B068B2900376976 /* UpdateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7045A332B068B2900376976 /* UpdateView.swift */; };
C7045A362B068DD700376976 /* PermView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7045A352B068DD700376976 /* PermView.swift */; };
C71848442B8D2D600046CB13 /* ZombieView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C71848432B8D2D600046CB13 /* ZombieView.swift */; };
C72893052AFD42E600C8C1CD /* DeepLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = C72893042AFD42E600C8C1CD /* DeepLink.swift */; };
@@ -17,12 +16,12 @@
C736E7652C2DF1C9009EDB6A /* AppPathsFetch-Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = C736E7642C2DF1C9009EDB6A /* AppPathsFetch-Async.swift */; };
C74412C72BBF249000DDFCA8 /* AppPathsFetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C74412C62BBF249000DDFCA8 /* AppPathsFetch.swift */; };
C74FDC632BCD833D00B8960F /* Conditions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C74FDC622BCD833D00B8960F /* Conditions.swift */; };
C75347642BE599FA00AAFCC3 /* PermissionChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = C75347632BE599FA00AAFCC3 /* PermissionChecker.swift */; };
C7575D392B0182DE006A600A /* PearcleanerSentinel in CopyFiles */ = {isa = PBXBuildFile; fileRef = C728930F2AFD51EA00C8C1CD /* PearcleanerSentinel */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
C76D08482AF83C3F00D07867 /* Update.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76D08472AF83C3F00D07867 /* Update.swift */; };
C76D084A2AF83C6E00D07867 /* General.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76D08492AF83C6E00D07867 /* General.swift */; };
C76D08512AF850F300D07867 /* About.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76D08502AF850F300D07867 /* About.swift */; };
C76D08552AF89CDE00D07867 /* RegularMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76D08542AF89CDE00D07867 /* RegularMode.swift */; };
C76EDCD62C45DC82000CF29D /* AlinFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = C76EDCD52C45DC82000CF29D /* AlinFoundation */; };
C771CB762BD028AF00C33A18 /* SegmentedPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = C771CB752BD028AF00C33A18 /* SegmentedPicker.swift */; };
C77B90042AF18E2E009CC655 /* PearcleanerApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C77B90032AF18E2E009CC655 /* PearcleanerApp.swift */; };
C77B90082AF18E2F009CC655 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C77B90072AF18E2F009CC655 /* Assets.xcassets */; };
@@ -35,7 +34,6 @@
C78121662BC892A000BE06BD /* FinderOpen.swift in Sources */ = {isa = PBXBuildFile; fileRef = C78121652BC892A000BE06BD /* FinderOpen.swift */; };
C781216B2BC892A000BE06BD /* FinderOpen.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = C78121632BC892A000BE06BD /* FinderOpen.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
C79947962C34A794007279CC /* Tips.swift in Sources */ = {isa = PBXBuildFile; fileRef = C79947952C34A794007279CC /* Tips.swift */; };
C79D05202BC99EB40083F976 /* ThemeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C79D051F2BC99EB40083F976 /* ThemeManager.swift */; };
C7A27E812AFD7C4600166168 /* com.alienator88.PearcleanerSentinel.plist in Resources */ = {isa = PBXBuildFile; fileRef = C7A27E802AFD7C4600166168 /* com.alienator88.PearcleanerSentinel.plist */; };
C7A2AE5D2B9644F300161540 /* FileWatcher in Frameworks */ = {isa = PBXBuildFile; productRef = C7A2AE5C2B9644F300161540 /* FileWatcher */; };
C7A9CE472B89164700EB6E78 /* Authorization.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7A9CE462B89164700EB6E78 /* Authorization.swift */; };
@@ -48,7 +46,6 @@
C7DD49EA2BAB4D8100CCBA16 /* AppInfoFetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7DD49E92BAB4D8100CCBA16 /* AppInfoFetch.swift */; };
C7DD49EE2BAB7F6000CCBA16 /* ReversePathsFetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7DD49ED2BAB7F6000CCBA16 /* ReversePathsFetch.swift */; };
C7DD49F02BABA14400CCBA16 /* Folders.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7DD49EF2BABA14400CCBA16 /* Folders.swift */; };
C7DE17652BE54F4900197715 /* Updater.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7DE17642BE54F4900197715 /* Updater.swift */; };
C7DE672B2BA6343D00EB1633 /* MenuBarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7DE672A2BA6343D00EB1633 /* MenuBarItem.swift */; };
C7EA06332C1CBE2000F872FC /* ConditionBuilderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7EA06322C1CBE2000F872FC /* ConditionBuilderView.swift */; };
C7ED86B12BA8A35500F13DE4 /* Interface.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7ED86B02BA8A35500F13DE4 /* Interface.swift */; };
@@ -104,7 +101,6 @@
/* Begin PBXFileReference section */
C7045A272B03E71D00376976 /* MiniMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MiniMode.swift; sourceTree = "<group>"; };
C7045A312B068AD700376976 /* NewWin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewWin.swift; sourceTree = "<group>"; };
C7045A332B068B2900376976 /* UpdateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateView.swift; sourceTree = "<group>"; };
C7045A352B068DD700376976 /* PermView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermView.swift; sourceTree = "<group>"; };
C71848432B8D2D600046CB13 /* ZombieView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZombieView.swift; sourceTree = "<group>"; };
C72893042AFD42E600C8C1CD /* DeepLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLink.swift; sourceTree = "<group>"; };
@@ -113,7 +109,6 @@
C736E7642C2DF1C9009EDB6A /* AppPathsFetch-Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppPathsFetch-Async.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>"; };
C75347632BE599FA00AAFCC3 /* PermissionChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionChecker.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>"; };
C76D08502AF850F300D07867 /* About.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = About.swift; sourceTree = "<group>"; };
@@ -136,7 +131,6 @@
C78121682BC892A000BE06BD /* FinderOpen.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = FinderOpen.entitlements; sourceTree = "<group>"; };
C786F1A72C29E599001B3800 /* Builds */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Builds; sourceTree = SOURCE_ROOT; };
C79947952C34A794007279CC /* Tips.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tips.swift; sourceTree = "<group>"; };
C79D051F2BC99EB40083F976 /* ThemeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeManager.swift; sourceTree = "<group>"; };
C7A27E7F2AFD70DD00166168 /* PearcleanerSentinel.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = PearcleanerSentinel.entitlements; sourceTree = "<group>"; };
C7A27E802AFD7C4600166168 /* com.alienator88.PearcleanerSentinel.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = com.alienator88.PearcleanerSentinel.plist; sourceTree = "<group>"; };
C7A9CE462B89164700EB6E78 /* Authorization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Authorization.swift; sourceTree = "<group>"; };
@@ -149,7 +143,6 @@
C7DD49E92BAB4D8100CCBA16 /* AppInfoFetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppInfoFetch.swift; sourceTree = "<group>"; };
C7DD49ED2BAB7F6000CCBA16 /* ReversePathsFetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReversePathsFetch.swift; sourceTree = "<group>"; };
C7DD49EF2BABA14400CCBA16 /* Folders.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Folders.swift; sourceTree = "<group>"; };
C7DE17642BE54F4900197715 /* Updater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Updater.swift; sourceTree = "<group>"; };
C7DE672A2BA6343D00EB1633 /* MenuBarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarItem.swift; sourceTree = "<group>"; };
C7EA06322C1CBE2000F872FC /* ConditionBuilderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConditionBuilderView.swift; sourceTree = "<group>"; };
C7ED86B02BA8A35500F13DE4 /* Interface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Interface.swift; sourceTree = "<group>"; };
@@ -172,6 +165,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
C76EDCD62C45DC82000CF29D /* AlinFoundation in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -189,7 +183,6 @@
isa = PBXGroup;
children = (
C7045A312B068AD700376976 /* NewWin.swift */,
C7045A332B068B2900376976 /* UpdateView.swift */,
C7D15BBD2BE85F4D007D57CF /* FeatureView.swift */,
C7045A352B068DD700376976 /* PermView.swift */,
C7DE672A2BA6343D00EB1633 /* MenuBarItem.swift */,
@@ -216,9 +209,6 @@
C7A9CE462B89164700EB6E78 /* Authorization.swift */,
C7ACADE52B92A737000B5845 /* Features.swift */,
C7ED86B22BA8ED8800F13DE4 /* Trash.swift */,
C79D051F2BC99EB40083F976 /* ThemeManager.swift */,
C7DE17642BE54F4900197715 /* Updater.swift */,
C75347632BE599FA00AAFCC3 /* PermissionChecker.swift */,
);
path = Logic;
sourceTree = "<group>";
@@ -374,6 +364,7 @@
);
name = Pearcleaner;
packageProductDependencies = (
C76EDCD52C45DC82000CF29D /* AlinFoundation */,
);
productName = PeakPurge;
productReference = C77B90002AF18E2E009CC655 /* Pearcleaner.app */;
@@ -428,6 +419,7 @@
mainGroup = C77B8FF72AF18E2E009CC655;
packageReferences = (
C7A2AE5B2B9644DA00161540 /* XCRemoteSwiftPackageReference "FileWatcher" */,
C76EDCD42C45DC82000CF29D /* XCLocalSwiftPackageReference "../AlinFoundation" */,
);
productRefGroup = C77B90012AF18E2E009CC655 /* Products */;
projectDirPath = "";
@@ -477,10 +469,7 @@
C79947962C34A794007279CC /* Tips.swift in Sources */,
C76D08482AF83C3F00D07867 /* Update.swift in Sources */,
C7FB173B2B96321300B96F9A /* AppsListView.swift in Sources */,
C7045A342B068B2900376976 /* UpdateView.swift in Sources */,
C77B90162AF19377009CC655 /* AppState.swift in Sources */,
C75347642BE599FA00AAFCC3 /* PermissionChecker.swift in Sources */,
C7DE17652BE54F4900197715 /* Updater.swift in Sources */,
C7DD49EE2BAB7F6000CCBA16 /* ReversePathsFetch.swift in Sources */,
C7045A322B068AD700376976 /* NewWin.swift in Sources */,
C7EA06332C1CBE2000F872FC /* ConditionBuilderView.swift in Sources */,
@@ -504,7 +493,6 @@
C77B901E2AF1A9DB009CC655 /* SettingsWindow.swift in Sources */,
C771CB762BD028AF00C33A18 /* SegmentedPicker.swift in Sources */,
C74412C72BBF249000DDFCA8 /* AppPathsFetch.swift in Sources */,
C79D05202BC99EB40083F976 /* ThemeManager.swift in Sources */,
C74FDC632BCD833D00B8960F /* Conditions.swift in Sources */,
C7ACADE62B92A737000B5845 /* Features.swift in Sources */,
C7F539382AF60865007DF1B2 /* Utilities.swift in Sources */,
@@ -905,6 +893,13 @@
};
/* End XCConfigurationList section */
/* Begin XCLocalSwiftPackageReference section */
C76EDCD42C45DC82000CF29D /* XCLocalSwiftPackageReference "../AlinFoundation" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = ../AlinFoundation;
};
/* End XCLocalSwiftPackageReference section */
/* Begin XCRemoteSwiftPackageReference section */
C7A2AE5B2B9644DA00161540 /* XCRemoteSwiftPackageReference "FileWatcher" */ = {
isa = XCRemoteSwiftPackageReference;
@@ -917,6 +912,10 @@
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
C76EDCD52C45DC82000CF29D /* AlinFoundation */ = {
isa = XCSwiftPackageProductDependency;
productName = AlinFoundation;
};
C7A2AE5C2B9644F300161540 /* FileWatcher */ = {
isa = XCSwiftPackageProductDependency;
package = C7A2AE5B2B9644DA00161540 /* XCRemoteSwiftPackageReference "FileWatcher" */;
@@ -1,4 +1,5 @@
{
"originHash" : "2070782a951fcb2eef783fefb42bac8d9dafc68491164378649d1525b43cbc83",
"pins" : [
{
"identity" : "filewatcher",
@@ -10,5 +11,5 @@
}
}
],
"version" : 2
"version" : 3
}
+14 -6
View File
@@ -6,17 +6,22 @@
//
import SwiftUI
import AlinFoundation
struct AppCommands: Commands {
let appState: AppState
let locations: Locations
let fsm: FolderSettingsManager
let updater: Updater
let themeManager: ThemeManager
init(appState: AppState, locations: Locations, fsm: FolderSettingsManager) {
init(appState: AppState, locations: Locations, fsm: FolderSettingsManager, updater: Updater, themeManager: ThemeManager) {
self.appState = appState
self.locations = locations
self.fsm = fsm
self.updater = updater
self.themeManager = themeManager
}
var body: some Commands {
@@ -25,7 +30,7 @@ struct AppCommands: Commands {
CommandGroup(replacing: .appInfo) {
Button {
loadGithubReleases(appState: appState, manual: true)
updater.checkForUpdates()
} label: {
Text("Check for Updates")
}
@@ -55,12 +60,15 @@ struct AppCommands: Commands {
Button
{
undoTrash(appState: appState) {
reloadAppsList(appState: appState, fsm: fsm)
for app in appState.trashedFiles {
AppPathFinder(appInfo: app, appState: appState, locations: locations, undo: true).findPaths()
if appState.currentView != .zombie {
undoTrash(appState: appState) {
reloadAppsList(appState: appState, fsm: fsm)
for app in appState.trashedFiles {
AppPathFinder(appInfo: app, appState: appState, locations: locations, undo: true).findPaths()
}
}
}
} label: {
Label("Undo Removal", systemImage: "clear")
}
+1
View File
@@ -7,6 +7,7 @@
import Foundation
import SwiftUI
import AlinFoundation
class AppInfoFetcher {
static let fileManager = FileManager.default
+1 -1
View File
@@ -8,7 +8,7 @@
import Foundation
import AppKit
import SwiftUI
import AlinFoundation
class AppPathFinder {
private var appInfo: AppInfo
+3 -6
View File
@@ -22,15 +22,14 @@ class AppState: ObservableObject {
@Published var currentView = CurrentDetailsView.empty
@Published var showAlert: Bool = false
@Published var sidebar: Bool = true
@Published var releases = [Release]()
@Published var progressBar: (String, Double) = ("Ready", 0.0)
@Published var reload: Bool = false
@Published var showProgress: Bool = false
@Published var finderExtensionEnabled: Bool = false
@Published var updateAvailable: Bool = false
// @Published var updateAvailable: Bool = false
@Published var featureAvailable: Bool = false
@Published var permissionsOkay: Bool = true
@Published var permissionResults: PermissionsCheckResults?
// @Published var permissionsOkay: Bool = true
// @Published var permissionResults: PermissionsCheckResults?
@Published var showUninstallAlert: Bool = false
@Published var oneShotMode: Bool = false
@Published var showConditionBuilder: Bool = false
@@ -182,8 +181,6 @@ enum CurrentDetailsView:Int
enum NewWindow:Int
{
case update
case no_update
case perm
case feature
}
+1
View File
@@ -7,6 +7,7 @@
import Foundation
import SwiftUI
import AlinFoundation
class DeeplinkManager {
@Binding var showPopover: Bool
+4 -2
View File
@@ -7,6 +7,8 @@
import Foundation
import SwiftUI
import AlinFoundation
func getFeatures(appState: AppState, features: Binding<String>) {
let url = URL(string: "https://api.github.com/repos/alienator88/Pearcleaner/contents/features.json")!
@@ -78,7 +80,7 @@ struct FeatureNotificationView: View {
.foregroundStyle(.white)
Text("Show")
Text("Check")
.foregroundStyle(.white)
}
@@ -103,7 +105,7 @@ struct FeatureNotificationView: View {
}
.frame(height: 30)
.padding(5)
.background(Color("mode").opacity(0.05))
.background(.primary.opacity(0.05))
.clipShape(RoundedRectangle(cornerRadius: 6))
.padding(.horizontal)
.padding(.bottom)
+1 -1
View File
@@ -26,7 +26,7 @@ class Locations: ObservableObject {
self.tempDir = tempDir
self.apps = Category(name: "Apps", paths: [
"\(home)",
// "\(home)", //MARK: Disable searching for files in user's home folder
"\(home)/Library",
"\(home)/Library/Application Scripts",
"\(home)/Library/Application Support",
+17 -16
View File
@@ -7,6 +7,7 @@
import Foundation
import SwiftUI
import AlinFoundation
// Get all apps from /Applications and ~/Applications
@@ -212,14 +213,14 @@ func moveFilesToTrash(appState: AppState, at fileURLs: [URL], completion: @escap
if let scriptObject = NSAppleScript(source: scriptSource) {
let output: NSAppleEventDescriptor = scriptObject.executeAndReturnError(&error)
if let error = error {
checkAllPermissions(appState: appState) { results in
appState.permissionResults = results
if !results.allPermissionsGranted {
updateOnMain {
appState.permissionsOkay = false
}
}
}
// checkAllPermissions(appState: appState) { results in
// appState.permissionResults = results
// if !results.allPermissionsGranted {
// updateOnMain {
// appState.permissionsOkay = false
// }
// }
// }
printOS("Trash Error: \(error)")
DispatchQueue.main.async {
completion(false) // Indicate failure
@@ -261,14 +262,14 @@ func undoTrash(appState: AppState, completion: @escaping () -> Void = {}) {
if let scriptObject = NSAppleScript(source: scriptSource) {
let output: NSAppleEventDescriptor = scriptObject.executeAndReturnError(&error)
if let error = error {
checkAllPermissions(appState: appState) { results in
appState.permissionResults = results
if !results.allPermissionsGranted {
updateOnMain {
appState.permissionsOkay = false
}
}
}
// checkAllPermissions(appState: appState) { results in
// appState.permissionResults = results
// if !results.allPermissionsGranted {
// updateOnMain {
// appState.permissionsOkay = false
// }
// }
// }
printOS("Undo Trash Error: \(error)")
} else if let outputString = output.stringValue {
printOS(outputString)
-176
View File
@@ -1,176 +0,0 @@
//
// PermissionChecker.swift
// Pearcleaner
//
// Created by Alin Lupascu on 5/3/24.
//
import SwiftUI
//import EventKit
struct PermissionsCheckResults {
var fullDiskAccess: Bool
var accessibility: Bool
var automation: Bool
// Computed property to check if all permissions are granted
var allPermissionsGranted: Bool {
return fullDiskAccess && accessibility && automation
}
}
func checkAllPermissions(appState: AppState, completion: @escaping (PermissionsCheckResults) -> Void) {
let dispatchGroup = DispatchGroup()
// Check Full Disk Access
var fullDiskAccess = false
@AppStorage("settings.permissions.hasLaunched") var hasLaunched: Bool = false
let process = Process()
process.launchPath = "/usr/bin/sqlite3"
process.arguments = ["/Library/Application Support/com.apple.TCC/TCC.db", "select client from access where auth_value and service = 'kTCCServiceSystemPolicyAllFiles' and client = 'com.alienator88.Pearcleaner'"]
let pipe = Pipe()
process.standardOutput = pipe
process.launch()
process.waitUntilExit() // Ensure process completes
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: data, encoding: .utf8)
fullDiskAccess = (output?.contains("com.alienator88.Pearcleaner") ?? false)
// Check Accessibility
let options: NSDictionary = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: true]
let accessibilityEnabled = AXIsProcessTrustedWithOptions(options)
// Check Automation Permission
var automationAccess = false
dispatchGroup.enter()
checkAutomationPermission(appState: appState) { success in
automationAccess = success
dispatchGroup.leave()
}
// Wait for all async checks to complete
dispatchGroup.notify(queue: .main) {
let results = PermissionsCheckResults(
fullDiskAccess: fullDiskAccess,
accessibility: accessibilityEnabled,
automation: automationAccess
)
// Check if any permission is denied and show window
if !(results.fullDiskAccess && results.accessibility && results.automation) {
updateOnMain {
appState.permissionsOkay = false
}
}
completion(results)
}
}
// Check Finder Automation permission
func checkAutomationPermission(appState: AppState, completion: @escaping (Bool) -> Void) {
DispatchQueue.global(qos: .background).async {
let scriptText = "tell application \"Finder\" to return name of home"
var error: NSDictionary?
if let script = NSAppleScript(source: scriptText) {
let _ = script.executeAndReturnError(&error)
DispatchQueue.main.async {
completion(error == nil)
}
} else {
DispatchQueue.main.async {
completion(false)
}
}
}
}
struct PermissionsNotificationView: View {
let appState: AppState
@State private var hovered: Bool = false
var body: some View {
HStack {
Text("Missing Permissions!")
.font(.callout)
.opacity(0.5)
.padding(.leading, 7)
Spacer()
settingsLinkButton
}
.frame(height: 30)
.padding(5)
.background(Color("mode").opacity(0.05))
.clipShape(RoundedRectangle(cornerRadius: 6))
.padding(.horizontal)
.padding(.bottom)
}
@ViewBuilder
private var settingsLinkButton: some View {
if #available(macOS 14.0, *) {
SettingsLink {
labelContent
}
.buttonStyle(PlainButtonStyle())
.padding(4)
.background(Color.red)
.clipShape(RoundedRectangle(cornerRadius: 6))
.onHover { hover in
withAnimation {
hovered = hover
}
}
.help("Check all permissions")
} else {
Button(action: {
if #available(macOS 13.0, *) {
NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil)
}
else {
NSApp.sendAction(Selector(("showPreferencesWindow:")), to: nil, from: nil)
}
}) {
labelContent
}
.buttonStyle(PlainButtonStyle())
.padding(4)
.background(Color.red)
.clipShape(RoundedRectangle(cornerRadius: 6))
.onHover { hover in
withAnimation {
hovered = hover
}
}
.help("Check all permissions")
}
}
@ViewBuilder
private var labelContent: some View {
HStack(alignment: .center) {
Image(systemName: !hovered ? "lock" : "lock.fill")
.resizable()
.scaledToFit()
.frame(width: 14, height: 14)
.foregroundStyle(.white)
Text("Check")
.foregroundStyle(.white)
}
.padding(3)
}
}
@@ -7,6 +7,7 @@
import Foundation
import AppKit
import AlinFoundation
class ReversePathsSearcher {
private let appState: AppState
+17 -651
View File
@@ -7,88 +7,7 @@
import Foundation
import SwiftUI
struct SimpleButtonStyle: ButtonStyle {
@State private var hovered = false
let icon: String
let iconFlip: String
let label: String
let help: String
let color: Color
let size: CGFloat
let padding: CGFloat
let rotate: Bool
init(icon: String, iconFlip: String = "", label: String = "", help: String, color: Color = Color("mode"), size: CGFloat = 20, padding: CGFloat = 5, rotate: Bool = false) {
self.icon = icon
self.iconFlip = iconFlip
self.label = label
self.help = help
self.color = color
self.size = size
self.padding = padding
self.rotate = rotate
}
func makeBody(configuration: Self.Configuration) -> some View {
HStack(alignment: .center) {
Image(systemName: (hovered && !iconFlip.isEmpty) ? iconFlip : icon)
.resizable()
.scaledToFit()
.frame(width: size, height: size)
.scaleEffect(hovered ? 1.1 : 1.0)
.rotationEffect(.degrees(rotate ? (hovered ? 90 : 0) : 0))
.animation(.easeInOut(duration: 0.2), value: hovered)
if !label.isEmpty {
Text(label)
}
}
.foregroundColor(hovered ? color : color.opacity(0.5))
.padding(padding)
.onHover { hovering in
withAnimation() {
hovered = hovering
}
}
.scaleEffect(configuration.isPressed ? 0.95 : 1)
.help(help)
}
}
struct RegularButtonStyle: ButtonStyle {
let icon: String
let label: String
let help: String
@State private var hovered = false
func makeBody(configuration: Configuration) -> some View {
HStack(alignment: .center) {
Image(systemName: icon).foregroundColor(Color("mode")).padding(.leading, 2).padding(.trailing, 0)
Text(label).textCase(.uppercase).foregroundColor(Color("mode")).fontWeight(.bold).padding(.trailing, 2)
}
.padding(8)
.background(!configuration.isPressed ? hovered ? Color("mode").opacity(0.4) : Color("mode").opacity(0) : Color("mode").opacity(0.5))
.cornerRadius(6)
.scaleEffect(configuration.isPressed ? 0.95 : 1)
.animation(.easeOut(duration: 0.2), value: configuration.isPressed)
.onHover { hovering in
withAnimation(Animation.easeIn(duration: 0.15)) {
hovered = hovering
}
}
.overlay(
RoundedRectangle(cornerRadius: 6)
.strokeBorder(Color("mode").opacity(0.5), lineWidth: 1)
.scaleEffect(configuration.isPressed ? 0.95 : 1)
)
.help(help)
}
}
import AlinFoundation
struct ResetSettingsButtonStyle: ButtonStyle {
@Binding var isResetting: Bool
@@ -105,13 +24,13 @@ struct ResetSettingsButtonStyle: ButtonStyle {
.frame(width: 15)
} else {
Image(systemName: "gear")
.foregroundColor(Color("mode")).padding(.leading, 2).padding(.trailing, 0)
.foregroundColor(.primary).padding(.leading, 2).padding(.trailing, 0)
}
Text(label)
.textCase(.uppercase).foregroundColor(Color("mode")).fontWeight(.bold).padding(.trailing, 2)
.textCase(.uppercase).foregroundColor(.primary).fontWeight(.bold).padding(.trailing, 2)
}
.padding(8)
.background(!configuration.isPressed ? hovered ? Color("mode").opacity(0.4) : Color("mode").opacity(0) : Color("mode").opacity(0.5))
.background(!configuration.isPressed ? hovered ? Color.primary.opacity(0.4) : Color.primary.opacity(0) : Color.primary.opacity(0.5))
.cornerRadius(6)
.scaleEffect(configuration.isPressed ? 0.95 : 1)
.animation(.easeOut(duration: 0.2), value: configuration.isPressed)
@@ -122,7 +41,7 @@ struct ResetSettingsButtonStyle: ButtonStyle {
}
.overlay(
RoundedRectangle(cornerRadius: 6)
.strokeBorder(Color("mode").opacity(0.5), lineWidth: 1)
.strokeBorder(.primary.opacity(0.5), lineWidth: 1)
.scaleEffect(configuration.isPressed ? 0.95 : 1)
)
.help(help)
@@ -131,18 +50,18 @@ struct ResetSettingsButtonStyle: ButtonStyle {
struct SimpleCheckboxToggleStyle: ToggleStyle {
@EnvironmentObject private var themeSettings: ThemeSettings
@EnvironmentObject private var themeManager: ThemeManager
@State private var isHovered: Bool = false
func makeBody(configuration: Configuration) -> some View {
HStack {
RoundedRectangle(cornerRadius: 4)
.fill(themeSettings.themeColor.darker(by: 5))
.fill(themeManager.pickerColor.adjustBrightness(5))
.frame(width: 18, height: 18)
.overlay {
if configuration.isOn {
Image(systemName: "checkmark")
.foregroundStyle(Color("mode"))
.foregroundStyle(.primary)
.scaleEffect(isHovered ? 0.8 : 1.0)
}
}
@@ -160,150 +79,6 @@ struct SimpleCheckboxToggleStyle: ToggleStyle {
}
struct InfoButton: View {
@State private var isPopoverPresented: Bool = false
let text: String
let color: Color
let label: String
let warning: Bool
let edge: Edge
init(text: String, color: Color = Color("mode"), label: String = "", warning: Bool = false, edge: Edge = .bottom) {
self.text = text
self.color = color
self.label = label
self.warning = warning
self.edge = edge
}
var body: some View {
Button(action: {
self.isPopoverPresented.toggle()
}) {
HStack(alignment: .center, spacing: 5) {
Image(systemName: !warning ? "info.circle.fill" : "exclamationmark.triangle.fill")
.resizable()
.scaledToFit()
.frame(width: 14, height: 14)
.foregroundColor(!warning ? color.opacity(0.7) : color)
.frame(height: 16)
if !label.isEmpty {
Text(label)
.font(.callout)
.foregroundColor(color.opacity(0.7))
}
}
}
.buttonStyle(PlainButtonStyle())
.onHover { isHovered in
if isHovered {
NSCursor.pointingHand.push()
} else {
NSCursor.pop()
}
}
.popover(isPresented: $isPopoverPresented, arrowEdge: edge) {
VStack {
Spacer()
Text(text)
.font(.callout)
.frame(maxWidth: .infinity)
.padding()
Spacer()
}
.frame(width: 300)
}
.padding(.horizontal, 5)
}
}
struct InfoButtonPerms: View {
@State private var isPopoverPresented: Bool = false
let color: Color
let label: String
let warning: Bool
init(color: Color = Color("mode"), label: String = "", warning: Bool = false) {
self.color = color
self.label = label
self.warning = warning
}
var body: some View {
Button(action: {
self.isPopoverPresented.toggle()
}) {
HStack(alignment: .center, spacing: 5) {
Image(systemName: !warning ? "info.circle.fill" : "exclamationmark.triangle.fill")
.resizable()
.scaledToFit()
.frame(width: 14, height: 14)
.foregroundColor(!warning ? color.opacity(0.7) : color)
.frame(height: 16)
if !label.isEmpty {
Text(label)
.font(.callout)
.foregroundColor(color.opacity(0.7))
}
}
}
.buttonStyle(PlainButtonStyle())
.onHover { isHovered in
if isHovered {
NSCursor.pointingHand.push()
} else {
NSCursor.pop()
}
}
.popover(isPresented: $isPopoverPresented, arrowEdge: .bottom) {
VStack(alignment: .leading, spacing: 15) {
HStack(alignment: .top, spacing: 20) {
Image(systemName: "externaldrive")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.foregroundStyle(Color("mode").opacity(0.5))
Text("Full Disk permission to access files/folders in system paths")
.font(.callout)
.foregroundStyle(Color("mode").opacity(0.5))
}
HStack(alignment: .top, spacing: 20) {
Image(systemName: "accessibility")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.foregroundStyle(Color("mode").opacity(0.5))
Text("Accessibility permission to allow execution of AppleScript")
.font(.callout)
.foregroundStyle(Color("mode").opacity(0.5))
}
HStack(alignment: .top, spacing: 20) {
Image(systemName: "gearshape.2")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.foregroundStyle(Color("mode").opacity(0.5))
Text("Automation permission to perform delete actions via Finder")
.font(.callout)
.foregroundStyle(Color("mode").opacity(0.5))
}
}
.padding()
}
.padding(.horizontal, 5)
}
}
struct UninstallButton: ButtonStyle {
@State private var hovered: Bool = false
@@ -371,40 +146,6 @@ struct RescanButton: ButtonStyle {
struct WarningPopoverView: View {
var label: String
var bodyText: String
@Binding var isPresented: Bool
var body: some View {
HStack {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundStyle(.red)
.popover(isPresented: $isPresented, arrowEdge: .top) {
VStack {
Spacer()
Text(bodyText)
.font(.callout)
.frame(maxWidth: .infinity)
.padding()
Spacer()
}
.frame(width: 300)
}
Text(label)
.foregroundStyle(Color.red)
Spacer()
}
.onTapGesture {
isPresented.toggle()
}
}
}
public struct NewFeatureView: View {
var text: String
var mini: Bool
@@ -425,7 +166,7 @@ public struct NewFeatureView: View {
showFeature = false
}
}
.buttonStyle(SimpleButtonStyle(icon: "x.circle.fill", help: "Close", color: (Color("mode").opacity(0.5))))
.buttonStyle(SimpleButtonStyle(icon: "x.circle.fill", help: "Close", color: (.primary.opacity(0.5))))
.onHover { isHovered in
if isHovered {
NSCursor.pointingHand.push()
@@ -451,7 +192,7 @@ public struct NewFeatureView: View {
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.strokeBorder(Color("mode").opacity(0.2), lineWidth: 0.6)
.strokeBorder(.primary.opacity(0.2), lineWidth: 0.6)
)
.padding()
}
@@ -459,7 +200,7 @@ public struct NewFeatureView: View {
public struct SlideableDivider: View {
@EnvironmentObject private var themeSettings: ThemeSettings
@EnvironmentObject private var themeManager: ThemeManager
@Binding var dimension: Double
@State private var dimensionStart: Double?
@State private var handleWidth: Double = 4
@@ -471,7 +212,7 @@ public struct SlideableDivider: View {
public var body: some View {
Rectangle()
.foregroundStyle(Color("mode").opacity(0.0))
.foregroundStyle(.primary.opacity(0.0))
.frame(width: 10)
.onHover { inside in
if inside {
@@ -518,117 +259,6 @@ public struct SlideableDivider: View {
}
struct LabeledDivider: View {
let label: String
@EnvironmentObject var appState: AppState
var body: some View {
HStack(spacing: 0) {
Rectangle()
.frame(height: 1)
.foregroundColor(Color("mode").opacity(0.2))
Text(label)
.textCase(.uppercase)
.font(.title2)
.foregroundColor(Color("mode").opacity(0.6))
.padding(.horizontal, 10)
.frame(minWidth: 80)
Rectangle()
.frame(height: 1)
.foregroundColor(Color("mode").opacity(0.2))
}
.frame(minHeight: 35)
}
}
struct AnimatedSearchStyle: TextFieldStyle {
@State private var isHovered = false
@FocusState private var isFocused: Bool
@Binding var text: String
@EnvironmentObject var appState: AppState
@EnvironmentObject var fsm: FolderSettingsManager
func _body(configuration: TextField<Self._Label>) -> some View {
HStack(spacing: 5) {
if isHovered || !text.isEmpty {
configuration
.frame(height: 15)
.font(.title3)
.textFieldStyle(PlainTextFieldStyle())
.padding(.leading, 50)
Image(systemName: "arrow.triangle.2.circlepath")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 15, height: 15)
.padding(.trailing, 5)
.foregroundStyle(isHovered ? Color("mode").opacity(0.8) : Color("mode").opacity(0.5))
.onTapGesture {
withAnimation {
reloadAppsList(appState: appState, fsm: fsm)
}
}
.help("Refresh app list")
if text != "" {
Image(systemName: "xmark")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 10, height: 10)
.padding(.trailing, 5)
.foregroundStyle(isHovered ? Color("mode").opacity(0.8) : Color("mode").opacity(0.5))
.onTapGesture {
withAnimation {
text = ""
}
}
.help("Clear search")
}
} else {
HStack {
Spacer()
Image(systemName: "magnifyingglass")
.resizable()
.scaledToFit()
.frame(width: 18, height: 18)
.foregroundColor(isHovered ? Color("mode") : Color("mode").opacity(0.5))
}
.frame(width: 150)
}
}
.padding(8)
.overlay(
Group {
if isHovered || !text.isEmpty {
RoundedRectangle(cornerRadius: 6)
.strokeBorder(Color("mode").opacity(0.4), lineWidth: 1)
.allowsHitTesting(false)
.padding(.leading, 50)
}
}
)
.onHover { hovering in
withAnimation(Animation.easeInOut(duration: 0.4)) {
self.isHovered = hovering
self.isFocused = hovering
}
}
.focused($isFocused)
}
}
struct RoundedTextFieldStyle: TextFieldStyle {
func _body(configuration: TextField<Self._Label>) -> some View {
@@ -638,7 +268,7 @@ struct RoundedTextFieldStyle: TextFieldStyle {
.textFieldStyle(.plain)
.overlay(
RoundedRectangle(cornerRadius: 8)
.strokeBorder(Color("mode").opacity(0.4), lineWidth: 0.8)
.strokeBorder(.primary.opacity(0.4), lineWidth: 0.8)
)
}
}
@@ -661,54 +291,6 @@ struct GlassEffect: NSViewRepresentable {
struct VerticalDivider: View {
var color: Color = Color("AccentColor")
var width: CGFloat = 1
var height: CGFloat = 20
var opacity: Double = 1
var body: some View {
Divider()
.foregroundColor(color)
.frame(width: width, height: height)
.opacity(opacity)
.ignoresSafeArea(.all)
}
}
struct WindowActionButton: ButtonStyle {
enum UserAction {
case accept
case cancel
}
@State private var hovered = false
let action: UserAction
func makeBody(configuration: Configuration) -> some View {
configuration.label
.frame(width: 100)
.padding(10)
.background(
!configuration.isPressed ?
!hovered ?
action == .accept ? Color("button") : Color.red :
action == .accept ? Color("button").opacity(0.8) : Color.red.opacity(0.8) :
action == .accept ? Color("button").opacity(0.5) : Color.red.opacity(0.5)
)
.foregroundColor(.white)
.cornerRadius(8)
.onHover { hovering in
withAnimation(Animation.easeIn(duration: 0.15)) {
hovered = hovering
}
}
}
}
struct SimpleButtonBrightStyle: ButtonStyle {
@State private var hovered = false
@@ -748,165 +330,6 @@ struct SimpleButtonBrightStyle: ButtonStyle {
struct FilesViewActionButton: ButtonStyle {
enum UserAction {
case uninstall
case close
}
@State private var hovered = false
let action: UserAction
func makeBody(configuration: Configuration) -> some View {
configuration.label
.frame(width: 70)
.padding(6)
.background(
RoundedRectangle(cornerRadius: 6)
.strokeBorder(
configuration.isPressed ?
Color("AccentColor").opacity(0.7) :
hovered ? Color("AccentColor").opacity(0.8) : Color("AccentColor").opacity(0.9), lineWidth: 2
)
)
.foregroundColor(
action == .uninstall ? Color("AccentColor") : Color("mode")
)
.cornerRadius(6)
.onHover { hovering in
withAnimation(Animation.easeIn(duration: 0.15)) {
hovered = hovering
}
}
}
}
struct SentinelToggleStyle: ToggleStyle {
func makeBody(configuration: Configuration) -> some View {
HStack {
ZStack{
RoundedRectangle(cornerRadius: 50)
.fill(configuration.isOn ? greenBG : redBG)
.frame(width: 70, height: 40)
HStack{
Image(systemName: "lock.shield")
.resizable()
.scaledToFit()
.frame(width: 26, height: 26)
.padding(.leading, 10)
.foregroundColor(.white)
Spacer()
Image(systemName: "shield")
.resizable()
.scaledToFit()
.frame(width: 26, height: 26)
.padding(.trailing, 10)
.foregroundColor(.white)
}
}
.frame(width: 70, height: 40, alignment: .center)
.overlay(
Circle()
.foregroundColor(.white)
.shadow(color: .black.opacity(0.5), radius: 3, x: 0, y: 0)
.padding(.all, 4)
.offset(x: configuration.isOn ? 15 : -15, y: 0)
)
.overlay(
Image(systemName: "power")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.foregroundColor(.black.opacity(0.3))
.offset(x: configuration.isOn ? 15 : -15, y: 0)
)
.onTapGesture {
withAnimation(.easeInOut(duration: 0.5)) {
configuration.isOn.toggle()
}
}
.animation(.default, value: configuration.isOn)
}
}
}
private let greenBG: AnyShapeStyle = AnyShapeStyle(
.green.shadow(.inner(radius: 2, x: 0, y: 1))
)
private let redBG: AnyShapeStyle = AnyShapeStyle(
.red.shadow(.inner(radius: 2, x: 0, y: 1))
)
struct PillPicker: View {
@Binding var index: Int
var onTapAction: () -> Void
@Environment(\.colorScheme) var colorScheme
var body: some View {
HStack(alignment: .center, spacing: 10) {
ForEach(0..<3) { i in
ZStack {
Capsule()
.fill(Color.white)
.offset(x: (CGFloat(index - i) * 100))
.animation(.default, value: index)
.opacity(index == i ? 1 : 0)
.scaleEffect(index == i ? 1 : 0.70)
.shadow(color: Color.black.opacity(0.2), radius: 2, x: 0, y: 2)
HStack(spacing: 0) {
Text(i == 0 ? "Apps" : (i == 1 ? "Widgets" : "Plugins"))
.font(.system(size: 12))
.foregroundColor(index == i ? (colorScheme == .dark ? .black : Color("AccentColor")) : Color("mode"))
}
.padding(5)
.frame(height: 25)
.background(.white.opacity(0.0001))
}
.onTapGesture {
withAnimation {
index = i
onTapAction()
}
}
}
}
.frame(height: 25)
.padding(5)
.background(
Capsule()
.fill(colorScheme == .dark ? .white.opacity(0.1) : .black.opacity(0.06))
.overlay(
Capsule()
.stroke(Color.black.opacity(colorScheme == .dark ? 0.8 : 0.2), lineWidth: 1)
.blur(radius: 1)
.offset(y: 1)
)
)
.clipShape(Capsule())
}
}
struct PearDropView: View {
var body: some View {
@@ -914,9 +337,9 @@ struct PearDropView: View {
HStack(spacing: 0) {
LinearGradient(gradient: Gradient(colors: [.green, .orange]), startPoint: .leading, endPoint: .trailing)
.frame(width: 220)
LinearGradient(gradient: Gradient(colors: [.orange, Color("mode").opacity(0.5)]), startPoint: .leading, endPoint: .trailing)
LinearGradient(gradient: Gradient(colors: [.orange, .primary.opacity(0.5)]), startPoint: .leading, endPoint: .trailing)
.frame(width: 10)
LinearGradient(gradient: Gradient(colors: [Color("mode").opacity(0.5), Color("mode").opacity(0.5)]), startPoint: .leading, endPoint: .trailing)
LinearGradient(gradient: Gradient(colors: [.primary.opacity(0.5), .primary.opacity(0.5)]), startPoint: .leading, endPoint: .trailing)
.frame(width: 300)
}
.mask(
@@ -934,71 +357,14 @@ struct PearDropView: View {
}
// Used for testing UI bounds
extension View {
func bounds() -> some View {
self.border(Color.red, width: 1)
}
}
// Background color/glass setter
@ViewBuilder
func backgroundView(themeSettings: ThemeSettings, darker: Bool = false, glass: Bool = false) -> some View {
func backgroundView(themeManager: ThemeManager = ThemeManager.shared, darker: Bool = false, glass: Bool = false) -> some View {
if glass {
GlassEffect(material: .sidebar, blendingMode: .behindWindow)
.edgesIgnoringSafeArea(.all)
} else {
darker ? themeSettings.themeColor.darker(by: 5).edgesIgnoringSafeArea(.all) : themeSettings.themeColor.edgesIgnoringSafeArea(.all)
darker ? themeManager.pickerColor.adjustBrightness(5).edgesIgnoringSafeArea(.all) : themeManager.pickerColor.edgesIgnoringSafeArea(.all)
}
}
struct PickerModifier: ViewModifier {
@State private var isHovered: Bool = false
let themeSettings: ThemeSettings
func body(content: Content) -> some View {
content
.buttonStyle(.borderless)
.padding(4)
// .background {
// backgroundView(themeSettings: themeSettings, darker: isHovered)
// .clipShape(RoundedRectangle(cornerRadius: 10))
// }
.onHover { hovering in
isHovered = hovering
}
}
}
extension View {
func pickerStyle(themeSettings: ThemeSettings) -> some View {
self.modifier(PickerModifier(themeSettings: themeSettings))
}
}
// Preset view elements
struct PresetColor: ButtonStyle {
var fillColor: Color
var label: String
func makeBody(configuration: Configuration) -> some View {
HStack(alignment: .center, spacing: 10) {
Circle()
.frame(width: 20, height: 20)
.fixedSize(horizontal: true, vertical: true)
.foregroundColor(fillColor)
.background(fillColor)
.clipShape(Circle())
.help(label)
.overlay(
RoundedRectangle(cornerRadius: 25)
.strokeBorder(Color("mode").opacity(0.8), lineWidth: 1))
// Text(label)
}
.onHover { inside in inside ? NSCursor.pointingHand.push() : NSCursor.pop() }
}
}
-131
View File
@@ -1,131 +0,0 @@
//
// ThemeManager.swift
// Pearcleaner
//
// Created by Alin Lupascu on 4/12/24.
//
import Foundation
import SwiftUI
import AppKit
class ThemeSettings: ObservableObject {
static let shared = ThemeSettings()
private let userDefaults = UserDefaults.standard
private let colorKey = "themeColor"
@Published var themeColor: Color {
didSet {
saveThemeColor()
}
}
// light - Color(.sRGB, red: 1.0, green: 1.0, blue: 1.0, opacity: 1)
// dark - Color(.sRGB, red: 0.149, green: 0.149, blue: 0.149, opacity: 1)
// pearcleaner - Color(.sRGB, red: 0.188143, green: 0.208556, blue: 0.262679, opacity: 1)
init() {
themeColor = userDefaults.color(forKey: colorKey) ?? .clear
}
func setupInitialColor(forcedDarkMode: Bool? = nil) {
// Determine dark mode either from forced setting or system setting
let darkMode = forcedDarkMode ?? isDarkMode()
// Set the theme color based on the dark mode status
themeColor = darkMode ? Color(.sRGB, red: 0.188143, green: 0.208556, blue: 0.262679, opacity: 1) :
Color(.sRGB, red: 1.0, green: 1.0, blue: 1.0, opacity: 1)
saveThemeColor()
}
private func loadThemeColor() {
if let components = UserDefaults.standard.array(forKey: colorKey) as? [CGFloat], components.count == 4 {
themeColor = Color(.sRGB, red: components[0], green: components[1], blue: components[2], opacity: components[3])
}
}
func saveThemeColor() {
let nsColor = NSColor(themeColor)
if let components = nsColor.cgColor.components {
UserDefaults.standard.set(components, forKey: colorKey)
}
}
func resetToDefault(dark: Bool = true) {
themeColor = dark ? Color(.sRGB, red: 0.188143, green: 0.208556, blue: 0.262679, opacity: 1) : Color(.sRGB, red: 1.0, green: 1.0, blue: 1.0, opacity: 1)
// themeColor = dark ? Color(.sRGB, red: 0.149, green: 0.149, blue: 0.149, opacity: 1) : Color(.sRGB, red: 1.0, green: 1.0, blue: 1.0, opacity: 1)
saveThemeColor()
}
func setPreset(preset: String, colorScheme: DisplayMode) {
themeColor = getColorForPreset(preset: preset, colorScheme: colorScheme)
saveThemeColor()
}
func getColorForPreset(preset: String, colorScheme: DisplayMode) -> Color {
let pearcleaner = colorScheme.colorScheme == .light ? Color(.sRGB, red: 0.499549, green: 0.545169, blue: 0.682028, opacity: 1) : Color(.sRGB, red: 0.188143, green: 0.208556, blue: 0.262679, opacity: 1)
let solarized = colorScheme.colorScheme == .light ? Color(.sRGB, red: 0.554372, green: 0.6557, blue: 0.734336, opacity: 1) : Color(.sRGB, red: 0.117257, green: 0.22506, blue: 0.249171, opacity: 1)
let dracula = colorScheme.colorScheme == .light ? Color(.sRGB, red: 0.567094, green: 0.562125, blue: 0.81285, opacity: 1) : Color(.sRGB, red: 0.268614, green: 0.264737, blue: 0.383503, opacity: 1)
let macOS = colorScheme.colorScheme == .light ? Color(.sRGB, red: 1.0, green: 1.0, blue: 1.0, opacity: 1) : Color(.sRGB, red: 0.149, green: 0.149, blue: 0.149, opacity: 1)
switch preset {
case "pearcleaner":
return pearcleaner
case "solarized":
return solarized
case "dracula":
return dracula
case "macOS":
return macOS
default:
return themeColor
}
}
}
extension Color {
/// Darkens the color by a percentage.
func darker(by percentage: CGFloat = 30.0) -> Color {
var hsb = (hue: CGFloat(0), saturation: CGFloat(0), brightness: CGFloat(0), alpha: CGFloat(0))
NSColor(self).getHue(&hsb.hue, saturation: &hsb.saturation, brightness: &hsb.brightness, alpha: &hsb.alpha)
return Color(hue: hsb.hue, saturation: hsb.saturation, brightness: max(hsb.brightness - percentage/100, 0), opacity: hsb.alpha)
}
/// Lightens the color by a percentage.
func lighter(by percentage: CGFloat = 30.0) -> Color {
var hsb = (hue: CGFloat(0), saturation: CGFloat(0), brightness: CGFloat(0), alpha: CGFloat(0))
NSColor(self).getHue(&hsb.hue, saturation: &hsb.saturation, brightness: &hsb.brightness, alpha: &hsb.alpha)
return Color(hue: hsb.hue, saturation: hsb.saturation, brightness: min(hsb.brightness + percentage / 100, 1.0), opacity: hsb.alpha)
}
}
extension Color {
/// Checks if the current themeColor is of a light or dark variant to set the appropriate displayMode on launch
func isLight() -> Bool? {
guard let components = NSColor(self).cgColor.components, components.count >= 3 else {
return nil
}
// Formula to calculate perceived luminance
let brightness = 0.299 * components[0] + 0.587 * components[1] + 0.114 * components[2]
return brightness > 0.5
}
}
extension UserDefaults {
func color(forKey key: String) -> Color? {
guard let components = array(forKey: key) as? [CGFloat], components.count == 4 else {
return nil
}
return Color(.sRGB, red: components[0], green: components[1], blue: components[2], opacity: components[3])
}
func setColor(_ color: Color, forKey key: String) {
let nsColor = NSColor(color)
if let components = nsColor.cgColor.components, components.count == 4 {
set(components, forKey: key)
} else {
removeObject(forKey: key)
}
}
}
-292
View File
@@ -1,292 +0,0 @@
//
// Updater.swift
// Pearcleaner
//
// Created by Alin Lupascu on 5/3/24.
//
import Foundation
import SwiftUI
struct Release: Codable {
let id: Int
let tag_name: String
let body: String
let assets: [Asset]
}
struct Asset: Codable {
let name: String
let url: String
let browser_download_url: String
}
extension Release {
var modifiedBody: String {
return body.replacingOccurrences(of: "- [x]", with: ">").replacingOccurrences(of: "###", with: "")
}
}
// --- Updater functionality
func loadGithubReleases(appState: AppState, manual: Bool = false, releaseOnly: Bool = false) {
let url = URL(string: "https://api.github.com/repos/alienator88/Pearcleaner/releases")!
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
if let decodedResponse = try? JSONDecoder().decode([Release].self, from: data) {
DispatchQueue.main.async {
let lastFewReleases = Array(decodedResponse.prefix(3)) // Get only the last 3 recent releases
appState.releases = lastFewReleases
if !releaseOnly {
checkForUpdate(appState: appState, manual: manual)
}
}
return
}
}
printOS("Fetch failed: \(error?.localizedDescription ?? "Unknown error")")
}.resume()
}
func checkForUpdate(appState: AppState, manual: Bool) {
guard let latestRelease = appState.releases.first else { return }
let currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0.0"
if latestRelease.tag_name > currentVersion {
updateOnMain {
appState.updateAvailable = true
}
} else {
if manual {
NewWin.show(appState: appState, width: 500, height: 300, newWin: .no_update)
}
}
}
func downloadUpdate(appState: AppState) {
updateOnMain {
appState.progressBar.0 = "UPDATER: Getting update link"
appState.progressBar.1 = 0.1
}
let fileManager = FileManager.default
guard let latestRelease = appState.releases.first else { return }
guard let asset = latestRelease.assets.first else { return }
guard let url = URL(string: asset.url) else { return }
var request = URLRequest(url: url)
request.setValue("application/octet-stream", forHTTPHeaderField: "Accept")
let downloadTask = URLSession.shared.downloadTask(with: request) { localURL, urlResponse, error in
updateOnMain {
appState.progressBar.0 = "UPDATER: Starting download of update file"
appState.progressBar.1 = 0.2
}
guard let localURL = localURL else { return }
let destinationURL = FileManager.default.temporaryDirectory.appendingPathComponent("\(asset.name)")
do {
if fileManager.fileExists(atPath: destinationURL.path) {
try? fileManager.removeItem(at: destinationURL)
}
updateOnMain {
appState.progressBar.0 = "UPDATER: File downloaded to temp directory"
appState.progressBar.1 = 0.3
}
try fileManager.moveItem(at: localURL, to: destinationURL)
updateOnMain {
appState.progressBar.0 = "UPDATER: File renamed using asset name"
appState.progressBar.1 = 0.4
}
UnzipAndReplace(DownloadedFileURL: destinationURL.path, appState: appState)
} catch {
printOS("Error moving downloaded file: \(error.localizedDescription)")
}
}
downloadTask.resume()
}
func UnzipAndReplace(DownloadedFileURL fileURL: String, appState: AppState) {
let appDirectory = Bundle.main.bundleURL.deletingLastPathComponent().path
let appBundle = Bundle.main.bundleURL.path
let fileManager = FileManager.default
do {
updateOnMain {
appState.progressBar.0 = "UPDATER: Removing currently installed application bundle"
appState.progressBar.1 = 0.5
}
// Remove the old version of your app
try fileManager.removeItem(atPath: appBundle)
updateOnMain {
appState.progressBar.0 = "UPDATER: Unziping file to original install location"
appState.progressBar.1 = 0.6
}
// Unzip the downloaded update file to your app's bundle path
let process = Process()
process.executableURL = URL(fileURLWithPath: "/usr/bin/ditto")
process.arguments = ["-xk", fileURL, appDirectory]
process.standardOutput = FileHandle.nullDevice
process.standardError = FileHandle.nullDevice
try process.run()
process.waitUntilExit()
updateOnMain {
appState.progressBar.0 = "UPDATER: Removing file from temp directory"
appState.progressBar.1 = 0.8
}
// After unzipping, remove the update file
try fileManager.removeItem(atPath: fileURL)
updateOnMain {
appState.progressBar.0 = "UPDATER: Completed, please restart!"
appState.progressBar.1 = 1.0
appState.updateAvailable = false
}
} catch {
printOS("Error updating the app: \(error)")
}
}
// --- Updater check frequency
enum UpdateFrequency: String, CaseIterable, Identifiable {
case none = "Never"
case daily = "Daily"
case weekly = "Weekly"
case monthly = "Monthly"
var id: String { self.rawValue }
var interval: TimeInterval? {
switch self {
case .none:
return nil
case .daily:
return 86400 // 1 day in seconds
case .weekly:
return 604800 // 7 days in seconds
case .monthly:
return 2592000 // 30 days in seconds
}
}
func updateNextUpdateDate() {
guard let updateInterval = self.interval else { return }
let newUpdateDate = Calendar.current.startOfDay(for: Date().addingTimeInterval(updateInterval))
UserDefaults.standard.set(newUpdateDate.timeIntervalSinceReferenceDate, forKey: "settings.updater.nextUpdateDate")
}
}
func checkAndUpdateIfNeeded(appState: AppState) {
@AppStorage("settings.updater.updateFrequency") var updateFrequency: UpdateFrequency = .daily
@AppStorage("settings.updater.nextUpdateDate") var nextUpdateDate = Date.now.timeIntervalSinceReferenceDate
guard let updateInterval = updateFrequency.interval else {
printOS("Updater: no update frequency set, skipping check")
return
}
let now = Date()
let nextUpdateDateLocal = Date(timeIntervalSinceReferenceDate: nextUpdateDate)
if !isSameDay(date1: nextUpdateDateLocal, date2: now) {
printOS("Updater: next update date is in the future, skipping (\(nextUpdateDateLocal))")
return
}
updateApp(appState: appState)
setNextUpdateDate(interval: updateInterval)
}
func updateApp(appState: AppState) {
// Perform your update logic here
printOS("Updater: checking for update")
loadGithubReleases(appState: appState)
}
func setNextUpdateDate(interval: TimeInterval) {
let newUpdateDate = Calendar.current.startOfDay(for: Date().addingTimeInterval(interval))
UserDefaults.standard.set(newUpdateDate.timeIntervalSinceReferenceDate, forKey: "settings.updater.nextUpdateDate")
}
func isSameDay(date1: Date, date2: Date) -> Bool {
return Calendar.current.isDate(date1, inSameDayAs: date2)
}
// --- Updater Badge View
struct UpdateNotificationView: View {
let appState: AppState
@State private var hovered: Bool = false
var body: some View {
HStack {
Text("Update Available!")
.font(.callout)
.opacity(0.5)
.padding(.leading, 7)
Spacer()
HStack(alignment: .center) {
Image(systemName: !hovered ? "square.and.arrow.down" : "square.and.arrow.down.fill")
.resizable()
.scaledToFit()
.frame(width: 14, height: 14)
.animation(.easeInOut(duration: 0.2), value: hovered)
.foregroundStyle(.white)
Text("v\(appState.releases.first?.tag_name ?? "Update")")
.foregroundStyle(.white)
}
.padding(3)
.onHover { hovering in
withAnimation() {
hovered = hovering
}
}
.onTapGesture {
NewWin.show(appState: appState, width: 500, height: 440, newWin: .update)
}
.help("Download latest update")
.padding(.horizontal, 5)
.padding(.vertical, 4)
.background(Color("pear"))
.clipShape(RoundedRectangle(cornerRadius: 6))
}
.frame(height: 30)
.padding(5)
.background(Color("mode").opacity(0.05))
.clipShape(RoundedRectangle(cornerRadius: 6))
.padding(.horizontal)
.padding(.bottom)
}
}
+318 -319
View File
@@ -7,30 +7,29 @@
import Foundation
import SwiftUI
import OSLog
import CoreImage
import AlinFoundation
import AppKit
// Make updates on main thread
func updateOnMain(after delay: Double? = nil, _ updates: @escaping () -> Void) {
if let delay = delay {
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
updates()
}
} else {
DispatchQueue.main.async {
updates()
}
}
}
// Execute functions on background thread
func updateOnBackground(_ updates: @escaping () -> Void) {
DispatchQueue.global(qos: .userInitiated).async {
updates()
}
}
//func updateOnMain(after delay: Double? = nil, _ updates: @escaping () -> Void) {
// if let delay = delay {
// DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
// updates()
// }
// } else {
// DispatchQueue.main.async {
// updates()
// }
// }
//}
//
//
//// Execute functions on background thread
//func updateOnBackground(_ updates: @escaping () -> Void) {
// DispatchQueue.global(qos: .userInitiated).async {
// updates()
// }
//}
// Reload apps list
func reloadAppsList(appState: AppState, fsm: FolderSettingsManager) {
@@ -57,57 +56,57 @@ func resizeWindowAuto(windowSettings: WindowSettings, title: String) {
// Check app directory based on user permission
func checkAppDirectoryAndUserRole(completion: @escaping ((isInCorrectDirectory: Bool, isAdmin: Bool)) -> Void) {
isCurrentUserAdmin { isAdmin in
let bundlePath = Bundle.main.bundlePath as NSString
let applicationsDir = "/Applications"
let userApplicationsDir = "\(home)/Applications"
var isInCorrectDirectory = false
if isAdmin {
// Admins can have the app in either /Applications or ~/Applications
isInCorrectDirectory = bundlePath.deletingLastPathComponent == applicationsDir ||
bundlePath.deletingLastPathComponent == userApplicationsDir
} else {
// Standard users should only have the app in ~/Applications
isInCorrectDirectory = bundlePath.deletingLastPathComponent == userApplicationsDir
}
// Return both conditions: if the app is in the correct directory and if the user is an admin
completion((isInCorrectDirectory, isAdmin))
}
}
// Check if user is admin or standard user
func isCurrentUserAdmin(completion: @escaping (Bool) -> Void) {
let process = Process()
process.executableURL = URL(fileURLWithPath: "/bin/zsh") // Using zsh, macOS default shell
process.arguments = ["-c", "groups $(whoami) | grep -q ' admin '"]
process.terminationHandler = { process in
// On macOS, a process's exit status of 0 indicates success (admin group found in this context)
completion(process.terminationStatus == 0)
}
do {
try process.run()
} catch {
print("Failed to execute command: \(error)")
completion(false)
}
}
//func checkAppDirectoryAndUserRole(completion: @escaping ((isInCorrectDirectory: Bool, isAdmin: Bool)) -> Void) {
// isCurrentUserAdmin { isAdmin in
// let bundlePath = Bundle.main.bundlePath as NSString
// let applicationsDir = "/Applications"
// let userApplicationsDir = "\(home)/Applications"
//
// var isInCorrectDirectory = false
//
// if isAdmin {
// // Admins can have the app in either /Applications or ~/Applications
// isInCorrectDirectory = bundlePath.deletingLastPathComponent == applicationsDir ||
// bundlePath.deletingLastPathComponent == userApplicationsDir
// } else {
// // Standard users should only have the app in ~/Applications
// isInCorrectDirectory = bundlePath.deletingLastPathComponent == userApplicationsDir
// }
//
// // Return both conditions: if the app is in the correct directory and if the user is an admin
// completion((isInCorrectDirectory, isAdmin))
// }
//}
//
//
//// Check if user is admin or standard user
//func isCurrentUserAdmin(completion: @escaping (Bool) -> Void) {
// let process = Process()
// process.executableURL = URL(fileURLWithPath: "/bin/zsh") // Using zsh, macOS default shell
// process.arguments = ["-c", "groups $(whoami) | grep -q ' admin '"]
//
// process.terminationHandler = { process in
// // On macOS, a process's exit status of 0 indicates success (admin group found in this context)
// completion(process.terminationStatus == 0)
// }
//
// do {
// try process.run()
// } catch {
// print("Failed to execute command: \(error)")
// completion(false)
// }
//}
// Check if appearance is dark mode
func isDarkMode() -> Bool {
return NSApp.effectiveAppearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua
}
// Set app color mode
func setAppearance(mode: NSAppearance.Name) {
NSApp.appearance = NSAppearance(named: mode)
}
//func isDarkMode() -> Bool {
// return NSApp.effectiveAppearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua
//}
//
//// Set app color mode
//func setAppearance(mode: NSAppearance.Name) {
// NSApp.appearance = NSAppearance(named: mode)
//}
// Check if Pearcleaner has any windows open
func hasWindowOpen() -> Bool {
@@ -118,13 +117,13 @@ func hasWindowOpen() -> Bool {
}
// 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.close()
}
}
}
//func findAndHideWindows(named titles: [String]) {
// for title in titles {
// if let window = NSApp.windows.first(where: { $0.title == title }) {
// window.close()
// }
// }
//}
func findAndSetWindowFrame(named titles: [String], windowSettings: WindowSettings) {
windowSettings.registerDefaultWindowSettings() {
@@ -139,20 +138,20 @@ func findAndSetWindowFrame(named titles: [String], windowSettings: WindowSetting
}
func findAndShowWindows(named titles: [String]) {
for title in titles {
if let window = NSApp.windows.first(where: { $0.title == title }) {
window.makeKeyAndOrderFront(nil)
}
}
}
//func findAndShowWindows(named titles: [String]) {
// for title in titles {
// if let window = NSApp.windows.first(where: { $0.title == title }) {
// window.makeKeyAndOrderFront(nil)
// }
// }
//}
// Copy to clipboard
func copyToClipboard(text: String) {
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
pasteboard.setString(text, forType: .string)
}
//func copyToClipboard(text: String) {
// let pasteboard = NSPasteboard.general
// pasteboard.clearContents()
// pasteboard.setString(text, forType: .string)
//}
@@ -224,15 +223,15 @@ func saveURLsToFile(urls: Set<URL>, appState: AppState) {
// Check if symlink
func isSymlink(atPath path: URL) -> Bool {
do {
let _ = try path.checkResourceIsReachable()
let resourceValues = try path.resourceValues(forKeys: [.isSymbolicLinkKey])
return resourceValues.isSymbolicLink == true
} catch {
return false
}
}
//func isSymlink(atPath path: URL) -> Bool {
// do {
// let _ = try path.checkResourceIsReachable()
// let resourceValues = try path.resourceValues(forKeys: [.isSymbolicLinkKey])
// return resourceValues.isSymbolicLink == true
// } catch {
// return false
// }
//}
// Open trash folder
@@ -297,62 +296,62 @@ func checkAppBundleArchitecture(at appBundlePath: String) -> Arch {
// Convert icon to png so colors render correctly
func convertICNSToPNG(icon: NSImage, size: NSSize) -> NSImage? {
// Resize the icon to the specified size
let resizedIcon = NSImage(size: size)
resizedIcon.lockFocus()
icon.draw(in: NSRect(x: 0, y: 0, width: size.width, height: size.height))
resizedIcon.unlockFocus()
// Convert the resized icon to PNG format
if let resizedImageData = resizedIcon.tiffRepresentation,
let resizedBitmap = NSBitmapImageRep(data: resizedImageData),
let pngData = resizedBitmap.representation(using: .png, properties: [:]) {
return NSImage(data: pngData)
}
return nil
}
//func convertICNSToPNG(icon: NSImage, size: NSSize) -> NSImage? {
// // Resize the icon to the specified size
// let resizedIcon = NSImage(size: size)
// resizedIcon.lockFocus()
// icon.draw(in: NSRect(x: 0, y: 0, width: size.width, height: size.height))
// resizedIcon.unlockFocus()
//
// // Convert the resized icon to PNG format
// if let resizedImageData = resizedIcon.tiffRepresentation,
// let resizedBitmap = NSBitmapImageRep(data: resizedImageData),
// let pngData = resizedBitmap.representation(using: .png, properties: [:]) {
// return NSImage(data: pngData)
// }
//
// return nil
//}
// Get icon for files and folders
func getIconForFileOrFolder(atPath path: URL) -> Image? {
return Image(nsImage: NSWorkspace.shared.icon(forFile: path.path))
}
func getIconForFileOrFolderNS(atPath path: URL) -> NSImage? {
return NSWorkspace.shared.icon(forFile: path.path)
}
//func getIconForFileOrFolder(atPath path: URL) -> Image? {
// return Image(nsImage: NSWorkspace.shared.icon(forFile: path.path))
//}
//
//func getIconForFileOrFolderNS(atPath path: URL) -> NSImage? {
// return NSWorkspace.shared.icon(forFile: path.path)
//}
// Get average color from image
extension NSImage {
var averageColor: NSColor? {
guard let tiffData = self.tiffRepresentation, let bitmapImage = NSBitmapImageRep(data: tiffData), let inputImage = CIImage(bitmapImageRep: bitmapImage) else { return nil }
let extentVector = CIVector(x: inputImage.extent.origin.x, y: inputImage.extent.origin.y, z: inputImage.extent.size.width, w: inputImage.extent.size.height)
guard let filter = CIFilter(name: "CIAreaAverage", parameters: [kCIInputImageKey: inputImage, kCIInputExtentKey: extentVector]) else { return nil }
guard let outputImage = filter.outputImage else { return nil }
var bitmap = [UInt8](repeating: 0, count: 4)
let context = CIContext(options: [.workingColorSpace: NSNull()])
context.render(outputImage, toBitmap: &bitmap, rowBytes: 4, bounds: CGRect(x: 0, y: 0, width: 1, height: 1), format: CIFormat.RGBA8, colorSpace: nil)
return NSColor(red: CGFloat(bitmap[0]) / 255, green: CGFloat(bitmap[1]) / 255, blue: CGFloat(bitmap[2]) / 255, alpha: CGFloat(bitmap[3]) / 255)
}
}
//extension NSImage {
// var averageColor: NSColor? {
// guard let tiffData = self.tiffRepresentation, let bitmapImage = NSBitmapImageRep(data: tiffData), let inputImage = CIImage(bitmapImageRep: bitmapImage) else { return nil }
//
// let extentVector = CIVector(x: inputImage.extent.origin.x, y: inputImage.extent.origin.y, z: inputImage.extent.size.width, w: inputImage.extent.size.height)
//
// guard let filter = CIFilter(name: "CIAreaAverage", parameters: [kCIInputImageKey: inputImage, kCIInputExtentKey: extentVector]) else { return nil }
// guard let outputImage = filter.outputImage else { return nil }
//
// var bitmap = [UInt8](repeating: 0, count: 4)
// let context = CIContext(options: [.workingColorSpace: NSNull()])
// context.render(outputImage, toBitmap: &bitmap, rowBytes: 4, bounds: CGRect(x: 0, y: 0, width: 1, height: 1), format: CIFormat.RGBA8, colorSpace: nil)
//
// return NSColor(red: CGFloat(bitmap[0]) / 255, green: CGFloat(bitmap[1]) / 255, blue: CGFloat(bitmap[2]) / 255, alpha: CGFloat(bitmap[3]) / 255)
// }
//}
// Relaunch app
func relaunchApp(afterDelay seconds: TimeInterval = 0.5) -> Never {
let task = Process()
task.launchPath = "/bin/sh"
task.arguments = ["-c", "sleep \(seconds); open \"\(Bundle.main.bundlePath)\""]
task.launch()
NSApp.terminate(nil)
exit(0)
}
//func relaunchApp(afterDelay seconds: TimeInterval = 0.5) -> Never {
// let task = Process()
// task.launchPath = "/bin/sh"
// task.arguments = ["-c", "sleep \(seconds); open \"\(Bundle.main.bundlePath)\""]
// task.launch()
//
// NSApp.terminate(nil)
// exit(0)
//}
// Check if app is running before deleting app files
func killApp(appId: String, completion: @escaping () -> Void = {}) {
@@ -416,7 +415,7 @@ func folderImages(for path: String) -> AnyView? {
.resizable()
.scaledToFit()
.frame(width: 13)
.foregroundStyle(Color("mode").opacity(0.5))
.foregroundStyle(.primary.opacity(0.5))
.help("Container")
)
} else if path.contains("/Library/Application Scripts/") {
@@ -425,7 +424,7 @@ func folderImages(for path: String) -> AnyView? {
.resizable()
.scaledToFit()
.frame(width: 13)
.foregroundStyle(Color("mode").opacity(0.5))
.foregroundStyle(.primary.opacity(0.5))
.help("Application Script")
)
} else if path.contains(".plist") {
@@ -434,7 +433,7 @@ func folderImages(for path: String) -> AnyView? {
.resizable()
.scaledToFit()
.frame(width: 13)
.foregroundStyle(Color("mode").opacity(0.5))
.foregroundStyle(.primary.opacity(0.5))
.help("Plist File")
)
}
@@ -461,22 +460,22 @@ func isNested(path: URL) -> Bool {
}
// Check if macOS is 14.0 or higher
func isMacOS14OrHigher() -> Bool {
if #available(macOS 14, *) {
return true
} else {
return false
}
}
//func isMacOS14OrHigher() -> Bool {
// if #available(macOS 14, *) {
// return true
// } else {
// return false
// }
//}
// --- Extend Int to convert hours to seconds ---
extension Int {
var daysToSeconds: Double {
return Double(self) * 24 * 60 * 60
}
}
//extension Int {
// var daysToSeconds: Double {
// return Double(self) * 24 * 60 * 60
// }
//}
// --- Extend String to remove periods, spaces and lowercase the string
@@ -495,15 +494,15 @@ extension String {
}
// --- Capitalize first letter of string only
extension String {
func capitalizingFirstLetter() -> String {
return prefix(1).capitalized + dropFirst()
}
mutating func capitalizeFirstLetter() {
self = self.capitalizingFirstLetter()
}
}
//extension String {
// func capitalizingFirstLetter() -> String {
// return prefix(1).capitalized + dropFirst()
// }
//
// mutating func capitalizeFirstLetter() {
// self = self.capitalizingFirstLetter()
// }
//}
// --- Returns comma separated string as array of strings
extension String {
@@ -517,18 +516,18 @@ extension String {
// --- Overload the greater than operator ">" to do a semantic check on the string versions
extension String {
func versionStringToTuple() -> (Int, Int, Int) {
let components = self.split(separator: ".").compactMap { Int($0) }
return (components[0], components[1], components[2])
}
static func > (lhs: String, rhs: String) -> Bool {
let lhsVersion = lhs.versionStringToTuple()
let rhsVersion = rhs.versionStringToTuple()
return lhsVersion > rhsVersion
}
}
//extension String {
// func versionStringToTuple() -> (Int, Int, Int) {
// let components = self.split(separator: ".").compactMap { Int($0) }
// return (components[0], components[1], components[2])
// }
//
// static func > (lhs: String, rhs: String) -> Bool {
// let lhsVersion = lhs.versionStringToTuple()
// let rhsVersion = rhs.versionStringToTuple()
// return lhsVersion > rhsVersion
// }
//}
// --- Trash Relationship ---
@@ -541,103 +540,103 @@ extension String {
//}
// --- Extend print command to also output to the Console ---
func printOS(_ items: Any..., separator: String = " ", terminator: String = "\n") {
let message = items.map { "\($0)" }.joined(separator: separator)
let log = OSLog(subsystem: "pearcleaner", category: "Application")
os_log("%@", log: log, type: .default, message)
}
//func printOS(_ items: Any..., separator: String = " ", terminator: String = "\n") {
// let message = items.map { "\($0)" }.joined(separator: separator)
// let log = OSLog(subsystem: "pearcleaner", category: "Application")
// os_log("%@", log: log, type: .default, message)
//}
// Get size of files
func totalSizeOnDisk(for paths: [URL]) -> (real: Int64, logical: Int64) {
let fileManager = FileManager.default
var totalAllocatedSize: Int64 = 0
var totalFileSize: Int64 = 0
for url in paths {
var isDirectory: ObjCBool = false
if fileManager.fileExists(atPath: url.path, isDirectory: &isDirectory) {
let keys: [URLResourceKey] = [.totalFileAllocatedSizeKey, .fileSizeKey]
if isDirectory.boolValue {
// It's a directory, recurse into it
if let enumerator = fileManager.enumerator(at: url, includingPropertiesForKeys: keys, errorHandler: nil) {
for case let fileURL as URL in enumerator {
do {
let fileAttributes = try fileURL.resourceValues(forKeys: Set(keys))
if let allocatedSize = fileAttributes.totalFileAllocatedSize {
totalAllocatedSize += Int64(allocatedSize)
}
if let fileSize = fileAttributes.fileSize {
totalFileSize += Int64(fileSize)
}
} catch {
print("Error getting file attributes for \(fileURL): \(error)")
}
}
}
} else {
// It's a file
do {
let fileAttributes = try url.resourceValues(forKeys: Set(keys))
if let allocatedSize = fileAttributes.totalFileAllocatedSize {
totalAllocatedSize += Int64(allocatedSize)
}
if let fileSize = fileAttributes.fileSize {
totalFileSize += Int64(fileSize)
}
} catch {
print("Error getting file attributes for \(url): \(error)")
}
}
}
}
return (real: totalAllocatedSize, logical: totalFileSize)
}
func totalSizeOnDisk(for path: URL) -> (real: Int64, logical: Int64) {
return totalSizeOnDisk(for: [path])
}
// ByteFormatter
func formatByte(size: Int64) -> (human: String, byte: String) {
let byteCountFormatter = ByteCountFormatter()
byteCountFormatter.countStyle = .file
byteCountFormatter.allowedUnits = [.useAll]
let human = byteCountFormatter.string(fromByteCount: size)
let numberformatter = NumberFormatter()
numberformatter.numberStyle = .decimal
let formattedNumber = numberformatter.string(from: NSNumber(value: size)) ?? "\(size)"
let byte = "\(formattedNumber)"
return (human: human, byte: byte)
}
//func totalSizeOnDisk(for paths: [URL]) -> (real: Int64, logical: Int64) {
// let fileManager = FileManager.default
// var totalAllocatedSize: Int64 = 0
// var totalFileSize: Int64 = 0
//
// for url in paths {
// var isDirectory: ObjCBool = false
// if fileManager.fileExists(atPath: url.path, isDirectory: &isDirectory) {
// let keys: [URLResourceKey] = [.totalFileAllocatedSizeKey, .fileSizeKey]
// if isDirectory.boolValue {
// // It's a directory, recurse into it
// if let enumerator = fileManager.enumerator(at: url, includingPropertiesForKeys: keys, errorHandler: nil) {
// for case let fileURL as URL in enumerator {
// do {
// let fileAttributes = try fileURL.resourceValues(forKeys: Set(keys))
// if let allocatedSize = fileAttributes.totalFileAllocatedSize {
// totalAllocatedSize += Int64(allocatedSize)
// }
// if let fileSize = fileAttributes.fileSize {
// totalFileSize += Int64(fileSize)
// }
// } catch {
// print("Error getting file attributes for \(fileURL): \(error)")
// }
// }
// }
// } else {
// // It's a file
// do {
// let fileAttributes = try url.resourceValues(forKeys: Set(keys))
// if let allocatedSize = fileAttributes.totalFileAllocatedSize {
// totalAllocatedSize += Int64(allocatedSize)
// }
// if let fileSize = fileAttributes.fileSize {
// totalFileSize += Int64(fileSize)
// }
// } catch {
// print("Error getting file attributes for \(url): \(error)")
// }
// }
// }
// }
//
// return (real: totalAllocatedSize, logical: totalFileSize)
//}
//
//
//
//func totalSizeOnDisk(for path: URL) -> (real: Int64, logical: Int64) {
// return totalSizeOnDisk(for: [path])
//}
//
//// ByteFormatter
//func formatByte(size: Int64) -> (human: String, byte: String) {
// let byteCountFormatter = ByteCountFormatter()
// byteCountFormatter.countStyle = .file
// byteCountFormatter.allowedUnits = [.useAll]
// let human = byteCountFormatter.string(fromByteCount: size)
//
// let numberformatter = NumberFormatter()
// numberformatter.numberStyle = .decimal
// let formattedNumber = numberformatter.string(from: NSNumber(value: size)) ?? "\(size)"
// let byte = "\(formattedNumber)"
//
// return (human: human, byte: byte)
//
//}
// Only process supported files
func isSupportedFileType(at path: String) -> Bool {
let fileManager = FileManager.default
do {
let attributes = try fileManager.attributesOfItem(atPath: path)
if let fileType = attributes[FileAttributeKey.type] as? FileAttributeType {
switch fileType {
case .typeRegular, .typeDirectory, .typeSymbolicLink:
// The file is a regular file, directory, or symbolic link
return true
default:
// The file is a socket, pipe, or another type not supported
return false
}
}
} catch {
printOS("Error getting file attributes: \(error)")
}
return false
}
//func isSupportedFileType(at path: String) -> Bool {
// let fileManager = FileManager.default
// do {
// let attributes = try fileManager.attributesOfItem(atPath: path)
// if let fileType = attributes[FileAttributeKey.type] as? FileAttributeType {
// switch fileType {
// case .typeRegular, .typeDirectory, .typeSymbolicLink:
// // The file is a regular file, directory, or symbolic link
// return true
// default:
// // The file is a socket, pipe, or another type not supported
// return false
// }
// }
// } catch {
// printOS("Error getting file attributes: \(error)")
// }
// return false
//}
@@ -671,43 +670,43 @@ func uninstallPearcleaner(appState: AppState, locations: Locations) {
// --- Create Application Support folder if it doesn't exist ---
func ensureApplicationSupportFolderExists(appState: AppState) {
let fileManager = FileManager.default
let supportURL = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!.appendingPathComponent("com.alienator88.Pearcleaner")
// Check to make sure Application Support/Pearcleaner folder exists
if !fileManager.fileExists(atPath: supportURL.path) {
try! fileManager.createDirectory(at: supportURL, withIntermediateDirectories: true)
printOS("Created Application Support/com.alienator88.Pearcleaner folder")
}
}
//func ensureApplicationSupportFolderExists(appState: AppState) {
// let fileManager = FileManager.default
// let supportURL = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!.appendingPathComponent("com.alienator88.Pearcleaner")
//
// // Check to make sure Application Support/Pearcleaner folder exists
// if !fileManager.fileExists(atPath: supportURL.path) {
// try! fileManager.createDirectory(at: supportURL, withIntermediateDirectories: true)
// printOS("Created Application Support/com.alienator88.Pearcleaner folder")
// }
//}
// --- Write Log to File for troubleshooting ---
func writeLog(string: String) {
let fileManager = FileManager.default
let home = fileManager.homeDirectoryForCurrentUser.path
let logFilePath = "\(home)/Downloads/log.txt"
// Check if the log file exists, and create it if it doesn't
if !fileManager.fileExists(atPath: logFilePath) {
if !fileManager.createFile(atPath: logFilePath, contents: nil, attributes: nil) {
printOS("Failed to create the log file.")
return
}
}
do {
if let fileHandle = FileHandle(forWritingAtPath: logFilePath) {
let ns = "\(string)\n"
fileHandle.seekToEndOfFile()
fileHandle.write(ns.data(using: .utf8)!)
fileHandle.closeFile()
} else {
printOS("Error opening file for appending")
}
}
}
//func writeLog(string: String) {
// let fileManager = FileManager.default
// let home = fileManager.homeDirectoryForCurrentUser.path
// let logFilePath = "\(home)/Downloads/log.txt"
//
// // Check if the log file exists, and create it if it doesn't
// if !fileManager.fileExists(atPath: logFilePath) {
// if !fileManager.createFile(atPath: logFilePath, contents: nil, attributes: nil) {
// printOS("Failed to create the log file.")
// return
// }
// }
//
// do {
// if let fileHandle = FileHandle(forWritingAtPath: logFilePath) {
// let ns = "\(string)\n"
// fileHandle.seekToEndOfFile()
// fileHandle.write(ns.data(using: .utf8)!)
// fileHandle.closeFile()
// } else {
// printOS("Error opening file for appending")
// }
// }
//}
// --- Load Plist file with launchctl ---
@@ -752,20 +751,20 @@ func sendStopNotificationFW() {
}
func getCurrentTimestamp() -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS"
return dateFormatter.string(from: Date())
}
//func getCurrentTimestamp() -> String {
// let dateFormatter = DateFormatter()
// dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS"
// return dateFormatter.string(from: Date())
//}
func formattedDate(_ date: Date?) -> String {
guard let date = date else { return "N/A" }
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .none
return formatter.string(from: date)
}
//func formattedDate(_ date: Date?) -> String {
// guard let date = date else { return "N/A" }
// let formatter = DateFormatter()
// formatter.dateStyle = .short
// formatter.timeStyle = .none
// return formatter.string(from: date)
//}
+31 -87
View File
@@ -7,6 +7,7 @@
import SwiftUI
import AppKit
import AlinFoundation
@main
struct PearcleanerApp: App {
@@ -14,10 +15,11 @@ struct PearcleanerApp: App {
@StateObject var appState = AppState()
@StateObject var locations = Locations()
@StateObject var fsm = FolderSettingsManager()
@StateObject private var updater = Updater(owner: "alienator88", repo: "Pearcleaner")
@StateObject private var themeManager = ThemeManager.shared
@StateObject private var permissionManager = PermissionManager.shared
@State private var windowSettings = WindowSettings()
@AppStorage("settings.updater.updateFrequency") private var updateFrequency: UpdateFrequency = .daily
@AppStorage("settings.permissions.hasLaunched") private var hasLaunched: Bool = false
@AppStorage("displayMode") var displayMode: DisplayMode = .system
@AppStorage("settings.general.mini") private var mini: Bool = false
@AppStorage("settings.general.miniview") private var miniView: Bool = true
@AppStorage("settings.general.features") private var features: String = ""
@@ -42,8 +44,10 @@ struct PearcleanerApp: App {
.environmentObject(appState)
.environmentObject(locations)
.environmentObject(fsm)
.environmentObject(ThemeSettings.shared)
.preferredColorScheme(displayMode.colorScheme)
.environmentObject(themeManager)
.environmentObject(updater)
.environmentObject(permissionManager)
.preferredColorScheme(themeManager.displayMode.colorScheme)
.handlesExternalEvents(preferring: Set(arrayLiteral: "pear"), allowing: Set(arrayLiteral: "*"))
.onOpenURL(perform: { url in
let deeplinkManager = DeeplinkManager(showPopover: $showPopover)
@@ -76,6 +80,11 @@ struct PearcleanerApp: App {
secondaryButton: .cancel()
)
}
.sheet(isPresented: $updater.showSheet, content: {
/// This will show the update sheet based on the frequency check function only
updater.getUpdateView()
.environmentObject(themeManager)
})
.onAppear {
if miniView {
@@ -98,8 +107,9 @@ struct PearcleanerApp: App {
.environmentObject(locations)
.environmentObject(appState)
.environmentObject(fsm)
.environmentObject(ThemeSettings.shared)
.preferredColorScheme(displayMode.colorScheme)
.environmentObject(themeManager)
.environmentObject(permissionManager)
.preferredColorScheme(themeManager.displayMode.colorScheme)
}, icon: selectedMenubarIcon)
}
@@ -107,29 +117,15 @@ struct PearcleanerApp: App {
#if !DEBUG
Task {
/// This will check for updates on load based on the update frequency
updater.checkAndUpdateIfNeeded()
/// Get new features
getFeatures(appState: appState, features: $features)
// Make sure App Support folder exists in the future if needed for storage
//MARK: This is not needed any longer as the update file is stored in /tmp directory. Use in the future for any local db functions the app might need
// ensureApplicationSupportFolderExists(appState: appState)
// Check for updates after app launch
checkAllPermissions(appState: appState) { results in
appState.permissionResults = results
if results.allPermissionsGranted {
// Get GH releases
loadGithubReleases(appState: appState, releaseOnly: true)
if updateFrequency != .none {
// Update checker
checkAndUpdateIfNeeded(appState: appState)
}
// Get new features
getFeatures(appState: appState, features: $features)
}
}
// ensureApplicationSupportFolderExists(appState: appState)
}
@@ -140,7 +136,7 @@ struct PearcleanerApp: App {
.windowStyle(.hiddenTitleBar)
.windowResizability(.contentMinSize)
.commands {
AppCommands(appState: appState, locations: locations, fsm: fsm)
AppCommands(appState: appState, locations: locations, fsm: fsm, updater: updater, themeManager: themeManager)
// CommandGroup(replacing: .newItem, addition: { })
}
@@ -153,9 +149,11 @@ struct PearcleanerApp: App {
.environmentObject(appState)
.environmentObject(locations)
.environmentObject(fsm)
.environmentObject(ThemeSettings.shared)
.environmentObject(themeManager)
.environmentObject(updater)
.environmentObject(permissionManager)
.toolbarBackground(.clear)
.preferredColorScheme(displayMode.colorScheme)
.preferredColorScheme(themeManager.displayMode.colorScheme)
}
}
}
@@ -164,8 +162,8 @@ struct PearcleanerApp: App {
class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
var observer: NSObjectProtocol?
var windowSettings = WindowSettings()
var themeManager = ThemeManager.shared
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
let menubarEnabled = UserDefaults.standard.bool(forKey: "settings.menubar.enabled")
@@ -178,72 +176,20 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
findAndSetWindowFrame(named: ["Pearcleaner"], windowSettings: windowSettings)
self.appearanceCheck()
themeManager.setupAppearance()
if menubarEnabled {
findAndHideWindows(named: ["Pearcleaner"])
NSApplication.shared.setActivationPolicy(.accessory)
}
// Start observing the appearance change
observer = DistributedNotificationCenter.default().addObserver(forName: NSNotification.Name(rawValue: "AppleInterfaceThemeChangedNotification"), object: nil, queue: OperationQueue.main) { [weak self] _ in
let themeMode = UserDefaults.standard.string(forKey: "settings.general.selectedTheme")
let themesEnabled = UserDefaults.standard.bool(forKey: "settings.interface.themesEnabled")
if themeMode == "Auto" && !themesEnabled {
self?.appearanceCheck(reset: true)
}
}
}
func appearanceCheck(reset: Bool = false) {
let themesEnabled = UserDefaults.standard.bool(forKey: "settings.interface.themesEnabled")
let themeMode = UserDefaults.standard.string(forKey: "settings.general.selectedTheme")
if themesEnabled {
// Custom theming is enabled; user's manual settings take priority.
} else {
switch themeMode {
case "Auto":
// Auto mode: adjust theme based on system appearance.
ThemeSettings.shared.setupInitialColor()
updateDisplayModeBasedOnThemeColor()
case "Light", "Dark":
// Specific mode set: ensure theme color matches the display mode.
ThemeSettings.shared.setupInitialColor(forcedDarkMode: themeMode == "Dark")
UserDefaults.standard.set(themeMode == "Dark" ? DisplayMode.dark.rawValue : DisplayMode.light.rawValue, forKey: "displayMode")
default:
// No specific handling needed or unrecognized mode; fall back to default behavior.
printOS("No specific theme mode set or unrecognized value.")
}
}
if reset && themeMode == "Auto" {
let dark = isDarkMode()
ThemeSettings.shared.resetToDefault(dark: dark)
}
}
func updateDisplayModeBasedOnThemeColor() {
let themeColor = ThemeSettings.shared.themeColor
if let isLightColor = themeColor.isLight() {
let shouldUseDarkMode = !isLightColor
UserDefaults.standard.set(shouldUseDarkMode ? DisplayMode.dark.rawValue : DisplayMode.light.rawValue, forKey: "displayMode")
} else {
// Default to system preference if brightness cannot be determined.
let dark = isDarkMode()
UserDefaults.standard.set(dark ? DisplayMode.dark.rawValue : DisplayMode.light.rawValue, forKey: "displayMode")
}
}
func applicationWillTerminate(_ notification: Notification) {
// Stop observing the appearance change
if let observer = observer {
DistributedNotificationCenter.default().removeObserver(observer)
}
// Perform actions on app termination here
}
@@ -255,10 +201,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
for window in sender.windows {
window.title = "Pearcleaner"
window.makeKeyAndOrderFront(self)
print(windowSettings.loadWindowSettings())
updateOnMain(after: 0.1, {
resizeWindowAuto(windowSettings: windowSettings, title: "Pearcleaner")
print(window.title)
})
}
return true // Indicates you've handled the re-open
@@ -1,38 +0,0 @@
{
"colors" : [
{
"color" : {
"color-space" : "display-p3",
"components" : {
"alpha" : "1.000",
"blue" : "0x00",
"green" : "0x00",
"red" : "0x00"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xFF",
"green" : "0xFF",
"red" : "0xFE"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -1,38 +0,0 @@
{
"colors" : [
{
"color" : {
"color-space" : "display-p3",
"components" : {
"alpha" : "1.000",
"blue" : "0xFF",
"green" : "0xFF",
"red" : "0xFF"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "display-p3",
"components" : {
"alpha" : "1.000",
"blue" : "0x26",
"green" : "0x26",
"red" : "0x26"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
+108 -114
View File
@@ -6,12 +6,18 @@
//
import SwiftUI
import AlinFoundation
struct AboutSettingsTab: View {
@EnvironmentObject var appState: AppState
@State private var disclose = false
@State private var discloseCredits = false
var body: some View {
let sponsors = Sponsor.sponsors
let credits = Credit.credits
VStack(alignment: .center) {
@@ -39,7 +45,7 @@ struct AboutSettingsTab: View {
Text("GitHub")
Text("Submit a bug or feature request via the repo")
.font(.callout)
.foregroundStyle(Color("mode").opacity(0.5))
.foregroundStyle(.primary.opacity(0.5))
}
Spacer()
@@ -53,133 +59,92 @@ struct AboutSettingsTab: View {
.padding(.leading)
.padding(.top, 5)
Divider()
.padding()
}
VStack(alignment: .leading) {
Divider()
HStack() {
Text("Credits").font(.title2)
Spacer()
}
.padding(.leading)
.padding(.bottom)
HStack{
Image(systemName: "paintbrush.pointed")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
VStack(alignment: .leading){
Text("Microsoft Designer")
Text("Application icon resource")
.font(.callout)
.foregroundStyle(Color("mode").opacity(0.5))
HStack{
Image(systemName: "dollarsign.circle")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
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))
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
Spacer()
Button(""){
NSWorkspace.shared.open(URL(string: "https://designer.microsoft.com/image-creator")!)
}
.buttonStyle(SimpleButtonStyle(icon: "link", help: "View"))
}
.padding(5)
.padding(.leading)
.frame(height: 100)
.padding(5)
}, label: {
Text("GitHub Sponsors")
.font(.title2)
})
HStack{
Image(systemName: "applescript")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
VStack(alignment: .leading){
Text("Privacy Guides")
Text("Inspired by open-source appcleaner script from Sun Knudsen")
.font(.callout)
.foregroundStyle(Color("mode").opacity(0.5))
.lineLimit(2)
}
Spacer()
Button("") {
NSWorkspace.shared.open(URL(string: "https://sunknudsen.com/privacy-guides/how-to-clean-uninstall-macos-apps-using-appcleaner-open-source-alternative")!)
}
.buttonStyle(SimpleButtonStyle(icon: "link", help: "View"))
}
.padding(5)
.padding(.leading)
HStack{
Image(systemName: "trash")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
VStack(alignment: .leading){
Text("AppCleaner")
Text("Inspired by AppCleaner from Freemacsoft")
.font(.callout)
.foregroundStyle(Color("mode").opacity(0.5))
}
Spacer()
Button(""){
NSWorkspace.shared.open(URL(string: "https://freemacsoft.net/appcleaner/")!)
}
.buttonStyle(SimpleButtonStyle(icon: "link", help: "View"))
}
.padding(5)
.padding(.leading)
HStack{
Image(systemName: "dollarsign.circle.fill")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
VStack(alignment: .leading){
Text("Apple Developer Account Sponsor")
Text("Many thanks to user DharsanB")
.font(.callout)
.foregroundStyle(Color("mode").opacity(0.5))
}
Spacer()
Button(""){
NSWorkspace.shared.open(URL(string: "https://github.com/dharsanb")!)
}
.buttonStyle(SimpleButtonStyle(icon: "link", help: "View"))
}
.padding(5)
.padding(.leading)
Spacer()
}
.padding(5)
.padding(.leading)
Spacer()
Divider()
HStack{
Image(systemName: "info.circle")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.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)
}, label: {
Text("Credits")
.font(.title2)
})
}
.padding(5)
.padding(.leading)
Text("Made with ❤️ by Alin Lupascu").font(.footnote).padding(.bottom)
}
.padding(20)
.frame(width: 500, height: 600)
.frame(width: 500)
}
}
@@ -207,3 +172,32 @@ extension Bundle {
}
//MARK: Sponsors
struct Sponsor: Identifiable {
let id = UUID()
let name: String
let url: URL
static let sponsors: [Sponsor] = [
Sponsor(name: "DharsanB", url: URL(string: "https://github.com/dharsanb")!),
Sponsor(name: "MadMacMad", url: URL(string: "https://github.com/MadMacMad")!),
Sponsor(name: "Butterdawgs", url: URL(string: "https://github.com/butterdawgs")!),
Sponsor(name: "y-u-s-u-f", url: URL(string: "https://github.com/y-u-s-u-f")!)
]
}
//MARK: Credits
struct Credit: Identifiable {
let id = UUID()
// let icon: String
let name: String
let description: String
let url: URL
static let credits: [Credit] = [
Credit(name: "Microsoft Designer", description: "Application icon resource", url: URL(string: "https://designer.microsoft.com/image-creator")!),
Credit(name: "Privacy Guides", description: "Inspired by open-source appcleaner script from Sun Knudsen", url: URL(string: "https://sunknudsen.com/privacy-guides/how-to-clean-uninstall-macos-apps-using-appcleaner-open-source-alternative")!),
Credit(name: "AppCleaner", description: "Inspired by AppCleaner from Freemacsoft", url: URL(string: "https://freemacsoft.net/appcleaner/")!)
]
}
+9 -10
View File
@@ -15,12 +15,13 @@
import Foundation
import SwiftUI
import AppKit
import AlinFoundation
struct FolderSettingsTab: View {
@EnvironmentObject var appState: AppState
@EnvironmentObject var locations: Locations
@EnvironmentObject var fsm: FolderSettingsManager
@EnvironmentObject var themeSettings: ThemeSettings
@EnvironmentObject var themeManager: ThemeManager
@State private var isHovered = false
var body: some View {
@@ -82,8 +83,9 @@ struct FolderSettingsTab: View {
}
.scrollIndicators(.automatic)
.padding()
// .background(Color("mode").opacity(0.05))
.background(backgroundView(themeSettings: themeSettings, darker: true))
.frame(height: 200)
// .background(.primary.opacity(0.05))
.background(backgroundView(themeManager: themeManager, darker: true))
.clipShape(RoundedRectangle(cornerRadius: 10))
.onDrop(of: ["public.file-url"], isTargeted: nil) { providers -> Bool in
providers.forEach { provider in
@@ -175,8 +177,9 @@ struct FolderSettingsTab: View {
}
.scrollIndicators(.automatic)
.padding()
.background(backgroundView(themeSettings: themeSettings, darker: true))
// .background(Color("mode").opacity(0.05))
.frame(height: 200)
.background(backgroundView(themeManager: themeManager, darker: true))
// .background(.primary.opacity(0.05))
.clipShape(RoundedRectangle(cornerRadius: 10))
.onDrop(of: ["public.file-url"], isTargeted: nil) { providers -> Bool in
providers.forEach { provider in
@@ -200,15 +203,11 @@ struct FolderSettingsTab: View {
Spacer()
}
.padding(.top, 5)
Spacer()
}
}
.padding(20)
.frame(width: 500, height: 600)
.frame(width: 500)//, height: 600)
}
+121 -145
View File
@@ -7,13 +7,13 @@
import Foundation
import SwiftUI
//import ServiceManagement
import FinderSync
import AlinFoundation
struct GeneralSettingsTab: View {
@EnvironmentObject var appState: AppState
@EnvironmentObject var locations: Locations
@EnvironmentObject var themeSettings: ThemeSettings
@EnvironmentObject var themeManager: ThemeManager
@State private var windowSettings = WindowSettings()
@AppStorage("settings.general.glass") private var glass: Bool = true
@AppStorage("settings.general.mini") private var mini: Bool = false
@@ -52,11 +52,11 @@ struct GeneralSettingsTab: View {
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(Color("mode").opacity(0.5))
.foregroundStyle(.primary.opacity(0.5))
VStack(alignment: .leading, spacing: 5) {
Text("\(brew ? "Homebrew cleanup is enabled" : "Homebrew cleanup is disabled")")
.font(.callout)
.foregroundStyle(Color("mode").opacity(0.5))
.foregroundStyle(.primary.opacity(0.5))
}
InfoButton(text: "When homebrew cleanup is enabled, Pearcleaner will check if the app you are removing was installed via homebrew and execute a brew uninstall and brew cleanup command as well to let homebrew know that the app is removed. This way your homebrew list will be synced up correctly and caching will be removed.\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")
@@ -76,11 +76,11 @@ struct GeneralSettingsTab: View {
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(Color("mode").opacity(0.5))
.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(Color("mode").opacity(0.5))
.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.")
@@ -100,11 +100,11 @@ struct GeneralSettingsTab: View {
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(Color("mode").opacity(0.5))
.foregroundStyle(.primary.opacity(0.5))
VStack(alignment: .leading, spacing: 5) {
Text("File list sorting mode")
.font(.callout)
.foregroundStyle(Color("mode").opacity(0.5))
.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()
@@ -114,7 +114,7 @@ struct GeneralSettingsTab: View {
Text("Size")
.tag(false)
}
.pickerStyle(themeSettings: themeSettings)
.buttonStyle(.borderless)
}
.padding(5)
.padding(.leading)
@@ -126,11 +126,11 @@ struct GeneralSettingsTab: View {
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(Color("mode").opacity(0.5))
.foregroundStyle(.primary.opacity(0.5))
VStack(alignment: .leading, spacing: 5) {
Text("File size display mode")
.font(.callout)
.foregroundStyle(Color("mode").opacity(0.5))
.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()
@@ -142,8 +142,7 @@ struct GeneralSettingsTab: View {
Text("Finder")
.tag("Finder")
}
.pickerStyle(themeSettings: themeSettings)
.buttonStyle(.borderless)
}
.padding(5)
@@ -153,120 +152,116 @@ struct GeneralSettingsTab: View {
// === Perms ================================================================================================
Divider()
.padding()
// Divider()
// .padding()
// HStack() {
// Text("Permissions").font(.title2)
//
//// InfoButtonPerms()
//
// Spacer()
//
// Button("Refresh") {
// checkAllPermissions(appState: appState) { results in
// updateOnMain {
// appState.permissionResults = results
// }
// diskStatus = results.fullDiskAccess
// accessStatus = results.accessibility
// autoStatus = results.automation
// if results.allPermissionsGranted {
// updateOnMain {
// appState.permissionsOkay = true
// }
// }
// }
// }
// .buttonStyle(SimpleButtonStyle(icon: "arrow.triangle.2.circlepath", label: "Refresh", help: "Refresh permissions"))
// .padding(.trailing, 5)
//
// }
// .padding(.leading)
HStack() {
Text("Permissions").font(.title2)
InfoButtonPerms()
Spacer()
Button("Refresh") {
checkAllPermissions(appState: appState) { results in
updateOnMain {
appState.permissionResults = results
}
diskStatus = results.fullDiskAccess
accessStatus = results.accessibility
autoStatus = results.automation
if results.allPermissionsGranted {
updateOnMain {
appState.permissionsOkay = true
}
}
}
}
.buttonStyle(SimpleButtonStyle(icon: "arrow.triangle.2.circlepath", label: "Refresh", help: "Refresh permissions"))
.padding(.trailing, 5)
}
.padding(.leading)
HStack(spacing: 0) {
Image(systemName: "externaldrive")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(diskStatus ? .green : .red)
.saturation(displayMode.colorScheme == .dark ? 0.8 : 1)
Text(diskStatus ? "Full Disk permission granted" : "Full Disk permission not granted")
.font(.callout)
.foregroundStyle(Color("mode").opacity(0.5))
Spacer()
Button("") {
if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles") {
NSWorkspace.shared.open(url)
}
}
.buttonStyle(SimpleButtonStyle(icon: "folder", help: "View disk permissions pane"))
}
.padding(5)
.padding(.leading)
Spacer()
HStack(spacing: 0) {
Image(systemName: isMacOS14OrHigher() ? "accessibility" : "figure.arms.open")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(accessStatus ? .green : .red)
.saturation(displayMode.colorScheme == .dark ? 0.8 : 1)
Text(accessStatus ? "Accessibility permission granted" : "Accessibility permission not granted")
.font(.callout)
.foregroundStyle(Color("mode").opacity(0.5))
Spacer()
Button("") {
if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility") {
NSWorkspace.shared.open(url)
}
}
.buttonStyle(SimpleButtonStyle(icon: "folder", help: "View accessibility permissions pane"))
}
.padding(5)
.padding(.leading)
Spacer()
HStack(spacing: 0) {
Image(systemName: "gearshape.2")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(autoStatus ? .green : .red)
.saturation(displayMode.colorScheme == .dark ? 0.8 : 1)
Text(autoStatus ? "Automation permission granted" : "Automation permission not granted")
.font(.callout)
.foregroundStyle(Color("mode").opacity(0.5))
Spacer()
Button("") {
if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Automation") {
NSWorkspace.shared.open(url)
}
}
.buttonStyle(SimpleButtonStyle(icon: "folder", help: "View automation permissions pane"))
}
.padding(5)
.padding(.leading)
// HStack(spacing: 0) {
// Image(systemName: "externaldrive")
// .resizable()
// .scaledToFit()
// .frame(width: 20, height: 20)
// .padding(.trailing)
// .foregroundStyle(diskStatus ? .green : .red)
// .saturation(displayMode.colorScheme == .dark ? 0.8 : 1)
// Text(diskStatus ? "Full Disk permission granted" : "Full Disk permission not granted")
// .font(.callout)
// .foregroundStyle(.primary.opacity(0.5))
//
// Spacer()
//
// Button("") {
// if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles") {
// NSWorkspace.shared.open(url)
// }
// }
// .buttonStyle(SimpleButtonStyle(icon: "folder", help: "View disk permissions pane"))
//
// }
// .padding(5)
// .padding(.leading)
//
//
//
// HStack(spacing: 0) {
// Image(systemName: isVersionOrHigher(version: 14) ? "accessibility" : "figure.arms.open")
// .resizable()
// .scaledToFit()
// .frame(width: 20, height: 20)
// .padding(.trailing)
// .foregroundStyle(accessStatus ? .green : .red)
// .saturation(displayMode.colorScheme == .dark ? 0.8 : 1)
// Text(accessStatus ? "Accessibility permission granted" : "Accessibility permission not granted")
// .font(.callout)
// .foregroundStyle(.primary.opacity(0.5))
//
// Spacer()
//
// Button("") {
// if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility") {
// NSWorkspace.shared.open(url)
// }
// }
// .buttonStyle(SimpleButtonStyle(icon: "folder", help: "View accessibility permissions pane"))
//
// }
// .padding(5)
// .padding(.leading)
//
//
// HStack(spacing: 0) {
// Image(systemName: "gearshape.2")
// .resizable()
// .scaledToFit()
// .frame(width: 20, height: 20)
// .padding(.trailing)
// .foregroundStyle(autoStatus ? .green : .red)
// .saturation(displayMode.colorScheme == .dark ? 0.8 : 1)
// Text(autoStatus ? "Automation permission granted" : "Automation permission not granted")
// .font(.callout)
// .foregroundStyle(.primary.opacity(0.5))
//
// Spacer()
//
// Button("") {
// if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Automation") {
// NSWorkspace.shared.open(url)
// }
// }
// .buttonStyle(SimpleButtonStyle(icon: "folder", help: "View automation permissions pane"))
//
// }
// .padding(5)
// .padding(.leading)
@@ -291,7 +286,7 @@ struct GeneralSettingsTab: View {
.saturation(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(Color("mode").opacity(0.5))
.foregroundStyle(.primary.opacity(0.5))
Spacer()
Toggle(isOn: $sentinel, label: {
@@ -332,7 +327,7 @@ struct GeneralSettingsTab: View {
.saturation(displayMode.colorScheme == .dark ? 0.8 : 1)
Text(appState.finderExtensionEnabled ? "Context menu extension for Finder is enabled" : "Context menu extension for Finder is disabled")
.font(.callout)
.foregroundStyle(Color("mode").opacity(0.5))
.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()
@@ -362,33 +357,14 @@ struct GeneralSettingsTab: View {
}
.padding(.vertical, 5)
Spacer()
}
.onAppear {
checkAllPermissions(appState: appState) { results in
updateOnMain {
appState.permissionResults = results
}
diskStatus = results.fullDiskAccess
accessStatus = results.accessibility
autoStatus = results.automation
if results.allPermissionsGranted {
updateOnMain {
appState.permissionsOkay = true
}
}
}
appState.updateExtensionStatus()
}
}
.padding(20)
.frame(width: 500, height: 710)
.frame(width: 500)//, height: 710)
}
+222 -200
View File
@@ -9,24 +9,25 @@
import Foundation
import SwiftUI
import ServiceManagement
import AlinFoundation
struct InterfaceSettingsTab: View {
@EnvironmentObject var appState: AppState
@EnvironmentObject var locations: Locations
@EnvironmentObject var fsm: FolderSettingsManager
@EnvironmentObject var themeSettings: ThemeSettings
@EnvironmentObject var themeManager: ThemeManager
@State private var windowSettings = WindowSettings()
@Environment(\.colorScheme) var colorScheme: ColorScheme
@AppStorage("settings.menubar.enabled") private var menubarEnabled: Bool = false
@AppStorage("settings.general.mini") private var mini: Bool = false
@AppStorage("displayMode") var displayMode: DisplayMode = .system
@AppStorage("settings.general.selectedTab") private var selectedTab: CurrentTabView = .general
// @AppStorage("displayMode") var displayMode: DisplayMode = .system
// @AppStorage("settings.general.selectedTab") private var selectedTab: CurrentTabView = .general
@AppStorage("settings.general.glass") private var glass: Bool = false
@AppStorage("settings.general.dark") var isDark: Bool = true
@AppStorage("settings.interface.themesEnabled") var themesEnabled: Bool = false
// @AppStorage("settings.interface.themesEnabled") var themesEnabled: Bool = false
@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.general.selectedTheme") var selectedTheme: String = "Auto"
@AppStorage("settings.interface.selectedMenubarIcon") var selectedMenubarIcon: String = "pear-4"
@State private var isLaunchAtLoginEnabled: Bool = false
@Binding var showPopover: Bool
@@ -51,11 +52,11 @@ struct InterfaceSettingsTab: View {
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(Color("mode").opacity(0.5))
.foregroundStyle(.primary.opacity(0.5))
VStack(alignment: .leading, spacing: 5) {
Text("\(glass ? "Transparent material enabled" : "Transparent material disabled")")
.font(.callout)
.foregroundStyle(Color("mode").opacity(0.5))
.foregroundStyle(.primary.opacity(0.5))
}
Spacer()
Toggle(isOn: $glass, label: {
@@ -69,191 +70,213 @@ struct InterfaceSettingsTab: View {
.padding(.leading)
HStack(spacing: 0) {
Image(systemName: themesEnabled ? "paintpalette.fill" : "paintpalette")
Image(systemName: "paintbrush")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(Color("mode").opacity(0.5))
.foregroundStyle(.primary.opacity(0.5))
VStack(alignment: .leading, spacing: 5) {
Text("\(themesEnabled ? "Custom theming is enabled" : "Custom theming is disabled")")
Text("Theme Mode")
.font(.callout)
.foregroundStyle(Color("mode").opacity(0.5))
.foregroundStyle(.primary.opacity(0.5))
}
Spacer()
Toggle(isOn: $themesEnabled, label: {
})
.toggleStyle(.switch)
.onChange(of: themesEnabled) { newVal in
themeSettings.resetToDefault(dark: isDarkMode())
if isDarkMode() {
displayMode.colorScheme = .dark
} else {
displayMode.colorScheme = .light
ThemeSettingsView(opacity: 1)
.onChange(of: themeManager.displayMode) { _ in
MenuBarExtraManager.shared.restartMenuBarExtra()
}
if newVal {
selectedTheme = isDarkMode() ? "Dark" : "Light"
} else {
selectedTheme = "Auto"
}
}
}
.padding(5)
.padding(.leading)
/// Show theme color selector if theming is enabled
if themesEnabled {
HStack(spacing: 0) {
Image(systemName: "paintbrush")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(Color("mode").opacity(0.5))
VStack(alignment: .leading, spacing: 5) {
Text("Application theme color")
.font(.callout)
.foregroundStyle(Color("mode").opacity(0.5))
}
Spacer()
HStack(spacing: 10) {
Button("") {
themeSettings.setPreset(preset: "pearcleaner", colorScheme: displayMode)
}
.buttonStyle(PresetColor(fillColor: themeSettings.getColorForPreset(preset: "pearcleaner", colorScheme: displayMode), label: "Pearcleaner"))
Button("") {
themeSettings.setPreset(preset: "dracula", colorScheme: displayMode)
}
.buttonStyle(PresetColor(fillColor: themeSettings.getColorForPreset(preset: "dracula", colorScheme: displayMode), label: "Dracula"))
Button("") {
themeSettings.setPreset(preset: "solarized", colorScheme: displayMode)
}
.buttonStyle(PresetColor(fillColor: themeSettings.getColorForPreset(preset: "solarized", colorScheme: displayMode), label: "Solarized"))
Button("") {
themeSettings.setPreset(preset: "macOS", colorScheme: displayMode)
}
.buttonStyle(PresetColor(fillColor: themeSettings.getColorForPreset(preset: "macOS", colorScheme: displayMode), label: "macOS"))
}
// HStack(spacing: 0) {
// Image(systemName: themesEnabled ? "paintpalette.fill" : "paintpalette")
// .resizable()
// .scaledToFit()
// .frame(width: 20, height: 20)
// .padding(.trailing)
// .foregroundStyle(.primary.opacity(0.5))
// VStack(alignment: .leading, spacing: 5) {
// Text("\(themesEnabled ? "Custom theming is enabled" : "Custom theming is disabled")")
// .font(.callout)
// .foregroundStyle(.primary.opacity(0.5))
// }
// Spacer()
// Toggle(isOn: $themesEnabled, label: {
// })
// .toggleStyle(.switch)
// .onChange(of: themesEnabled) { newVal in
// themeManager.resetToDefault(dark: isDarkMode())
// if isDarkMode() {
// displayMode.colorScheme = .dark
// } else {
// displayMode.colorScheme = .light
// }
// if newVal {
// selectedTheme = isDarkMode() ? "Dark" : "Light"
// } else {
// selectedTheme = "Auto"
// }
// }
// }
// .padding(5)
// .padding(.leading)
//
//
// /// Show theme color selector if theming is enabled
// if themesEnabled {
// 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("Application theme color")
// .font(.callout)
// .foregroundStyle(.primary.opacity(0.5))
// }
//
// Spacer()
//
//
// HStack(spacing: 10) {
// Button("") {
// themeManager.setPreset(preset: "pearcleaner", colorScheme: displayMode)
// }
//// .buttonStyle(PresetColor(fillColor: themeManager.getColorForPreset(preset: "pearcleaner", colorScheme: displayMode), label: "Pearcleaner"))
//
// Button("") {
// themeManager.setPreset(preset: "dracula", colorScheme: displayMode)
// }
//// .buttonStyle(PresetColor(fillColor: themeManager.getColorForPreset(preset: "dracula", colorScheme: displayMode), label: "Dracula"))
//
// Button("") {
// themeManager.setPreset(preset: "solarized", colorScheme: displayMode)
// }
//// .buttonStyle(PresetColor(fillColor: themeManager.getColorForPreset(preset: "solarized", colorScheme: displayMode), label: "Solarized"))
//
// Button("") {
// themeManager.setPreset(preset: "macOS", colorScheme: displayMode)
// }
//// .buttonStyle(PresetColor(fillColor: themeManager.getColorForPreset(preset: "macOS", colorScheme: displayMode), label: "macOS"))
// }
//
//
//
//
// Spacer()
//
// ColorPicker("", selection: $themeManager.themeColor, supportsOpacity: false)
// .onChange(of: themeManager.themeColor) { newValue in
// themeManager.saveThemeColor()
// }
// .padding(.horizontal, 5)
//
// Button("") {
// themeManager.resetToDefault(dark: isDarkMode())
// if isDarkMode() {
// displayMode.colorScheme = .dark
// } else {
// displayMode.colorScheme = .light
// }
// selectedTheme = isDarkMode() ? "Dark" : "Light"
// }
// .buttonStyle(SimpleButtonStyle(icon: "arrow.uturn.left.circle", help: "Reset color to default"))
//
// }
// .padding(5)
// .padding(.leading)
// }
Spacer()
ColorPicker("", selection: $themeSettings.themeColor, supportsOpacity: false)
.onChange(of: themeSettings.themeColor) { newValue in
themeSettings.saveThemeColor()
}
.padding(.horizontal, 5)
Button("") {
themeSettings.resetToDefault(dark: isDarkMode())
if isDarkMode() {
displayMode.colorScheme = .dark
} else {
displayMode.colorScheme = .light
}
selectedTheme = isDarkMode() ? "Dark" : "Light"
}
.buttonStyle(SimpleButtonStyle(icon: "arrow.uturn.left.circle", help: "Reset color to default"))
}
.padding(5)
.padding(.leading)
}
HStack(spacing: 0) {
Image(systemName: {
switch displayMode {
case .dark:
return "moon.fill"
case .light:
return "sun.max.fill"
case .system:
return "circle.righthalf.filled"
}
}())
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(Color("mode").opacity(0.5))
VStack(alignment: .leading, spacing: 5) {
Text(themesEnabled ? "Application font color" : "Application color mode")
.font(.callout)
.foregroundStyle(Color("mode").opacity(0.5))
}
InfoButton(text: "When custom theming is disabled, you can set the application color mode to follow the operating system using Auto. Or manually set it to Light or Dark. When custom theming is enabled, this selector acts mainly as a font color picker between light or dark to match your custom theme color.")
Spacer()
Picker("", selection: $selectedTheme) {
if !themesEnabled {
Text("Auto")
.tag("Auto")
}
Text(themesEnabled ? "Light" : "Dark")
.tag("Dark")
Text(themesEnabled ? "Dark" : "Light")
.tag("Light")
}
.pickerStyle(themeSettings: themeSettings)
.onChange(of: selectedTheme) { newTheme in
switch newTheme {
case "Auto":
if isDarkMode() {
displayMode.colorScheme = .dark
} else {
displayMode.colorScheme = .light
}
if !themesEnabled {
themeSettings.resetToDefault(dark: isDarkMode())
}
// Refresh foreground colors
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.selectedTab = .interface
}
if menubarEnabled{
MenuBarExtraManager.shared.restartMenuBarExtra()
}
case "Dark":
displayMode.colorScheme = .dark
if !themesEnabled {
themeSettings.resetToDefault(dark: true)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.selectedTab = .interface
}
if menubarEnabled{
MenuBarExtraManager.shared.restartMenuBarExtra()
}
case "Light":
displayMode.colorScheme = .light
if !themesEnabled {
themeSettings.resetToDefault(dark: false)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.selectedTab = .interface
}
if menubarEnabled{
MenuBarExtraManager.shared.restartMenuBarExtra()
}
default:
break
}
}
}
.padding(5)
.padding(.leading)
// HStack(spacing: 0) {
// Image(systemName: {
// switch displayMode {
// case .dark:
// return "moon.fill"
// case .light:
// return "sun.max.fill"
// case .system:
// return "circle.righthalf.filled"
// }
// }())
// .resizable()
// .scaledToFit()
// .frame(width: 20, height: 20)
// .padding(.trailing)
// .foregroundStyle(.primary.opacity(0.5))
// VStack(alignment: .leading, spacing: 5) {
// Text(themesEnabled ? "Application font color" : "Application color mode")
// .font(.callout)
// .foregroundStyle(.primary.opacity(0.5))
// }
// InfoButton(text: "When custom theming is disabled, you can set the application color mode to follow the operating system using Auto. Or manually set it to Light or Dark. When custom theming is enabled, this selector acts mainly as a font color picker between light or dark to match your custom theme color.")
// Spacer()
// Picker("", selection: $selectedTheme) {
// if !themesEnabled {
// Text("Auto")
// .tag("Auto")
// }
// Text(themesEnabled ? "Light" : "Dark")
// .tag("Dark")
// Text(themesEnabled ? "Dark" : "Light")
// .tag("Light")
// }
// .buttonStyle(.borderless)
// .onChange(of: selectedTheme) { newTheme in
// switch newTheme {
// case "Auto":
// if isDarkMode() {
// displayMode.colorScheme = .dark
// } else {
// displayMode.colorScheme = .light
// }
// if !themesEnabled {
// themeManager.resetToDefault(dark: isDarkMode())
// }
// // Refresh foreground colors
// DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
// self.selectedTab = .interface
// }
// if menubarEnabled{
// MenuBarExtraManager.shared.restartMenuBarExtra()
// }
// case "Dark":
// displayMode.colorScheme = .dark
// if !themesEnabled {
// themeManager.resetToDefault(dark: true)
// }
// DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
// self.selectedTab = .interface
// }
// if menubarEnabled{
// MenuBarExtraManager.shared.restartMenuBarExtra()
// }
// case "Light":
// displayMode.colorScheme = .light
// if !themesEnabled {
// themeManager.resetToDefault(dark: false)
// }
// DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
// self.selectedTab = .interface
// }
// if menubarEnabled{
// MenuBarExtraManager.shared.restartMenuBarExtra()
// }
// default:
// break
// }
// }
// }
// .padding(5)
// .padding(.leading)
@@ -271,16 +294,16 @@ struct InterfaceSettingsTab: View {
.padding(.leading)
HStack(spacing: 0) {
Image(systemName: isMacOS14OrHigher() ? "square.resize.up" : "macwindow.on.rectangle")
Image(systemName: isVersionOrHigher(version: 14) ? "square.resize.up" : "macwindow.on.rectangle")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(Color("mode").opacity(0.5))
.foregroundStyle(.primary.opacity(0.5))
VStack(alignment: .leading, spacing: 5) {
Text("\(mini ? "Mini window mode" : "Full size window mode")")
.font(.callout)
.foregroundStyle(Color("mode").opacity(0.5))
.foregroundStyle(.primary.opacity(0.5))
}
Spacer()
Toggle(isOn: $mini, label: {
@@ -297,8 +320,8 @@ struct InterfaceSettingsTab: View {
.environmentObject(locations)
.environmentObject(appState)
.environmentObject(fsm)
.environmentObject(ThemeSettings.shared)
.preferredColorScheme(displayMode.colorScheme)
.environmentObject(themeManager)
.preferredColorScheme(themeManager.displayMode.colorScheme)
}) {
resizeWindowAuto(windowSettings: windowSettings, title: "Pearcleaner")
}
@@ -313,8 +336,8 @@ struct InterfaceSettingsTab: View {
.environmentObject(locations)
.environmentObject(appState)
.environmentObject(fsm)
.environmentObject(ThemeSettings.shared)
.preferredColorScheme(displayMode.colorScheme)
.environmentObject(themeManager)
.preferredColorScheme(themeManager.displayMode.colorScheme)
}
) {
resizeWindowAuto(windowSettings: windowSettings, title: "Pearcleaner")
@@ -334,11 +357,11 @@ struct InterfaceSettingsTab: View {
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(Color("mode").opacity(0.5))
.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(Color("mode").opacity(0.5))
.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.")
@@ -361,11 +384,11 @@ struct InterfaceSettingsTab: View {
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(Color("mode").opacity(0.5))
.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(Color("mode").opacity(0.5))
.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.")
@@ -397,11 +420,11 @@ struct InterfaceSettingsTab: View {
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(Color("mode").opacity(0.5))
.foregroundStyle(.primary.opacity(0.5))
VStack(alignment: .leading, spacing: 5) {
Text("\(menubarEnabled ? "Menubar icon enabled" : "Menubar icon disabled")")
.font(.callout)
.foregroundStyle(Color("mode").opacity(0.5))
.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()
@@ -415,8 +438,8 @@ struct InterfaceSettingsTab: View {
.environmentObject(locations)
.environmentObject(appState)
.environmentObject(fsm)
.environmentObject(ThemeSettings.shared)
.preferredColorScheme(displayMode.colorScheme)
.environmentObject(themeManager)
.preferredColorScheme(themeManager.displayMode.colorScheme)
}, icon: selectedMenubarIcon)
NSApplication.shared.setActivationPolicy(.accessory)
findAndHideWindows(named: ["Pearcleaner"])
@@ -431,8 +454,8 @@ struct InterfaceSettingsTab: View {
.environmentObject(locations)
.environmentObject(appState)
.environmentObject(fsm)
.environmentObject(ThemeSettings.shared)
.preferredColorScheme(displayMode.colorScheme)
.environmentObject(themeManager)
.preferredColorScheme(themeManager.displayMode.colorScheme)
}) {
resizeWindowAuto(windowSettings: windowSettings, title: "Pearcleaner")
}
@@ -442,8 +465,8 @@ struct InterfaceSettingsTab: View {
.environmentObject(locations)
.environmentObject(appState)
.environmentObject(fsm)
.environmentObject(ThemeSettings.shared)
.preferredColorScheme(displayMode.colorScheme)
.environmentObject(themeManager)
.preferredColorScheme(themeManager.displayMode.colorScheme)
}) {
resizeWindowAuto(windowSettings: windowSettings, title: "Pearcleaner")
}
@@ -464,11 +487,11 @@ struct InterfaceSettingsTab: View {
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(Color("mode").opacity(0.5))
.foregroundStyle(.primary.opacity(0.5))
VStack(alignment: .leading, spacing: 5) {
Text("\(isLaunchAtLoginEnabled ? "Launch at login enabled" : "Launch at login disabled")")
.font(.callout)
.foregroundStyle(Color("mode").opacity(0.5))
.foregroundStyle(.primary.opacity(0.5))
}
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()
@@ -505,11 +528,11 @@ struct InterfaceSettingsTab: View {
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(Color("mode").opacity(0.5))
.foregroundStyle(.primary.opacity(0.5))
VStack(alignment: .leading, spacing: 5) {
Text("Menubar icon")
.font(.callout)
.foregroundStyle(Color("mode").opacity(0.5))
.foregroundStyle(.primary.opacity(0.5))
}
Spacer()
Picker("", selection: $selectedMenubarIcon) {
@@ -533,19 +556,18 @@ struct InterfaceSettingsTab: View {
.onChange(of: selectedMenubarIcon) { newValue in
MenuBarExtraManager.shared.swapMenuBarIcon(icon: newValue)
}
.pickerStyle(themeSettings: themeSettings)
.buttonStyle(.borderless)
}
.padding(5)
.padding(.leading)
Spacer()
// Spacer()
}
}
.padding(20)
.frame(width: 500, height: themesEnabled ? 600 : 580)
.frame(width: 500)//, height: themesEnabled ? 600 : 580)
}
+5 -2
View File
@@ -6,11 +6,13 @@
//
import SwiftUI
import AlinFoundation
struct SettingsView: View {
@EnvironmentObject var appState: AppState
@EnvironmentObject var fsm: FolderSettingsManager
@EnvironmentObject var themeSettings: ThemeSettings
@EnvironmentObject var themeManager: ThemeManager
@EnvironmentObject var updater: Updater
@Binding var showPopover: Bool
@Binding var search: String
@AppStorage("settings.general.glass") private var glass: Bool = false
@@ -42,6 +44,7 @@ struct SettingsView: View {
Label(CurrentTabView.update.title, systemImage: "cloud")
}
.tag(CurrentTabView.update)
.environmentObject(themeManager)
TipsSettingsTab()
.tabItem {
@@ -55,7 +58,7 @@ struct SettingsView: View {
}
.tag(CurrentTabView.about)
}
.background(backgroundView(themeSettings: themeSettings, glass: glass))
.background(backgroundView(themeManager: themeManager, glass: glass))
}
+5 -4
View File
@@ -8,10 +8,11 @@
import SwiftUI
import Foundation
import AlinFoundation
struct TipsSettingsTab: View {
@EnvironmentObject var appState: AppState
@EnvironmentObject var themeSettings: ThemeSettings
@EnvironmentObject var themeManager: ThemeManager
var body: some View {
VStack {
@@ -37,16 +38,16 @@ struct TipsSettingsTab: View {
.padding()
}
.scrollIndicators(.visible)
.frame(minHeight: 0, maxHeight: .infinity)
.frame(minHeight: 0, maxHeight: 500)
.frame(minWidth: 0, maxWidth: .infinity)
.background(backgroundView(themeSettings: themeSettings, darker: true))
.background(backgroundView(themeManager: themeManager, darker: true))
.clipShape(RoundedRectangle(cornerRadius: 8))
.padding(.bottom)
}
.padding(20)
.frame(width: 500, height: 520)
.frame(width: 500)//, height: 520)
}
}
+16 -66
View File
@@ -8,82 +8,34 @@
import SwiftUI
import Foundation
import AlinFoundation
struct UpdateSettingsTab: View {
@EnvironmentObject var appState: AppState
@EnvironmentObject var themeSettings: ThemeSettings
@EnvironmentObject var themeManager: ThemeManager
@EnvironmentObject var updater: Updater
@State private var showAlert = false
@State private var showDone = false
@AppStorage("settings.updater.nextUpdateDate") private var nextUpdateDate = Date.now.timeIntervalSinceReferenceDate
@AppStorage("settings.updater.updateFrequency") private var updateFrequency: UpdateFrequency = .daily
var body: some View {
VStack {
VStack {
HStack(spacing: 0) {
Text("Pearcleaner will check for updates")
.font(.callout)
.foregroundStyle(Color("mode").opacity(0.5))
Spacer()
Picker("", selection: $updateFrequency) {
ForEach(UpdateFrequency.allCases, id: \.self) { frequency in
Text(frequency.rawValue).tag(frequency)
}
}
.onChange(of: updateFrequency) { frequency in
frequency.updateNextUpdateDate()
}
.pickerStyle(themeSettings: themeSettings)
}
if updateFrequency != .none {
HStack {
Text("Next update check: \(formattedDate(Date(timeIntervalSinceReferenceDate: nextUpdateDate)))")
.font(.footnote)
.foregroundStyle(Color("mode").opacity(0.3))
Spacer()
}
}
}
FrequencyView(updater: updater)
.padding(5)
.padding(.horizontal)
ScrollView {
VStack() {
ForEach(appState.releases, id: \.id) { release in
VStack(alignment: .leading) {
LabeledDivider(label: "\(release.tag_name)")
Text(release.modifiedBody)
}
}
}
.padding()
}
.frame(minHeight: 0, maxHeight: .infinity)
.frame(minWidth: 0, maxWidth: .infinity)
// .background(Color("mode").opacity(0.05))
.background(backgroundView(themeSettings: themeSettings, darker: true))
.clipShape(RoundedRectangle(cornerRadius: 8))
.padding(.bottom)
Text("Showing last 3 releases")
.font(.callout)
.foregroundStyle(Color("mode").opacity(0.5))
ReleasesView(updater: updater)
.frame(maxWidth: .infinity, maxHeight: 400)
.backgroundAF(opacity: 0.5)
HStack(alignment: .center, spacing: 20) {
Spacer()
Button(""){
loadGithubReleases(appState: appState)
updater.checkForUpdates(showSheet: false)
}
.buttonStyle(SimpleButtonStyle(icon: "arrow.uturn.left.circle", help: "Reload release notes"))
.buttonStyle(SimpleButtonStyle(icon: "arrow.uturn.left.circle", help: "Refresh updater"))
Spacer()
@@ -94,12 +46,12 @@ struct UpdateSettingsTab: View {
Spacer()
Button(""){
loadGithubReleases(appState: appState, manual: true)
}
.buttonStyle(SimpleButtonStyle(icon: "arrow.down.square", help: "Check for updates"))
Spacer()
// Button(""){
// updater.checkForUpdates(showSheet: true)
// }
// .buttonStyle(SimpleButtonStyle(icon: "arrow.down.square", help: "Check for updates"))
//
// Spacer()
Button(""){
NSWorkspace.shared.open(URL(string: "https://github.com/alienator88/Pearcleaner/releases")!)
@@ -110,11 +62,9 @@ struct UpdateSettingsTab: View {
}
.padding()
}
.padding(20)
.frame(width: 500, height: 520)
.frame(width: 500)//, height: 520)
}
}
+9 -8
View File
@@ -7,10 +7,11 @@
import Foundation
import SwiftUI
import AlinFoundation
struct AppListItems: View {
@EnvironmentObject var appState: AppState
@EnvironmentObject var themeSettings: ThemeSettings
@EnvironmentObject var themeManager: ThemeManager
@Binding var search: String
@State private var isHovered = false
@State private var windowSettings = WindowSettings()
@@ -60,21 +61,21 @@ struct AppListItems: View {
if appInfo.webApp {
Text("web")
.font(.footnote)
.foregroundStyle(Color("mode").opacity(0.3))
.foregroundStyle(.primary.opacity(0.3))
.frame(minWidth: 30, minHeight: 15)
.padding(2)
.background(
Capsule().strokeBorder(Color("mode").opacity(0.3), lineWidth: 1)
Capsule().strokeBorder(.primary.opacity(0.3), lineWidth: 1)
)
}
if appInfo.wrapped {
Text("iOS")
.font(.footnote)
.foregroundStyle(Color("mode").opacity(0.3))
.foregroundStyle(.primary.opacity(0.3))
.frame(minWidth: 30, minHeight: 15)
.padding(2)
.background(
Capsule().strokeBorder(Color("mode").opacity(0.3), lineWidth: 1)
Capsule().strokeBorder(.primary.opacity(0.3), lineWidth: 1)
)
}
@@ -83,7 +84,7 @@ struct AppListItems: View {
} else {
Text("\(isHovered ? "v\(appInfo.appVersion)" : formatByte(size: bundleSize).human)")
.font(.system(size: (isHovered || isSelected) ? 12 : 10))
.foregroundStyle(Color("mode").opacity(0.5))
.foregroundStyle(.primary.opacity(0.5))
}
}
@@ -112,14 +113,14 @@ struct AppListItems: View {
}
.background{
Rectangle()
.fill(isSelected && !glass ? themeSettings.themeColor : .clear)
.fill(isSelected && !glass ? themeManager.pickerColor : .clear)
}
.overlay{
if (isHovered || isSelected) {
HStack {
Spacer()
RoundedRectangle(cornerRadius: 50)
.fill(isSelected ? Color("pear") : Color("mode").opacity(0.5))
.fill(isSelected ? Color("pear") : .primary.opacity(0.5))
.frame(width: isSelected ? 4 : 2, height: 25)
.padding(.trailing, 5)
}
+27 -11
View File
@@ -6,12 +6,15 @@
//
import SwiftUI
import AlinFoundation
struct AppSearchView: View {
@EnvironmentObject var appState: AppState
@EnvironmentObject var themeSettings: ThemeSettings
@EnvironmentObject var themeManager: ThemeManager
@EnvironmentObject var locations: Locations
@EnvironmentObject var fsm: FolderSettingsManager
@EnvironmentObject var updater: Updater
@EnvironmentObject var permissionManager: PermissionManager
var glass: Bool
var sidebarWidth: Double
var menubarEnabled: Bool
@@ -21,6 +24,7 @@ struct AppSearchView: View {
@State private var showMenu = false
@State private var showSys: Bool = true
@State private var showUsr: Bool = true
@State private var showFeatures: Bool = false
@Binding var isMenuBar: Bool
@AppStorage("settings.general.selectedSortAppsList") var selectedSortAlpha: Bool = true
@State private var progress: Double = 0.0
@@ -32,14 +36,25 @@ struct AppSearchView: View {
.frame(height: 10)
.padding(.top, !isMenuBar ? 25 : 0)
if appState.updateAvailable {
UpdateNotificationView(appState: appState)
} else if !appState.permissionsOkay {
PermissionsNotificationView(appState: appState)
if updater.updateAvailable {
UpdateButton(updater: updater, dark: false, opacity: 1)
.padding(.horizontal)
} else if let _ = permissionManager.results, !permissionManager.allPermissionsGranted {
PermissionsView(dark: false, opacity: 1)
.padding(.horizontal)
} else if appState.featureAvailable {
FeatureNotificationView(appState: appState)
AlertNotification(label: "New Message!", icon: "star", buttonAction: {
showFeatures.toggle()
}, btnColor: Color.blue, opacity: 1, themeManager: themeManager)
.padding(.horizontal)
.sheet(isPresented: $showFeatures, content: {
FeatureView()
.environmentObject(themeManager)
})
}
AppsListView(search: $search, showPopover: $showPopover, filteredApps: filteredApps)
Divider()
@@ -137,7 +152,7 @@ struct AppSearchView: View {
}
.padding()
.background(backgroundView(themeSettings: themeSettings, glass: glass).padding(-80))
.background(backgroundView(themeManager: themeManager, glass: glass).padding(-80))
}
}
@@ -148,6 +163,7 @@ struct AppSearchView: View {
.padding(.horizontal, search.isEmpty ? 10 : 5)
.padding(.vertical, 5)
}
}
private var filteredApps: [AppInfo] {
@@ -181,7 +197,7 @@ struct SimpleSearchStyle: TextFieldStyle {
@State var padding: CGFloat = 5
@State var sidebar: Bool = true
@EnvironmentObject var appState: AppState
@EnvironmentObject var themeSettings: ThemeSettings
@EnvironmentObject var themeManager: ThemeManager
@AppStorage("settings.general.mini") private var mini: Bool = false
@AppStorage("settings.menubar.enabled") private var menubarEnabled: Bool = false
@@ -189,7 +205,7 @@ struct SimpleSearchStyle: TextFieldStyle {
ZStack {
RoundedRectangle(cornerRadius: 8)
.fill(darker ? themeSettings.themeColor.darker(by: 5) : themeSettings.themeColor)
.fill(darker ? themeManager.pickerColor.adjustBrightness(5) : themeManager.pickerColor)
.allowsHitTesting(false)
.frame(height: 30)
.opacity((glass && (sidebar || !mini && !menubarEnabled)) || mini || menubarEnabled ? 0.0 : 1.0)
@@ -201,7 +217,7 @@ struct SimpleSearchStyle: TextFieldStyle {
Spacer()
Text(isFocused ? "Type to search" : "Click to search")
.font(.subheadline)
.foregroundColor(Color("mode").opacity(0.2))
.foregroundColor(.primary.opacity(0.2))
Spacer()
}
}
@@ -243,7 +259,7 @@ struct SimpleSearchStyle: TextFieldStyle {
extension NSTextView {
open override var frame: CGRect {
didSet {
insertionPointColor = NSColor(Color("mode").opacity(0.2))//.clear
insertionPointColor = NSColor(.primary.opacity(0.2))//.clear
}
}
}
+2 -2
View File
@@ -73,13 +73,13 @@ struct Header: View {
var body: some View {
HStack {
Text("\(title)").foregroundStyle(Color("mode")).opacity(0.5)
Text("\(title)").foregroundStyle(.primary).opacity(0.5)
Text("\(count)")
.font(.system(size: 10))
.monospacedDigit()
.frame(minWidth: count > 99 ? 30 : 24, minHeight: 17)
.background(Color("mode").opacity(0.1))
.background(.primary.opacity(0.1))
.clipShape(.capsule)
.padding(.leading, 2)
@@ -7,6 +7,7 @@
import Foundation
import SwiftUI
import AlinFoundation
struct ConditionBuilderView: View {
@Binding var showAlert:Bool
+15 -14
View File
@@ -7,6 +7,7 @@
import Foundation
import SwiftUI
import AlinFoundation
struct FilesView: View {
@EnvironmentObject var appState: AppState
@@ -51,7 +52,7 @@ struct FilesView: View {
VStack {
Spacer()
Text("Searching the file system").font(.title3)
.foregroundStyle((Color("mode").opacity(0.5)))
.foregroundStyle((.primary.opacity(0.5)))
ProgressView()
.progressViewStyle(.linear)
@@ -59,7 +60,7 @@ struct FilesView: View {
Text("\(elapsedTime)")
.font(.title).monospacedDigit()
.foregroundStyle((Color("mode").opacity(0.5)))
.foregroundStyle((.primary.opacity(0.5)))
.opacity(elapsedTime == 0 ? 0 : 1)
.contentTransition(.numericText())
@@ -133,7 +134,7 @@ struct FilesView: View {
Text("\(appState.appInfo.bundleIdentifier)")
.lineLimit(1)
.font(.title3)
.foregroundStyle((Color("mode").opacity(0.5)))
.foregroundStyle((.primary.opacity(0.5)))
}
.padding(.leading)
@@ -143,7 +144,7 @@ struct FilesView: View {
Text("\(displaySizeTotal)").font(.title).fontWeight(.bold).help("Total size on disk")
Text("\(appState.appInfo.fileSize.count == 1 ? "\(appState.selectedItems.count) / \(appState.appInfo.fileSize.count) item" : "\(appState.selectedItems.count) / \(appState.appInfo.fileSize.count) items")")
.font(.callout).foregroundStyle((Color("mode").opacity(0.5)))
.font(.callout).foregroundStyle((.primary.opacity(0.5)))
}
}
@@ -177,11 +178,11 @@ struct FilesView: View {
if appState.appInfo.webApp {
Text("web")
.font(.footnote)
.foregroundStyle(Color("mode").opacity(0.5))
.foregroundStyle(.primary.opacity(0.5))
.frame(minWidth: 30, minHeight: 15)
.padding(2)
.padding(.horizontal, 2)
.background(Color("mode").opacity(0.1))
.background(.primary.opacity(0.1))
.clipShape(.capsule)
.help("This is a web app")
@@ -190,11 +191,11 @@ struct FilesView: View {
if appState.appInfo.wrapped {
Text("iOS")
.font(.footnote)
.foregroundStyle(Color("mode").opacity(0.5))
.foregroundStyle(.primary.opacity(0.5))
.frame(minWidth: 30, minHeight: 15)
.padding(2)
.padding(.horizontal, 2)
.background(Color("mode").opacity(0.1))
.background(.primary.opacity(0.1))
.clipShape(.capsule)
.help("This is a wrapped iOS app")
@@ -203,11 +204,11 @@ struct FilesView: View {
if appState.appInfo.arch != .empty {
Text("\(appState.appInfo.arch)")
.font(.footnote)
.foregroundStyle(Color("mode").opacity(0.5))
.foregroundStyle(.primary.opacity(0.5))
.frame(minWidth: 30, minHeight: 15)
.padding(2)
.padding(.horizontal, 2)
.background(Color("mode").opacity(0.1))
.background(.primary.opacity(0.1))
.clipShape(.capsule)
.help("This bundle's architecture is \(appState.appInfo.arch)")
@@ -215,11 +216,11 @@ struct FilesView: View {
Text(appState.appInfo.system ? "system" : "user")
.font(.footnote)
.foregroundStyle(Color("mode").opacity(0.5))
.foregroundStyle(.primary.opacity(0.5))
.frame(minWidth: 30, minHeight: 15)
.padding(2)
.padding(.horizontal, 2)
.background(Color("mode").opacity(0.1))
.background(.primary.opacity(0.1))
.clipShape(.capsule)
.help("This app is located in \(appState.appInfo.system ? "/Applications" : "\(home)")")
}
@@ -293,7 +294,7 @@ struct FilesView: View {
Spacer()
InfoButton(text: "Always double-check the files/folders marked for removal. In some rare cases, Pearcleaner may find some unrelated files when app names are too similar.", color: Color("mode").opacity(0.5), warning: true, edge: .top)
InfoButton(text: "Always double-check the files/folders marked for removal. In some rare cases, Pearcleaner may find some unrelated files when app names are too similar.", color: .primary.opacity(0.5), warning: true, edge: .top)
.padding(.top)
@@ -470,7 +471,7 @@ struct FileDetailsItem: View {
VStack {
Spacer()
RoundedRectangle(cornerRadius: 10)
.fill(Color("mode").opacity(0.5))
.fill(.primary.opacity(0.5))
.frame(height: 1.5)
.offset(y: 3)
}
+7 -6
View File
@@ -8,10 +8,11 @@
import Foundation
import SwiftUI
import AlinFoundation
struct MiniMode: View {
@EnvironmentObject var appState: AppState
@EnvironmentObject var themeSettings: ThemeSettings
@EnvironmentObject var themeManager: ThemeManager
@Binding var search: String
@State private var showSys: Bool = true
@State private var showUsr: Bool = true
@@ -38,7 +39,7 @@ struct MiniMode: View {
}
.frame(minWidth: 300, minHeight: 345)
.edgesIgnoringSafeArea(.all)
.background(backgroundView(themeSettings: themeSettings, glass: glass))
.background(backgroundView(themeManager: themeManager, glass: glass))
}
}
@@ -151,7 +152,7 @@ struct MiniAppView: View {
@Environment(\.colorScheme) var colorScheme
@EnvironmentObject var appState: AppState
@EnvironmentObject var locations: Locations
@EnvironmentObject var themeSettings: ThemeSettings
@EnvironmentObject var themeManager: ThemeManager
// @State private var animateGradient: Bool = false
@Binding var search: String
@State private var showSys: Bool = true
@@ -176,7 +177,7 @@ struct MiniAppView: View {
ProgressView() {
Text("Gathering app details")
.font(.callout)
.foregroundStyle(Color("mode").opacity(0.5))
.foregroundStyle(.primary.opacity(0.5))
.padding(5)
}
Spacer()
@@ -190,7 +191,7 @@ struct MiniAppView: View {
.transition(.opacity)
.frame(minWidth: 300, minHeight: 370)
.edgesIgnoringSafeArea(.all)
.background(backgroundView(themeSettings: themeSettings, glass: glass).padding(-80))
.background(backgroundView(themeManager: themeManager, glass: glass).padding(-80))
.transition(.opacity)
.popover(isPresented: $showPopover, arrowEdge: .trailing) {
VStack {
@@ -204,7 +205,7 @@ struct MiniAppView: View {
}
.interactiveDismissDisabled(popoverStay)
.background(backgroundView(themeSettings: themeSettings, glass: glass).padding(-80))
.background(backgroundView(themeManager: themeManager, glass: glass).padding(-80))
.frame(width: 650, height: 500)
}
@@ -122,7 +122,7 @@ extension View {
// content: { item, isSelected in
// Text(item)
// .font(.callout)
// .foregroundColor(isSelected ? Color("mode") : Color("mode").opacity(0.5))
// .foregroundColor(isSelected ? .primary : .primary.opacity(0.5))
// .padding(.horizontal)
// .padding(.bottom, 5)
// .frame(width: 75)
@@ -161,7 +161,7 @@ extension View {
// content: { item, isSelected in
// Text(item)
// .font(.callout)
// .foregroundColor(isSelected ? Color("mode") : Color("mode").opacity(0.5) )
// .foregroundColor(isSelected ? .primary : .primary.opacity(0.5) )
// .padding(.horizontal)
// .padding(.bottom, 5)
// .frame(width: 75)
@@ -198,7 +198,7 @@ extension View {
// content: { item, isSelected in
// Text(item)
// .font(.callout)
// .foregroundColor(isSelected ? Color("mode") : Color("mode").opacity(0.5) )
// .foregroundColor(isSelected ? .primary : .primary.opacity(0.5) )
// .padding(.horizontal)
// .padding(.bottom, 5)
// .frame(width: 65)
+6 -5
View File
@@ -7,10 +7,11 @@
import Foundation
import SwiftUI
import AlinFoundation
struct RegularMode: View {
@EnvironmentObject var appState: AppState
@EnvironmentObject var themeSettings: ThemeSettings
@EnvironmentObject var themeManager: ThemeManager
@EnvironmentObject var locations: Locations
@AppStorage("settings.general.glass") private var glass: Bool = false
@AppStorage("settings.general.sidebarWidth") private var sidebarWidth: Double = 280
@@ -36,7 +37,7 @@ struct RegularMode: View {
ProgressView() {
Text("Gathering app details")
.font(.callout)
.foregroundStyle(Color("mode").opacity(0.5))
.foregroundStyle(.primary.opacity(0.5))
.padding(5)
}
Spacer()
@@ -49,11 +50,11 @@ struct RegularMode: View {
}
}
.background(backgroundView(themeSettings: themeSettings, darker: true, glass: glass))
.background(backgroundView(themeManager: themeManager, darker: true, glass: glass))
.transition(.opacity)
SlideableDivider(dimension: $sidebarWidth)
.background(backgroundView(themeSettings: themeSettings))
.background(backgroundView(themeManager: themeManager))
.zIndex(3)
@@ -75,7 +76,7 @@ struct RegularMode: View {
Spacer()
}
.zIndex(2)
.background(backgroundView(themeSettings: themeSettings))
.background(backgroundView(themeManager: themeManager))
}
.frame(minWidth: 900, minHeight: 600)
.edgesIgnoringSafeArea(.all)
+23 -11
View File
@@ -7,6 +7,7 @@
import Foundation
import SwiftUI
import AlinFoundation
struct ZombieView: View {
@EnvironmentObject var appState: AppState
@@ -45,7 +46,7 @@ struct ZombieView: View {
Spacer()
Text("Searching the file system").font(.title3)
.foregroundStyle(Color("mode").opacity(0.5))
.foregroundStyle(.primary.opacity(0.5))
ProgressView()
.progressViewStyle(.linear)
@@ -54,7 +55,7 @@ struct ZombieView: View {
Text("\(elapsedTime)")
.font(.title).monospacedDigit()
.foregroundStyle(Color("mode").opacity(0.5))
.foregroundStyle(.primary.opacity(0.5))
.opacity(elapsedTime == 0 ? 0 : 1)
.contentTransition(.numericText())
@@ -116,7 +117,7 @@ struct ZombieView: View {
Spacer()
}
Text("Remaining files and folders from previous applications")
.font(.callout).foregroundStyle(Color("mode").opacity(0.5))
.font(.callout).foregroundStyle(.primary.opacity(0.5))
}
Spacer()
@@ -125,7 +126,7 @@ struct ZombieView: View {
Text("\(displaySizeTotal)").font(.title).fontWeight(.bold).help("Total size on disk")
Text("\(selectedZombieItemsLocal.count) / \(searchZ.isEmpty ? appState.zombieFile.fileSize.count : memoizedFiles.count) \(appState.zombieFile.fileSize.count == 1 ? "item" : "items")")
.font(.callout).foregroundStyle(Color("mode").opacity(0.5))
.font(.callout).foregroundStyle(.primary.opacity(0.5))
}
}
@@ -258,17 +259,28 @@ struct ZombieView: View {
}
}
}
updateOnMain {
} else {
// Remove items from the list
appState.zombieFile.fileSize = appState.zombieFile.fileSize.filter { !selectedZombieItemsLocal.contains($0.key) }
// Update the selectedZombieFiles to remove references that are no longer present
selectedZombieItemsLocal.removeAll()
updateTotalSizes()
appState.zombieFile.fileSizeLogical = appState.zombieFile.fileSizeLogical.filter { !selectedZombieItemsLocal.contains($0.key) }
appState.zombieFile.fileIcon = appState.zombieFile.fileIcon.filter { !selectedZombieItemsLocal.contains($0.key) }
// Clear the selection
selectedZombieItemsLocal.removeAll()
// Update memoized files and total sizes
updateMemoizedFiles(for: searchZ, sizeType: sizeType, selectedSortAlpha: selectedSortAlpha, force: true)
}
// updateOnMain {
// // Remove items from the list
// appState.zombieFile.fileSize = appState.zombieFile.fileSize.filter { !selectedZombieItemsLocal.contains($0.key) }
// // Update the selectedZombieFiles to remove references that are no longer present
// selectedZombieItemsLocal.removeAll()
// updateTotalSizes()
//
// }
}
// updateMemoizedFiles(for: searchZ, sizeType: sizeType, selectedSortAlpha: selectedSortAlpha, force: true)
@@ -472,7 +484,7 @@ struct ZombieFileDetailsItem: View {
VStack {
Spacer()
RoundedRectangle(cornerRadius: 10)
.fill(Color("mode").opacity(0.5))
.fill(.primary.opacity(0.5))
.frame(height: 1.5)
.offset(y: 3)
}
+16 -10
View File
@@ -6,10 +6,11 @@
//
import SwiftUI
import AlinFoundation
struct FeatureView: View {
@EnvironmentObject var appState: AppState
@EnvironmentObject var themeManager: ThemeManager
@Environment(\.dismiss) var dismiss
@AppStorage("settings.general.features") private var features: String = ""
var body: some View {
@@ -20,34 +21,39 @@ struct FeatureView: View {
Text("New features for v\((Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String)!)!") .font(.title)
.bold()
.padding(.vertical)
// .padding(.bottom)
Spacer()
}
Divider()
.padding([.horizontal])
.padding([.vertical])
Text(features)
.font(.body)
.multilineTextAlignment(.leading)
.padding()
.padding(.horizontal)
Spacer()
HStack(alignment: .center, spacing: 20) {
Button(action: {
NewWin.close()
dismiss()
// NewWin.close()
}) {
Text("Okay")
Text("Close")
}
.buttonStyle(SimpleButtonBrightStyle(icon: "checkmark.circle", label: "Ok", help: "Ok", color: Color("mode")))
// .buttonStyle(SimpleButtonBrightStyle(icon: "checkmark.circle", label: "Ok", help: "Ok", color: .primary))
}
.padding(.bottom)
.padding(.vertical)
}
.padding(EdgeInsets(top: -25, leading: 0, bottom: 25, trailing: 0))
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(themeManager.pickerColor)
// .padding(EdgeInsets(top: -25, leading: 0, bottom: 25, trailing: 0))
}
-9
View File
@@ -67,15 +67,6 @@ class NewWin: NSWindowController {
private func makeNewView(appState: AppState, newWin: NewWindow) -> some View {
switch newWin {
case .update:
return AnyView(UpdateView()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.black.opacity(0.2))
.environmentObject(appState))
case .no_update:
return AnyView(NoUpdateView()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.environmentObject(appState))
case .perm:
return AnyView(PermView()
.frame(maxWidth: .infinity, maxHeight: .infinity)
+11 -11
View File
@@ -39,10 +39,10 @@ struct PermView: View {
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.foregroundStyle(Color("mode").opacity(0.5))
.foregroundStyle(.primary.opacity(0.5))
Text("Full Disk Access permission to find and delete files in system paths")
.font(.callout)
.foregroundStyle(Color("mode").opacity(0.5))
.foregroundStyle(.primary.opacity(0.5))
}
HStack(alignment: .top, spacing: 20) {
@@ -50,10 +50,10 @@ struct PermView: View {
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.foregroundStyle(Color("mode").opacity(0.5))
.foregroundStyle(.primary.opacity(0.5))
Text("Accessibility permission to delete files via Finder")
.font(.callout)
.foregroundStyle(Color("mode").opacity(0.5))
.foregroundStyle(.primary.opacity(0.5))
}
HStack(alignment: .top, spacing: 20) {
@@ -61,10 +61,10 @@ struct PermView: View {
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.foregroundStyle(Color("mode").opacity(0.5))
.foregroundStyle(.primary.opacity(0.5))
Text("Automation permission to perform extension actions via Finder")
.font(.callout)
.foregroundStyle(Color("mode").opacity(0.5))
.foregroundStyle(.primary.opacity(0.5))
}
HStack(alignment: .top, spacing: 20) {
@@ -72,10 +72,10 @@ struct PermView: View {
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.foregroundStyle(Color("mode").opacity(0.5))
.foregroundStyle(.primary.opacity(0.5))
Text("Reminders permission to find and delete some reminder files that certain apps cache")
.font(.callout)
.foregroundStyle(Color("mode").opacity(0.5))
.foregroundStyle(.primary.opacity(0.5))
}
HStack(alignment: .top, spacing: 20) {
@@ -83,10 +83,10 @@ struct PermView: View {
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.foregroundStyle(Color("mode").opacity(0.5))
.foregroundStyle(.primary.opacity(0.5))
Text("Add Pearcleaner in both Privacy panes via the + or by dragging the app over the pane. If the app is already pre-populated in the list, just toggle On if neeeded. Restart app when both permissions are granted")
.font(.callout)
.foregroundStyle(Color("mode").opacity(0.5))
.foregroundStyle(.primary.opacity(0.5))
}
}
.padding(.horizontal, 20)
@@ -97,7 +97,7 @@ struct PermView: View {
if #available(macOS 14.0, *) {
SettingsLink {}
.buttonStyle(SimpleButtonBrightStyle(icon: "gear", label: "Settings", help: "Check permissions in Settings", color: Color("mode")))
.buttonStyle(SimpleButtonBrightStyle(icon: "gear", label: "Settings", help: "Check permissions in Settings", color: .primary))
} else {
Button("Settings") {
if #available(macOS 13.0, *) {
-200
View File
@@ -1,200 +0,0 @@
//
// UpdateView.swift
// Pearcleaner
//
// Created by Alin Lupascu on 11/16/23.
//
import SwiftUI
struct UpdateView: View {
@EnvironmentObject var appState: AppState
@State private var isAppInCorrectDirectory: Bool = false
@State private var isUserAdmin: Bool = false
var body: some View {
VStack(spacing: 5) {
HStack {
Text("App: \(Bundle.main.version)")
.font(.title3)
.fontWeight(.semibold)
.opacity(0.5)
.padding()
Spacer()
Text("\(appState.progressBar.1 != 1.0 ? "Update Available 🥳" : "Completed 🚀")")
.font(.title)
.bold()
.padding(.vertical)
Spacer()
Text("GitHub: \(appState.releases.first?.tag_name ?? "")")
.font(.title3)
.fontWeight(.semibold)
.opacity(0.5)
.padding()
}
Divider()
.padding([.horizontal])
ScrollView {
Text("\(appState.releases.first?.modifiedBody ?? "No release information")")
.font(.body)
.multilineTextAlignment(.leading)
.padding(20)
}
Spacer()
if !isAppInCorrectDirectory {
Text("To avoid permission issues, please move Pearcleaner to the \(isUserAdmin ? "/Applications" : "\(home)/Applications") folder before updating!")
.font(.callout)
.foregroundStyle(Color("mode"))
.padding(.horizontal)
.padding(.top, 0)
}
VStack() {
ProgressView("\(appState.progressBar.0)", value: appState.progressBar.1, total: 1.0)
.progressViewStyle(LinearProgressViewStyle(tint: .blue))
.frame(height: 10)
}
.padding()
HStack(alignment: .center, spacing: 20) {
Button(action: NewWin.close) {
Text("Cancel")
}
.buttonStyle(SimpleButtonBrightStyle(icon: "x.circle", label: "Close", help: "Close", color: .red))
if appState.progressBar.1 != 1.0 {
Button(action: {
downloadUpdate(appState: appState)
}) {
Text("Update")
}
.buttonStyle(SimpleButtonBrightStyle(icon: "arrow.down.circle", label: "Update", help: "Update", color: Color("mode")))
} else {
Button(action: {
NewWin.close()
relaunchApp(afterDelay: 1)
}) {
Text("Restart")
}
.buttonStyle(SimpleButtonBrightStyle(icon: "arrow.uturn.left.circle", label: "Restart", help: "Restart", color: Color("mode")))
}
}
.padding(.bottom)
}
.padding(EdgeInsets(top: -25, leading: 0, bottom: 25, trailing: 0))
.onAppear {
checkAppDirectoryAndUserRole { result in
isAppInCorrectDirectory = result.isInCorrectDirectory
isUserAdmin = result.isAdmin
}
}
}
}
struct NoUpdateView: View {
@EnvironmentObject var appState: AppState
var body: some View {
VStack(spacing: 5) {
HStack {
Text("App: \(Bundle.main.version)")
.font(.title3)
.fontWeight(.semibold)
.opacity(0.5)
.padding()
Spacer()
Text("\(appState.progressBar.1 != 1.0 ? "No Update 😌" : "Completed 🚀")")
.font(.title)
.bold()
.padding(.vertical)
Spacer()
Text("GitHub: \(appState.releases.first?.tag_name ?? "")")
.font(.title3)
.fontWeight(.semibold)
.opacity(0.5)
.padding()
}
Divider()
.padding([.horizontal])
Text("Pearcleaner is on the most current release available, but you may force a re-download of the same version below.")
.font(.body)
.lineLimit(3)
.multilineTextAlignment(.leading)
.padding()
Spacer()
VStack() {
ProgressView("\(appState.progressBar.0)", value: appState.progressBar.1, total: 1.0)
.progressViewStyle(LinearProgressViewStyle(tint: .blue))
.frame(height: 10)
}
.padding()
HStack(alignment: .center, spacing: 20) {
if appState.progressBar.1 < 0.1 {
Button(action: {
downloadUpdate(appState: appState)
}) {
Text("Force Update")
}
.buttonStyle(SimpleButtonBrightStyle(icon: "arrow.down.circle", label: "Force Update", help: "Force-Update", color: .red))
Button(action: {
NewWin.close()
}) {
Text("Okay")
}
.buttonStyle(SimpleButtonBrightStyle(icon: "checkmark.circle", label: "Ok", help: "Ok", color: Color("mode")))
}
if appState.progressBar.1 == 1.0 {
Button(action: {
NewWin.close()
relaunchApp()
}) {
Text("Restart")
}
.buttonStyle(SimpleButtonBrightStyle(icon: "arrow.uturn.left.circle", label: "Restart", help: "Restart", color: Color("mode")))
}
}
.padding(.bottom)
}
.padding(EdgeInsets(top: -25, leading: 0, bottom: 25, trailing: 0))
}
}
+1
View File
@@ -6,6 +6,7 @@
//
import SwiftUI
import AlinFoundation
class WindowSettings {
private let windowWidthKey = "windowWidthKey"