diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000..dd84ea7
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,38 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**To Reproduce**
+Steps to reproduce the behavior:
+1. Go to '...'
+2. Click on '....'
+3. Scroll down to '....'
+4. See error
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**Screenshots**
+If applicable, add screenshots to help explain your problem.
+
+**Desktop (please complete the following information):**
+ - OS: [e.g. iOS]
+ - Browser [e.g. chrome, safari]
+ - Version [e.g. 22]
+
+**Smartphone (please complete the following information):**
+ - Device: [e.g. iPhone6]
+ - OS: [e.g. iOS8.1]
+ - Browser [e.g. stock browser, safari]
+ - Version [e.g. 22]
+
+**Additional context**
+Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 0000000..bbcbbe7
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,20 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..4f20167
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,91 @@
+# Xcode
+#
+# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
+
+## User settings
+xcuserdata/
+
+## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
+*.xcscmblueprint
+*.xccheckout
+
+## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
+build/
+DerivedData/
+*.moved-aside
+*.pbxuser
+!default.pbxuser
+*.mode1v3
+!default.mode1v3
+*.mode2v3
+!default.mode2v3
+*.perspectivev3
+!default.perspectivev3
+
+## Obj-C/Swift specific
+*.hmap
+
+## App packaging
+*.ipa
+*.dSYM.zip
+*.dSYM
+
+## Playgrounds
+timeline.xctimeline
+playground.xcworkspace
+
+# Swift Package Manager
+#
+# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
+# Packages/
+# Package.pins
+# Package.resolved
+# *.xcodeproj
+#
+# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
+# hence it is not needed unless you have added a package configuration file to your project
+# .swiftpm
+
+.build/
+
+# CocoaPods
+#
+# We recommend against adding the Pods directory to your .gitignore. However
+# you should judge for yourself, the pros and cons are mentioned at:
+# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
+#
+# Pods/
+#
+# Add this line if you want to avoid checking in source code from the Xcode workspace
+# *.xcworkspace
+
+# Carthage
+#
+# Add this line if you want to avoid checking in source code from Carthage dependencies.
+# Carthage/Checkouts
+
+Carthage/Build/
+
+# Accio dependency management
+Dependencies/
+.accio/
+
+# fastlane
+#
+# It is recommended to not store the screenshots in the git repo.
+# Instead, use fastlane to re-generate the screenshots whenever they are needed.
+# For more information about the recommended setup visit:
+# https://docs.fastlane.tools/best-practices/source-control/#source-control
+
+fastlane/report.xml
+fastlane/Preview.html
+fastlane/screenshots/**/*.png
+fastlane/test_output
+
+# Code Injection
+#
+# After new code Injection tools there's a generated folder /iOSInjectionProject
+# https://github.com/johnno1962/injectionforxcode
+
+iOSInjectionProject/
+.DS_Store
diff --git a/Builds/ExportOptions.plist b/Builds/ExportOptions.plist
new file mode 100644
index 0000000..ae1ab83
--- /dev/null
+++ b/Builds/ExportOptions.plist
@@ -0,0 +1,10 @@
+
+
+
+
+ method
+ mac-application
+ teamID
+ lupascu.marius@gmail.com
+
+
diff --git a/Builds/changes.md b/Builds/changes.md
new file mode 100644
index 0000000..a44c846
--- /dev/null
+++ b/Builds/changes.md
@@ -0,0 +1,11 @@
+## New Features
+
+- N/A
+
+## Fixed Issues
+
+- N/A
+
+## Changes
+
+- N/A
diff --git a/Pearcleaner.xcodeproj/project.pbxproj b/Pearcleaner.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..b62be2c
--- /dev/null
+++ b/Pearcleaner.xcodeproj/project.pbxproj
@@ -0,0 +1,631 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 56;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ C72893052AFD42E600C8C1CD /* DeepLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = C72893042AFD42E600C8C1CD /* DeepLink.swift */; };
+ C72893122AFD51EA00C8C1CD /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = C72893112AFD51EA00C8C1CD /* main.swift */; };
+ C7575D382B0181FC006A600A /* FileWatcher in Frameworks */ = {isa = PBXBuildFile; productRef = C7575D372B0181FC006A600A /* FileWatcher */; };
+ 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 */; };
+ C76D084C2AF83C9D00D07867 /* Permissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76D084B2AF83C9D00D07867 /* Permissions.swift */; };
+ C76D08512AF850F300D07867 /* About.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76D08502AF850F300D07867 /* About.swift */; };
+ C76D08552AF89CDE00D07867 /* AppListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76D08542AF89CDE00D07867 /* AppListView.swift */; };
+ C77B90042AF18E2E009CC655 /* PearcleanerApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C77B90032AF18E2E009CC655 /* PearcleanerApp.swift */; };
+ C77B90062AF18E2E009CC655 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C77B90052AF18E2E009CC655 /* ContentView.swift */; };
+ C77B90082AF18E2F009CC655 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C77B90072AF18E2F009CC655 /* Assets.xcassets */; };
+ C77B90162AF19377009CC655 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = C77B90152AF19377009CC655 /* AppState.swift */; };
+ C77B90182AF19382009CC655 /* AppCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = C77B90172AF19382009CC655 /* AppCommands.swift */; };
+ C77B901A2AF1938F009CC655 /* Logic.swift in Sources */ = {isa = PBXBuildFile; fileRef = C77B90192AF1938F009CC655 /* Logic.swift */; };
+ C77B901C2AF193A3009CC655 /* Styles.swift in Sources */ = {isa = PBXBuildFile; fileRef = C77B901B2AF193A3009CC655 /* Styles.swift */; };
+ C77B901E2AF1A9DB009CC655 /* SettingsWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C77B901D2AF1A9DB009CC655 /* SettingsWindow.swift */; };
+ C77B90232AF2D616009CC655 /* FilesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C77B90222AF2D616009CC655 /* FilesView.swift */; };
+ C7A27E812AFD7C4600166168 /* com.alienator88.PearcleanerSentinel.plist in Resources */ = {isa = PBXBuildFile; fileRef = C7A27E802AFD7C4600166168 /* com.alienator88.PearcleanerSentinel.plist */; };
+ C7D31D462AFDA90C00C7ED9E /* Sentinel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7D31D452AFDA90C00C7ED9E /* Sentinel.swift */; };
+ C7D31D482AFEB23700C7ED9E /* TopBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7D31D472AFEB23700C7ED9E /* TopBar.swift */; };
+ C7D31D4A2AFEB26700C7ED9E /* AppListItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7D31D492AFEB26700C7ED9E /* AppListItems.swift */; };
+ C7D31D512AFF00F300C7ED9E /* Locations.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7D31D502AFF00F300C7ED9E /* Locations.swift */; };
+ C7D31D532B0048BE00C7ED9E /* TrashView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7D31D522B0048BE00C7ED9E /* TrashView.swift */; };
+ C7D31D552B008CB900C7ED9E /* Shapes.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7D31D542B008CB900C7ED9E /* Shapes.swift */; };
+ C7D31D592B00A49300C7ED9E /* DropTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7D31D582B00A49300C7ED9E /* DropTarget.swift */; };
+ C7F539382AF60865007DF1B2 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7F539372AF60865007DF1B2 /* Utilities.swift */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ C728930D2AFD51EA00C8C1CD /* CopyFiles */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = /usr/share/man/man1/;
+ dstSubfolderSpec = 0;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 1;
+ };
+ C7A27E7D2AFD65C400166168 /* CopyFiles */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 6;
+ files = (
+ C7575D392B0182DE006A600A /* PearcleanerSentinel in CopyFiles */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ C72893042AFD42E600C8C1CD /* DeepLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLink.swift; sourceTree = ""; };
+ C728930F2AFD51EA00C8C1CD /* PearcleanerSentinel */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = PearcleanerSentinel; sourceTree = BUILT_PRODUCTS_DIR; };
+ C72893112AFD51EA00C8C1CD /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; };
+ C76D08472AF83C3F00D07867 /* Update.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Update.swift; sourceTree = ""; };
+ C76D08492AF83C6E00D07867 /* General.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = General.swift; sourceTree = ""; };
+ C76D084B2AF83C9D00D07867 /* Permissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Permissions.swift; sourceTree = ""; };
+ C76D084D2AF84D8C00D07867 /* Builds */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Builds; sourceTree = ""; };
+ C76D08502AF850F300D07867 /* About.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = About.swift; sourceTree = ""; };
+ C76D08542AF89CDE00D07867 /* AppListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppListView.swift; sourceTree = ""; };
+ C77B90002AF18E2E009CC655 /* Pearcleaner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Pearcleaner.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ C77B90032AF18E2E009CC655 /* PearcleanerApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PearcleanerApp.swift; sourceTree = ""; };
+ C77B90052AF18E2E009CC655 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
+ C77B90072AF18E2F009CC655 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
+ C77B90142AF18E7C009CC655 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+ C77B90152AF19377009CC655 /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = ""; };
+ C77B90172AF19382009CC655 /* AppCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCommands.swift; sourceTree = ""; };
+ C77B90192AF1938F009CC655 /* Logic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logic.swift; sourceTree = ""; };
+ C77B901B2AF193A3009CC655 /* Styles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Styles.swift; sourceTree = ""; };
+ C77B901D2AF1A9DB009CC655 /* SettingsWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsWindow.swift; sourceTree = ""; };
+ C77B90222AF2D616009CC655 /* FilesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilesView.swift; sourceTree = ""; };
+ C77B90242AF2D796009CC655 /* Pearcleaner.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Pearcleaner.entitlements; sourceTree = ""; };
+ C7A27E7F2AFD70DD00166168 /* PearcleanerSentinel.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = PearcleanerSentinel.entitlements; sourceTree = ""; };
+ C7A27E802AFD7C4600166168 /* com.alienator88.PearcleanerSentinel.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = com.alienator88.PearcleanerSentinel.plist; sourceTree = ""; };
+ C7D31D452AFDA90C00C7ED9E /* Sentinel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sentinel.swift; sourceTree = ""; };
+ C7D31D472AFEB23700C7ED9E /* TopBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopBar.swift; sourceTree = ""; };
+ C7D31D492AFEB26700C7ED9E /* AppListItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppListItems.swift; sourceTree = ""; };
+ C7D31D502AFF00F300C7ED9E /* Locations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Locations.swift; sourceTree = ""; };
+ C7D31D522B0048BE00C7ED9E /* TrashView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrashView.swift; sourceTree = ""; };
+ C7D31D542B008CB900C7ED9E /* Shapes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shapes.swift; sourceTree = ""; };
+ C7D31D582B00A49300C7ED9E /* DropTarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropTarget.swift; sourceTree = ""; };
+ C7F539372AF60865007DF1B2 /* Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utilities.swift; sourceTree = ""; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ C728930C2AFD51EA00C8C1CD /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ C7575D382B0181FC006A600A /* FileWatcher in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ C77B8FFD2AF18E2E009CC655 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ C72893072AFD442600C8C1CD /* Logic */ = {
+ isa = PBXGroup;
+ children = (
+ C77B90192AF1938F009CC655 /* Logic.swift */,
+ C77B90172AF19382009CC655 /* AppCommands.swift */,
+ C77B90152AF19377009CC655 /* AppState.swift */,
+ C7F539372AF60865007DF1B2 /* Utilities.swift */,
+ C72893042AFD42E600C8C1CD /* DeepLink.swift */,
+ C77B901B2AF193A3009CC655 /* Styles.swift */,
+ C7D31D502AFF00F300C7ED9E /* Locations.swift */,
+ C7D31D542B008CB900C7ED9E /* Shapes.swift */,
+ );
+ path = Logic;
+ sourceTree = "";
+ };
+ C72893102AFD51EA00C8C1CD /* PearcleanerSentinel */ = {
+ isa = PBXGroup;
+ children = (
+ C7A27E822AFD996100166168 /* Resources */,
+ C7A27E7F2AFD70DD00166168 /* PearcleanerSentinel.entitlements */,
+ C72893112AFD51EA00C8C1CD /* main.swift */,
+ );
+ path = PearcleanerSentinel;
+ sourceTree = "";
+ };
+ C72893162AFD531500C8C1CD /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ );
+ name = Frameworks;
+ sourceTree = "";
+ };
+ C76D08462AF83C2F00D07867 /* Settings */ = {
+ isa = PBXGroup;
+ children = (
+ C77B901D2AF1A9DB009CC655 /* SettingsWindow.swift */,
+ C76D08492AF83C6E00D07867 /* General.swift */,
+ C76D084B2AF83C9D00D07867 /* Permissions.swift */,
+ C7D31D452AFDA90C00C7ED9E /* Sentinel.swift */,
+ C76D08472AF83C3F00D07867 /* Update.swift */,
+ C76D08502AF850F300D07867 /* About.swift */,
+ );
+ path = Settings;
+ sourceTree = "";
+ };
+ C76D084F2AF84E0100D07867 /* Resources */ = {
+ isa = PBXGroup;
+ children = (
+ C7A27E802AFD7C4600166168 /* com.alienator88.PearcleanerSentinel.plist */,
+ C77B90142AF18E7C009CC655 /* Info.plist */,
+ C77B90072AF18E2F009CC655 /* Assets.xcassets */,
+ C77B90242AF2D796009CC655 /* Pearcleaner.entitlements */,
+ );
+ path = Resources;
+ sourceTree = "";
+ };
+ C77B8FF72AF18E2E009CC655 = {
+ isa = PBXGroup;
+ children = (
+ C76D084D2AF84D8C00D07867 /* Builds */,
+ C77B90022AF18E2E009CC655 /* Pearcleaner */,
+ C72893102AFD51EA00C8C1CD /* PearcleanerSentinel */,
+ C77B90012AF18E2E009CC655 /* Products */,
+ C72893162AFD531500C8C1CD /* Frameworks */,
+ );
+ sourceTree = "";
+ };
+ C77B90012AF18E2E009CC655 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ C77B90002AF18E2E009CC655 /* Pearcleaner.app */,
+ C728930F2AFD51EA00C8C1CD /* PearcleanerSentinel */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ C77B90022AF18E2E009CC655 /* Pearcleaner */ = {
+ isa = PBXGroup;
+ children = (
+ C77B90032AF18E2E009CC655 /* PearcleanerApp.swift */,
+ C76D084F2AF84E0100D07867 /* Resources */,
+ C76D08462AF83C2F00D07867 /* Settings */,
+ C77B901F2AF1B390009CC655 /* Views */,
+ C72893072AFD442600C8C1CD /* Logic */,
+ );
+ path = Pearcleaner;
+ sourceTree = "";
+ };
+ C77B901F2AF1B390009CC655 /* Views */ = {
+ isa = PBXGroup;
+ children = (
+ C77B90052AF18E2E009CC655 /* ContentView.swift */,
+ C77B90222AF2D616009CC655 /* FilesView.swift */,
+ C76D08542AF89CDE00D07867 /* AppListView.swift */,
+ C7D31D472AFEB23700C7ED9E /* TopBar.swift */,
+ C7D31D492AFEB26700C7ED9E /* AppListItems.swift */,
+ C7D31D522B0048BE00C7ED9E /* TrashView.swift */,
+ C7D31D582B00A49300C7ED9E /* DropTarget.swift */,
+ );
+ path = Views;
+ sourceTree = "";
+ };
+ C7A27E822AFD996100166168 /* Resources */ = {
+ isa = PBXGroup;
+ children = (
+ );
+ path = Resources;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ C728930E2AFD51EA00C8C1CD /* PearcleanerSentinel */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = C72893132AFD51EA00C8C1CD /* Build configuration list for PBXNativeTarget "PearcleanerSentinel" */;
+ buildPhases = (
+ C728930B2AFD51EA00C8C1CD /* Sources */,
+ C728930C2AFD51EA00C8C1CD /* Frameworks */,
+ C728930D2AFD51EA00C8C1CD /* CopyFiles */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = PearcleanerSentinel;
+ packageProductDependencies = (
+ C7575D372B0181FC006A600A /* FileWatcher */,
+ );
+ productName = PeakPurgeMonitor;
+ productReference = C728930F2AFD51EA00C8C1CD /* PearcleanerSentinel */;
+ productType = "com.apple.product-type.tool";
+ };
+ C77B8FFF2AF18E2E009CC655 /* Pearcleaner */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = C77B900F2AF18E2F009CC655 /* Build configuration list for PBXNativeTarget "Pearcleaner" */;
+ buildPhases = (
+ C77B8FFC2AF18E2E009CC655 /* Sources */,
+ C77B8FFD2AF18E2E009CC655 /* Frameworks */,
+ C77B8FFE2AF18E2E009CC655 /* Resources */,
+ C7A27E7D2AFD65C400166168 /* CopyFiles */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = Pearcleaner;
+ packageProductDependencies = (
+ );
+ productName = PeakPurge;
+ productReference = C77B90002AF18E2E009CC655 /* Pearcleaner.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ C77B8FF82AF18E2E009CC655 /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ BuildIndependentTargetsInParallel = 1;
+ LastSwiftUpdateCheck = 1500;
+ LastUpgradeCheck = 1500;
+ TargetAttributes = {
+ C728930E2AFD51EA00C8C1CD = {
+ CreatedOnToolsVersion = 15.0.1;
+ };
+ C77B8FFF2AF18E2E009CC655 = {
+ CreatedOnToolsVersion = 15.0.1;
+ };
+ };
+ };
+ buildConfigurationList = C77B8FFB2AF18E2E009CC655 /* Build configuration list for PBXProject "Pearcleaner" */;
+ compatibilityVersion = "Xcode 14.0";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = C77B8FF72AF18E2E009CC655;
+ packageReferences = (
+ C7575D362B0181F3006A600A /* XCRemoteSwiftPackageReference "FileWatcher" */,
+ );
+ productRefGroup = C77B90012AF18E2E009CC655 /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ C77B8FFF2AF18E2E009CC655 /* Pearcleaner */,
+ C728930E2AFD51EA00C8C1CD /* PearcleanerSentinel */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ C77B8FFE2AF18E2E009CC655 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ C77B90082AF18E2F009CC655 /* Assets.xcassets in Resources */,
+ C7A27E812AFD7C4600166168 /* com.alienator88.PearcleanerSentinel.plist in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ C728930B2AFD51EA00C8C1CD /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ C72893122AFD51EA00C8C1CD /* main.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ C77B8FFC2AF18E2E009CC655 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ C76D08482AF83C3F00D07867 /* Update.swift in Sources */,
+ C77B90162AF19377009CC655 /* AppState.swift in Sources */,
+ C7D31D512AFF00F300C7ED9E /* Locations.swift in Sources */,
+ C77B901A2AF1938F009CC655 /* Logic.swift in Sources */,
+ C7D31D532B0048BE00C7ED9E /* TrashView.swift in Sources */,
+ C7D31D462AFDA90C00C7ED9E /* Sentinel.swift in Sources */,
+ C7D31D592B00A49300C7ED9E /* DropTarget.swift in Sources */,
+ C77B90062AF18E2E009CC655 /* ContentView.swift in Sources */,
+ C77B901C2AF193A3009CC655 /* Styles.swift in Sources */,
+ C72893052AFD42E600C8C1CD /* DeepLink.swift in Sources */,
+ C76D084C2AF83C9D00D07867 /* Permissions.swift in Sources */,
+ C76D084A2AF83C6E00D07867 /* General.swift in Sources */,
+ C77B90042AF18E2E009CC655 /* PearcleanerApp.swift in Sources */,
+ C7D31D552B008CB900C7ED9E /* Shapes.swift in Sources */,
+ C76D08512AF850F300D07867 /* About.swift in Sources */,
+ C77B90182AF19382009CC655 /* AppCommands.swift in Sources */,
+ C7D31D4A2AFEB26700C7ED9E /* AppListItems.swift in Sources */,
+ C77B901E2AF1A9DB009CC655 /* SettingsWindow.swift in Sources */,
+ C7D31D482AFEB23700C7ED9E /* TopBar.swift in Sources */,
+ C7F539382AF60865007DF1B2 /* Utilities.swift in Sources */,
+ C76D08552AF89CDE00D07867 /* AppListView.swift in Sources */,
+ C77B90232AF2D616009CC655 /* FilesView.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin XCBuildConfiguration section */
+ C72893142AFD51EA00C8C1CD /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_ENTITLEMENTS = PearcleanerSentinel/PearcleanerSentinel.entitlements;
+ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
+ CODE_SIGN_STYLE = Automatic;
+ DEVELOPMENT_TEAM = BK8443AXLU;
+ ENABLE_HARDENED_RUNTIME = YES;
+ MACOSX_DEPLOYMENT_TARGET = 13.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.alienator88.PearcleanerSentinel;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SKIP_INSTALL = YES;
+ SWIFT_VERSION = 5.0;
+ };
+ name = Debug;
+ };
+ C72893152AFD51EA00C8C1CD /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_ENTITLEMENTS = PearcleanerSentinel/PearcleanerSentinel.entitlements;
+ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
+ CODE_SIGN_STYLE = Automatic;
+ DEVELOPMENT_TEAM = BK8443AXLU;
+ ENABLE_HARDENED_RUNTIME = YES;
+ MACOSX_DEPLOYMENT_TARGET = 13.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.alienator88.PearcleanerSentinel;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SKIP_INSTALL = YES;
+ SWIFT_VERSION = 5.0;
+ };
+ name = Release;
+ };
+ C77B900D2AF18E2F009CC655 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ CODE_SIGN_ENTITLEMENTS = "";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MACOSX_DEPLOYMENT_TARGET = 13.0;
+ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+ MTL_FAST_MATH = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = macosx;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ };
+ name = Debug;
+ };
+ C77B900E2AF18E2F009CC655 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ CODE_SIGN_ENTITLEMENTS = "";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MACOSX_DEPLOYMENT_TARGET = 13.0;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ MTL_FAST_MATH = YES;
+ SDKROOT = macosx;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ };
+ name = Release;
+ };
+ C77B90102AF18E2F009CC655 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_ENTITLEMENTS = Pearcleaner/Resources/Pearcleaner.entitlements;
+ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
+ CODE_SIGN_STYLE = Automatic;
+ COMBINE_HIDPI_IMAGES = YES;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_ASSET_PATHS = "";
+ DEVELOPMENT_TEAM = BK8443AXLU;
+ ENABLE_HARDENED_RUNTIME = YES;
+ ENABLE_PREVIEWS = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = Pearcleaner/Resources/Info.plist;
+ INFOPLIST_KEY_CFBundleDisplayName = Pearcleaner;
+ INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
+ INFOPLIST_KEY_NSAppleEventsUsageDescription = "App needs to send apple events to FInder to delete files";
+ INFOPLIST_KEY_NSHumanReadableCopyright = "";
+ INFOPLIST_KEY_NSSystemAdministrationUsageDescription = "Requires full disk access to be able to delete files in locations with no permission";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/../Frameworks",
+ );
+ MACOSX_DEPLOYMENT_TARGET = 13.0;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.alienator88.Pearcleaner;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ };
+ name = Debug;
+ };
+ C77B90112AF18E2F009CC655 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_ENTITLEMENTS = Pearcleaner/Resources/Pearcleaner.entitlements;
+ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
+ CODE_SIGN_STYLE = Automatic;
+ COMBINE_HIDPI_IMAGES = YES;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_ASSET_PATHS = "";
+ DEVELOPMENT_TEAM = BK8443AXLU;
+ ENABLE_HARDENED_RUNTIME = YES;
+ ENABLE_PREVIEWS = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = Pearcleaner/Resources/Info.plist;
+ INFOPLIST_KEY_CFBundleDisplayName = Pearcleaner;
+ INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
+ INFOPLIST_KEY_NSAppleEventsUsageDescription = "App needs to send apple events to FInder to delete files";
+ INFOPLIST_KEY_NSHumanReadableCopyright = "";
+ INFOPLIST_KEY_NSSystemAdministrationUsageDescription = "Requires full disk access to be able to delete files in locations with no permission";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/../Frameworks",
+ );
+ MACOSX_DEPLOYMENT_TARGET = 13.0;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.alienator88.Pearcleaner;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ C72893132AFD51EA00C8C1CD /* Build configuration list for PBXNativeTarget "PearcleanerSentinel" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ C72893142AFD51EA00C8C1CD /* Debug */,
+ C72893152AFD51EA00C8C1CD /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ C77B8FFB2AF18E2E009CC655 /* Build configuration list for PBXProject "Pearcleaner" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ C77B900D2AF18E2F009CC655 /* Debug */,
+ C77B900E2AF18E2F009CC655 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ C77B900F2AF18E2F009CC655 /* Build configuration list for PBXNativeTarget "Pearcleaner" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ C77B90102AF18E2F009CC655 /* Debug */,
+ C77B90112AF18E2F009CC655 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+
+/* Begin XCRemoteSwiftPackageReference section */
+ C7575D362B0181F3006A600A /* XCRemoteSwiftPackageReference "FileWatcher" */ = {
+ isa = XCRemoteSwiftPackageReference;
+ repositoryURL = "https://github.com/eonist/FileWatcher.git";
+ requirement = {
+ kind = upToNextMajorVersion;
+ minimumVersion = 0.2.3;
+ };
+ };
+/* End XCRemoteSwiftPackageReference section */
+
+/* Begin XCSwiftPackageProductDependency section */
+ C7575D372B0181FC006A600A /* FileWatcher */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = C7575D362B0181F3006A600A /* XCRemoteSwiftPackageReference "FileWatcher" */;
+ productName = FileWatcher;
+ };
+/* End XCSwiftPackageProductDependency section */
+ };
+ rootObject = C77B8FF82AF18E2E009CC655 /* Project object */;
+}
diff --git a/Pearcleaner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Pearcleaner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..919434a
--- /dev/null
+++ b/Pearcleaner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/Pearcleaner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Pearcleaner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/Pearcleaner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/Pearcleaner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Pearcleaner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
new file mode 100644
index 0000000..89f2a9b
--- /dev/null
+++ b/Pearcleaner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -0,0 +1,14 @@
+{
+ "pins" : [
+ {
+ "identity" : "filewatcher",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/eonist/FileWatcher.git",
+ "state" : {
+ "revision" : "e67c2a99502eade343fecabeca8c57e749a55b59",
+ "version" : "0.2.3"
+ }
+ }
+ ],
+ "version" : 2
+}
diff --git a/Pearcleaner.xcodeproj/xcshareddata/xcschemes/Pearcleaner Debug.xcscheme b/Pearcleaner.xcodeproj/xcshareddata/xcschemes/Pearcleaner Debug.xcscheme
new file mode 100644
index 0000000..9aa3ff6
--- /dev/null
+++ b/Pearcleaner.xcodeproj/xcshareddata/xcschemes/Pearcleaner Debug.xcscheme
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Pearcleaner.xcodeproj/xcshareddata/xcschemes/Pearcleaner Release.xcscheme b/Pearcleaner.xcodeproj/xcshareddata/xcschemes/Pearcleaner Release.xcscheme
new file mode 100644
index 0000000..0f406db
--- /dev/null
+++ b/Pearcleaner.xcodeproj/xcshareddata/xcschemes/Pearcleaner Release.xcscheme
@@ -0,0 +1,103 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Pearcleaner.xcodeproj/xcshareddata/xcschemes/PearcleanerSentinel.xcscheme b/Pearcleaner.xcodeproj/xcshareddata/xcschemes/PearcleanerSentinel.xcscheme
new file mode 100644
index 0000000..0134fda
--- /dev/null
+++ b/Pearcleaner.xcodeproj/xcshareddata/xcschemes/PearcleanerSentinel.xcscheme
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Pearcleaner/Logic/AppCommands.swift b/Pearcleaner/Logic/AppCommands.swift
new file mode 100644
index 0000000..01c94ef
--- /dev/null
+++ b/Pearcleaner/Logic/AppCommands.swift
@@ -0,0 +1,106 @@
+//
+// AppCommands.swift
+// Pearcleaner
+//
+// Created by Alin Lupascu on 10/31/23.
+//
+
+import SwiftUI
+
+struct AppCommands: Commands {
+
+ let appState: AppState
+
+ init(appState: AppState) {
+ self.appState = appState
+ }
+
+ var body: some Commands {
+
+ // Pearcleaner Menu
+ CommandGroup(replacing: .appInfo) {
+
+ Button {
+ loadGithubReleases(appState: appState, manual: true)
+ } label: {
+ Text("Updates")
+ }
+ .keyboardShortcut("u", modifiers: .command)
+
+ Button {
+ withAnimation(.easeInOut(duration: 0.5)) {
+ // Refresh Apps list
+ let sortedApps = getSortedApps()
+ appState.sortedApps.userApps = sortedApps.userApps
+ appState.sortedApps.systemApps = sortedApps.systemApps
+ }
+ } label: {
+ Text("Refresh Apps")
+ }
+ .keyboardShortcut("r", modifiers: .command)
+
+ }
+
+
+
+ // Edit Menu
+ CommandGroup(replacing: .undoRedo) {
+ Button
+ {
+ undoTrash(appState: appState) {
+ let sortedApps = getSortedApps()
+ appState.sortedApps.userApps = []
+ appState.sortedApps.systemApps = []
+ appState.sortedApps.userApps = sortedApps.userApps
+ appState.sortedApps.systemApps = sortedApps.systemApps
+ }
+ } label: {
+ Label("Undo Removal", systemImage: "clear")
+ }
+ .keyboardShortcut("z", modifiers: .command)
+ }
+
+
+
+ // GitHub Menu
+ CommandMenu(Text("GitHub", comment: "Github Repo")) {
+ Button
+ {
+ NSWorkspace.shared.open(URL(string: "https://github.com/alienator88/Pearcleaner")!)
+ } label: {
+ Label("View Repository", systemImage: "paperplane")
+ }
+
+
+ Button
+ {
+ NSWorkspace.shared.open(URL(string: "https://github.com/alienator88/Pearcleaner/releases")!)
+ } label: {
+ Label("View Releases", systemImage: "paperplane")
+ }
+
+
+ Button
+ {
+ NSWorkspace.shared.open(URL(string: "https://github.com/alienator88/Pearcleaner/issues")!)
+ } label: {
+ Label("View Issues", systemImage: "paperplane")
+ }
+
+
+ Divider()
+
+
+ Button
+ {
+ NSWorkspace.shared.open(URL(string: "https://github.com/alienator88/Pearcleaner/issues/new/choose")!)
+ } label: {
+ Label("Submit New Issue", systemImage: "paperplane")
+ }
+ }
+
+
+
+ }
+}
+
diff --git a/Pearcleaner/Logic/AppState.swift b/Pearcleaner/Logic/AppState.swift
new file mode 100644
index 0000000..15c9348
--- /dev/null
+++ b/Pearcleaner/Logic/AppState.swift
@@ -0,0 +1,169 @@
+//
+// AppState.swift
+// Pearcleaner
+//
+// Created by Alin Lupascu on 10/31/23.
+//
+
+import Foundation
+import SwiftUI
+
+let home = FileManager.default.homeDirectoryForCurrentUser.path
+
+class AppState: ObservableObject
+{
+ @Published var showFiles: Bool = false
+ @Published var appInfo: AppInfo
+ @Published var paths: [URL] = []
+ @Published var sortedApps: (userApps: [AppInfo], systemApps: [AppInfo]) = ([], [])
+ @Published var selectedItems = Set()
+ @Published var alertType = AlertType.off
+ @Published var currentView = CurrentDetailsView.empty
+ @Published var showAlert: Bool = false
+ @Published var sidebar: Bool = true
+ @Published var isReminderVisible: Bool = false
+ @Published var releases = [Release]()
+
+ //Window
+// @Published var winWidth: CGFloat = 1020
+
+ init() {
+ self.appInfo = AppInfo(
+ id: UUID(),
+ path: URL(fileURLWithPath: ""),
+ bundleIdentifier: "",
+ appName: "",
+ appVersion: "",
+ appIcon: nil
+ )
+ }
+}
+
+struct AppInfo: Identifiable, Hashable {
+ let id: UUID
+ let path: URL
+ let bundleIdentifier: String
+ let appName: String
+ let appVersion: String
+ let appIcon: NSImage?
+
+ static let empty = AppInfo(id: UUID(), path: URL(fileURLWithPath: ""), bundleIdentifier: "", appName: "", appVersion: "", appIcon: nil)
+}
+
+
+enum CurrentTabView:Int
+{
+ case general
+ case permissions
+ case sentinel
+ case update
+ case about
+
+ var title: String {
+ switch self {
+ case .general: return "General"
+ case .permissions: return "Permissions"
+ case .sentinel: return "Sentinel"
+ case .update: return "Update"
+ case .about: return "About"
+ }
+ }
+}
+
+enum CurrentDetailsView:Int
+{
+ case empty
+ case files
+}
+
+enum AlertType:Int
+{
+ case diskAccess
+ case update
+ case no_update
+ case restartApp
+ case off
+}
+
+
+
+enum DisplayMode: Int, CaseIterable {
+ case system, dark, light
+
+ var colorScheme: ColorScheme? {
+ get {
+ switch self {
+ case .system: return nil
+ case .dark: return ColorScheme.dark
+ case .light: return ColorScheme.light
+ }
+ }
+ set {
+ switch newValue {
+ case .none:
+ self = .system
+ case .dark?:
+ self = .dark
+ case .light?:
+ self = .light
+ default:
+ break
+ }
+ }
+ }
+
+ var description: String {
+ switch self {
+ case .system: return "System"
+ case .dark: return "Dark"
+ case .light: return "Light"
+ }
+ }
+}
+
+
+
+//let (cacheDir, tempDir) = darwinCT()
+//let locations: [String] = [
+// "\(home)/Library",
+// "\(home)/Library/Application Scripts",
+// "\(home)/Library/Application Support",
+// "\(home)/Library/Application Support/CrashReporter",
+// "\(home)/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.ApplicationRecentDocuments",
+// "\(home)/Library/Containers",
+// "\(home)/Library/Group Containers",
+// "\(home)/Library/Caches",
+// "\(home)/Library/HTTPStorages",
+// "\(home)/Library/Group Containers",
+// "\(home)/Library/Internet Plug-Ins",
+// "\(home)/Library/LaunchAgents",
+// "\(home)/Library/Logs",
+// "\(home)/Library/Preferences",
+// "\(home)/Library/Preferences/ByHost",
+// "\(home)/Library/Saved Application State",
+// "\(home)/Library/WebKit",
+// "/Users/Shared",
+// "/Users/Library",
+// "/Library",
+// "/Library/Application Support",
+// "/Library/Application Support/CrashReporter",
+// "/Library/Caches",
+// "/Library/Extensions",
+// "/Library/Internet Plug-Ins",
+// "/Library/LaunchAgents",
+// "/Library/LaunchDaemons",
+// "/Library/Logs",
+// "/Library/Logs/DiagnosticReports",
+// "/Library/Preferences",
+// "/Library/PrivilegedHelperTools",
+// "/private/var/db/receipts",
+// "/private/tmp",
+// "/usr/local/bin",
+// "/usr/local/etc",
+// "/usr/local/opt",
+// "/usr/local/sbin",
+// "/usr/local/share",
+// "/usr/local/var",
+// cacheDir,
+// tempDir
+//]
diff --git a/Pearcleaner/Logic/DeepLink.swift b/Pearcleaner/Logic/DeepLink.swift
new file mode 100644
index 0000000..0949af2
--- /dev/null
+++ b/Pearcleaner/Logic/DeepLink.swift
@@ -0,0 +1,41 @@
+//
+// DeepLink.swift
+// Pearcleaner
+//
+// Created by Alin Lupascu on 11/9/23.
+//
+
+import Foundation
+import SwiftUI
+
+class DeeplinkManager {
+
+ class DeepLinkConstants {
+ static let scheme = "pear"
+ static let host = "com.alienator88.Pearcleaner"
+ static let query = "path"
+ }
+
+ func manage(url: URL, appState: AppState) {
+ guard url.scheme == DeepLinkConstants.scheme,
+ url.host == DeepLinkConstants.host,
+ let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
+ let queryItems = components.queryItems
+ else {
+ print("URL does not match the expected scheme and host")
+ return
+ }
+
+ if let path = queryItems.first(where: { $0.name == DeepLinkConstants.query })?.value {
+ let pathURL = URL(fileURLWithPath: path)
+ let appInfo = getAppInfo(atPath: pathURL)
+ updateOnMain {
+ appState.appInfo = appInfo!
+ findPathsForApp(appState: appState, appInfo: appState.appInfo)
+ appState.currentView = .files
+ }
+ } else {
+ print("No path query parameter found in the URL")
+ }
+ }
+}
diff --git a/Pearcleaner/Logic/Locations.swift b/Pearcleaner/Logic/Locations.swift
new file mode 100644
index 0000000..6dbb0a8
--- /dev/null
+++ b/Pearcleaner/Logic/Locations.swift
@@ -0,0 +1,105 @@
+//
+// Locations.swift
+// Pearcleaner
+//
+// Created by Alin Lupascu on 11/10/23.
+//
+
+import Foundation
+
+
+struct Locations {
+ struct Category {
+ let name: String
+ var paths: [String]
+ }
+
+ let home: String
+ let cacheDir: String
+ let tempDir: String
+
+ var apps: Category
+ var widgets: Category
+ var plugins: Category
+
+ init() {
+ self.home = FileManager.default.homeDirectoryForCurrentUser.path
+ let (cacheDir, tempDir) = darwinCT()
+ self.cacheDir = cacheDir
+ self.tempDir = tempDir
+
+ self.apps = Category(name: "Apps", paths: [
+ "\(home)/Library",
+ "\(home)/Library/Application Scripts",
+ "\(home)/Library/Application Support",
+ "\(home)/Library/Application Support/CrashReporter",
+ "\(home)/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.ApplicationRecentDocuments",
+ "\(home)/Library/Containers",
+ "\(home)/Library/Group Containers",
+ "\(home)/Library/Caches",
+ "\(home)/Library/HTTPStorages",
+ "\(home)/Library/Group Containers",
+ "\(home)/Library/Internet Plug-Ins",
+ "\(home)/Library/LaunchAgents",
+ "\(home)/Library/Logs",
+ "\(home)/Library/Preferences",
+ "\(home)/Library/Preferences/ByHost",
+ "\(home)/Library/Saved Application State",
+ "\(home)/Library/WebKit",
+ "/Users/Shared",
+ "/Users/Library",
+ "/Library",
+ "/Library/Application Support",
+ "/Library/Application Support/CrashReporter",
+ "/Library/Caches",
+ "/Library/Extensions",
+ "/Library/Internet Plug-Ins",
+ "/Library/LaunchAgents",
+ "/Library/LaunchDaemons",
+ "/Library/Logs",
+ "/Library/Logs/DiagnosticReports",
+ "/Library/Preferences",
+ "/Library/PrivilegedHelperTools",
+ "/private/var/db/receipts",
+ "/private/tmp",
+ "/usr/local/bin",
+ "/usr/local/etc",
+ "/usr/local/opt",
+ "/usr/local/sbin",
+ "/usr/local/share",
+ "/usr/local/var",
+ cacheDir,
+ tempDir
+ ])
+
+ self.widgets = Category(name: "Widgets", paths: [
+ // User
+ "~/Library/Widgets",
+ // System
+ "/Library/Widgets"
+ ])
+
+ self.plugins = Category(name: "Plugins", paths: [
+ // User
+ "~/Library/Contextual Menu Items",
+ "~/Library/InputManagers",
+ "~/Library/Internet Plug-Ins",
+ "~/Library/Mail/Bundles",
+ "~/Library/PreferencePanes",
+ "~/Library/QuickLook",
+ "~/Library/QuickTime",
+ "~/Library/Screen Savers",
+ "~/Library/Spotlight",
+ // System
+ "/Library/Contextual Menu Items",
+ "/Library/InputManagers",
+ "/Library/Internet Plug-Ins",
+ "/Library/Mail/Bundles",
+ "/Library/PreferencePanes",
+ "/Library/QuickLook",
+ "/Library/QuickTime",
+ "/Library/Screen Savers",
+ "/Library/Spotlight",
+ ])
+ }
+}
diff --git a/Pearcleaner/Logic/Logic.swift b/Pearcleaner/Logic/Logic.swift
new file mode 100644
index 0000000..3e89a4a
--- /dev/null
+++ b/Pearcleaner/Logic/Logic.swift
@@ -0,0 +1,362 @@
+//
+// Logic.swift
+// Pearcleaner
+//
+// Created by Alin Lupascu on 10/31/23.
+//
+
+import Foundation
+import SwiftUI
+
+
+// Get list of apps and sort it
+func getSortedApps() -> (systemApps: [AppInfo], userApps: [AppInfo]) {
+ let apps = getApplications()
+ let sortedSystemApps = apps.systemApps
+ .compactMap { getAppInfo(atPath: $0) }
+ .sorted { $0.appName.replacingOccurrences(of: ".", with: "").lowercased() < $1.appName.replacingOccurrences(of: ".", with: "").lowercased() }
+ let sortedUserApps = apps.userApps
+ .compactMap { getAppInfo(atPath: $0) }
+ .sorted { $0.appName.replacingOccurrences(of: ".", with: "").lowercased() < $1.appName.replacingOccurrences(of: ".", with: "").lowercased() }
+
+ return (systemApps: sortedSystemApps, userApps: sortedUserApps)
+}
+
+
+// Get all applications from /Applications and ~/Applications
+func getApplications() -> (systemApps: [URL], userApps: [URL]) {
+ let fileManager = FileManager.default
+
+ // Define the paths for system and user applications
+ let systemAppsPath = "/Applications"
+ let userAppsPath = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Applications").path
+
+ var systemApps: [URL] = []
+ var userApps: [URL] = []
+
+ func collectAppPaths(at directoryPath: String, isSystem: Bool) {
+ do {
+ let appURLs = try fileManager.contentsOfDirectory(at: URL(fileURLWithPath: directoryPath), includingPropertiesForKeys: nil, options: [])
+
+ for appURL in appURLs {
+ if appURL.pathExtension == "app" {
+ // Add the path to the appropriate list
+ if isSystem && !isRestricted(atPath: appURL) {
+ systemApps.append(appURL)
+ } else if !isSystem && !isRestricted(atPath: appURL) {
+ userApps.append(appURL)
+ }
+ } else if appURL.hasDirectoryPath {
+ // If it's a directory, recursively explore it
+ collectAppPaths(at: appURL.path, isSystem: isSystem)
+ }
+ }
+ } catch {
+ // Handle any potential errors here
+ print("Error: \(error)")
+ }
+ }
+
+ // Collect system applications
+ collectAppPaths(at: systemAppsPath, isSystem: true)
+
+ // Collect user applications
+ collectAppPaths(at: userAppsPath, isSystem: false)
+
+ // Filter out unwanted system folders like /Applications/Utilities
+ systemApps = systemApps.filter { !$0.absoluteString.contains("/Applications/Utilities/") }
+ return (systemApps, userApps)
+}
+
+
+// Get app bundle information from provided path
+func getAppInfo(atPath path: URL) -> AppInfo? {
+ if let bundle = Bundle(url: path) {
+ if let bundleIdentifier = bundle.bundleIdentifier,
+ let appVersion = bundle.infoDictionary?["CFBundleShortVersionString"] as? String,
+ var appIconFileName = bundle.infoDictionary?["CFBundleIconFile"] as? String {
+ var appIcon: NSImage?
+ var appName: String?
+ if let localizedName = bundle.localizedInfoDictionary?[kCFBundleNameKey as String] as? String {
+ appName = localizedName
+ } else if let bundleName = bundle.infoDictionary?["CFBundleName"] as? String {
+ appName = bundleName
+ }
+ // Check if the icon file name has an .icns extension, if not, add it
+ if !appIconFileName.hasSuffix(".icns") {
+ appIconFileName += ".icns"
+ }
+
+ // Try to find the icon in the main bundle
+ if let iconPath = bundle.path(forResource: appIconFileName, ofType: nil),
+ let icon = NSImage(contentsOfFile: iconPath) {
+ appIcon = icon
+ }
+
+ // If not found, try to find it in the resources directory
+ if appIcon == nil,
+ let resourcesPath = bundle.resourcePath {
+ let iconURL = URL(fileURLWithPath: resourcesPath).appendingPathComponent(appIconFileName)
+ if let icon = NSImage(contentsOfFile: iconURL.path) {
+ appIcon = icon
+ }
+ }
+
+ // Convert the icon to a 100x100 PNG image
+ if let pngIcon = appIcon.flatMap({ convertICNSToPNG(icon: $0, size: NSSize(width: 100, height: 100)) }) {
+ appIcon = pngIcon
+ }
+
+ if appIcon == nil {
+ print("App Icon not found for app at path: \(path)")
+ }
+
+ return AppInfo(id: UUID(), path: path, bundleIdentifier: bundleIdentifier, appName: appName ?? "", appVersion: appVersion, appIcon: appIcon)
+
+ } else {
+ print("One or more variables missing for app at path: \(path)")
+ }
+ } else {
+ print("Bundle not found at path: \(path)")
+ }
+ return nil
+}
+
+
+
+// Get directory path for darwin cache and temp directories
+func darwinCT() -> (String, String) {
+ let task = Process()
+ task.launchPath = "/bin/bash"
+ task.arguments = ["-c", "getconf DARWIN_USER_CACHE_DIR; getconf DARWIN_USER_TEMP_DIR"]
+ let pipe = Pipe()
+ task.standardOutput = pipe
+ task.launch()
+ let data = pipe.fileHandleForReading.readDataToEndOfFile()
+ if let output = String(data: data, encoding: .utf8) {
+ let components = output.components(separatedBy: "\n")
+ if components.count >= 2 {
+ var cacheDir = components[0].trimmingCharacters(in: .whitespacesAndNewlines)
+ var tempDir = components[1].trimmingCharacters(in: .whitespacesAndNewlines)
+ if cacheDir.hasSuffix("/") {
+ cacheDir.removeLast()
+ }
+ if tempDir.hasSuffix("/") {
+ tempDir.removeLast()
+ }
+ return (cacheDir, tempDir)
+ }
+ }
+ print("Could not get DARWIN_USER_CACHE_DIR or DARWIN_USER_TEMP_DIR")
+ return ("", "")
+}
+
+
+
+// Check if app is running before deleting app files
+func killApp(appId: String, completion: @escaping () -> Void = {}) {
+ let runningApps = NSWorkspace.shared.runningApplications
+ for app in runningApps {
+ if app.bundleIdentifier == appId {
+ app.terminate()
+ }
+ }
+ completion()
+}
+
+
+
+
+// Find all possible paths for an app based on name/bundle id
+func findPathsForApp(appState: AppState, appInfo: AppInfo) {
+ Task(priority: .high) {
+ updateOnMain {
+ appState.paths = []
+ }
+ var collection: [URL] = []
+ if let url = URL(string: appInfo.path.absoluteString) {
+ // appState.paths.insert(url, at: 0)
+ collection.insert(url, at: 0)
+ }
+ let fileManager = FileManager.default
+ let dispatchGroup = DispatchGroup()
+ let bundleComponents = appInfo.bundleIdentifier.components(separatedBy: ".")
+ var bundle: String = ""
+ if bundleComponents.count >= 3 { // get last 2 or middle 2 components
+ bundle = bundleComponents[1...2].joined(separator: "").lowercased()
+ }
+ let nameL = appInfo.appName.pearFormat()
+ let bundleIdentifierL = appInfo.bundleIdentifier.pearFormat()
+ let locations = Locations()
+
+ for location in locations.apps.paths {
+ dispatchGroup.enter() // Enter the dispatch group
+
+ do {
+ let contents = try fileManager.contentsOfDirectory(atPath: location)
+
+ for item in contents {
+
+ let itemURL = URL(fileURLWithPath: location).appendingPathComponent(item)
+ let itemL = ("\(item)").replacingOccurrences(of: ".", with: "").replacingOccurrences(of: " ", with: "").lowercased()
+
+ if collection.contains(itemURL) {
+ return
+ }
+
+ if itemL.contains("comapple") {
+ if itemL.contains(bundle) || itemL.contains(bundleIdentifierL) || (nameL.count > 4 && itemL.contains(nameL)) {
+ collection.append(itemURL)
+ }
+ }
+ else if itemL.contains("xcode") {
+ if itemL.contains(bundle) || itemL.contains(bundleIdentifierL) || (nameL.count > 4 && itemL.contains(nameL)) {
+ collection.append(itemURL)
+ }
+ } else if itemL.contains("logi") && !itemL.contains("login") {
+ if itemL.contains(bundleComponents[1]) || itemL.contains(bundle) || itemL.contains(bundleIdentifierL) || (nameL.count > 3 && itemL.contains(nameL)) {
+ collection.append(itemURL)
+ }
+ } else if itemL.contains("office") || itemL.contains("oneauth") || itemL.suffix(2).contains("ms") || itemL.contains("onenote") {
+ if itemL.contains(bundle) || itemL.contains(bundleIdentifierL) || (nameL.count > 4 && itemL.contains(nameL)) {
+ collection.append(itemURL)
+ }
+ } else {
+ if itemL.contains(bundleIdentifierL) || itemL.contains(bundle) || (nameL.count > 3 && itemL.contains(nameL)) {
+ collection.append(itemURL)
+ }
+ }
+ }
+ } catch {
+ continue
+ }
+
+ dispatchGroup.leave() // Leave the dispatch group
+
+ dispatchGroup.notify(queue: .main) {
+ updateOnMain {
+ appState.paths = collection
+ appState.selectedItems = Set(collection)
+ }
+ }
+
+ }
+ }
+}
+
+
+
+
+// Move files to trash using applescript/Finder so it asks for user password if needed
+func moveFilesToTrash(at fileURLs: [URL], completion: @escaping () -> Void = {}) {
+ updateOnBackground {
+ let posixFiles = fileURLs.map { "POSIX file \"\($0.path)\", " }.joined().dropLast(3)
+ let scriptSource = """
+ tell application \"Finder\" to delete { \(posixFiles)" }
+ """
+ var error: NSDictionary?
+ if let scriptObject = NSAppleScript(source: scriptSource) {
+ let output: NSAppleEventDescriptor = scriptObject.executeAndReturnError(&error)
+ if let error = error {
+ print("Error: \(error)")
+ } else if let outputString = output.stringValue {
+ print(outputString)
+ }
+ }
+ DispatchQueue.main.async {
+ completion()
+ }
+ }
+}
+
+
+// Undo trash action
+func undoTrash(appState: AppState, completion: @escaping () -> Void = {}) {
+ let scriptSource = """
+ tell application "Finder"
+ activate
+ end tell
+
+ tell application "System Events"
+ keystroke "z" using command down
+ end tell
+
+ tell application "Pearcleaner"
+ activate
+ end tell
+ """
+ var error: NSDictionary?
+
+ updateOnBackground {
+ if let scriptObject = NSAppleScript(source: scriptSource) {
+ let output: NSAppleEventDescriptor = scriptObject.executeAndReturnError(&error)
+ if let error = error {
+ if let value = error["NSAppleScriptErrorNumber"] {
+ if value as! Int == 1002 {
+ _ = checkAndRequestAccessibilityAccess(appState: appState)
+ }
+ }
+ print("Error: \(error)")
+ } else if let outputString = output.stringValue {
+ print(outputString)
+ }
+ }
+ DispatchQueue.main.async {
+ completion()
+ }
+ }
+}
+
+
+
+
+
+
+
+
+
+
+
+
+//func isAppRunning(appName: String) -> Bool {
+// let task = Process()
+// task.executableURL = URL(fileURLWithPath: "/usr/bin/pgrep")
+// task.arguments = ["-af", appName]
+//
+// let outputPipe = Pipe()
+// task.standardOutput = outputPipe
+//
+// do {
+// try task.run()
+// } catch {
+// print("Failed to run task: \(error)")
+// return false
+// }
+//
+// let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
+// let output = String(decoding: outputData, as: UTF8.self)
+//
+// if output.isEmpty {
+// return false
+// } else {
+// return true
+// }
+//}
+//
+//// Kill app if running
+//func killApp2(appName: String, completion: @escaping () -> Void = {}) {
+// if isAppRunning(appName: appName) {
+// let task = Process()
+// task.executableURL = URL(fileURLWithPath: "/usr/bin/pkill")
+// task.arguments = ["-af", appName]
+//
+// do {
+// try task.run()
+// } catch {
+// print("Failed to run task: \(error)")
+// return
+// }
+//
+// }
+// completion()
+//}
diff --git a/Pearcleaner/Logic/Shapes.swift b/Pearcleaner/Logic/Shapes.swift
new file mode 100644
index 0000000..86ef766
--- /dev/null
+++ b/Pearcleaner/Logic/Shapes.swift
@@ -0,0 +1,599 @@
+//
+// Shapes.swift
+// Pearcleaner
+//
+// Created by Alin Lupascu on 11/11/23.
+//
+
+import Foundation
+import SwiftUI
+
+// Pear Logo
+struct Pear: Shape {
+ func path(in rect: CGRect) -> Path {
+ var path = Path()
+ let width = rect.size.width
+ let height = rect.size.height
+ path.move(to: CGPoint(x: 0.5985*width, y: 0.045*height))
+ path.addCurve(to: CGPoint(x: 0.508*width, y: 0.141*height), control1: CGPoint(x: 0.571*width, y: 0.0635*height), control2: CGPoint(x: 0.521*width, y: 0.116*height))
+ path.addLine(to: CGPoint(x: 0.4985*width, y: 0.159*height))
+ path.addLine(to: CGPoint(x: 0.4725*width, y: 0.1325*height))
+ path.addCurve(to: CGPoint(x: 0.318*width, y: 0.0975*height), control1: CGPoint(x: 0.433*width, y: 0.092*height), control2: CGPoint(x: 0.383*width, y: 0.0805*height))
+ path.addCurve(to: CGPoint(x: 0.2105*width, y: 0.15*height), control1: CGPoint(x: 0.2875*width, y: 0.1055*height), control2: CGPoint(x: 0.231*width, y: 0.133*height))
+ path.addLine(to: CGPoint(x: 0.1985*width, y: 0.16*height))
+ path.addLine(to: CGPoint(x: 0.2125*width, y: 0.18*height))
+ path.addCurve(to: CGPoint(x: 0.316*width, y: 0.273*height), control1: CGPoint(x: 0.2335*width, y: 0.2105*height), control2: CGPoint(x: 0.287*width, y: 0.2585*height))
+ path.addCurve(to: CGPoint(x: 0.371*width, y: 0.289*height), control1: CGPoint(x: 0.331*width, y: 0.2805*height), control2: CGPoint(x: 0.354*width, y: 0.287*height))
+ path.addLine(to: CGPoint(x: 0.4*width, y: 0.2915*height))
+ path.addLine(to: CGPoint(x: 0.375*width, y: 0.318*height))
+ path.addCurve(to: CGPoint(x: 0.3195*width, y: 0.443*height), control1: CGPoint(x: 0.3415*width, y: 0.3535*height), control2: CGPoint(x: 0.326*width, y: 0.388*height))
+ path.addCurve(to: CGPoint(x: 0.2635*width, y: 0.564*height), control1: CGPoint(x: 0.3125*width, y: 0.501*height), control2: CGPoint(x: 0.302*width, y: 0.5235*height))
+ path.addCurve(to: CGPoint(x: 0.19*width, y: 0.7345*height), control1: CGPoint(x: 0.21*width, y: 0.62*height), control2: CGPoint(x: 0.19*width, y: 0.6665*height))
+ path.addCurve(to: CGPoint(x: 0.3235*width, y: 0.949*height), control1: CGPoint(x: 0.19*width, y: 0.8305*height), control2: CGPoint(x: 0.2385*width, y: 0.9085*height))
+ path.addCurve(to: CGPoint(x: 0.4125*width, y: 0.9675*height), control1: CGPoint(x: 0.3605*width, y: 0.9665*height), control2: CGPoint(x: 0.365*width, y: 0.9675*height))
+ path.addCurve(to: CGPoint(x: 0.4875*width, y: 0.958*height), control1: CGPoint(x: 0.449*width, y: 0.9675*height), control2: CGPoint(x: 0.4695*width, y: 0.965*height))
+ path.addCurve(to: CGPoint(x: 0.547*width, y: 0.9595*height), control1: CGPoint(x: 0.5125*width, y: 0.949*height), control2: CGPoint(x: 0.513*width, y: 0.949*height))
+ path.addCurve(to: CGPoint(x: 0.759*width, y: 0.9135*height), control1: CGPoint(x: 0.6225*width, y: 0.983*height), control2: CGPoint(x: 0.702*width, y: 0.9655*height))
+ path.addCurve(to: CGPoint(x: 0.7975*width, y: 0.817*height), control1: CGPoint(x: 0.8085*width, y: 0.8675*height), control2: CGPoint(x: 0.8225*width, y: 0.8335*height))
+ path.addCurve(to: CGPoint(x: 0.7615*width, y: 0.817*height), control1: CGPoint(x: 0.786*width, y: 0.8095*height), control2: CGPoint(x: 0.7835*width, y: 0.8095*height))
+ path.addCurve(to: CGPoint(x: 0.661*width, y: 0.8015*height), control1: CGPoint(x: 0.723*width, y: 0.83*height), control2: CGPoint(x: 0.6935*width, y: 0.8255*height))
+ path.addCurve(to: CGPoint(x: 0.634*width, y: 0.6745*height), control1: CGPoint(x: 0.6245*width, y: 0.7745*height), control2: CGPoint(x: 0.611*width, y: 0.711*height))
+ path.addCurve(to: CGPoint(x: 0.6545*width, y: 0.618*height), control1: CGPoint(x: 0.659*width, y: 0.6345*height), control2: CGPoint(x: 0.6605*width, y: 0.631*height))
+ path.addCurve(to: CGPoint(x: 0.6325*width, y: 0.6*height), control1: CGPoint(x: 0.651*width, y: 0.611*height), control2: CGPoint(x: 0.6415*width, y: 0.603*height))
+ path.addCurve(to: CGPoint(x: 0.575*width, y: 0.5175*height), control1: CGPoint(x: 0.603*width, y: 0.59*height), control2: CGPoint(x: 0.575*width, y: 0.55*height))
+ path.addCurve(to: CGPoint(x: 0.6595*width, y: 0.4255*height), control1: CGPoint(x: 0.575*width, y: 0.4685*height), control2: CGPoint(x: 0.609*width, y: 0.4315*height))
+ path.addCurve(to: CGPoint(x: 0.691*width, y: 0.4095*height), control1: CGPoint(x: 0.6755*width, y: 0.4235*height), control2: CGPoint(x: 0.684*width, y: 0.419*height))
+ path.addCurve(to: CGPoint(x: 0.691*width, y: 0.3745*height), control1: CGPoint(x: 0.701*width, y: 0.3965*height), control2: CGPoint(x: 0.701*width, y: 0.396*height))
+ path.addCurve(to: CGPoint(x: 0.565*width, y: 0.2645*height), control1: CGPoint(x: 0.667*width, y: 0.3215*height), control2: CGPoint(x: 0.6175*width, y: 0.278*height))
+ path.addLine(to: CGPoint(x: 0.54*width, y: 0.258*height))
+ path.addLine(to: CGPoint(x: 0.54*width, y: 0.2355*height))
+ path.addCurve(to: CGPoint(x: 0.6215*width, y: 0.102*height), control1: CGPoint(x: 0.54*width, y: 0.192*height), control2: CGPoint(x: 0.5745*width, y: 0.1355*height))
+ path.addCurve(to: CGPoint(x: 0.66*width, y: 0.0575*height), control1: CGPoint(x: 0.653*width, y: 0.0795*height), control2: CGPoint(x: 0.66*width, y: 0.0715*height))
+ path.addCurve(to: CGPoint(x: 0.6315*width, y: 0.03*height), control1: CGPoint(x: 0.66*width, y: 0.044*height), control2: CGPoint(x: 0.6455*width, y: 0.03*height))
+ path.addCurve(to: CGPoint(x: 0.5985*width, y: 0.045*height), control1: CGPoint(x: 0.626*width, y: 0.03*height), control2: CGPoint(x: 0.6115*width, y: 0.037*height))
+ path.closeSubpath()
+ path.move(to: CGPoint(x: 0.4155*width, y: 0.1615*height))
+ path.addCurve(to: CGPoint(x: 0.444*width, y: 0.19*height), control1: CGPoint(x: 0.4245*width, y: 0.1675*height), control2: CGPoint(x: 0.4375*width, y: 0.1805*height))
+ path.addLine(to: CGPoint(x: 0.4555*width, y: 0.2075*height))
+ path.addLine(to: CGPoint(x: 0.445*width, y: 0.216*height))
+ path.addCurve(to: CGPoint(x: 0.369*width, y: 0.23*height), control1: CGPoint(x: 0.4265*width, y: 0.231*height), control2: CGPoint(x: 0.397*width, y: 0.2365*height))
+ path.addCurve(to: CGPoint(x: 0.285*width, y: 0.175*height), control1: CGPoint(x: 0.345*width, y: 0.224*height), control2: CGPoint(x: 0.285*width, y: 0.185*height))
+ path.addCurve(to: CGPoint(x: 0.2865*width, y: 0.17*height), control1: CGPoint(x: 0.285*width, y: 0.1725*height), control2: CGPoint(x: 0.2855*width, y: 0.17*height))
+ path.addCurve(to: CGPoint(x: 0.315*width, y: 0.16*height), control1: CGPoint(x: 0.287*width, y: 0.17*height), control2: CGPoint(x: 0.3*width, y: 0.1655*height))
+ path.addCurve(to: CGPoint(x: 0.4155*width, y: 0.1615*height), control1: CGPoint(x: 0.3535*width, y: 0.1465*height), control2: CGPoint(x: 0.394*width, y: 0.147*height))
+ path.closeSubpath()
+ path.move(to: CGPoint(x: 0.553*width, y: 0.3225*height))
+ path.addCurve(to: CGPoint(x: 0.62*width, y: 0.369*height), control1: CGPoint(x: 0.5755*width, y: 0.329*height), control2: CGPoint(x: 0.62*width, y: 0.3605*height))
+ path.addCurve(to: CGPoint(x: 0.599*width, y: 0.382*height), control1: CGPoint(x: 0.62*width, y: 0.371*height), control2: CGPoint(x: 0.6105*width, y: 0.377*height))
+ path.addCurve(to: CGPoint(x: 0.5295*width, y: 0.453*height), control1: CGPoint(x: 0.572*width, y: 0.3945*height), control2: CGPoint(x: 0.5445*width, y: 0.4225*height))
+ path.addCurve(to: CGPoint(x: 0.5315*width, y: 0.5815*height), control1: CGPoint(x: 0.512*width, y: 0.4895*height), control2: CGPoint(x: 0.5125*width, y: 0.5425*height))
+ path.addCurve(to: CGPoint(x: 0.565*width, y: 0.626*height), control1: CGPoint(x: 0.5395*width, y: 0.597*height), control2: CGPoint(x: 0.5545*width, y: 0.6175*height))
+ path.addCurve(to: CGPoint(x: 0.576*width, y: 0.6575*height), control1: CGPoint(x: 0.584*width, y: 0.642*height), control2: CGPoint(x: 0.584*width, y: 0.6425*height))
+ path.addCurve(to: CGPoint(x: 0.572*width, y: 0.7725*height), control1: CGPoint(x: 0.5645*width, y: 0.68*height), control2: CGPoint(x: 0.562*width, y: 0.744*height))
+ path.addCurve(to: CGPoint(x: 0.6785*width, y: 0.8755*height), control1: CGPoint(x: 0.5875*width, y: 0.8195*height), control2: CGPoint(x: 0.632*width, y: 0.862*height))
+ path.addCurve(to: CGPoint(x: 0.6665*width, y: 0.902*height), control1: CGPoint(x: 0.7045*width, y: 0.883*height), control2: CGPoint(x: 0.701*width, y: 0.8905*height))
+ path.addCurve(to: CGPoint(x: 0.546*width, y: 0.897*height), control1: CGPoint(x: 0.6305*width, y: 0.9145*height), control2: CGPoint(x: 0.585*width, y: 0.9125*height))
+ path.addCurve(to: CGPoint(x: 0.498*width, y: 0.89*height), control1: CGPoint(x: 0.517*width, y: 0.885*height), control2: CGPoint(x: 0.5115*width, y: 0.8845*height))
+ path.addCurve(to: CGPoint(x: 0.405*width, y: 0.909*height), control1: CGPoint(x: 0.451*width, y: 0.909*height), control2: CGPoint(x: 0.4415*width, y: 0.911*height))
+ path.addCurve(to: CGPoint(x: 0.2925*width, y: 0.8535*height), control1: CGPoint(x: 0.3585*width, y: 0.9065*height), control2: CGPoint(x: 0.3245*width, y: 0.8895*height))
+ path.addCurve(to: CGPoint(x: 0.2485*width, y: 0.734*height), control1: CGPoint(x: 0.2625*width, y: 0.8205*height), control2: CGPoint(x: 0.2485*width, y: 0.7825*height))
+ path.addCurve(to: CGPoint(x: 0.3115*width, y: 0.597*height), control1: CGPoint(x: 0.2485*width, y: 0.6825*height), control2: CGPoint(x: 0.267*width, y: 0.642*height))
+ path.addCurve(to: CGPoint(x: 0.375*width, y: 0.465*height), control1: CGPoint(x: 0.3535*width, y: 0.5545*height), control2: CGPoint(x: 0.3695*width, y: 0.5215*height))
+ path.addCurve(to: CGPoint(x: 0.385*width, y: 0.4075*height), control1: CGPoint(x: 0.3775*width, y: 0.441*height), control2: CGPoint(x: 0.382*width, y: 0.415*height))
+ path.addCurve(to: CGPoint(x: 0.438*width, y: 0.3395*height), control1: CGPoint(x: 0.394*width, y: 0.3835*height), control2: CGPoint(x: 0.4195*width, y: 0.351*height))
+ path.addCurve(to: CGPoint(x: 0.553*width, y: 0.3225*height), control1: CGPoint(x: 0.4785*width, y: 0.3145*height), control2: CGPoint(x: 0.5105*width, y: 0.31*height))
+ path.closeSubpath()
+ return path
+ }
+}
+
+// Pear text
+struct Pearcleaner: Shape {
+ func path(in rect: CGRect) -> Path {
+ var path = Path()
+ let width = rect.size.width
+ let height = rect.size.height
+ path.move(to: CGPoint(x: 1.10767*width, y: -0.23519*height))
+ path.addCurve(to: CGPoint(x: 1.10292*width, y: -0.22248*height), control1: CGPoint(x: 1.10519*width, y: -0.23943*height), control2: CGPoint(x: 1.10375*width, y: -0.23519*height))
+ path.addCurve(to: CGPoint(x: 1.09362*width, y: -0.14302*height), control1: CGPoint(x: 1.10044*width, y: -0.1854*height), control2: CGPoint(x: 1.09755*width, y: -0.16315*height))
+ path.addCurve(to: CGPoint(x: 1.0744*width, y: -0.09005*height), control1: CGPoint(x: 1.08783*width, y: -0.11442*height), control2: CGPoint(x: 1.07977*width, y: -0.09005*height))
+ path.addCurve(to: CGPoint(x: 1.09052*width, y: -0.24684*height), control1: CGPoint(x: 1.07977*width, y: -0.10594*height), control2: CGPoint(x: 1.08928*width, y: -0.16103*height))
+ path.addCurve(to: CGPoint(x: 1.05869*width, y: -0.43648*height), control1: CGPoint(x: 1.09217*width, y: -0.36126*height), control2: CGPoint(x: 1.0775*width, y: -0.43648*height))
+ path.addCurve(to: CGPoint(x: 1.02955*width, y: -0.36868*height), control1: CGPoint(x: 1.04546*width, y: -0.43648*height), control2: CGPoint(x: 1.03513*width, y: -0.39834*height))
+ path.addLine(to: CGPoint(x: 1.03058*width, y: -0.40364*height))
+ path.addCurve(to: CGPoint(x: 1.02686*width, y: -0.42377*height), control1: CGPoint(x: 1.031*width, y: -0.41529*height), control2: CGPoint(x: 1.02934*width, y: -0.42377*height))
+ path.addLine(to: CGPoint(x: 1.01054*width, y: -0.42377*height))
+ path.addCurve(to: CGPoint(x: 1.00578*width, y: -0.40258*height), control1: CGPoint(x: 1.00805*width, y: -0.42377*height), control2: CGPoint(x: 1.00619*width, y: -0.41529*height))
+ path.addLine(to: CGPoint(x: 0.98532*width, y: 0.26062*height))
+ path.addCurve(to: CGPoint(x: 0.98883*width, y: 0.2818*height), control1: CGPoint(x: 0.98491*width, y: 0.27227*height), control2: CGPoint(x: 0.98656*width, y: 0.2818*height))
+ path.addLine(to: CGPoint(x: 1.00516*width, y: 0.2818*height))
+ path.addCurve(to: CGPoint(x: 1.01012*width, y: 0.25956*height), control1: CGPoint(x: 1.00805*width, y: 0.2818*height), control2: CGPoint(x: 1.00971*width, y: 0.27333*height))
+ path.addLine(to: CGPoint(x: 1.02087*width, y: -0.09111*height))
+ path.addCurve(to: CGPoint(x: 1.05146*width, y: -0.31888*height), control1: CGPoint(x: 1.02583*width, y: -0.2532*height), control2: CGPoint(x: 1.03926*width, y: -0.31888*height))
+ path.addCurve(to: CGPoint(x: 1.06282*width, y: -0.20023*height), control1: CGPoint(x: 1.06262*width, y: -0.31888*height), control2: CGPoint(x: 1.06572*width, y: -0.26379*height))
+ path.addCurve(to: CGPoint(x: 1.04298*width, y: -0.09429*height), control1: CGPoint(x: 1.06034*width, y: -0.14302*height), control2: CGPoint(x: 1.05352*width, y: -0.09429*height))
+ path.addCurve(to: CGPoint(x: 1.02976*width, y: -0.09111*height), control1: CGPoint(x: 1.03203*width, y: -0.10594*height), control2: CGPoint(x: 1.03058*width, y: -0.1017*height))
+ path.addLine(to: CGPoint(x: 1.02562*width, y: -0.04132*height))
+ path.addCurve(to: CGPoint(x: 1.03017*width, y: -0.00848*height), control1: CGPoint(x: 1.02438*width, y: -0.02649*height), control2: CGPoint(x: 1.02542*width, y: -0.01801*height))
+ path.addCurve(to: CGPoint(x: 1.05538*width, y: 0.01271*height), control1: CGPoint(x: 1.03348*width, y: -0.00106*height), control2: CGPoint(x: 1.04154*width, y: 0.01271*height))
+ path.addCurve(to: CGPoint(x: 1.11801*width, y: -0.19917*height), control1: CGPoint(x: 1.08411*width, y: 0.01271*height), control2: CGPoint(x: 1.10891*width, y: -0.05615*height))
+ path.addCurve(to: CGPoint(x: 1.11573*width, y: -0.22248*height), control1: CGPoint(x: 1.11883*width, y: -0.21188*height), control2: CGPoint(x: 1.1178*width, y: -0.2193*height))
+ path.closeSubpath()
+ path.move(to: CGPoint(x: 1.18208*width, y: -0.214*height))
+ path.addCurve(to: CGPoint(x: 1.14695*width, y: -0.09641*height), control1: CGPoint(x: 1.17423*width, y: -0.13137*height), control2: CGPoint(x: 1.15956*width, y: -0.09641*height))
+ path.addCurve(to: CGPoint(x: 1.12959*width, y: -0.14302*height), control1: CGPoint(x: 1.13806*width, y: -0.09641*height), control2: CGPoint(x: 1.13248*width, y: -0.11442*height))
+ path.addCurve(to: CGPoint(x: 1.17712*width, y: -0.28604*height), control1: CGPoint(x: 1.14757*width, y: -0.1409*height), control2: CGPoint(x: 1.17237*width, y: -0.18434*height))
+ path.addCurve(to: CGPoint(x: 1.14984*width, y: -0.43648*height), control1: CGPoint(x: 1.18126*width, y: -0.37186*height), control2: CGPoint(x: 1.16844*width, y: -0.43648*height))
+ path.addCurve(to: CGPoint(x: 1.10148*width, y: -0.23095*height), control1: CGPoint(x: 1.12918*width, y: -0.43648*height), control2: CGPoint(x: 1.10644*width, y: -0.35702*height))
+ path.addCurve(to: CGPoint(x: 1.1424*width, y: 0.01271*height), control1: CGPoint(x: 1.0959*width, y: -0.09323*height), control2: CGPoint(x: 1.11533*width, y: 0.01271*height))
+ path.addCurve(to: CGPoint(x: 1.198*width, y: -0.19917*height), control1: CGPoint(x: 1.16824*width, y: 0.01271*height), control2: CGPoint(x: 1.18973*width, y: -0.08263*height))
+ path.addCurve(to: CGPoint(x: 1.19573*width, y: -0.22248*height), control1: CGPoint(x: 1.19883*width, y: -0.21082*height), control2: CGPoint(x: 1.19821*width, y: -0.21824*height))
+ path.addLine(to: CGPoint(x: 1.18766*width, y: -0.23519*height))
+ path.addCurve(to: CGPoint(x: 1.18208*width, y: -0.214*height), control1: CGPoint(x: 1.18518*width, y: -0.23943*height), control2: CGPoint(x: 1.18415*width, y: -0.23519*height))
+ path.closeSubpath()
+ path.move(to: CGPoint(x: 1.12773*width, y: -0.20659*height))
+ path.addCurve(to: CGPoint(x: 1.12794*width, y: -0.214*height), control1: CGPoint(x: 1.12773*width, y: -0.20871*height), control2: CGPoint(x: 1.12794*width, y: -0.21188*height))
+ path.addCurve(to: CGPoint(x: 1.14922*width, y: -0.32736*height), control1: CGPoint(x: 1.13021*width, y: -0.27863*height), control2: CGPoint(x: 1.14137*width, y: -0.32736*height))
+ path.addCurve(to: CGPoint(x: 1.15563*width, y: -0.2977*height), control1: CGPoint(x: 1.15336*width, y: -0.32736*height), control2: CGPoint(x: 1.15563*width, y: -0.31465*height))
+ path.addCurve(to: CGPoint(x: 1.12773*width, y: -0.20659*height), control1: CGPoint(x: 1.15563*width, y: -0.25426*height), control2: CGPoint(x: 1.14178*width, y: -0.20976*height))
+ path.closeSubpath()
+ path.move(to: CGPoint(x: 1.21075*width, y: 0.01271*height))
+ path.addCurve(to: CGPoint(x: 1.24237*width, y: -0.0678*height), control1: CGPoint(x: 1.22604*width, y: 0.01271*height), control2: CGPoint(x: 1.23782*width, y: -0.0339*height))
+ path.addCurve(to: CGPoint(x: 1.26552*width, y: 0.01271*height), control1: CGPoint(x: 1.24547*width, y: -0.01907*height), control2: CGPoint(x: 1.25373*width, y: 0.01271*height))
+ path.addCurve(to: CGPoint(x: 1.30644*width, y: -0.19917*height), control1: CGPoint(x: 1.28866*width, y: 0.01271*height), control2: CGPoint(x: 1.30189*width, y: -0.11018*height))
+ path.addCurve(to: CGPoint(x: 1.30416*width, y: -0.22248*height), control1: CGPoint(x: 1.30706*width, y: -0.21082*height), control2: CGPoint(x: 1.30664*width, y: -0.21824*height))
+ path.addLine(to: CGPoint(x: 1.2961*width, y: -0.23519*height))
+ path.addCurve(to: CGPoint(x: 1.29135*width, y: -0.22248*height), control1: CGPoint(x: 1.29362*width, y: -0.23943*height), control2: CGPoint(x: 1.29218*width, y: -0.23519*height))
+ path.addCurve(to: CGPoint(x: 1.27192*width, y: -0.09429*height), control1: CGPoint(x: 1.28846*width, y: -0.18328*height), control2: CGPoint(x: 1.28143*width, y: -0.09429*height))
+ path.addCurve(to: CGPoint(x: 1.26717*width, y: -0.18434*height), control1: CGPoint(x: 1.26614*width, y: -0.09429*height), control2: CGPoint(x: 1.26552*width, y: -0.12819*height))
+ path.addLine(to: CGPoint(x: 1.27378*width, y: -0.40258*height))
+ path.addCurve(to: CGPoint(x: 1.27027*width, y: -0.42377*height), control1: CGPoint(x: 1.2742*width, y: -0.41423*height), control2: CGPoint(x: 1.27275*width, y: -0.42377*height))
+ path.addLine(to: CGPoint(x: 1.25373*width, y: -0.42377*height))
+ path.addCurve(to: CGPoint(x: 1.24898*width, y: -0.40258*height), control1: CGPoint(x: 1.25125*width, y: -0.42377*height), control2: CGPoint(x: 1.24939*width, y: -0.41529*height))
+ path.addLine(to: CGPoint(x: 1.24877*width, y: -0.39622*height))
+ path.addCurve(to: CGPoint(x: 1.22769*width, y: -0.43648*height), control1: CGPoint(x: 1.24547*width, y: -0.41105*height), control2: CGPoint(x: 1.23885*width, y: -0.43648*height))
+ path.addCurve(to: CGPoint(x: 1.18264*width, y: -0.24473*height), control1: CGPoint(x: 1.20765*width, y: -0.43648*height), control2: CGPoint(x: 1.18925*width, y: -0.35596*height))
+ path.addCurve(to: CGPoint(x: 1.21075*width, y: 0.01271*height), control1: CGPoint(x: 1.1752*width, y: -0.11865*height), control2: CGPoint(x: 1.18512*width, y: 0.01271*height))
+ path.closeSubpath()
+ path.move(to: CGPoint(x: 1.24567*width, y: -0.29346*height))
+ path.addCurve(to: CGPoint(x: 1.21901*width, y: -0.09429*height), control1: CGPoint(x: 1.24278*width, y: -0.18858*height), control2: CGPoint(x: 1.23183*width, y: -0.09429*height))
+ path.addCurve(to: CGPoint(x: 1.20806*width, y: -0.21294*height), control1: CGPoint(x: 1.2093*width, y: -0.09429*height), control2: CGPoint(x: 1.20496*width, y: -0.15044*height))
+ path.addCurve(to: CGPoint(x: 1.23245*width, y: -0.3263*height), control1: CGPoint(x: 1.21178*width, y: -0.28604*height), control2: CGPoint(x: 1.22232*width, y: -0.3263*height))
+ path.addCurve(to: CGPoint(x: 1.24567*width, y: -0.29346*height), control1: CGPoint(x: 1.23968*width, y: -0.3263*height), control2: CGPoint(x: 1.24361*width, y: -0.30511*height))
+ path.closeSubpath()
+ path.move(to: CGPoint(x: 1.36936*width, y: -0.22248*height))
+ path.addCurve(to: CGPoint(x: 1.34807*width, y: -0.09429*height), control1: CGPoint(x: 1.36626*width, y: -0.17904*height), control2: CGPoint(x: 1.35799*width, y: -0.09429*height))
+ path.addCurve(to: CGPoint(x: 1.34228*width, y: -0.16527*height), control1: CGPoint(x: 1.34331*width, y: -0.09429*height), control2: CGPoint(x: 1.34001*width, y: -0.11442*height))
+ path.addCurve(to: CGPoint(x: 1.34972*width, y: -0.31253*height), control1: CGPoint(x: 1.34497*width, y: -0.22777*height), control2: CGPoint(x: 1.34931*width, y: -0.30405*height))
+ path.addCurve(to: CGPoint(x: 1.34393*width, y: -0.38245*height), control1: CGPoint(x: 1.35117*width, y: -0.34749*height), control2: CGPoint(x: 1.35075*width, y: -0.36656*height))
+ path.addCurve(to: CGPoint(x: 1.315*width, y: -0.44813*height), control1: CGPoint(x: 1.3336*width, y: -0.40576*height), control2: CGPoint(x: 1.32099*width, y: -0.42589*height))
+ path.addCurve(to: CGPoint(x: 1.30322*width, y: -0.52017*height), control1: CGPoint(x: 1.31541*width, y: -0.48945*height), control2: CGPoint(x: 1.31128*width, y: -0.52441*height))
+ path.addCurve(to: CGPoint(x: 1.29495*width, y: -0.40258*height), control1: CGPoint(x: 1.29392*width, y: -0.51594*height), control2: CGPoint(x: 1.28999*width, y: -0.46508*height))
+ path.addCurve(to: CGPoint(x: 1.29061*width, y: -0.21506*height), control1: CGPoint(x: 1.29619*width, y: -0.3655*height), control2: CGPoint(x: 1.29537*width, y: -0.27969*height))
+ path.addCurve(to: CGPoint(x: 1.29206*width, y: -0.19493*height), control1: CGPoint(x: 1.28979*width, y: -0.20553*height), control2: CGPoint(x: 1.2902*width, y: -0.19811*height))
+ path.addLine(to: CGPoint(x: 1.30177*width, y: -0.17586*height))
+ path.addCurve(to: CGPoint(x: 1.30549*width, y: -0.18434*height), control1: CGPoint(x: 1.30363*width, y: -0.17268*height), control2: CGPoint(x: 1.30487*width, y: -0.17374*height))
+ path.addCurve(to: CGPoint(x: 1.31211*width, y: -0.35279*height), control1: CGPoint(x: 1.30859*width, y: -0.23519*height), control2: CGPoint(x: 1.31087*width, y: -0.29346*height))
+ path.addLine(to: CGPoint(x: 1.32017*width, y: -0.33372*height))
+ path.addCurve(to: CGPoint(x: 1.32347*width, y: -0.29664*height), control1: CGPoint(x: 1.32347*width, y: -0.3263*height), control2: CGPoint(x: 1.32451*width, y: -0.31783*height))
+ path.addCurve(to: CGPoint(x: 1.31583*width, y: -0.14408*height), control1: CGPoint(x: 1.32079*width, y: -0.23625*height), control2: CGPoint(x: 1.31748*width, y: -0.19387*height))
+ path.addCurve(to: CGPoint(x: 1.3429*width, y: 0.01271*height), control1: CGPoint(x: 1.31335*width, y: -0.06145*height), control2: CGPoint(x: 1.32554*width, y: 0.01271*height))
+ path.addCurve(to: CGPoint(x: 1.38444*width, y: -0.19917*height), control1: CGPoint(x: 1.36543*width, y: 0.01271*height), control2: CGPoint(x: 1.3799*width, y: -0.11018*height))
+ path.addCurve(to: CGPoint(x: 1.38217*width, y: -0.22248*height), control1: CGPoint(x: 1.38506*width, y: -0.21082*height), control2: CGPoint(x: 1.38465*width, y: -0.21824*height))
+ path.addLine(to: CGPoint(x: 1.37411*width, y: -0.23519*height))
+ path.addCurve(to: CGPoint(x: 1.36936*width, y: -0.22248*height), control1: CGPoint(x: 1.37163*width, y: -0.23943*height), control2: CGPoint(x: 1.37018*width, y: -0.23519*height))
+ path.closeSubpath()
+ path.move(to: CGPoint(x: 1.45862*width, y: -0.22248*height))
+ path.addLine(to: CGPoint(x: 1.45056*width, y: -0.23519*height))
+ path.addCurve(to: CGPoint(x: 1.44539*width, y: -0.21824*height), control1: CGPoint(x: 1.44787*width, y: -0.23943*height), control2: CGPoint(x: 1.44705*width, y: -0.23413*height))
+ path.addCurve(to: CGPoint(x: 1.41253*width, y: -0.09429*height), control1: CGPoint(x: 1.4363*width, y: -0.13666*height), control2: CGPoint(x: 1.42514*width, y: -0.09429*height))
+ path.addCurve(to: CGPoint(x: 1.39455*width, y: -0.22036*height), control1: CGPoint(x: 1.39703*width, y: -0.09429*height), control2: CGPoint(x: 1.39166*width, y: -0.15573*height))
+ path.addCurve(to: CGPoint(x: 1.41956*width, y: -0.32948*height), control1: CGPoint(x: 1.39744*width, y: -0.28498*height), control2: CGPoint(x: 1.40778*width, y: -0.32948*height))
+ path.addCurve(to: CGPoint(x: 1.42968*width, y: -0.31783*height), control1: CGPoint(x: 1.42348*width, y: -0.32948*height), control2: CGPoint(x: 1.427*width, y: -0.32418*height))
+ path.addCurve(to: CGPoint(x: 1.43506*width, y: -0.32312*height), control1: CGPoint(x: 1.43216*width, y: -0.31253*height), control2: CGPoint(x: 1.4332*width, y: -0.31253*height))
+ path.addLine(to: CGPoint(x: 1.44126*width, y: -0.3602*height))
+ path.addCurve(to: CGPoint(x: 1.44043*width, y: -0.38987*height), control1: CGPoint(x: 1.44271*width, y: -0.36974*height), control2: CGPoint(x: 1.44312*width, y: -0.37609*height))
+ path.addCurve(to: CGPoint(x: 1.4148*width, y: -0.43648*height), control1: CGPoint(x: 1.43568*width, y: -0.41211*height), control2: CGPoint(x: 1.42576*width, y: -0.43648*height))
+ path.addCurve(to: CGPoint(x: 1.36768*width, y: -0.2246*height), control1: CGPoint(x: 1.39455*width, y: -0.43648*height), control2: CGPoint(x: 1.37202*width, y: -0.3549*height))
+ path.addCurve(to: CGPoint(x: 1.40592*width, y: 0.01271*height), control1: CGPoint(x: 1.36355*width, y: -0.09535*height), control2: CGPoint(x: 1.37926*width, y: 0.01271*height))
+ path.addCurve(to: CGPoint(x: 1.46089*width, y: -0.19917*height), control1: CGPoint(x: 1.42968*width, y: 0.01271*height), control2: CGPoint(x: 1.45139*width, y: -0.0731*height))
+ path.addCurve(to: CGPoint(x: 1.45862*width, y: -0.22248*height), control1: CGPoint(x: 1.46172*width, y: -0.21082*height), control2: CGPoint(x: 1.4611*width, y: -0.21824*height))
+ path.closeSubpath()
+ path.move(to: CGPoint(x: 1.52396*width, y: -0.22354*height))
+ path.addLine(to: CGPoint(x: 1.5159*width, y: -0.23625*height))
+ path.addCurve(to: CGPoint(x: 1.51114*width, y: -0.22566*height), control1: CGPoint(x: 1.51362*width, y: -0.23943*height), control2: CGPoint(x: 1.51218*width, y: -0.23731*height))
+ path.addCurve(to: CGPoint(x: 1.48428*width, y: -0.09641*height), control1: CGPoint(x: 1.50164*width, y: -0.12395*height), control2: CGPoint(x: 1.4911*width, y: -0.09641*height))
+ path.addCurve(to: CGPoint(x: 1.46692*width, y: -0.25214*height), control1: CGPoint(x: 1.47312*width, y: -0.09641*height), control2: CGPoint(x: 1.46754*width, y: -0.17374*height))
+ path.addCurve(to: CGPoint(x: 1.51301*width, y: -0.44284*height), control1: CGPoint(x: 1.48325*width, y: -0.27333*height), control2: CGPoint(x: 1.50267*width, y: -0.33372*height))
+ path.addCurve(to: CGPoint(x: 1.49813*width, y: -0.70133*height), control1: CGPoint(x: 1.52231*width, y: -0.54242*height), control2: CGPoint(x: 1.52603*width, y: -0.70133*height))
+ path.addCurve(to: CGPoint(x: 1.44584*width, y: -0.36762*height), control1: CGPoint(x: 1.47705*width, y: -0.70133*height), control2: CGPoint(x: 1.45452*width, y: -0.57314*height))
+ path.addCurve(to: CGPoint(x: 1.47829*width, y: 0.01271*height), control1: CGPoint(x: 1.43922*width, y: -0.21188*height), control2: CGPoint(x: 1.44294*width, y: 0.01271*height))
+ path.addCurve(to: CGPoint(x: 1.52499*width, y: -0.18964*height), control1: CGPoint(x: 1.49854*width, y: 0.01271*height), control2: CGPoint(x: 1.51569*width, y: -0.06145*height))
+ path.addCurve(to: CGPoint(x: 1.52396*width, y: -0.22354*height), control1: CGPoint(x: 1.52665*width, y: -0.21082*height), control2: CGPoint(x: 1.52685*width, y: -0.21824*height))
+ path.closeSubpath()
+ path.move(to: CGPoint(x: 1.46878*width, y: -0.34961*height))
+ path.addCurve(to: CGPoint(x: 1.49523*width, y: -0.59221*height), control1: CGPoint(x: 1.47394*width, y: -0.47462*height), control2: CGPoint(x: 1.488*width, y: -0.5901*height))
+ path.addCurve(to: CGPoint(x: 1.49296*width, y: -0.46508*height), control1: CGPoint(x: 1.5006*width, y: -0.59327*height), control2: CGPoint(x: 1.49957*width, y: -0.52759*height))
+ path.addCurve(to: CGPoint(x: 1.46878*width, y: -0.34961*height), control1: CGPoint(x: 1.48655*width, y: -0.40258*height), control2: CGPoint(x: 1.47684*width, y: -0.36444*height))
+ path.closeSubpath()
+ path.move(to: CGPoint(x: 1.58615*width, y: -0.214*height))
+ path.addCurve(to: CGPoint(x: 1.55101*width, y: -0.09641*height), control1: CGPoint(x: 1.5783*width, y: -0.13137*height), control2: CGPoint(x: 1.56362*width, y: -0.09641*height))
+ path.addCurve(to: CGPoint(x: 1.53366*width, y: -0.14302*height), control1: CGPoint(x: 1.54213*width, y: -0.09641*height), control2: CGPoint(x: 1.53655*width, y: -0.11442*height))
+ path.addCurve(to: CGPoint(x: 1.58119*width, y: -0.28604*height), control1: CGPoint(x: 1.55164*width, y: -0.1409*height), control2: CGPoint(x: 1.57644*width, y: -0.18434*height))
+ path.addCurve(to: CGPoint(x: 1.55391*width, y: -0.43648*height), control1: CGPoint(x: 1.58533*width, y: -0.37186*height), control2: CGPoint(x: 1.57251*width, y: -0.43648*height))
+ path.addCurve(to: CGPoint(x: 1.50555*width, y: -0.23095*height), control1: CGPoint(x: 1.53324*width, y: -0.43648*height), control2: CGPoint(x: 1.51051*width, y: -0.35702*height))
+ path.addCurve(to: CGPoint(x: 1.54647*width, y: 0.01271*height), control1: CGPoint(x: 1.49997*width, y: -0.09323*height), control2: CGPoint(x: 1.5194*width, y: 0.01271*height))
+ path.addCurve(to: CGPoint(x: 1.60207*width, y: -0.19917*height), control1: CGPoint(x: 1.57231*width, y: 0.01271*height), control2: CGPoint(x: 1.5938*width, y: -0.08263*height))
+ path.addCurve(to: CGPoint(x: 1.59979*width, y: -0.22248*height), control1: CGPoint(x: 1.60289*width, y: -0.21082*height), control2: CGPoint(x: 1.60227*width, y: -0.21824*height))
+ path.addLine(to: CGPoint(x: 1.59173*width, y: -0.23519*height))
+ path.addCurve(to: CGPoint(x: 1.58615*width, y: -0.214*height), control1: CGPoint(x: 1.58925*width, y: -0.23943*height), control2: CGPoint(x: 1.58822*width, y: -0.23519*height))
+ path.closeSubpath()
+ path.move(to: CGPoint(x: 1.5318*width, y: -0.20659*height))
+ path.addCurve(to: CGPoint(x: 1.532*width, y: -0.214*height), control1: CGPoint(x: 1.5318*width, y: -0.20871*height), control2: CGPoint(x: 1.532*width, y: -0.21188*height))
+ path.addCurve(to: CGPoint(x: 1.55329*width, y: -0.32736*height), control1: CGPoint(x: 1.53428*width, y: -0.27863*height), control2: CGPoint(x: 1.54544*width, y: -0.32736*height))
+ path.addCurve(to: CGPoint(x: 1.5597*width, y: -0.2977*height), control1: CGPoint(x: 1.55742*width, y: -0.32736*height), control2: CGPoint(x: 1.5597*width, y: -0.31465*height))
+ path.addCurve(to: CGPoint(x: 1.5318*width, y: -0.20659*height), control1: CGPoint(x: 1.5597*width, y: -0.25426*height), control2: CGPoint(x: 1.54585*width, y: -0.20976*height))
+ path.closeSubpath()
+ path.move(to: CGPoint(x: 1.61481*width, y: 0.01271*height))
+ path.addCurve(to: CGPoint(x: 1.64643*width, y: -0.0678*height), control1: CGPoint(x: 1.63011*width, y: 0.01271*height), control2: CGPoint(x: 1.64189*width, y: -0.0339*height))
+ path.addCurve(to: CGPoint(x: 1.66958*width, y: 0.01271*height), control1: CGPoint(x: 1.64953*width, y: -0.01907*height), control2: CGPoint(x: 1.6578*width, y: 0.01271*height))
+ path.addCurve(to: CGPoint(x: 1.7105*width, y: -0.19917*height), control1: CGPoint(x: 1.69273*width, y: 0.01271*height), control2: CGPoint(x: 1.70596*width, y: -0.11018*height))
+ path.addCurve(to: CGPoint(x: 1.70823*width, y: -0.22248*height), control1: CGPoint(x: 1.71112*width, y: -0.21082*height), control2: CGPoint(x: 1.71071*width, y: -0.21824*height))
+ path.addLine(to: CGPoint(x: 1.70017*width, y: -0.23519*height))
+ path.addCurve(to: CGPoint(x: 1.69542*width, y: -0.22248*height), control1: CGPoint(x: 1.69769*width, y: -0.23943*height), control2: CGPoint(x: 1.69624*width, y: -0.23519*height))
+ path.addCurve(to: CGPoint(x: 1.67599*width, y: -0.09429*height), control1: CGPoint(x: 1.69252*width, y: -0.18328*height), control2: CGPoint(x: 1.68549*width, y: -0.09429*height))
+ path.addCurve(to: CGPoint(x: 1.67123*width, y: -0.18434*height), control1: CGPoint(x: 1.6702*width, y: -0.09429*height), control2: CGPoint(x: 1.66958*width, y: -0.12819*height))
+ path.addLine(to: CGPoint(x: 1.67785*width, y: -0.40258*height))
+ path.addCurve(to: CGPoint(x: 1.67433*width, y: -0.42377*height), control1: CGPoint(x: 1.67826*width, y: -0.41423*height), control2: CGPoint(x: 1.67681*width, y: -0.42377*height))
+ path.addLine(to: CGPoint(x: 1.6578*width, y: -0.42377*height))
+ path.addCurve(to: CGPoint(x: 1.65305*width, y: -0.40258*height), control1: CGPoint(x: 1.65532*width, y: -0.42377*height), control2: CGPoint(x: 1.65346*width, y: -0.41529*height))
+ path.addLine(to: CGPoint(x: 1.65284*width, y: -0.39622*height))
+ path.addCurve(to: CGPoint(x: 1.63176*width, y: -0.43648*height), control1: CGPoint(x: 1.64953*width, y: -0.41105*height), control2: CGPoint(x: 1.64292*width, y: -0.43648*height))
+ path.addCurve(to: CGPoint(x: 1.5867*width, y: -0.24473*height), control1: CGPoint(x: 1.61171*width, y: -0.43648*height), control2: CGPoint(x: 1.59332*width, y: -0.35596*height))
+ path.addCurve(to: CGPoint(x: 1.61481*width, y: 0.01271*height), control1: CGPoint(x: 1.57926*width, y: -0.11865*height), control2: CGPoint(x: 1.58918*width, y: 0.01271*height))
+ path.closeSubpath()
+ path.move(to: CGPoint(x: 1.64974*width, y: -0.29346*height))
+ path.addCurve(to: CGPoint(x: 1.62308*width, y: -0.09429*height), control1: CGPoint(x: 1.64685*width, y: -0.18858*height), control2: CGPoint(x: 1.63589*width, y: -0.09429*height))
+ path.addCurve(to: CGPoint(x: 1.61212*width, y: -0.21294*height), control1: CGPoint(x: 1.61336*width, y: -0.09429*height), control2: CGPoint(x: 1.60902*width, y: -0.15044*height))
+ path.addCurve(to: CGPoint(x: 1.63651*width, y: -0.3263*height), control1: CGPoint(x: 1.61585*width, y: -0.28604*height), control2: CGPoint(x: 1.62639*width, y: -0.3263*height))
+ path.addCurve(to: CGPoint(x: 1.64974*width, y: -0.29346*height), control1: CGPoint(x: 1.64375*width, y: -0.3263*height), control2: CGPoint(x: 1.64767*width, y: -0.30511*height))
+ path.closeSubpath()
+ path.move(to: CGPoint(x: 1.68931*width, y: -0.02119*height))
+ path.addCurve(to: CGPoint(x: 1.69282*width, y: 0), control1: CGPoint(x: 1.6891*width, y: -0.00953*height), control2: CGPoint(x: 1.69055*width, y: 0))
+ path.addLine(to: CGPoint(x: 1.70935*width, y: 0))
+ path.addCurve(to: CGPoint(x: 1.71411*width, y: -0.02119*height), control1: CGPoint(x: 1.71225*width, y: 0), control2: CGPoint(x: 1.71369*width, y: -0.00848*height))
+ path.addLine(to: CGPoint(x: 1.71597*width, y: -0.08263*height))
+ path.addCurve(to: CGPoint(x: 1.74366*width, y: -0.31677*height), control1: CGPoint(x: 1.71886*width, y: -0.17798*height), control2: CGPoint(x: 1.73292*width, y: -0.31677*height))
+ path.addCurve(to: CGPoint(x: 1.748*width, y: -0.2532*height), control1: CGPoint(x: 1.74821*width, y: -0.31677*height), control2: CGPoint(x: 1.74924*width, y: -0.29346*height))
+ path.addLine(to: CGPoint(x: 1.7447*width, y: -0.14832*height))
+ path.addCurve(to: CGPoint(x: 1.76888*width, y: 0.01271*height), control1: CGPoint(x: 1.74201*width, y: -0.05933*height), control2: CGPoint(x: 1.7511*width, y: 0.01271*height))
+ path.addCurve(to: CGPoint(x: 1.8098*width, y: -0.19917*height), control1: CGPoint(x: 1.79202*width, y: 0.01271*height), control2: CGPoint(x: 1.80525*width, y: -0.11018*height))
+ path.addCurve(to: CGPoint(x: 1.80753*width, y: -0.22248*height), control1: CGPoint(x: 1.81042*width, y: -0.21082*height), control2: CGPoint(x: 1.81001*width, y: -0.21824*height))
+ path.addLine(to: CGPoint(x: 1.79946*width, y: -0.23519*height))
+ path.addCurve(to: CGPoint(x: 1.79471*width, y: -0.22248*height), control1: CGPoint(x: 1.79698*width, y: -0.23943*height), control2: CGPoint(x: 1.79574*width, y: -0.23519*height))
+ path.addCurve(to: CGPoint(x: 1.77528*width, y: -0.09429*height), control1: CGPoint(x: 1.79161*width, y: -0.18328*height), control2: CGPoint(x: 1.78479*width, y: -0.09429*height))
+ path.addCurve(to: CGPoint(x: 1.77053*width, y: -0.18434*height), control1: CGPoint(x: 1.7695*width, y: -0.09429*height), control2: CGPoint(x: 1.76888*width, y: -0.12819*height))
+ path.addLine(to: CGPoint(x: 1.7728*width, y: -0.26274*height))
+ path.addCurve(to: CGPoint(x: 1.75358*width, y: -0.43648*height), control1: CGPoint(x: 1.7759*width, y: -0.35914*height), control2: CGPoint(x: 1.77032*width, y: -0.43648*height))
+ path.addCurve(to: CGPoint(x: 1.72403*width, y: -0.34749*height), control1: CGPoint(x: 1.74056*width, y: -0.43648*height), control2: CGPoint(x: 1.73044*width, y: -0.39092*height))
+ path.addLine(to: CGPoint(x: 1.72568*width, y: -0.40258*height))
+ path.addCurve(to: CGPoint(x: 1.72217*width, y: -0.42377*height), control1: CGPoint(x: 1.7261*width, y: -0.41423*height), control2: CGPoint(x: 1.72465*width, y: -0.42377*height))
+ path.addLine(to: CGPoint(x: 1.70584*width, y: -0.42377*height))
+ path.addCurve(to: CGPoint(x: 1.70109*width, y: -0.40258*height), control1: CGPoint(x: 1.70336*width, y: -0.42377*height), control2: CGPoint(x: 1.7015*width, y: -0.41529*height))
+ path.closeSubpath()
+ path.move(to: CGPoint(x: 1.87397*width, y: -0.214*height))
+ path.addCurve(to: CGPoint(x: 1.83883*width, y: -0.09641*height), control1: CGPoint(x: 1.86611*width, y: -0.13137*height), control2: CGPoint(x: 1.85144*width, y: -0.09641*height))
+ path.addCurve(to: CGPoint(x: 1.82147*width, y: -0.14302*height), control1: CGPoint(x: 1.82994*width, y: -0.09641*height), control2: CGPoint(x: 1.82436*width, y: -0.11442*height))
+ path.addCurve(to: CGPoint(x: 1.86901*width, y: -0.28604*height), control1: CGPoint(x: 1.83945*width, y: -0.1409*height), control2: CGPoint(x: 1.86425*width, y: -0.18434*height))
+ path.addCurve(to: CGPoint(x: 1.84172*width, y: -0.43648*height), control1: CGPoint(x: 1.87314*width, y: -0.37186*height), control2: CGPoint(x: 1.86032*width, y: -0.43648*height))
+ path.addCurve(to: CGPoint(x: 1.79336*width, y: -0.23095*height), control1: CGPoint(x: 1.82105*width, y: -0.43648*height), control2: CGPoint(x: 1.79832*width, y: -0.35702*height))
+ path.addCurve(to: CGPoint(x: 1.83428*width, y: 0.01271*height), control1: CGPoint(x: 1.78778*width, y: -0.09323*height), control2: CGPoint(x: 1.80721*width, y: 0.01271*height))
+ path.addCurve(to: CGPoint(x: 1.88988*width, y: -0.19917*height), control1: CGPoint(x: 1.86012*width, y: 0.01271*height), control2: CGPoint(x: 1.88161*width, y: -0.08263*height))
+ path.addCurve(to: CGPoint(x: 1.8876*width, y: -0.22248*height), control1: CGPoint(x: 1.8907*width, y: -0.21082*height), control2: CGPoint(x: 1.89008*width, y: -0.21824*height))
+ path.addLine(to: CGPoint(x: 1.87954*width, y: -0.23519*height))
+ path.addCurve(to: CGPoint(x: 1.87396*width, y: -0.214*height), control1: CGPoint(x: 1.87706*width, y: -0.23943*height), control2: CGPoint(x: 1.87603*width, y: -0.23519*height))
+ path.closeSubpath()
+ path.move(to: CGPoint(x: 1.81961*width, y: -0.20659*height))
+ path.addCurve(to: CGPoint(x: 1.81982*width, y: -0.214*height), control1: CGPoint(x: 1.81961*width, y: -0.20871*height), control2: CGPoint(x: 1.81982*width, y: -0.21188*height))
+ path.addCurve(to: CGPoint(x: 1.8411*width, y: -0.32736*height), control1: CGPoint(x: 1.82209*width, y: -0.27863*height), control2: CGPoint(x: 1.83325*width, y: -0.32736*height))
+ path.addCurve(to: CGPoint(x: 1.84751*width, y: -0.2977*height), control1: CGPoint(x: 1.84524*width, y: -0.32736*height), control2: CGPoint(x: 1.84751*width, y: -0.31465*height))
+ path.addCurve(to: CGPoint(x: 1.81961*width, y: -0.20659*height), control1: CGPoint(x: 1.84751*width, y: -0.25426*height), control2: CGPoint(x: 1.83366*width, y: -0.20976*height))
+ path.closeSubpath()
+ path.move(to: CGPoint(x: 1.95305*width, y: -0.22248*height))
+ path.addCurve(to: CGPoint(x: 1.93177*width, y: -0.09429*height), control1: CGPoint(x: 1.94995*width, y: -0.17904*height), control2: CGPoint(x: 1.94169*width, y: -0.09429*height))
+ path.addCurve(to: CGPoint(x: 1.92598*width, y: -0.16527*height), control1: CGPoint(x: 1.92701*width, y: -0.09429*height), control2: CGPoint(x: 1.92371*width, y: -0.11442*height))
+ path.addCurve(to: CGPoint(x: 1.93342*width, y: -0.31253*height), control1: CGPoint(x: 1.92867*width, y: -0.22777*height), control2: CGPoint(x: 1.93301*width, y: -0.30405*height))
+ path.addCurve(to: CGPoint(x: 1.92763*width, y: -0.38245*height), control1: CGPoint(x: 1.93487*width, y: -0.34749*height), control2: CGPoint(x: 1.93445*width, y: -0.36656*height))
+ path.addCurve(to: CGPoint(x: 1.8987*width, y: -0.44813*height), control1: CGPoint(x: 1.9173*width, y: -0.40576*height), control2: CGPoint(x: 1.90469*width, y: -0.42589*height))
+ path.addCurve(to: CGPoint(x: 1.88692*width, y: -0.52017*height), control1: CGPoint(x: 1.89911*width, y: -0.48945*height), control2: CGPoint(x: 1.89498*width, y: -0.52441*height))
+ path.addCurve(to: CGPoint(x: 1.87865*width, y: -0.40258*height), control1: CGPoint(x: 1.87762*width, y: -0.51594*height), control2: CGPoint(x: 1.87369*width, y: -0.46508*height))
+ path.addCurve(to: CGPoint(x: 1.87431*width, y: -0.21506*height), control1: CGPoint(x: 1.87989*width, y: -0.3655*height), control2: CGPoint(x: 1.87906*width, y: -0.27969*height))
+ path.addCurve(to: CGPoint(x: 1.87576*width, y: -0.19493*height), control1: CGPoint(x: 1.87348*width, y: -0.20553*height), control2: CGPoint(x: 1.8739*width, y: -0.19811*height))
+ path.addLine(to: CGPoint(x: 1.88547*width, y: -0.17586*height))
+ path.addCurve(to: CGPoint(x: 1.88919*width, y: -0.18434*height), control1: CGPoint(x: 1.88733*width, y: -0.17268*height), control2: CGPoint(x: 1.88857*width, y: -0.17374*height))
+ path.addCurve(to: CGPoint(x: 1.8958*width, y: -0.35279*height), control1: CGPoint(x: 1.89229*width, y: -0.23519*height), control2: CGPoint(x: 1.89456*width, y: -0.29346*height))
+ path.addLine(to: CGPoint(x: 1.90386*width, y: -0.33372*height))
+ path.addCurve(to: CGPoint(x: 1.90717*width, y: -0.29664*height), control1: CGPoint(x: 1.90717*width, y: -0.3263*height), control2: CGPoint(x: 1.9082*width, y: -0.31783*height))
+ path.addCurve(to: CGPoint(x: 1.89952*width, y: -0.14408*height), control1: CGPoint(x: 1.90448*width, y: -0.23625*height), control2: CGPoint(x: 1.90118*width, y: -0.19387*height))
+ path.addCurve(to: CGPoint(x: 1.9266*width, y: 0.01271*height), control1: CGPoint(x: 1.89704*width, y: -0.06145*height), control2: CGPoint(x: 1.90924*width, y: 0.01271*height))
+ path.addCurve(to: CGPoint(x: 1.96814*width, y: -0.19917*height), control1: CGPoint(x: 1.94913*width, y: 0.01271*height), control2: CGPoint(x: 1.96359*width, y: -0.11018*height))
+ path.addCurve(to: CGPoint(x: 1.96587*width, y: -0.22248*height), control1: CGPoint(x: 1.96876*width, y: -0.21082*height), control2: CGPoint(x: 1.96835*width, y: -0.21824*height))
+ path.addLine(to: CGPoint(x: 1.95781*width, y: -0.23519*height))
+ path.addCurve(to: CGPoint(x: 1.95305*width, y: -0.22248*height), control1: CGPoint(x: 1.95533*width, y: -0.23943*height), control2: CGPoint(x: 1.95388*width, y: -0.23519*height))
+ path.closeSubpath()
+ return path
+ }
+}
+
+// Ghost
+struct Ghost: Shape {
+ func path(in rect: CGRect) -> Path {
+ var path = Path()
+ let width = rect.size.width
+ let height = rect.size.height
+ path.move(to: CGPoint(x: 0.47*width, y: 0.0815*height))
+ path.addCurve(to: CGPoint(x: 0.3148*width, y: 0.1708*height), control1: CGPoint(x: 0.4054*width, y: 0.0893*height), control2: CGPoint(x: 0.353*width, y: 0.1195*height))
+ path.addCurve(to: CGPoint(x: 0.2539*width, y: 0.3318*height), control1: CGPoint(x: 0.2841*width, y: 0.2119*height), control2: CGPoint(x: 0.2634*width, y: 0.2668*height))
+ path.addCurve(to: CGPoint(x: 0.2517*width, y: 0.4035*height), control1: CGPoint(x: 0.2515*width, y: 0.3483*height), control2: CGPoint(x: 0.2504*width, y: 0.3846*height))
+ path.addCurve(to: CGPoint(x: 0.2525*width, y: 0.4167*height), control1: CGPoint(x: 0.2522*width, y: 0.4106*height), control2: CGPoint(x: 0.2526*width, y: 0.4166*height))
+ path.addCurve(to: CGPoint(x: 0.2447*width, y: 0.4189*height), control1: CGPoint(x: 0.2524*width, y: 0.4169*height), control2: CGPoint(x: 0.2489*width, y: 0.4179*height))
+ path.addCurve(to: CGPoint(x: 0.1841*width, y: 0.4547*height), control1: CGPoint(x: 0.2245*width, y: 0.4241*height), control2: CGPoint(x: 0.2051*width, y: 0.4355*height))
+ path.addCurve(to: CGPoint(x: 0.15*width, y: 0.513*height), control1: CGPoint(x: 0.1646*width, y: 0.4723*height), control2: CGPoint(x: 0.15*width, y: 0.4974*height))
+ path.addCurve(to: CGPoint(x: 0.1569*width, y: 0.5309*height), control1: CGPoint(x: 0.15*width, y: 0.5195*height), control2: CGPoint(x: 0.1528*width, y: 0.5268*height))
+ path.addCurve(to: CGPoint(x: 0.2085*width, y: 0.5309*height), control1: CGPoint(x: 0.165*width, y: 0.539*height), control2: CGPoint(x: 0.1775*width, y: 0.539*height))
+ path.addCurve(to: CGPoint(x: 0.264*width, y: 0.5252*height), control1: CGPoint(x: 0.2538*width, y: 0.5191*height), control2: CGPoint(x: 0.2539*width, y: 0.5191*height))
+ path.addCurve(to: CGPoint(x: 0.272*width, y: 0.5335*height), control1: CGPoint(x: 0.2701*width, y: 0.529*height), control2: CGPoint(x: 0.2711*width, y: 0.53*height))
+ path.addCurve(to: CGPoint(x: 0.4178*width, y: 0.7853*height), control1: CGPoint(x: 0.3036*width, y: 0.6544*height), control2: CGPoint(x: 0.355*width, y: 0.7433*height))
+ path.addCurve(to: CGPoint(x: 0.5902*width, y: 0.7895*height), control1: CGPoint(x: 0.4685*width, y: 0.8193*height), control2: CGPoint(x: 0.5271*width, y: 0.8207*height))
+ path.addCurve(to: CGPoint(x: 0.6492*width, y: 0.7534*height), control1: CGPoint(x: 0.6081*width, y: 0.7807*height), control2: CGPoint(x: 0.6235*width, y: 0.7713*height))
+ path.addCurve(to: CGPoint(x: 0.7453*width, y: 0.709*height), control1: CGPoint(x: 0.7026*width, y: 0.7165*height), control2: CGPoint(x: 0.7188*width, y: 0.709*height))
+ path.addCurve(to: CGPoint(x: 0.8012*width, y: 0.7341*height), control1: CGPoint(x: 0.7664*width, y: 0.709*height), control2: CGPoint(x: 0.7842*width, y: 0.717*height))
+ path.addCurve(to: CGPoint(x: 0.816*width, y: 0.744*height), control1: CGPoint(x: 0.81*width, y: 0.7429*height), control2: CGPoint(x: 0.8116*width, y: 0.744*height))
+ path.addCurve(to: CGPoint(x: 0.8269*width, y: 0.7335*height), control1: CGPoint(x: 0.8201*width, y: 0.744*height), control2: CGPoint(x: 0.8254*width, y: 0.7389*height))
+ path.addCurve(to: CGPoint(x: 0.8265*width, y: 0.707*height), control1: CGPoint(x: 0.8284*width, y: 0.7278*height), control2: CGPoint(x: 0.8283*width, y: 0.7154*height))
+ path.addCurve(to: CGPoint(x: 0.7937*width, y: 0.6544*height), control1: CGPoint(x: 0.8223*width, y: 0.687*height), control2: CGPoint(x: 0.8115*width, y: 0.6696*height))
+ path.addCurve(to: CGPoint(x: 0.7576*width, y: 0.6304*height), control1: CGPoint(x: 0.7808*width, y: 0.6433*height), control2: CGPoint(x: 0.7711*width, y: 0.6369*height))
+ path.addCurve(to: CGPoint(x: 0.6647*width, y: 0.6285*height), control1: CGPoint(x: 0.725*width, y: 0.6149*height), control2: CGPoint(x: 0.6977*width, y: 0.6143*height))
+ path.addCurve(to: CGPoint(x: 0.6694*width, y: 0.6162*height), control1: CGPoint(x: 0.6633*width, y: 0.6291*height), control2: CGPoint(x: 0.6646*width, y: 0.6257*height))
+ path.addCurve(to: CGPoint(x: 0.7549*width, y: 0.3918*height), control1: CGPoint(x: 0.7139*width, y: 0.5273*height), control2: CGPoint(x: 0.7443*width, y: 0.4478*height))
+ path.addCurve(to: CGPoint(x: 0.759*width, y: 0.31*height), control1: CGPoint(x: 0.76*width, y: 0.3651*height), control2: CGPoint(x: 0.7616*width, y: 0.3337*height))
+ path.addCurve(to: CGPoint(x: 0.6846*width, y: 0.1554*height), control1: CGPoint(x: 0.7528*width, y: 0.252*height), control2: CGPoint(x: 0.7264*width, y: 0.1972*height))
+ path.addCurve(to: CGPoint(x: 0.5329*width, y: 0.0815*height), control1: CGPoint(x: 0.6431*width, y: 0.114*height), control2: CGPoint(x: 0.5913*width, y: 0.0888*height))
+ path.addCurve(to: CGPoint(x: 0.47*width, y: 0.0815*height), control1: CGPoint(x: 0.518*width, y: 0.0797*height), control2: CGPoint(x: 0.4851*width, y: 0.0797*height))
+ path.closeSubpath()
+ path.move(to: CGPoint(x: 0.534*width, y: 0.0985*height))
+ path.addCurve(to: CGPoint(x: 0.6525*width, y: 0.1488*height), control1: CGPoint(x: 0.577*width, y: 0.1042*height), control2: CGPoint(x: 0.6175*width, y: 0.1214*height))
+ path.addCurve(to: CGPoint(x: 0.6948*width, y: 0.1922*height), control1: CGPoint(x: 0.663*width, y: 0.157*height), control2: CGPoint(x: 0.6865*width, y: 0.1812*height))
+ path.addCurve(to: CGPoint(x: 0.741*width, y: 0.374*height), control1: CGPoint(x: 0.7344*width, y: 0.245*height), control2: CGPoint(x: 0.7506*width, y: 0.3087*height))
+ path.addCurve(to: CGPoint(x: 0.6504*width, y: 0.6166*height), control1: CGPoint(x: 0.732*width, y: 0.435*height), control2: CGPoint(x: 0.7061*width, y: 0.5045*height))
+ path.addCurve(to: CGPoint(x: 0.6311*width, y: 0.6609*height), control1: CGPoint(x: 0.6311*width, y: 0.6556*height), control2: CGPoint(x: 0.6301*width, y: 0.6579*height))
+ path.addCurve(to: CGPoint(x: 0.6403*width, y: 0.6664*height), control1: CGPoint(x: 0.6324*width, y: 0.6646*height), control2: CGPoint(x: 0.6371*width, y: 0.6674*height))
+ path.addCurve(to: CGPoint(x: 0.6505*width, y: 0.6588*height), control1: CGPoint(x: 0.6415*width, y: 0.666*height), control2: CGPoint(x: 0.6461*width, y: 0.6626*height))
+ path.addCurve(to: CGPoint(x: 0.704*width, y: 0.6353*height), control1: CGPoint(x: 0.6679*width, y: 0.6436*height), control2: CGPoint(x: 0.685*width, y: 0.6362*height))
+ path.addCurve(to: CGPoint(x: 0.7833*width, y: 0.6673*height), control1: CGPoint(x: 0.7305*width, y: 0.634*height), control2: CGPoint(x: 0.757*width, y: 0.6447*height))
+ path.addCurve(to: CGPoint(x: 0.8108*width, y: 0.7143*height), control1: CGPoint(x: 0.7983*width, y: 0.6803*height), control2: CGPoint(x: 0.8081*width, y: 0.6969*height))
+ path.addCurve(to: CGPoint(x: 0.8101*width, y: 0.719*height), control1: CGPoint(x: 0.8116*width, y: 0.7193*height), control2: CGPoint(x: 0.8115*width, y: 0.7198*height))
+ path.addCurve(to: CGPoint(x: 0.8065*width, y: 0.716*height), control1: CGPoint(x: 0.8092*width, y: 0.7186*height), control2: CGPoint(x: 0.8076*width, y: 0.7172*height))
+ path.addCurve(to: CGPoint(x: 0.7675*width, y: 0.6948*height), control1: CGPoint(x: 0.8*width, y: 0.7088*height), control2: CGPoint(x: 0.7812*width, y: 0.6986*height))
+ path.addCurve(to: CGPoint(x: 0.7209*width, y: 0.6953*height), control1: CGPoint(x: 0.7554*width, y: 0.6916*height), control2: CGPoint(x: 0.7341*width, y: 0.6918*height))
+ path.addCurve(to: CGPoint(x: 0.6629*width, y: 0.724*height), control1: CGPoint(x: 0.7013*width, y: 0.7005*height), control2: CGPoint(x: 0.6868*width, y: 0.7076*height))
+ path.addCurve(to: CGPoint(x: 0.5548*width, y: 0.7864*height), control1: CGPoint(x: 0.5998*width, y: 0.7672*height), control2: CGPoint(x: 0.583*width, y: 0.7769*height))
+ path.addCurve(to: CGPoint(x: 0.508*width, y: 0.7947*height), control1: CGPoint(x: 0.5376*width, y: 0.7921*height), control2: CGPoint(x: 0.5265*width, y: 0.7941*height))
+ path.addCurve(to: CGPoint(x: 0.4626*width, y: 0.7888*height), control1: CGPoint(x: 0.489*width, y: 0.7953*height), control2: CGPoint(x: 0.4788*width, y: 0.794*height))
+ path.addCurve(to: CGPoint(x: 0.3776*width, y: 0.725*height), control1: CGPoint(x: 0.4334*width, y: 0.7796*height), control2: CGPoint(x: 0.4033*width, y: 0.757*height))
+ path.addCurve(to: CGPoint(x: 0.3451*width, y: 0.6763*height), control1: CGPoint(x: 0.3696*width, y: 0.715*height), control2: CGPoint(x: 0.3525*width, y: 0.6894*height))
+ path.addCurve(to: CGPoint(x: 0.2886*width, y: 0.5293*height), control1: CGPoint(x: 0.3234*width, y: 0.638*height), control2: CGPoint(x: 0.3017*width, y: 0.5813*height))
+ path.addCurve(to: CGPoint(x: 0.2683*width, y: 0.3864*height), control1: CGPoint(x: 0.2758*width, y: 0.4785*height), control2: CGPoint(x: 0.2692*width, y: 0.4321*height))
+ path.addCurve(to: CGPoint(x: 0.2726*width, y: 0.3205*height), control1: CGPoint(x: 0.2677*width, y: 0.3559*height), control2: CGPoint(x: 0.2684*width, y: 0.3444*height))
+ path.addCurve(to: CGPoint(x: 0.4818*width, y: 0.097*height), control1: CGPoint(x: 0.2961*width, y: 0.1862*height), control2: CGPoint(x: 0.3708*width, y: 0.1063*height))
+ path.addCurve(to: CGPoint(x: 0.534*width, y: 0.0985*height), control1: CGPoint(x: 0.4951*width, y: 0.0959*height), control2: CGPoint(x: 0.5201*width, y: 0.0966*height))
+ path.closeSubpath()
+ path.move(to: CGPoint(x: 0.2544*width, y: 0.4372*height))
+ path.addCurve(to: CGPoint(x: 0.2575*width, y: 0.4589*height), control1: CGPoint(x: 0.2547*width, y: 0.439*height), control2: CGPoint(x: 0.2561*width, y: 0.4488*height))
+ path.addCurve(to: CGPoint(x: 0.2656*width, y: 0.5051*height), control1: CGPoint(x: 0.2597*width, y: 0.4749*height), control2: CGPoint(x: 0.2642*width, y: 0.5003*height))
+ path.addCurve(to: CGPoint(x: 0.2618*width, y: 0.5053*height), control1: CGPoint(x: 0.266*width, y: 0.5067*height), control2: CGPoint(x: 0.2657*width, y: 0.5067*height))
+ path.addCurve(to: CGPoint(x: 0.2233*width, y: 0.5101*height), control1: CGPoint(x: 0.2554*width, y: 0.503*height), control2: CGPoint(x: 0.246*width, y: 0.5042*height))
+ path.addCurve(to: CGPoint(x: 0.1775*width, y: 0.5205*height), control1: CGPoint(x: 0.1947*width, y: 0.5175*height), control2: CGPoint(x: 0.1846*width, y: 0.5198*height))
+ path.addCurve(to: CGPoint(x: 0.169*width, y: 0.519*height), control1: CGPoint(x: 0.1718*width, y: 0.5211*height), control2: CGPoint(x: 0.171*width, y: 0.521*height))
+ path.addCurve(to: CGPoint(x: 0.1791*width, y: 0.4849*height), control1: CGPoint(x: 0.1641*width, y: 0.5141*height), control2: CGPoint(x: 0.1686*width, y: 0.499*height))
+ path.addCurve(to: CGPoint(x: 0.2285*width, y: 0.4427*height), control1: CGPoint(x: 0.1894*width, y: 0.4709*height), control2: CGPoint(x: 0.2135*width, y: 0.4504*height))
+ path.addCurve(to: CGPoint(x: 0.2527*width, y: 0.434*height), control1: CGPoint(x: 0.2342*width, y: 0.4398*height), control2: CGPoint(x: 0.2501*width, y: 0.4341*height))
+ path.addCurve(to: CGPoint(x: 0.2544*width, y: 0.4372*height), control1: CGPoint(x: 0.2534*width, y: 0.434*height), control2: CGPoint(x: 0.2541*width, y: 0.4355*height))
+ path.closeSubpath()
+ path.move(to: CGPoint(x: 0.4813*width, y: 0.2149*height))
+ path.addCurve(to: CGPoint(x: 0.4432*width, y: 0.2634*height), control1: CGPoint(x: 0.4651*width, y: 0.2208*height), control2: CGPoint(x: 0.4513*width, y: 0.2383*height))
+ path.addCurve(to: CGPoint(x: 0.4356*width, y: 0.312*height), control1: CGPoint(x: 0.4376*width, y: 0.2805*height), control2: CGPoint(x: 0.4356*width, y: 0.2933*height))
+ path.addCurve(to: CGPoint(x: 0.4404*width, y: 0.3387*height), control1: CGPoint(x: 0.4356*width, y: 0.3288*height), control2: CGPoint(x: 0.4366*width, y: 0.3342*height))
+ path.addCurve(to: CGPoint(x: 0.4655*width, y: 0.3364*height), control1: CGPoint(x: 0.4439*width, y: 0.3427*height), control2: CGPoint(x: 0.4488*width, y: 0.3423*height))
+ path.addCurve(to: CGPoint(x: 0.492*width, y: 0.3315*height), control1: CGPoint(x: 0.4793*width, y: 0.3316*height), control2: CGPoint(x: 0.4796*width, y: 0.3316*height))
+ path.addCurve(to: CGPoint(x: 0.5195*width, y: 0.3324*height), control1: CGPoint(x: 0.4989*width, y: 0.3315*height), control2: CGPoint(x: 0.5113*width, y: 0.3319*height))
+ path.addCurve(to: CGPoint(x: 0.5466*width, y: 0.3276*height), control1: CGPoint(x: 0.5369*width, y: 0.3334*height), control2: CGPoint(x: 0.5416*width, y: 0.3326*height))
+ path.addCurve(to: CGPoint(x: 0.552*width, y: 0.2905*height), control1: CGPoint(x: 0.5526*width, y: 0.3216*height), control2: CGPoint(x: 0.5542*width, y: 0.3104*height))
+ path.addCurve(to: CGPoint(x: 0.516*width, y: 0.22*height), control1: CGPoint(x: 0.5484*width, y: 0.2589*height), control2: CGPoint(x: 0.5346*width, y: 0.2319*height))
+ path.addCurve(to: CGPoint(x: 0.4813*width, y: 0.2149*height), control1: CGPoint(x: 0.5055*width, y: 0.2133*height), control2: CGPoint(x: 0.4916*width, y: 0.2112*height))
+ path.closeSubpath()
+ path.move(to: CGPoint(x: 0.3452*width, y: 0.2165*height))
+ path.addCurve(to: CGPoint(x: 0.306*width, y: 0.2848*height), control1: CGPoint(x: 0.3275*width, y: 0.2247*height), control2: CGPoint(x: 0.3104*width, y: 0.2544*height))
+ path.addCurve(to: CGPoint(x: 0.3052*width, y: 0.3052*height), control1: CGPoint(x: 0.3053*width, y: 0.2892*height), control2: CGPoint(x: 0.305*width, y: 0.2984*height))
+ path.addCurve(to: CGPoint(x: 0.3129*width, y: 0.3277*height), control1: CGPoint(x: 0.3056*width, y: 0.3193*height), control2: CGPoint(x: 0.3072*width, y: 0.3239*height))
+ path.addCurve(to: CGPoint(x: 0.3367*width, y: 0.3279*height), control1: CGPoint(x: 0.3171*width, y: 0.3306*height), control2: CGPoint(x: 0.3204*width, y: 0.3306*height))
+ path.addCurve(to: CGPoint(x: 0.3708*width, y: 0.3305*height), control1: CGPoint(x: 0.3529*width, y: 0.3252*height), control2: CGPoint(x: 0.3592*width, y: 0.3257*height))
+ path.addCurve(to: CGPoint(x: 0.3942*width, y: 0.3371*height), control1: CGPoint(x: 0.3862*width, y: 0.3369*height), control2: CGPoint(x: 0.3912*width, y: 0.3383*height))
+ path.addCurve(to: CGPoint(x: 0.3985*width, y: 0.3331*height), control1: CGPoint(x: 0.3957*width, y: 0.3366*height), control2: CGPoint(x: 0.3976*width, y: 0.3347*height))
+ path.addCurve(to: CGPoint(x: 0.3964*width, y: 0.2647*height), control1: CGPoint(x: 0.4036*width, y: 0.3231*height), control2: CGPoint(x: 0.4026*width, y: 0.2895*height))
+ path.addCurve(to: CGPoint(x: 0.3657*width, y: 0.2155*height), control1: CGPoint(x: 0.39*width, y: 0.2389*height), control2: CGPoint(x: 0.3788*width, y: 0.221*height))
+ path.addCurve(to: CGPoint(x: 0.3452*width, y: 0.2165*height), control1: CGPoint(x: 0.3601*width, y: 0.2132*height), control2: CGPoint(x: 0.3514*width, y: 0.2136*height))
+ path.closeSubpath()
+ path.move(to: CGPoint(x: 0.4847*width, y: 0.4141*height))
+ path.addCurve(to: CGPoint(x: 0.4028*width, y: 0.4568*height), control1: CGPoint(x: 0.4575*width, y: 0.417*height), control2: CGPoint(x: 0.4287*width, y: 0.432*height))
+ path.addCurve(to: CGPoint(x: 0.372*width, y: 0.5039*height), control1: CGPoint(x: 0.3864*width, y: 0.4726*height), control2: CGPoint(x: 0.3753*width, y: 0.4895*height))
+ path.addCurve(to: CGPoint(x: 0.3808*width, y: 0.5335*height), control1: CGPoint(x: 0.3692*width, y: 0.5161*height), control2: CGPoint(x: 0.3726*width, y: 0.5277*height))
+ path.addCurve(to: CGPoint(x: 0.4297*width, y: 0.5298*height), control1: CGPoint(x: 0.389*width, y: 0.5392*height), control2: CGPoint(x: 0.4035*width, y: 0.5381*height))
+ path.addCurve(to: CGPoint(x: 0.463*width, y: 0.521*height), control1: CGPoint(x: 0.4497*width, y: 0.5235*height), control2: CGPoint(x: 0.4592*width, y: 0.521*height))
+ path.addCurve(to: CGPoint(x: 0.4835*width, y: 0.5305*height), control1: CGPoint(x: 0.4655*width, y: 0.521*height), control2: CGPoint(x: 0.4712*width, y: 0.5237*height))
+ path.addLine(to: CGPoint(x: 0.5004*width, y: 0.5399*height))
+ path.addLine(to: CGPoint(x: 0.5042*width, y: 0.5332*height))
+ path.addCurve(to: CGPoint(x: 0.508*width, y: 0.5258*height), control1: CGPoint(x: 0.5063*width, y: 0.5295*height), control2: CGPoint(x: 0.508*width, y: 0.5262*height))
+ path.addCurve(to: CGPoint(x: 0.4681*width, y: 0.5044*height), control1: CGPoint(x: 0.508*width, y: 0.5246*height), control2: CGPoint(x: 0.4716*width, y: 0.505*height))
+ path.addCurve(to: CGPoint(x: 0.4329*width, y: 0.5114*height), control1: CGPoint(x: 0.4626*width, y: 0.5033*height), control2: CGPoint(x: 0.4517*width, y: 0.5055*height))
+ path.addCurve(to: CGPoint(x: 0.3905*width, y: 0.5196*height), control1: CGPoint(x: 0.4037*width, y: 0.5206*height), control2: CGPoint(x: 0.3946*width, y: 0.5223*height))
+ path.addCurve(to: CGPoint(x: 0.388*width, y: 0.5122*height), control1: CGPoint(x: 0.3883*width, y: 0.5182*height), control2: CGPoint(x: 0.388*width, y: 0.5172*height))
+ path.addCurve(to: CGPoint(x: 0.4404*width, y: 0.448*height), control1: CGPoint(x: 0.3881*width, y: 0.4946*height), control2: CGPoint(x: 0.4088*width, y: 0.4693*height))
+ path.addCurve(to: CGPoint(x: 0.4875*width, y: 0.4309*height), control1: CGPoint(x: 0.4554*width, y: 0.438*height), control2: CGPoint(x: 0.4691*width, y: 0.433*height))
+ path.addCurve(to: CGPoint(x: 0.5648*width, y: 0.4481*height), control1: CGPoint(x: 0.5106*width, y: 0.4283*height), control2: CGPoint(x: 0.5396*width, y: 0.4347*height))
+ path.addCurve(to: CGPoint(x: 0.5766*width, y: 0.453*height), control1: CGPoint(x: 0.5699*width, y: 0.4508*height), control2: CGPoint(x: 0.5752*width, y: 0.453*height))
+ path.addCurve(to: CGPoint(x: 0.584*width, y: 0.445*height), control1: CGPoint(x: 0.5801*width, y: 0.453*height), control2: CGPoint(x: 0.584*width, y: 0.4488*height))
+ path.addCurve(to: CGPoint(x: 0.5658*width, y: 0.43*height), control1: CGPoint(x: 0.584*width, y: 0.4402*height), control2: CGPoint(x: 0.5797*width, y: 0.4367*height))
+ path.addCurve(to: CGPoint(x: 0.5108*width, y: 0.414*height), control1: CGPoint(x: 0.5463*width, y: 0.4206*height), control2: CGPoint(x: 0.5309*width, y: 0.4161*height))
+ path.addCurve(to: CGPoint(x: 0.4847*width, y: 0.4141*height), control1: CGPoint(x: 0.5003*width, y: 0.4129*height), control2: CGPoint(x: 0.4958*width, y: 0.4129*height))
+ path.closeSubpath()
+ path.move(to: CGPoint(x: 0.4665*width, y: 0.893*height))
+ path.addCurve(to: CGPoint(x: 0.3071*width, y: 0.9064*height), control1: CGPoint(x: 0.3762*width, y: 0.8947*height), control2: CGPoint(x: 0.3136*width, y: 0.8999*height))
+ path.addCurve(to: CGPoint(x: 0.3093*width, y: 0.9095*height), control1: CGPoint(x: 0.3058*width, y: 0.9078*height), control2: CGPoint(x: 0.306*width, y: 0.9081*height))
+ path.addCurve(to: CGPoint(x: 0.6915*width, y: 0.9164*height), control1: CGPoint(x: 0.3396*width, y: 0.9221*height), control2: CGPoint(x: 0.5731*width, y: 0.9264*height))
+ path.addCurve(to: CGPoint(x: 0.7394*width, y: 0.9075*height), control1: CGPoint(x: 0.7204*width, y: 0.914*height), control2: CGPoint(x: 0.7389*width, y: 0.9105*height))
+ path.addCurve(to: CGPoint(x: 0.4665*width, y: 0.893*height), control1: CGPoint(x: 0.7408*width, y: 0.8978*height), control2: CGPoint(x: 0.604*width, y: 0.8905*height))
+ path.closeSubpath()
+ return path
+ }
+}
+
+// Aurora
+struct AuroraView: View {
+
+ private enum AnimationProperties {
+ static let animationSpeed: Double = 4
+ static let timerDuration: TimeInterval = 3
+ static let blurRadius: CGFloat = 130
+ }
+
+ @State private var timer = Timer.publish(every: AnimationProperties.timerDuration, on: .main, in: .common).autoconnect()
+ @ObservedObject private var animator = CircleAnimator(colors: AuroraColors.all)
+
+ var body: some View {
+ ZStack {
+ ZStack {
+ ForEach(animator.circles) { circle in
+ MovingCircle(originOffset: circle.position)
+ .foregroundColor(circle.color)
+ }
+ }.blur(radius: AnimationProperties.blurRadius)
+ }
+ .onDisappear {
+ timer.upstream.connect().cancel()
+ }
+ .onAppear {
+ animateCircles()
+ timer = Timer.publish(every: AnimationProperties.timerDuration, on: .main, in: .common).autoconnect()
+ }
+ .onReceive(timer) { _ in
+ animateCircles()
+ }
+ }
+
+ private func animateCircles() {
+ withAnimation(.easeInOut(duration: AnimationProperties.animationSpeed)) {
+ animator.animate()
+ }
+ }
+
+}
+
+private enum AuroraColors {
+ static var all: [Color] {
+ [
+ // Color(red: 0/255, green: 0/255, blue: 128/255), // Dark Blue
+ Color.purple, // Purple
+ Color.pink, // Pink
+ Color.orange, // Orange
+ ]
+ }
+}
+
+private struct MovingCircle: Shape {
+
+ var originOffset: CGPoint
+
+ var animatableData: CGPoint.AnimatableData {
+ get {
+ originOffset.animatableData
+ }
+ set {
+ originOffset.animatableData = newValue
+ }
+ }
+
+ func path(in rect: CGRect) -> Path {
+ var path = Path()
+
+ let adjustedX = rect.width * originOffset.x
+ let adjustedY = rect.height * originOffset.y
+ let smallestDimension = min(rect.width, rect.height)
+ path.addArc(center: CGPoint(x: adjustedX, y: adjustedY), radius: smallestDimension/2, startAngle: .zero, endAngle: .degrees(360), clockwise: true)
+ return path
+ }
+}
+
+private class CircleAnimator: ObservableObject {
+ class Circle: Identifiable {
+ internal init(position: CGPoint, color: Color) {
+ self.position = position
+ self.color = color
+ }
+ var position: CGPoint
+ let id = UUID().uuidString
+ let color: Color
+ }
+
+ @Published private(set) var circles: [Circle] = []
+
+
+ init(colors: [Color]) {
+ circles = colors.map({ color in
+ Circle(position: CircleAnimator.generateRandomPosition(), color: color)
+ })
+ }
+
+ func animate() {
+ objectWillChange.send()
+ for circle in circles {
+ circle.position = CircleAnimator.generateRandomPosition()
+ }
+ }
+
+ static func generateRandomPosition() -> CGPoint {
+ CGPoint(x: CGFloat.random(in: 0 ... 1), y: CGFloat.random(in: 0 ... 1))
+ }
+}
+
+// Ghost View
+// ZStack {
+// LinearGradient(colors: [.pink, .orange], startPoint: .topLeading, endPoint: .bottomTrailing)
+// .edgesIgnoringSafeArea(.all)
+// .hueRotation(.degrees(animateGradient ? 45 : 0))
+// .onAppear {
+// withAnimation(.easeInOut(duration: 3).repeatForever(autoreverses: true)) {
+// animateGradient.toggle()
+// }
+// }
+// .mask(
+// Ghost()
+// .frame(width: 200, height: 200)
+// )
+// }
+
+// Gradient Background
+//ZStack {
+// LinearGradient(colors: [.pink, .orange], startPoint: .topLeading, endPoint: .bottomTrailing)
+// .edgesIgnoringSafeArea(.all)
+// .hueRotation(.degrees(animateGradient ? 45 : 0))
+// .onAppear {
+// withAnimation(.easeInOut(duration: 3).repeatForever(autoreverses: true)) {
+// animateGradient.toggle()
+// }
+// }
+// .mask(
+// Ghost()
+// .frame(width: 200, height: 200)
+// )
+//}
diff --git a/Pearcleaner/Logic/Styles.swift b/Pearcleaner/Logic/Styles.swift
new file mode 100644
index 0000000..34f1393
--- /dev/null
+++ b/Pearcleaner/Logic/Styles.swift
@@ -0,0 +1,418 @@
+//
+// Styles.swift
+// Pearcleaner
+//
+// Created by Alin Lupascu on 10/31/23.
+//
+
+import Foundation
+import SwiftUI
+
+struct SimpleButtonStyle: ButtonStyle {
+ @State private var hovered = false
+ let icon: String
+ let help: String
+ let color: Color
+
+ func makeBody(configuration: Self.Configuration) -> some View {
+ HStack {
+ Image(systemName: icon)
+ .resizable()
+ .scaledToFit()
+ .frame(width: 20)
+ .foregroundColor(color)
+ }
+ .padding(8)
+ .background {
+ if hovered {
+// Circle()
+// .strokeBorder(Color("AccentColor"), lineWidth: 1)
+ RoundedRectangle(cornerRadius: 8)
+ .strokeBorder(Color("AccentColor"), lineWidth: 1)
+ }
+
+ }
+ .onHover { hovering in
+ withAnimation() {
+ hovered = hovering
+ }
+ }
+ .help(help)
+ }
+}
+
+
+struct LabeledDivider: View {
+ let label: String
+ @EnvironmentObject var appState: AppState
+
+ var body: some View {
+ HStack(spacing: 0) {
+ Rectangle()
+ .frame(height: 1)
+ .foregroundColor(Color("AccentColor").opacity(0.2))
+
+ Text(label)
+ .textCase(.uppercase)
+ .font(.title2)
+ .foregroundColor(Color("AccentColor").opacity(0.6))
+ .padding(.horizontal, 10)
+ .frame(minWidth: 80)
+
+ Rectangle()
+ .frame(height: 1)
+ .foregroundColor(Color("AccentColor").opacity(0.2))
+ }
+ .frame(minHeight: 35)
+ }
+}
+
+
+struct AnimatedSearchStyle: TextFieldStyle {
+ @State private var isHovered = false
+ @State var icon: Image?
+ @State var trash: Bool = false
+ @Binding var text: String
+
+ func _body(configuration: TextField) -> some View {
+
+ HStack(spacing: 5) {
+
+ if isHovered || !text.isEmpty {
+ HStack(spacing: 5) {
+ if icon != nil {
+ icon
+ .foregroundColor(Color("AccentColor").opacity(0.5))
+ Image(systemName: "chevron.right")
+ .foregroundColor(Color("AccentColor").opacity(0.3))
+ .controlSize(.small)
+
+ }
+
+ configuration
+ .frame(maxWidth: 300)
+ .font(.title3)
+ .textFieldStyle(PlainTextFieldStyle())
+ if trash {
+ Image(systemName: "xmark")
+ .resizable()
+ .aspectRatio(contentMode: .fit)
+ .frame(width: 10, height: 10)
+ .padding(.trailing, 5)
+ .foregroundStyle(text.isEmpty ? Color.clear : (isHovered ? Color("AccentColor").opacity(0.8) : Color("AccentColor").opacity(0.5)))
+ .onTapGesture {
+ text = ""
+ }
+ }
+ }
+ } else {
+ Image(systemName: "magnifyingglass")
+ .foregroundColor(Color("AccentColor").opacity(0.5))
+ .padding(.trailing, 150)
+
+ }
+ }
+ .padding(6)
+ .overlay(
+ Group {
+ if isHovered || !text.isEmpty {
+ RoundedRectangle(cornerRadius: 6)
+ .strokeBorder(Color("AccentColor").opacity(0.4), lineWidth: 1)
+ .allowsHitTesting(false)
+ }
+ }
+ )
+ .onHover { hovering in
+ withAnimation(Animation.easeIn(duration: 0.15)) {
+ self.isHovered = hovering
+ }
+ }
+ }
+}
+
+
+struct SimpleSearchStyle: TextFieldStyle {
+ @State private var isHovered = false
+ @State var icon: Image?
+ @State var trash: Bool = false
+ @Binding var text: String
+
+ func _body(configuration: TextField) -> some View {
+
+ HStack(spacing: 5) {
+
+ HStack(spacing: 5) {
+ if icon != nil {
+ icon
+ .foregroundColor(Color("mode").opacity(0.5))
+ }
+
+ configuration
+ .frame(maxWidth: 300)
+ .font(.title3)
+ .textFieldStyle(PlainTextFieldStyle())
+ if trash {
+ 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 {
+ text = ""
+ }
+ .opacity(text.isEmpty ? 0 : 1)
+ }
+ }
+
+ }
+ .padding(6)
+ .overlay(
+ Group {
+ RoundedRectangle(cornerRadius: 6)
+ .strokeBorder(Color("mode").opacity(0.2), lineWidth: 1)
+ .allowsHitTesting(false)
+
+ }
+ )
+ .onHover { hovering in
+ withAnimation(Animation.easeIn(duration: 0.15)) {
+ self.isHovered = hovering
+ }
+ }
+ }
+}
+
+
+struct GlassEffect: NSViewRepresentable {
+ var material: NSVisualEffectView.Material // Choose the material you want
+ var blendingMode: NSVisualEffectView.BlendingMode // Choose the blending mode
+
+ func makeNSView(context: Self.Context) -> NSView {
+ let visualEffectView = NSVisualEffectView()
+ visualEffectView.material = material
+ visualEffectView.blendingMode = blendingMode
+ return visualEffectView
+ }
+
+ func updateNSView(_ nsView: NSView, context: Context) { }
+}
+
+
+
+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("AccentColor") : Color.red :
+ action == .accept ? Color("AccentColor").opacity(0.8) : Color.red.opacity(0.8) :
+ action == .accept ? Color("AccentColor").opacity(0.5) : Color.red.opacity(0.5)
+ )
+ .foregroundColor(.white)
+ .cornerRadius(8)
+ .onHover { hovering in
+ withAnimation(Animation.easeIn(duration: 0.15)) {
+ hovered = hovering
+ }
+ }
+ }
+}
+
+
+
+
+
+
+
+// Picker
+//struct PillPicker: View {
+// @Binding var index: Int
+// var onTapAction: () -> Void
+//
+// var body: some View {
+//
+// HStack(alignment: .center, spacing: 20) {
+// ForEach(0..<3) { i in
+// HStack(spacing: 4) {
+// Image(systemName: i == 0 ? "appclip" : "ellipsis.circle")
+// .resizable()
+// .scaledToFit()
+// .frame(width: 16)
+// .foregroundColor(index == i ? .black : .gray)
+//
+// Text(i == 0 ? "Apps" : (i == 1 ? "Widgets" : "Plugins"))
+// .font(.system(size: 12))
+// .foregroundColor(index == i ? .black : .gray)
+// }
+// .padding(8)
+// .background((Color.white).opacity(index == i ? 1 : 0))
+// .clipShape(Capsule())
+// .onTapGesture {
+// withAnimation {
+// index = i
+// onTapAction()
+// }
+// }
+// }
+// }
+// .background(Color.black.opacity(0.06))
+// .clipShape(Capsule())
+// }
+//}
+
+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) {
+ // Uncomment below for icon next to labels
+// Image(systemName: i == 0 ? "appclip" : (i == 1 ? "macwindow" : "cube.box.fill"))
+// .resizable()
+// .scaledToFit()
+// .frame(width: 16)
+// .foregroundColor(index == i ? .black : .gray)
+
+ Text(i == 0 ? "Apps" : (i == 1 ? "Widgets" : "Plugins"))
+ .font(.system(size: 12))
+ .foregroundColor(index == i ? (colorScheme == .dark ? .black : Color("AccentColor")) : .gray)
+ }
+ .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)
+ )
+ )
+// .background(Color.black.opacity(0.06))
+ .clipShape(Capsule())
+ }
+}
+
+
+
+
+
+
+
+
+
+
+// .frame(height: 50)
+
+// HStack(spacing: 10) {
+// HStack(spacing: 2) {
+// Image(systemName: "appclip")
+// .foregroundColor(index == 0 ? .black : .gray)
+// Text("Apps")
+// .font(.footnote)
+// .foregroundColor(index == 0 ? .black : .gray)
+// }
+// .padding(8)
+// .background((Color.white).opacity(index == 0 ? 1 : 0))
+// .clipShape(Capsule())
+// .onTapGesture {
+// withAnimation {
+// index = 0
+// onTapAction()
+// }
+// }
+//
+// HStack(spacing: 2) {
+// Image(systemName: "ellipsis.circle")
+// .foregroundColor(index == 1 ? .black : .gray)
+// Text("Widgets")
+// .foregroundColor(index == 1 ? .black : .gray)
+//
+//
+// }
+// .padding(8)
+// .background((Color.white).opacity(index == 1 ? 1 : 0))
+// .clipShape(Capsule())
+// .onTapGesture {
+// withAnimation {
+// index = 1
+// onTapAction()
+// }
+// }
+//
+// HStack(spacing: 2) {
+// Image(systemName: "ellipsis.circle")
+// .foregroundColor(index == 2 ? .black : .gray)
+// Text("Plugins")
+// .foregroundColor(index == 2 ? .black : .gray)
+//
+//
+// }
+// .padding(8)
+// .background((Color.white).opacity(index == 2 ? 2 : 0))
+// .clipShape(Capsule())
+// .onTapGesture {
+// withAnimation {
+// index = 2
+// onTapAction()
+// }
+// }
+// }
+// .background(Color.black.opacity(0.06))
+// .clipShape(Capsule())
+// .frame(height: 50)
diff --git a/Pearcleaner/Logic/Utilities.swift b/Pearcleaner/Logic/Utilities.swift
new file mode 100644
index 0000000..dad15f0
--- /dev/null
+++ b/Pearcleaner/Logic/Utilities.swift
@@ -0,0 +1,509 @@
+//
+// Utilities.swift
+// Pearcleaner
+//
+// Created by Alin Lupascu on 11/3/23.
+//
+
+import Foundation
+import SwiftUI
+
+// Make updates on main thread
+func updateOnMain(_ updates: @escaping () -> Void) {
+ DispatchQueue.main.async {
+ updates()
+ }
+}
+
+
+// Execute functions on background thread
+func updateOnBackground(_ updates: @escaping () -> Void) {
+ DispatchQueue.global(qos: .background).async {
+ updates()
+ }
+}
+
+
+// Check for access to Full Disk access
+func checkAndRequestFullDiskAccess(appState: AppState, skipAlert: Bool = false) -> Bool {
+ @AppStorage("settings.permissions.disk") var diskP: Bool = false
+
+ let fileURL = URL(fileURLWithPath: "\(home)/Library/Application Support/com.apple.TCC/TCC.db")
+
+ let accessStatus = FileManager.default.isWritableFile(atPath: fileURL.path)
+ if accessStatus {
+ diskP = true
+ _ = checkAndRequestAccessibilityAccess(appState: appState)
+
+ return true
+ } else {
+ diskP = false
+ if !skipAlert {
+ appState.alertType = .diskAccess
+ appState.showAlert = true
+ }
+
+ return false
+ }
+}
+
+
+// Check for access to System Events
+func checkAndRequestAccessibilityAccess(appState: AppState) -> Bool {
+ @AppStorage("settings.permissions.events") var diskE: Bool = false
+
+ let options: NSDictionary = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: true]
+ let accessEnabled = AXIsProcessTrustedWithOptions(options)
+ if accessEnabled {
+ diskE = true
+ return accessEnabled
+ } else {
+ diskE = false
+ return false
+
+ }
+}
+
+
+
+
+// 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
+ }
+}
+
+
+// Open trash folder
+func openTrash() {
+ if let trashURL = try? FileManager.default.url(for: .trashDirectory, in: .userDomainMask, appropriateFor: nil, create: false) {
+ NSWorkspace.shared.open(trashURL)
+ }
+}
+
+// Check if restricted app
+func isRestricted(atPath path: URL) -> Bool {
+ if path.path.contains("Safari") || path.path.contains(Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String ?? "") {
+ return true
+ } else {
+ return false
+ }
+}
+
+
+// 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
+}
+
+// Get icon for files and folders
+func getIconForFileOrFolder(atPath path: URL) -> Image? {
+ let icon = NSWorkspace.shared.icon(forFile: path.path)
+ let nsImage = icon
+ return Image(nsImage: nsImage)
+}
+
+
+// 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)
+}
+
+
+// --- Extend Int to convert hours to seconds ---
+extension Int {
+ var daysToSeconds: Double {
+ return Double(self) * 24 * 60 * 60
+ }
+}
+
+
+// --- Extend String to remove periods, spaces and lowercase the string
+extension String {
+ func pearFormat() -> String {
+ return self.replacingOccurrences(of: ".", with: "").replacingOccurrences(of: " ", with: "").lowercased()
+ }
+}
+
+
+// --- Trash Relationship ---
+extension FileManager {
+ public func isInTrash(_ file: URL) -> Bool {
+ var relationship: URLRelationship = .other
+ try? getRelationship(&relationship, of: .trashDirectory, in: .userDomainMask, toItemAt: file)
+ return relationship == .contains
+ }
+}
+
+
+// --- Gradient ---
+func schemeGradient(for colorScheme: ColorScheme) -> LinearGradient {
+ return LinearGradient(gradient: Gradient(colors: [.pink, .orange]), startPoint: .leading, endPoint: .trailing)
+// switch colorScheme {
+// case .light:
+// return LinearGradient(gradient: Gradient(colors: [Color("AccentColor")]), startPoint: .leading, endPoint: .trailing)
+// case .dark:
+// return LinearGradient(gradient: Gradient(colors: [.pink, .orange]), startPoint: .leading, endPoint: .trailing)
+// @unknown default:
+// return LinearGradient(gradient: Gradient(colors: [Color("AccentColor")]), startPoint: .leading, endPoint: .trailing)
+// }
+}
+
+
+
+// Get total size of folders and files using DU cli command
+func totalSizeOnDisk(for paths: [URL]) -> String? {
+ var totalSize = 0
+
+ let process = Process()
+ process.launchPath = "/usr/bin/du"
+ process.arguments = ["-sk"] + paths.map { (url: URL) -> String in
+ return url.path
+ }
+ let pipe = Pipe()
+ process.standardOutput = pipe
+
+ try? process.run()
+ process.waitUntilExit()
+
+ let data = pipe.fileHandleForReading.readDataToEndOfFile()
+ if let output = String(data: data, encoding: .utf8) {
+ let lines = output.components(separatedBy: .newlines)
+ for line in lines {
+ let components = line.components(separatedBy: "\t")
+ if let sizeString = components.first, let size = Int(sizeString) {
+ totalSize += size * 1024 // Convert the size from kilobytes to bytes
+ }
+ }
+ }
+
+ let byteCountFormatter = ByteCountFormatter()
+ byteCountFormatter.countStyle = .file
+ byteCountFormatter.allowedUnits = [.useAll]
+ return byteCountFormatter.string(fromByteCount: Int64(totalSize))
+}
+
+func totalSizeOnDisk(for path: URL) -> String? {
+ return totalSizeOnDisk(for: [path])
+}
+
+
+// Get total size of folders and files using straight swift
+extension URL {
+ func totalAllocatedSize(includingSubfolders: Bool = false) throws -> Int? {
+ return try [self].totalAllocatedSize(includingSubfolders: includingSubfolders)
+ }
+
+ func totalSizeOnDisk(includingSubfolders: Bool = false) throws -> String? {
+ return try [self].totalSizeOnDisk(includingSubfolders: includingSubfolders)
+ }
+}
+
+extension Array where Element == URL {
+ func totalAllocatedSize(includingSubfolders: Bool = false) throws -> Int? {
+ var totalSize = 0
+
+ for path in self {
+// if path.absoluteString.contains(".app") {
+ let resourceValues = try path.resourceValues(forKeys: [.isDirectoryKey, .isPackageKey, .totalFileAllocatedSizeKey])
+
+ if resourceValues.isDirectory == true || path.pathExtension == "app" {
+ if includingSubfolders {
+ let filePaths = FileManager.default.subpaths(atPath: path.path) ?? []
+ for filePath in filePaths {
+ let fileUrl = path.appendingPathComponent(filePath)
+ let fileAttributes = try FileManager.default.attributesOfItem(atPath: fileUrl.path)
+ let fileSize = fileAttributes[.size] as? Int64 ?? 0
+ totalSize += Int(fileSize)
+ }
+ }
+ } else {
+ totalSize += resourceValues.totalFileAllocatedSize ?? 0
+ }
+// }
+
+ }
+
+ return totalSize
+ }
+
+ func totalSizeOnDisk(includingSubfolders: Bool = false) throws -> String? {
+ if let totalSize = try self.totalAllocatedSize(includingSubfolders: includingSubfolders) {
+ let byteCountFormatter = ByteCountFormatter()
+ byteCountFormatter.countStyle = .file
+ byteCountFormatter.allowedUnits = [.useBytes, .useKB, .useMB, .useTB]
+ return byteCountFormatter.string(fromByteCount: Int64(totalSize))
+ }
+ return nil
+ }
+}
+
+
+// Alerts
+func presentAlert(appState: AppState) -> Alert {
+
+ switch appState.alertType {
+ case .update:
+ return Alert(title: Text("Update Available 🥳"), message: Text("You may choose to install the update now, otherwise you may start the update later from Settings"), primaryButton: .default(Text("Install")) {
+ downloadUpdate(appState: appState)
+ appState.alertType = .off
+ }, secondaryButton: .cancel())
+ case .no_update:
+ return Alert(title: Text("No Updates 😌"), message: Text("The application is on the latest release available"), primaryButton: .cancel(Text("Okay")), secondaryButton: .default(Text("Force Update")) {
+ downloadUpdate(appState: appState)
+ appState.alertType = .off
+ })
+ case .diskAccess:
+ return Alert(title: Text("Permissions"), message: Text("This application requires Full Disk and Accessibility permissions. Once you grant full disk, don't restart the app yet. Wait a second for a second prompt for accessibility and then restart"), primaryButton: .default(Text("Allow in Settings")) {
+ if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles") {
+ NSWorkspace.shared.open(url)
+ }
+ appState.alertType = .off
+ }, secondaryButton: .cancel(Text("Later")))
+// case .eventsAccess:
+// return Alert(title: Text("Accessibility Permission"), message: Text("This application requires Accessibility permission"), primaryButton: .default(Text("Allow in Settings")) {
+// if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility") {
+// NSWorkspace.shared.open(url)
+// }
+// appState.alertType = .off
+// }, secondaryButton: .cancel(Text("Later")))
+ case .restartApp:
+ return Alert(title: Text("Update Completed!"), message: Text("The application has been updated to the latest version, would you like to restart now?"), primaryButton: .default(Text("Restart")), secondaryButton: .cancel(Text("Later")) {
+ appState.alertType = .off
+ relaunchApp(afterDelay: 1)
+ })
+ case .off:
+ return Alert(title: Text(""))
+ }
+}
+
+
+
+// --- 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("Pearcleaner")
+
+ // Check to make sure Application Support/Support Admin folder exists
+ if !fileManager.fileExists(atPath: supportURL.path) {
+ try! fileManager.createDirectory(at: supportURL, withIntermediateDirectories: true)
+ print("Created Application Support/Pearcleaner folder")
+ }
+}
+
+
+
+func launchctl(load: Bool) {
+ let cmd = load ? "load" : "unload"
+ if let plistPath = Bundle.main.path(forResource: "com.alienator88.PearcleanerSentinel", ofType: "plist") {
+ var plistContent = try! String(contentsOfFile: plistPath)
+ let executableURL = Bundle.main.bundleURL.appendingPathComponent("Contents/MacOS/PearcleanerSentinel")
+
+ // Replace the placeholder with the actual executable path
+ plistContent = plistContent.replacingOccurrences(of: "__EXECUTABLE_PATH__", with: executableURL.path)
+
+ // Create a temporary plist file with the updated content
+ let temporaryPlistURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("com.alienator88.PearcleanerSentinel.plist")
+
+ do {
+ try plistContent.write(to: temporaryPlistURL, atomically: false, encoding: .utf8)
+ } catch {
+ print("Error writing the temporary plist file: \(error)")
+ return
+ }
+ let task = Process()
+ task.launchPath = "/bin/launchctl"
+ task.arguments = [cmd, "-w", temporaryPlistURL.path]
+
+ let pipe = Pipe()
+ task.standardOutput = pipe
+ task.standardError = pipe
+
+ task.launch()
+
+// let data = pipe.fileHandleForReading.readDataToEndOfFile()
+// if let output = String(data: data, encoding: .utf8) {
+// print("Output: \(output)")
+// }
+ }
+}
+
+//func unloadAgent() {
+// if let plistPath = Bundle.main.path(forResource: "com.alienator88.PearcleanerMonitor", ofType: "plist") {
+// var plistContent = try! String(contentsOfFile: plistPath)
+// let executableURL = Bundle.main.bundleURL.appendingPathComponent("Contents/MacOS/PearcleanerMonitor")
+//
+// // Replace the placeholder with the actual executable path
+// plistContent = plistContent.replacingOccurrences(of: "__EXECUTABLE_PATH__", with: executableURL.path)
+//
+// // Create a temporary plist file with the updated content
+// let temporaryPlistURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("com.alienator88.PearcleanerMonitor.plist")
+//
+// do {
+// try plistContent.write(to: temporaryPlistURL, atomically: false, encoding: .utf8)
+// } catch {
+// print("Error writing the temporary plist file: \(error)")
+// return
+// }
+// let task = Process()
+// task.launchPath = "/bin/launchctl"
+// task.arguments = ["unload", "-w", temporaryPlistURL.path]
+//
+// let pipe = Pipe()
+// task.standardOutput = pipe
+// task.standardError = pipe
+//
+// task.launch()
+//
+//// let data = pipe.fileHandleForReading.readDataToEndOfFile()
+//// if let output = String(data: data, encoding: .utf8) {
+//// print("Output: \(output)")
+//// }
+// }
+//}
+
+
+
+
+
+
+
+
+
+
+
+
+
+//import FileWatcher
+//
+//// MARK: https://github.com/eonist/FileWatcher
+//
+//func fileW() {
+// let filewatcher = FileWatcher([NSString(string: "~/.Trash").expandingTildeInPath])
+// filewatcher.queue = DispatchQueue.global()
+// filewatcher.callback = { event in
+// print("Something happened here: " + event.path)
+// }
+//
+// filewatcher.start()
+//}
+
+
+
+//extension Array where Element == URL {
+// // DU shell way of getting sizes
+// func totalAllocatedSize2(includingSubfolders: Bool = false) throws -> Int? {
+// var totalSize = 0
+//
+// for path in self {
+// let process = Process()
+// process.launchPath = "/usr/bin/du"
+// process.arguments = ["-sk", path.path]
+//
+// let pipe = Pipe()
+// process.standardOutput = pipe
+//
+// try process.run()
+// process.waitUntilExit()
+//
+// let data = pipe.fileHandleForReading.readDataToEndOfFile()
+// if let output = String(data: data, encoding: .utf8) {
+// let sizeString = output.components(separatedBy: "\t").first ?? ""
+// if let size = Int(sizeString) {
+// totalSize += size * 1024
+// }
+// }
+// }
+// return totalSize
+// }
+//
+// func totalAllocatedSize(includingSubfolders: Bool = false) throws -> Int? {
+// var totalSize = 0
+//
+// for path in self {
+// if includingSubfolders {
+// guard
+// let urls = FileManager.default.enumerator(at: path, includingPropertiesForKeys: nil)?.allObjects as? [URL] else { return nil }
+// let pathSize = try urls.lazy.reduce(0) {
+// (try $1.resourceValues(forKeys: [.totalFileAllocatedSizeKey]).totalFileAllocatedSize ?? 0) + $0
+// }
+// totalSize += pathSize
+// } else {
+// let pathSize = try FileManager.default.contentsOfDirectory(at: path, includingPropertiesForKeys: nil).lazy.reduce(0) {
+// (try $1.resourceValues(forKeys: [.totalFileAllocatedSizeKey])
+// .totalFileAllocatedSize ?? 0) + $0
+// }
+// totalSize += pathSize
+// }
+// }
+//
+// return totalSize
+// }
+//
+// func totalSizeOnDisk(includingSubfolders: Bool = false) throws -> String? {
+// if let totalSize = try self.totalAllocatedSize(includingSubfolders: includingSubfolders) {
+// let byteCountFormatter = ByteCountFormatter()
+// byteCountFormatter.countStyle = .file
+// byteCountFormatter.allowedUnits = [.useBytes, .useKB, .useMB, .useTB]
+// return byteCountFormatter.string(fromByteCount: Int64(totalSize))
+// }
+// return nil
+// }
+//}
+
+
+
+
+
+
+
+//func isSymbolicLink(atPath path: URL) -> Bool {
+//// let fileManager = FileManager.default
+//
+// do {
+//// let path = "/Applications/Safari.app"
+//// let url = URL(fileURLWithPath: path)
+// let destinationPath = path.resolvingSymlinksInPath().path
+// print("The symlink at \(path) points to \(destinationPath)")
+// return true
+// } catch {
+// print("An error occurred: \(error)")
+// return false
+// }
+// return false
+//// var isDirectory = false
+//// let exists = FileManager.default.fileExists(atPath: path, isDirectory: isDirectory)
+//// if exists && isDirectory {
+//// do {
+//// let _ = try FileManager.default.destinationOfSymbolicLink(atPath: path)
+//// return true
+//// } catch {
+//// return false
+//// }
+//// }
+//// return false
+//}
diff --git a/Pearcleaner/PearcleanerApp.swift b/Pearcleaner/PearcleanerApp.swift
new file mode 100644
index 0000000..ae686f0
--- /dev/null
+++ b/Pearcleaner/PearcleanerApp.swift
@@ -0,0 +1,92 @@
+//
+// PearcleanerApp.swift
+// Pearcleaner
+//
+// Created by Alin Lupascu on 10/31/23.
+//
+
+import SwiftUI
+import AppKit
+
+@main
+struct PearcleanerApp: App {
+ @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
+ @StateObject var appState = AppState()
+ @AppStorage("settings.updater.updateTimeframe") private var updateTimeframe: Int = 1
+ @AppStorage("settings.permissions.disk") private var diskP: Bool = false
+ @AppStorage("settings.permissions.events") private var diskE: Bool = false
+ @AppStorage("displayMode") var displayMode: DisplayMode = .dark
+ @State private var search = ""
+
+ var body: some Scene {
+
+ WindowGroup {
+ AppListView(search: $search)
+ .environmentObject(appState)
+ .frame(minWidth: 1020, minHeight: 700)
+ .preferredColorScheme(displayMode.colorScheme)
+ .alert(isPresented: $appState.showAlert) { presentAlert(appState: appState) }
+ .handlesExternalEvents(preferring: Set(arrayLiteral: "pear"), allowing: Set(arrayLiteral: "*"))
+ .onAppear {
+ NSWindow.allowsAutomaticWindowTabbing = false
+
+ // Get Apps
+ let sortedApps = getSortedApps()
+ appState.sortedApps.userApps = sortedApps.userApps
+ appState.sortedApps.systemApps = sortedApps.systemApps
+
+ Task {
+
+#if !DEBUG
+
+ // Make sure App Support folder exists in the future if needed for storage
+// ensureApplicationSupportFolderExists(appState: appState)
+
+ // Check for updates
+ if diskP && diskE {
+ loadGithubReleases(appState: appState)
+ }
+
+ // Check for disk/accessibility permissions
+ _ = checkAndRequestFullDiskAccess(appState: appState)
+
+
+ // TIMERS ////////////////////////////////////////////////////////////////////////////////////
+
+ // Check for app updates every 8 hours or whatever user saved setting. Also refresh autosuggestion list
+ let updateSeconds = updateTimeframe.daysToSeconds
+ _ = Timer.scheduledTimer(withTimeInterval: updateSeconds, repeats: true) { _ in
+ DispatchQueue.main.async {
+ loadGithubReleases(appState: appState)
+ }
+ }
+
+#endif
+ }
+ }
+ }
+ .windowStyle(.hiddenTitleBar)
+ .windowToolbarStyle(.unified)
+ .windowResizability(.contentMinSize)
+ .commands {
+ AppCommands(appState: appState)
+ CommandGroup(replacing: .newItem, addition: { })
+ }
+
+ Settings {
+ SettingsView()
+ .environmentObject(appState)
+ .toolbarBackground(.clear)
+ .preferredColorScheme(displayMode.colorScheme)
+ }
+ }
+}
+
+
+
+
+class AppDelegate: NSObject, NSApplicationDelegate {
+ func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
+ return true
+ }
+}
diff --git a/Pearcleaner/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/Pearcleaner/Resources/Assets.xcassets/AccentColor.colorset/Contents.json
new file mode 100644
index 0000000..4f9ac77
--- /dev/null
+++ b/Pearcleaner/Resources/Assets.xcassets/AccentColor.colorset/Contents.json
@@ -0,0 +1,38 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "display-p3",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0x59",
+ "green" : "0x48",
+ "red" : "0xFF"
+ }
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "color" : {
+ "color-space" : "display-p3",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0x59",
+ "green" : "0x48",
+ "red" : "0xFF"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Pearcleaner/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/Pearcleaner/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..64dc11e
--- /dev/null
+++ b/Pearcleaner/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,68 @@
+{
+ "images" : [
+ {
+ "filename" : "icon_16x16.png",
+ "idiom" : "mac",
+ "scale" : "1x",
+ "size" : "16x16"
+ },
+ {
+ "filename" : "icon_16x16@2x.png",
+ "idiom" : "mac",
+ "scale" : "2x",
+ "size" : "16x16"
+ },
+ {
+ "filename" : "icon_32x32.png",
+ "idiom" : "mac",
+ "scale" : "1x",
+ "size" : "32x32"
+ },
+ {
+ "filename" : "icon_32x32@2x.png",
+ "idiom" : "mac",
+ "scale" : "2x",
+ "size" : "32x32"
+ },
+ {
+ "filename" : "icon_128x128.png",
+ "idiom" : "mac",
+ "scale" : "1x",
+ "size" : "128x128"
+ },
+ {
+ "filename" : "icon_128x128@2x.png",
+ "idiom" : "mac",
+ "scale" : "2x",
+ "size" : "128x128"
+ },
+ {
+ "filename" : "icon_256x256.png",
+ "idiom" : "mac",
+ "scale" : "1x",
+ "size" : "256x256"
+ },
+ {
+ "filename" : "icon_256x256@2x.png",
+ "idiom" : "mac",
+ "scale" : "2x",
+ "size" : "256x256"
+ },
+ {
+ "filename" : "icon_512x512.png",
+ "idiom" : "mac",
+ "scale" : "1x",
+ "size" : "512x512"
+ },
+ {
+ "filename" : "icon_512x512@2x.png",
+ "idiom" : "mac",
+ "scale" : "2x",
+ "size" : "512x512"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Pearcleaner/Resources/Assets.xcassets/AppIcon.appiconset/icon_128x128.png b/Pearcleaner/Resources/Assets.xcassets/AppIcon.appiconset/icon_128x128.png
new file mode 100644
index 0000000..3100fc4
Binary files /dev/null and b/Pearcleaner/Resources/Assets.xcassets/AppIcon.appiconset/icon_128x128.png differ
diff --git a/Pearcleaner/Resources/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png b/Pearcleaner/Resources/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png
new file mode 100644
index 0000000..1792688
Binary files /dev/null and b/Pearcleaner/Resources/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png differ
diff --git a/Pearcleaner/Resources/Assets.xcassets/AppIcon.appiconset/icon_16x16.png b/Pearcleaner/Resources/Assets.xcassets/AppIcon.appiconset/icon_16x16.png
new file mode 100644
index 0000000..0808d05
Binary files /dev/null and b/Pearcleaner/Resources/Assets.xcassets/AppIcon.appiconset/icon_16x16.png differ
diff --git a/Pearcleaner/Resources/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png b/Pearcleaner/Resources/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png
new file mode 100644
index 0000000..c1ba583
Binary files /dev/null and b/Pearcleaner/Resources/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png differ
diff --git a/Pearcleaner/Resources/Assets.xcassets/AppIcon.appiconset/icon_256x256.png b/Pearcleaner/Resources/Assets.xcassets/AppIcon.appiconset/icon_256x256.png
new file mode 100644
index 0000000..1792688
Binary files /dev/null and b/Pearcleaner/Resources/Assets.xcassets/AppIcon.appiconset/icon_256x256.png differ
diff --git a/Pearcleaner/Resources/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png b/Pearcleaner/Resources/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png
new file mode 100644
index 0000000..15cec3a
Binary files /dev/null and b/Pearcleaner/Resources/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png differ
diff --git a/Pearcleaner/Resources/Assets.xcassets/AppIcon.appiconset/icon_32x32.png b/Pearcleaner/Resources/Assets.xcassets/AppIcon.appiconset/icon_32x32.png
new file mode 100644
index 0000000..c1ba583
Binary files /dev/null and b/Pearcleaner/Resources/Assets.xcassets/AppIcon.appiconset/icon_32x32.png differ
diff --git a/Pearcleaner/Resources/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png b/Pearcleaner/Resources/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png
new file mode 100644
index 0000000..75d18ce
Binary files /dev/null and b/Pearcleaner/Resources/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png differ
diff --git a/Pearcleaner/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512.png b/Pearcleaner/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512.png
new file mode 100644
index 0000000..15cec3a
Binary files /dev/null and b/Pearcleaner/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512.png differ
diff --git a/Pearcleaner/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png b/Pearcleaner/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png
new file mode 100644
index 0000000..98ca819
Binary files /dev/null and b/Pearcleaner/Resources/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png differ
diff --git a/Pearcleaner/Resources/Assets.xcassets/Contents.json b/Pearcleaner/Resources/Assets.xcassets/Contents.json
new file mode 100644
index 0000000..73c0059
--- /dev/null
+++ b/Pearcleaner/Resources/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Pearcleaner/Resources/Assets.xcassets/logo.imageset/Contents.json b/Pearcleaner/Resources/Assets.xcassets/logo.imageset/Contents.json
new file mode 100644
index 0000000..38a858c
--- /dev/null
+++ b/Pearcleaner/Resources/Assets.xcassets/logo.imageset/Contents.json
@@ -0,0 +1,21 @@
+{
+ "images" : [
+ {
+ "filename" : "logo_combo.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Pearcleaner/Resources/Assets.xcassets/logo.imageset/logo_combo.png b/Pearcleaner/Resources/Assets.xcassets/logo.imageset/logo_combo.png
new file mode 100644
index 0000000..ae575d5
Binary files /dev/null and b/Pearcleaner/Resources/Assets.xcassets/logo.imageset/logo_combo.png differ
diff --git a/Pearcleaner/Resources/Assets.xcassets/mode.colorset/Contents.json b/Pearcleaner/Resources/Assets.xcassets/mode.colorset/Contents.json
new file mode 100644
index 0000000..40af8f3
--- /dev/null
+++ b/Pearcleaner/Resources/Assets.xcassets/mode.colorset/Contents.json
@@ -0,0 +1,38 @@
+{
+ "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
+ }
+}
diff --git a/Pearcleaner/Resources/Info.plist b/Pearcleaner/Resources/Info.plist
new file mode 100644
index 0000000..a6aff2e
--- /dev/null
+++ b/Pearcleaner/Resources/Info.plist
@@ -0,0 +1,19 @@
+
+
+
+
+ CFBundleURLTypes
+
+
+ CFBundleTypeRole
+ Editor
+ CFBundleURLName
+ com.alienator88.Pearcleaner
+ CFBundleURLSchemes
+
+ pear
+
+
+
+
+
diff --git a/Pearcleaner/Resources/Pearcleaner.entitlements b/Pearcleaner/Resources/Pearcleaner.entitlements
new file mode 100644
index 0000000..757dd9c
--- /dev/null
+++ b/Pearcleaner/Resources/Pearcleaner.entitlements
@@ -0,0 +1,10 @@
+
+
+
+
+ com.apple.security.automation.apple-events
+
+ com.apple.security.files.user-selected.read-only
+
+
+
diff --git a/Pearcleaner/Resources/com.alienator88.PearcleanerSentinel.plist b/Pearcleaner/Resources/com.alienator88.PearcleanerSentinel.plist
new file mode 100644
index 0000000..fde0cbe
--- /dev/null
+++ b/Pearcleaner/Resources/com.alienator88.PearcleanerSentinel.plist
@@ -0,0 +1,16 @@
+
+
+
+
+ Label
+ com.alienator88.PearcleanerSentinel
+ ProgramArguments
+
+ __EXECUTABLE_PATH__
+
+ RunAtLoad
+
+ KeepAlive
+
+
+
diff --git a/Pearcleaner/Settings/About.swift b/Pearcleaner/Settings/About.swift
new file mode 100644
index 0000000..6edc9dc
--- /dev/null
+++ b/Pearcleaner/Settings/About.swift
@@ -0,0 +1,172 @@
+//
+// About.swift
+// Pearcleaner
+//
+// Created by Alin Lupascu on 11/5/23.
+//
+
+import SwiftUI
+
+struct AboutSettingsTab: View {
+ @EnvironmentObject var appState: AppState
+
+ var body: some View {
+
+ VStack {
+
+ HStack(alignment: .center) {
+ VStack(alignment: .center, spacing: 10){
+ Spacer()
+
+ Image(nsImage: NSApp.applicationIconImage)
+ .padding()
+// .padding(.bottom, 5)
+
+ HStack(alignment: .center, spacing: 20) {
+ Button("") {
+ loadGithubReleases(appState: appState, manual: true)
+ }
+ .buttonStyle(SimpleButtonStyle(icon: "cloud", help: "Update application", color: Color("AccentColor")))
+
+ Button("") {
+ NSWorkspace.shared.open(URL(string: "https://github.com/ghosted-labs/Pearcleaner")!)
+ }
+ .buttonStyle(SimpleButtonStyle(icon: "paperplane", help: "View repository", color: Color("AccentColor")))
+ }
+
+ Spacer()
+
+
+ }
+// .padding()
+ // .padding(.top, 10)
+ .frame(width: 250)
+
+ VStack(alignment: .center, spacing: 20) {
+ HStack(alignment: .firstTextBaseline) {
+ Text(Bundle.main.name)
+ .font(.title)
+ .bold()
+ Spacer()
+ HStack {
+ Text("v\(Bundle.main.version)")
+ Text(" (build \(Bundle.main.buildVersion))")
+ }
+
+ }
+ Divider()
+ .padding(.top, -8)
+
+ VStack(alignment: .leading) {
+
+ HStack{
+ Image(systemName: "applescript.fill")
+ .resizable()
+ .aspectRatio(contentMode: .fit)
+ .frame(width: 16, height: 16)
+
+ VStack(alignment: .leading){
+ Text("Privacy Guides")
+ Text("Inspired by open-source appcleaner script from Sun Knudsen").font(.footnote).foregroundStyle(.gray)
+
+ }
+ 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: "paperplane", help: "View", color: Color("AccentColor")))
+
+ }
+ .padding()
+ .background(Color("mode").opacity(0.05))
+ .cornerRadius(8)
+
+
+ HStack{
+ Image(systemName: "trash.fill")
+ .resizable()
+ .aspectRatio(contentMode: .fit)
+ .frame(width: 16, height: 16)
+
+ VStack(alignment: .leading){
+ Text("AppCleaner")
+ Text("Inspired by AppCleaner from Freemacsoft").font(.footnote).foregroundStyle(.gray)
+
+ }
+ Spacer()
+ Button(""){
+ NSWorkspace.shared.open(URL(string: "https://freemacsoft.net/appcleaner/")!)
+ }
+ .buttonStyle(SimpleButtonStyle(icon: "paperplane", help: "View", color: Color("AccentColor")))
+
+ }
+ .padding()
+ .background(Color("mode").opacity(0.05))
+ .cornerRadius(8)
+
+
+// HStack{
+// Image(systemName: "paintbrush.fill")
+// .resizable()
+// .aspectRatio(contentMode: .fit)
+// .frame(width: 16, height: 16)
+// VStack(alignment: .leading){
+// Text("Icon")
+// Text("Created by Oviotti on DeviantArt").font(.footnote).foregroundStyle(.gray)
+// }
+// Spacer()
+//
+// Button("") {
+// NSWorkspace.shared.open(URL(string: "https://www.deviantart.com/oviotti/art/AppCleaner-for-macOS-632260251")!)
+// }
+// .buttonStyle(SimpleButtonStyle(icon: "paperplane", help: "View", color: Color("AccentColor")))
+//
+// }
+// .padding()
+// .background(Color("mode").opacity(0.05))
+// .cornerRadius(8)
+
+ }
+
+ Spacer()
+
+ }
+ .padding(.trailing)
+// .padding()
+// .padding(.top, 30)
+ }
+
+ Spacer()
+
+ Text("Made with ❤️ by Alin Lupascu").font(.footnote).padding(.bottom)
+ }
+ .padding(20)
+ .frame(width: 750, height: 325)
+
+ }
+}
+
+
+
+extension Bundle {
+
+ var name: String {
+ func string(for key: String) -> String? {
+ object(forInfoDictionaryKey: key) as? String
+ }
+ return string(for: "CFBundleDisplayName")
+ ?? string(for: "CFBundleName")
+ ?? "N/A"
+ }
+
+ var version: String {
+ infoDictionary?["CFBundleShortVersionString"] as? String ?? "N/A"
+ }
+
+ var buildVersion: String {
+ infoDictionary?["CFBundleVersion"] as? String ?? "N/A"
+ }
+
+}
+
diff --git a/Pearcleaner/Settings/General.swift b/Pearcleaner/Settings/General.swift
new file mode 100644
index 0000000..8fd7896
--- /dev/null
+++ b/Pearcleaner/Settings/General.swift
@@ -0,0 +1,64 @@
+//
+// General.swift
+// Pearcleaner
+//
+// Created by Alin Lupascu on 11/5/23.
+//
+
+import Foundation
+import SwiftUI
+
+struct GeneralSettingsTab: View {
+ @EnvironmentObject var appState: AppState
+ @AppStorage("settings.general.glass") private var glass: Bool = true
+// @AppStorage("settings.general.ants") private var ants: Bool = false
+
+ var body: some View {
+ Form {
+ VStack {
+ HStack {
+ VStack(alignment: .leading, spacing: 5) {
+ Text("Transparency").font(.title2)
+ Text("Toggles the sidebar material on the app list drawer")
+ .font(.footnote)
+ .foregroundStyle(.gray)
+ }
+ Spacer()
+ Toggle(isOn: $glass, label: {
+ })
+ .toggleStyle(.switch)
+ }
+ .padding()
+ .background(
+ RoundedRectangle(cornerRadius: 8)
+ .fill(Color("mode").opacity(0.05))
+ )
+
+// HStack {
+// VStack(alignment: .leading, spacing: 5) {
+// Text("Animation").font(.title2)
+// Text("Toggles the marching ants animation on the drop zone")
+// .font(.footnote)
+// .foregroundStyle(.gray)
+// }
+// Spacer()
+// Toggle(isOn: $ants, label: {
+// })
+// .toggleStyle(.switch)
+// }
+// .padding()
+// .background(
+// RoundedRectangle(cornerRadius: 8)
+// .fill(Color("mode").opacity(0.05))
+// )
+
+ Spacer()
+ }
+
+ }
+ .padding(20)
+ .frame(width: 400, height: 100)
+
+ }
+
+}
diff --git a/Pearcleaner/Settings/Permissions.swift b/Pearcleaner/Settings/Permissions.swift
new file mode 100644
index 0000000..1272121
--- /dev/null
+++ b/Pearcleaner/Settings/Permissions.swift
@@ -0,0 +1,104 @@
+//
+// Permissions.swift
+// Pearcleaner
+//
+// Created by Alin Lupascu on 11/5/23.
+//
+
+import Foundation
+import SwiftUI
+
+struct PermissionsSettingsTab: View {
+ @EnvironmentObject var appState: AppState
+ @State private var diskStatus: Bool = false
+ @State private var accessStatus: Bool = false
+
+
+
+ var body: some View {
+ Form {
+ VStack(alignment: .leading, spacing: 10) {
+
+ HStack(spacing: 15) {
+ Image(systemName: "internaldrive")
+ .resizable()
+ .scaledToFit()
+ .frame(width: 20)
+
+ VStack(alignment: .leading, spacing: 5) {
+ Text("Full Disk")
+ Text(diskStatus ? "Permission granted" : "Permission not granted")
+ .font(.footnote)
+ .foregroundStyle(diskStatus ? .green : .red)
+ }
+
+ Spacer()
+
+ Button("") {
+ if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles") {
+ NSWorkspace.shared.open(url)
+ }
+ }
+ .buttonStyle(SimpleButtonStyle(icon: "paperplane", help: "View disk permission pane", color: Color("AccentColor")))
+ }
+ .padding()
+ .background(
+ RoundedRectangle(cornerRadius: 8)
+ .fill(Color("mode").opacity(0.05))
+ )
+
+
+ HStack(spacing: 15) {
+ Image(systemName: "accessibility")
+ .resizable()
+ .scaledToFit()
+ .frame(width: 20)
+
+ VStack(alignment: .leading, spacing: 5) {
+ Text("Accessibility")
+ Text(accessStatus ? "Permission granted" : "Permission not granted")
+ .font(.footnote)
+ .foregroundStyle(accessStatus ? .green : .red)
+ }
+
+ Spacer()
+
+ Button("") {
+ if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility") {
+ NSWorkspace.shared.open(url)
+ }
+ }
+ .buttonStyle(SimpleButtonStyle(icon: "paperplane", help: "View accessibility pane", color: Color("AccentColor")))
+ }
+ .padding()
+ .background(
+ RoundedRectangle(cornerRadius: 8)
+ .fill(Color("mode").opacity(0.05))
+ )
+
+
+
+// HStack(alignment: .center) {
+// Spacer()
+// Button("") {
+// relaunchApp()
+// }
+// .buttonStyle(SimpleButtonStyle(icon: "restart", help: "Restart application", color: Color("AccentColor")))
+// Text("Restart")
+// Spacer()
+// }
+// .padding()
+
+
+ }
+
+ }
+ .padding(20)
+ .frame(width: 400, height: 200)
+ .onAppear {
+ diskStatus = checkAndRequestFullDiskAccess(appState: appState, skipAlert: true)
+ accessStatus = checkAndRequestAccessibilityAccess(appState: appState)
+ }
+ }
+
+}
diff --git a/Pearcleaner/Settings/Sentinel.swift b/Pearcleaner/Settings/Sentinel.swift
new file mode 100644
index 0000000..8b8e253
--- /dev/null
+++ b/Pearcleaner/Settings/Sentinel.swift
@@ -0,0 +1,52 @@
+//
+// Sentinel.swift
+// Pearcleaner
+//
+// Created by Alin Lupascu on 11/9/23.
+//
+
+import Foundation
+import SwiftUI
+
+struct SentinelSettingsTab: View {
+ @EnvironmentObject var appState: AppState
+ @AppStorage("settings.sentinel.enable") private var sentinel: Bool = false
+
+ var body: some View {
+ Form {
+ VStack {
+ HStack {
+ VStack(alignment: .leading, spacing: 5) {
+ Text("Sentinel Monitor").font(.title2)
+ Text("Detects when apps are moved to Trash for file removal")
+ .font(.footnote)
+ .foregroundStyle(.gray)
+ }
+ Spacer()
+ Toggle(isOn: $sentinel, label: {
+ })
+ .toggleStyle(.switch)
+ .onChange(of: sentinel) { newValue in
+ if newValue {
+ launchctl(load: true)
+ } else {
+ launchctl(load: false)
+ }
+ }
+ }
+ .padding()
+ .background(
+ RoundedRectangle(cornerRadius: 8)
+ .fill(Color("mode").opacity(0.05))
+ )
+ }
+
+
+
+ }
+ .padding(20)
+ .frame(width: 400, height: 100)
+
+ }
+
+}
diff --git a/Pearcleaner/Settings/SettingsWindow.swift b/Pearcleaner/Settings/SettingsWindow.swift
new file mode 100644
index 0000000..fd820f3
--- /dev/null
+++ b/Pearcleaner/Settings/SettingsWindow.swift
@@ -0,0 +1,57 @@
+//
+// SettingsWindow.swift
+// Pearcleaner
+//
+// Created by Alin Lupascu on 10/31/23.
+//
+
+import SwiftUI
+
+struct SettingsView: View {
+ @EnvironmentObject var appState: AppState
+
+ var body: some View {
+
+ TabView() {
+ GeneralSettingsTab()
+ .tabItem {
+ Label(CurrentTabView.general.title, systemImage: "gear")
+ }
+ .tag(CurrentTabView.general)
+
+ PermissionsSettingsTab()
+ .tabItem {
+ Label(CurrentTabView.permissions.title, systemImage: "lock")
+ }
+ .tag(CurrentTabView.permissions)
+
+ SentinelSettingsTab()
+ .tabItem {
+ Label(CurrentTabView.sentinel.title, systemImage: "eye.circle")
+ }
+ .tag(CurrentTabView.sentinel)
+
+ UpdateSettingsTab()
+ .tabItem {
+ Label(CurrentTabView.update.title, systemImage: "tray.and.arrow.down")
+ }
+ .tag(CurrentTabView.update)
+
+ AboutSettingsTab()
+ .tabItem {
+ Label(CurrentTabView.about.title, systemImage: "info.circle")
+ }
+ .tag(CurrentTabView.about)
+
+ }
+
+ }
+
+}
+
+
+
+
+
+
+
diff --git a/Pearcleaner/Settings/Update.swift b/Pearcleaner/Settings/Update.swift
new file mode 100644
index 0000000..5a5529b
--- /dev/null
+++ b/Pearcleaner/Settings/Update.swift
@@ -0,0 +1,186 @@
+//
+// Update.swift
+// Pearcleaner
+//
+// Created by Alin Lupascu on 11/5/23.
+//
+
+
+import SwiftUI
+import Foundation
+
+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
+}
+
+
+struct UpdateSettingsTab: View {
+ @EnvironmentObject var appState: AppState
+ @State private var showAlert = false
+ @State private var showDone = false
+ @AppStorage("settings.updater.updateTimeframe") private var updateTimeframe: Int = 1
+
+ var body: some View {
+ VStack {
+
+ HStack {
+ Text("Check for updates every ") +
+ Text("**\(updateTimeframe)**").foregroundColor(.red) +
+ Text(updateTimeframe == 1 ? " day" : " days")
+ Stepper(value: $updateTimeframe, in: 1...7) {
+ Text("")
+ }
+ }
+
+ ScrollView {
+ VStack() {
+ ForEach(appState.releases, id: \.id) { release in
+ VStack(alignment: .leading) {
+ LabeledDivider(label: "\(release.tag_name)")
+ Text(release.body)
+ }
+
+ }
+ }
+ .padding()
+ }
+ .frame(minHeight: 0, maxHeight: .infinity)
+ .frame(minWidth: 0, maxWidth: .infinity)
+ .padding()
+
+ Text("Showing last 3").opacity(0.5).font(.footnote)
+
+
+
+ HStack(alignment: .center, spacing: 5) {
+ Spacer()
+ Button("Check"){
+ loadGithubReleases(appState: appState)
+ }.buttonStyle(BorderedButtonStyle())
+
+ Button("Force Update"){
+ loadGithubReleases(appState: appState, manual: true)
+ }.buttonStyle(BorderedButtonStyle())
+
+ Button("GitHub"){
+ NSWorkspace.shared.open(URL(string: "https://github.com/alienator88/Pearcleaner/releases")!)
+ }.buttonStyle(BorderedButtonStyle())
+
+
+ Spacer()
+ }
+ .padding()
+
+
+
+ }
+ .padding(20)
+ .frame(width: 500, height: 400)
+ }
+
+}
+
+
+func loadGithubReleases(appState: AppState, manual: Bool = false) {
+ let url = URL(string: "https://api.github.com/repos/alienator88/Pearcleaner/releases")!
+ let request = URLRequest(url: url)
+// request.setValue("token \(ghToken)", forHTTPHeaderField: "Authorization")
+ 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 lastFiveReleases = Array(decodedResponse.prefix(3)) // Get only the last 3 recent releases
+ appState.releases = lastFiveReleases
+ checkForUpdate(appState: appState, manual: manual)
+
+ }
+ return
+ }
+ }
+ print("Fetch failed: \(error?.localizedDescription ?? "Unknown error")")
+ }.resume()
+}
+
+func checkForUpdate(appState: AppState, manual: Bool = false) {
+ guard let latestRelease = appState.releases.first else { return }
+ let currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
+ if latestRelease.tag_name > currentVersion ?? "" {
+ appState.alertType = .update
+ appState.showAlert = true
+ } else {
+ if manual {
+ appState.alertType = .no_update
+ appState.showAlert = true
+ }
+ }
+}
+
+func downloadUpdate(appState: AppState) {
+ 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("token \(ghToken)", forHTTPHeaderField: "Authorization")
+ request.setValue("application/octet-stream", forHTTPHeaderField: "Accept")
+
+ let downloadTask = URLSession.shared.downloadTask(with: request) { localURL, urlResponse, error in
+ guard let localURL = localURL else { return }
+
+ let fileManager = FileManager.default
+ let destinationURL = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!.appendingPathComponent("Pearcleaner").appendingPathComponent("\(asset.name)")
+
+ do {
+ if fileManager.fileExists(atPath: destinationURL.path) {
+ try fileManager.removeItem(at: destinationURL)
+ }
+ try fileManager.moveItem(at: localURL, to: destinationURL)
+
+ UnzipAndReplace(DownloadedFileURL: destinationURL.path, appState: appState) {
+ updateOnMain {
+ // Restart App
+ relaunchApp(afterDelay: 1)
+ }
+ }
+
+ } catch {
+ print("Error moving downloaded file: \(error.localizedDescription)")
+ }
+ }
+
+ downloadTask.resume()
+}
+
+func UnzipAndReplace(DownloadedFileURL fileURL: String, appState: AppState, completion: @escaping () -> Void = {}) {
+ let appDirectory = Bundle.main.bundleURL.deletingLastPathComponent().path
+ let appBundle = Bundle.main.bundleURL.path
+ let fileManager = FileManager.default
+
+ do {
+ // Remove the old version of your app
+ try fileManager.removeItem(atPath: appBundle)
+
+ // Unzip the downloaded update file to your app's bundle path
+ let process = Process()
+ process.executableURL = URL(fileURLWithPath: "/usr/bin/unzip")
+ process.arguments = [fileURL, "-d", appDirectory]
+ try process.run()
+ process.waitUntilExit()
+
+ // After unzipping, remove the update file
+ try fileManager.removeItem(atPath: fileURL)
+
+ } catch {
+ print("Error updating the app: \(error)")
+ }
+
+ completion()
+}
diff --git a/Pearcleaner/Views/AppListItems.swift b/Pearcleaner/Views/AppListItems.swift
new file mode 100644
index 0000000..7089964
--- /dev/null
+++ b/Pearcleaner/Views/AppListItems.swift
@@ -0,0 +1,90 @@
+//
+// AppListDetails.swift
+// Pearcleaner
+//
+// Created by Alin Lupascu on 11/10/23.
+//
+
+import Foundation
+import SwiftUI
+
+struct AppListItems: View {
+ @EnvironmentObject var appState: AppState
+ @State private var isHovered = false
+ @Environment(\.colorScheme) var colorScheme
+ let appInfo: AppInfo
+ var isSelected: Bool {
+ appState.appInfo == appInfo
+ }
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 5) {
+ HStack(alignment: .center) {
+// if isHovered || isSelected {
+// Circle()
+// .fill(Color("AccentColor"))
+// .frame(width: 10)
+// .padding(5)
+// }
+ if let appIcon = appInfo.appIcon {
+ Image(nsImage: appIcon)
+ .resizable()
+ .aspectRatio(contentMode: .fit)
+ .frame(width: 30, height: 30)
+ }
+ VStack(alignment: .leading, spacing: 5) {
+ Text(appInfo.appName)
+ .font(.system(size: 12))
+ .lineLimit(1)
+ .truncationMode(.tail)
+ .padding(.horizontal, 5)
+ }
+
+ Spacer()
+ Text(appInfo.appVersion)
+ .font(.footnote)
+ .foregroundStyle(Color("mode").opacity(0.5))
+
+ if isHovered || isSelected {
+ RoundedRectangle(cornerRadius: 50)
+ .fill(isSelected ? Color("AccentColor") : Color("mode").opacity(0.5))
+ .frame(width: isSelected ? 4 : 2, height: 35)
+ .padding(.leading, 5)
+ .offset(x: 20)
+ }
+
+ }
+
+// .background(
+// RoundedRectangle(cornerRadius: 6)
+// .fill(isSelected ? Color("AccentColor").opacity(0.15) : (isHovered ? Color("AccentColor").opacity(0.1) : Color.white.opacity(colorScheme == .dark ? 0.05 : 0.8)))
+// .shadow(color: .black.opacity(0.2), radius: 2, x: 0, y: 1)
+// .overlay(
+// RoundedRectangle(cornerRadius: 6)
+// .strokeBorder(Color("AccentColor").opacity(isHovered ? 0.7 : 0.1), lineWidth: 1)
+// )
+// )
+
+// Rectangle()
+// .frame(maxWidth: isSelected ? .infinity : (isHovered ? .infinity : 30), maxHeight: 2)
+// .foregroundStyle(Color("AccentColor"))
+
+ }
+ .padding(.horizontal, 5)
+ .help(appInfo.appName)
+ .onHover { hovering in
+ withAnimation(Animation.easeIn(duration: 0.2)) {
+ self.isHovered = hovering
+ }
+ }
+ .onTapGesture {
+ updateOnMain {
+ appState.appInfo = appInfo
+ findPathsForApp(appState: appState, appInfo: appState.appInfo)
+ withAnimation(Animation.easeIn(duration: 0.4)) {
+ appState.currentView = .files
+ }
+ }
+ }
+ }
+}
diff --git a/Pearcleaner/Views/AppListView.swift b/Pearcleaner/Views/AppListView.swift
new file mode 100644
index 0000000..6ae1120
--- /dev/null
+++ b/Pearcleaner/Views/AppListView.swift
@@ -0,0 +1,213 @@
+//
+// AppListH.swift
+// Pearcleaner
+//
+// Created by Alin Lupascu on 11/5/23.
+//
+
+import Foundation
+import SwiftUI
+
+struct AppListView: View {
+ @EnvironmentObject var appState: AppState
+ @AppStorage("settings.general.glass") private var glass: Bool = false
+ @Binding var search: String
+ @State private var showSys: Bool = true
+ @State private var showUsr: Bool = true
+// @State private var sidebar: Bool = true
+ @State private var reload: Bool = false
+
+ var filteredUserApps: [AppInfo] {
+ if search.isEmpty {
+ return appState.sortedApps.userApps
+ } else {
+ return appState.sortedApps.userApps.filter { $0.appName.localizedCaseInsensitiveContains(search) }
+ }
+ }
+
+ var filteredSystemApps: [AppInfo] {
+ if search.isEmpty {
+ return appState.sortedApps.systemApps
+ } else {
+ return appState.sortedApps.systemApps.filter { $0.appName.localizedCaseInsensitiveContains(search) }
+ }
+ }
+
+ var body: some View {
+
+ HStack(alignment: .center, spacing: 0) {
+ // App List
+ if appState.sidebar {
+ ZStack {
+ HStack(spacing: 0){
+
+ if reload {
+ VStack {
+ Spacer()
+ ProgressView("Refreshing applications")
+ Spacer()
+ }
+ .frame(width: 300)
+ .padding(.vertical)
+ } else {
+ VStack(alignment: .center) {
+
+ VStack(alignment: .center, spacing: 20) {
+ SearchBar(search: $search)
+ }
+ .padding(.horizontal)
+ .padding(.top, 20)
+ .padding(.bottom)
+
+ ScrollView {
+
+ VStack(alignment: .leading) {
+
+ if filteredUserApps.count > 0 {
+ VStack {
+ Header(title: "User", count: filteredUserApps.count)
+ ForEach(filteredUserApps, id: \.self) { appInfo in
+ AppListItems(appInfo: appInfo)
+ if appInfo != filteredUserApps.last {
+ Divider().padding(.horizontal, 5)
+ }
+ }
+ .padding(.bottom)
+ }
+
+ }
+
+ if filteredSystemApps.count > 0 {
+ VStack {
+ Header(title: "System", count: filteredSystemApps.count)
+ ForEach(filteredSystemApps, id: \.self) { appInfo in
+ AppListItems(appInfo: appInfo)
+ if appInfo != filteredSystemApps.last {
+ Divider().padding(.horizontal, 5)
+ }
+ }
+ }
+ }
+
+ }
+ .padding(.horizontal)
+
+ }
+ .scrollIndicators(.never)
+
+ }
+ .frame(width: 300)
+ .padding(.vertical)
+ }
+
+
+ Divider()
+ }
+ }
+ .background(glass ? GlassEffect(material: .sidebar, blendingMode: .behindWindow).edgesIgnoringSafeArea(.all) : nil)
+ .transition(.move(edge: .leading))
+
+ Spacer()
+ }
+
+
+ // Details View
+ VStack(spacing: 0) {
+ if appState.currentView == .empty {
+ TopBar(reload: $reload)
+ AppDetailsEmptyView()
+ } else if appState.currentView == .files {
+ TopBar(reload: $reload)
+ FilesView()
+ .id(appState.appInfo.id)
+ }
+ }
+// .padding(.leading, appState.sidebar ? 0 : 10)
+ .transition(.move(edge: .leading))
+
+ Spacer()
+ }
+ .edgesIgnoringSafeArea(.all)
+ .onOpenURL(perform: { url in
+ let deeplinkManager = DeeplinkManager()
+ deeplinkManager.manage(url: url, appState: appState)
+ })
+ // MARK: Background for whole app
+ // .background(Color("bg").opacity(1))
+ // .background(VisualEffect(material: .sidebar, blendingMode: .behindWindow).edgesIgnoringSafeArea(.all))
+ }
+}
+
+
+
+
+
+
+struct AppDetailsEmptyView: View {
+ @Environment(\.colorScheme) var colorScheme
+ @EnvironmentObject var appState: AppState
+ @State private var animateGradient: Bool = false
+
+ var body: some View {
+ VStack() {
+
+ Spacer()
+
+ DropTarget(appState: appState)
+
+ Spacer()
+
+ if appState.isReminderVisible {
+ Text(" + Z to undo")
+ .font(.title2)
+ .foregroundStyle(Color("AccentColor").opacity(0.5))
+ .fontWeight(.medium)
+ .onAppear {
+ DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
+ withAnimation {
+ appState.isReminderVisible = false
+ }
+ }
+ }
+ Spacer()
+ }
+
+ Text("Drop an app above or select one from the list to begin")
+ .font(.title3)
+ .padding(.bottom, 25)
+ .opacity(0.5)
+ }
+ }
+}
+
+
+struct SearchBar: View {
+ @Binding var search: String
+
+ var body: some View {
+ HStack {
+ TextField("Search", text: $search)
+ .textFieldStyle(SimpleSearchStyle(icon: Image(systemName: "magnifyingglass"),trash: true, text: $search))
+ }
+ .frame(height: 30)
+ }
+}
+
+
+struct Header: View {
+ let title: String
+ let count: Int
+
+ var body: some View {
+ HStack {
+ Text(title).opacity(0.5)
+ Spacer()
+ Text("\(count)")
+ .frame(minWidth: 30, minHeight: 15)
+ .padding(2)
+ .background(Color("mode").opacity(0.1))
+ .clipShape(.capsule)
+ // .clipShape(RoundedRectangle(cornerRadius: 6, style: .continuous))
+ }
+ }
+}
diff --git a/Pearcleaner/Views/ContentView.swift b/Pearcleaner/Views/ContentView.swift
new file mode 100644
index 0000000..3c895c1
--- /dev/null
+++ b/Pearcleaner/Views/ContentView.swift
@@ -0,0 +1,42 @@
+////
+//// ContentView.swift
+//// Pearcleaner
+////
+//// Created by Alin Lupascu on 10/31/23.
+////
+//
+//import SwiftUI
+//
+//struct ContentView: View {
+// @EnvironmentObject var appState: AppState
+// @State private var search = ""
+//
+// var body: some View {
+//
+// VStack(alignment: .leading) {
+//// AppListNav(search: $search)
+// AppListH(search: $search)
+//// HStack {
+//// Image(nsImage: NSApp.applicationIconImage)
+//// .imageScale(.large)
+////
+//// Text("Pearcleaner")
+//// .font(.custom("MontserratAlt1-Light", size: 30, relativeTo: .body))
+//// }
+//// .padding()
+//
+//
+//
+//// VStack() {
+////// AppListView(search: $search)
+////// AppListNav(search: $search)
+//// }
+// }
+//// .sheet(isPresented: $appState.showFiles, content: {
+//// FilesView()
+//// })
+// .alert(isPresented: $appState.showAlert) { presentAlert(appState: appState) }
+//
+// }
+//}
+//
diff --git a/Pearcleaner/Views/DropTarget.swift b/Pearcleaner/Views/DropTarget.swift
new file mode 100644
index 0000000..b2e0f10
--- /dev/null
+++ b/Pearcleaner/Views/DropTarget.swift
@@ -0,0 +1,168 @@
+//
+// DropTarget.swift
+// Pearcleaner
+//
+// Created by Alin Lupascu on 11/11/23.
+//
+
+import Foundation
+import SwiftUI
+import UniformTypeIdentifiers
+
+struct DropTarget: View, DropDelegate {
+ @AppStorage("settings.general.ants") private var ants: Bool = false
+ @ObservedObject var appState: AppState
+ @State private var shouldFlash = false
+ private var types: [UTType] = [.fileURL]
+ @Environment(\.colorScheme) var colorScheme
+ @State private var phase: CGFloat = 0
+
+ public init(appState: AppState) {
+ self.appState = appState
+ }
+
+ var body: some View {
+ ZStack {
+
+ FlashingBackground(shouldFlash: $shouldFlash)
+
+ HStack {
+ Spacer()
+
+ HStack {
+ Spacer()
+
+ ZStack {
+ LinearGradient(gradient: Gradient(colors: [.pink, .orange]), startPoint: .leading, endPoint: .trailing)
+ .mask(Image("logo")
+ .resizable()
+ .scaledToFit())
+ }
+ .frame(width: 500, height: 200)
+
+ Spacer()
+
+
+ }
+
+ Spacer()
+ }
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ .background {
+ RoundedRectangle(cornerRadius: 8)
+ .strokeBorder(LinearGradient(gradient: Gradient(colors: [.pink, .orange]), startPoint: .leading, endPoint: .trailing), style: StrokeStyle(lineWidth: 3, dash: [10], dashPhase: phase))
+ .onAppear {
+ withAnimation(.linear.speed(0.5).repeat(while: ants, autoreverses: false)) {
+ if ants {
+ phase -= 20
+ }
+ }
+ }
+ .onChange(of: ants) { newValue in
+ withAnimation(.linear.speed(0.5).repeat(while: newValue, autoreverses: false)) {
+ phase -= 20
+ }
+ }
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+
+ }
+
+
+ }
+ .onDrop(of: types, delegate: self)
+ .padding(20)
+
+ }
+
+ func dropEntered(info: DropInfo) {
+ DispatchQueue.main.async {
+ self.ants = true
+ }
+ }
+
+ func dropExited(info: DropInfo) {
+ DispatchQueue.main.async {
+ self.ants = false
+ }
+ }
+
+ func performDrop(info: DropInfo) -> Bool {
+
+ let itemProviders = info.itemProviders(for: [UTType.fileURL])
+
+ guard itemProviders.count == 1 else {
+ return false
+ }
+ for itemProvider in itemProviders {
+ itemProvider.loadItem(forTypeIdentifier: UTType.fileURL.identifier, options: nil) { item, error in
+ guard let data = item as? Data else {
+ dump(error)
+ return
+ }
+ guard let url = URL(dataRepresentation: data, relativeTo: nil) else {
+ print("Error: Not a valid URL.")
+ return
+ }
+ if url.pathExtension == "app" {
+ let appInfo = getAppInfo(atPath: url)
+
+ DispatchQueue.main.async {
+ self.ants = false
+ appState.appInfo = appInfo!
+ findPathsForApp(appState: appState, appInfo: appState.appInfo)
+ appState.currentView = .files
+ }
+ } else {
+ print("Error: Dropped file is not an application bundle")
+ DispatchQueue.main.async {
+ self.shouldFlash = true
+ }
+ }
+
+ }
+ }
+ DispatchQueue.main.async {
+ self.ants = false
+ }
+ return true
+ }
+
+
+}
+
+
+struct FlashingBackground: View {
+ @Binding var shouldFlash: Bool
+
+ var body: some View {
+ GeometryReader { geometry in
+ Rectangle()
+ .fill(shouldFlash ? Color.red : Color.clear)
+ .cornerRadius(8)
+ .padding()
+ .opacity(shouldFlash ? 0.5 : 0)
+ .animation(.easeInOut(duration: 0.2).repeatCount(1, autoreverses: true), value: shouldFlash)
+ .onChange(of: shouldFlash) { newValue in
+ if newValue {
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
+ shouldFlash = false
+ }
+ }
+ }
+ .ignoresSafeArea()
+ .frame(width: geometry.size.width, height: geometry.size.height)
+ }
+ }
+}
+
+
+
+extension Animation {
+ func `repeat`(while expression: Bool, autoreverses: Bool = true) -> Animation {
+ if expression {
+ return self.repeatForever(autoreverses: autoreverses)
+ } else {
+ return self
+ }
+ }
+}
diff --git a/Pearcleaner/Views/FilesView.swift b/Pearcleaner/Views/FilesView.swift
new file mode 100644
index 0000000..a1917c5
--- /dev/null
+++ b/Pearcleaner/Views/FilesView.swift
@@ -0,0 +1,443 @@
+//
+// FilesView.swift
+// Pearcleaner
+//
+// Created by Alin Lupascu on 11/1/23.
+//
+
+import Foundation
+import SwiftUI
+
+struct FilesView: View {
+ @EnvironmentObject var appState: AppState
+ @State private var appSize: String = ""
+ @State private var showDetails: Bool = false
+ @State private var showPop: Bool = false
+ @State private var itemDetails: [(size: String, icon: Image?)] = []
+ @Environment(\.colorScheme) var colorScheme
+
+ var body: some View {
+ VStack(alignment: .center) {
+ if !self.showDetails {
+ VStack {
+ Spacer()
+ ProgressView("Finding application files..")
+ Spacer()
+ }
+ .transition(.opacity)
+ } else {
+ VStack() {
+
+ // Main Group
+ HStack() {
+ //icon
+ if let appIcon = appState.appInfo.appIcon {
+ Image(nsImage: appIcon)
+ .resizable()
+ .aspectRatio(contentMode: .fit)
+ .frame(width: 100, height: 100)
+ .padding(.leading)
+ }
+ //app title, size and items
+ VStack(alignment: .center) {
+ HStack(alignment: .center) {
+ VStack(alignment: .leading, spacing: 5){
+ HStack {
+ Text("\(appState.appInfo.appName)").font(.title).fontWeight(.bold)
+// .foregroundStyle(Color("AccentColor"))
+ Text("•").foregroundStyle(Color("AccentColor"))
+ Text("\(appState.appInfo.appVersion)").font(.title3)
+// .foregroundStyle(.gray.opacity(0.8))
+ }
+ Text("\(appState.appInfo.bundleIdentifier)").font(.title3)
+ .foregroundStyle((.gray.opacity(0.8)))
+ }
+
+ Spacer()
+
+ Text("\(appSize)").font(.title).fontWeight(.bold)
+// .foregroundStyle(Color("AccentColor"))
+ }
+
+
+ Divider().padding(.bottom, 7)
+
+ HStack{
+ Text("Application files and folders")
+ .font(.callout)
+// .foregroundStyle(Color("AccentColor"))
+ .opacity(0.8)
+ Spacer()
+ Text("\(appState.paths.count > 1 ? "\(appState.paths.count) items" : "\(appState.paths.count) item")").font(.callout)
+// .foregroundStyle(Color("AccentColor").opacity(0.7))
+ .underline()
+ }
+ if appState.appInfo.appName.count < 5 {
+ HStack(alignment: .center) {
+ Button(""){
+ showPop = true
+ }
+ .buttonStyle(SimpleButtonStyle(icon: "exclamationmark.triangle.fill", help: "Warning", color: .red))
+ .popover(isPresented: $showPop, arrowEdge: .top) {
+ VStack() {
+ Text("Pearcleaner searches for files with a combination of bundle id and app name.\n**\(appState.appInfo.appName)** has a common or short app name so there might be unrelated files found.\nPlease check the list thoroughly before uninstalling.")
+ .padding(20)
+ .font(.title2)
+ }
+
+ }
+ Text("Read caution message")
+ .foregroundStyle(Color.red)
+
+ Spacer()
+ }
+ .padding(.top)
+ .onTapGesture {
+ showPop = true
+ }
+ }
+
+
+ }
+ .padding(20)
+ }
+ .padding([.horizontal, .bottom])
+ .background(
+ RoundedRectangle(cornerRadius: 8)
+// .strokeBorder(Color("AccentColor"), lineWidth: 0.5)
+ .fill(Color("mode").opacity(colorScheme == .dark ? 0.05 : 0.05))
+// .background(
+// RoundedRectangle(cornerRadius: 8)
+// .strokeBorder(Color("AccentColor").opacity(colorScheme == .dark ? 0.1 : 0.1), lineWidth: 1)
+// )
+ )
+
+
+
+ ScrollView {
+ VStack {
+ ForEach(Array(zip(appState.paths, itemDetails)), id: \.0) { path, details in
+ if let firstPath = appState.paths.first, path == firstPath {
+ FileDetailsItem(size: details.size, icon: details.icon, path: path)
+ .padding(.trailing)
+ Divider().padding(.leading, 40).padding(.trailing)
+ } else {
+ VStack {
+ FileDetailsItem(size: details.size, icon: details.icon, path: path)
+ if path != appState.paths.last {
+ Divider().padding(.leading, 40)
+ }
+ }
+ .padding(.leading, 47).padding(.trailing)
+
+
+ }
+ }
+ }
+ }
+ .padding()
+
+ HStack() {
+ Spacer()
+
+// if appState.trashLaunch {
+// Button("Close") {
+// Task {
+// NSApp.terminate(nil)
+// exit(0)
+// }
+// }
+// .buttonStyle(WindowActionButton(action: .cancel))
+// }
+
+
+ Button("Uninstall") {
+ Task {
+ updateOnMain {
+ appState.appInfo = AppInfo.empty
+ appState.currentView = .empty
+ }
+ let selectedItemsArray = Array(appState.selectedItems).filter { !$0.path.contains(".Trash") }
+ killApp(appId: appState.appInfo.bundleIdentifier) {
+ moveFilesToTrash(at: selectedItemsArray) {
+ withAnimation {
+ appState.isReminderVisible.toggle()
+ }
+ refreshAppList(appState.appInfo)
+ }
+ }
+ }
+ }
+ .disabled(appState.selectedItems.isEmpty)
+ .buttonStyle(WindowActionButton(action: .accept))
+ }
+// .padding(.top)
+
+ }
+ .transition(.opacity)
+ .padding(20)
+
+ }
+
+ }
+ .frame(minWidth: 700)
+ .onAppear {
+ Task {
+ calculateFileDetails()
+ }
+ }
+ }
+
+
+ func calculateFileDetails() {
+ if appState.paths.count != 0 {
+ itemDetails = Array(repeating: (size: "", icon: nil), count: appState.paths.count)
+ Task {
+ for (index, path) in appState.paths.enumerated() {
+ var size = ""
+ var icon: Image? = nil
+
+ if let appSize = totalSizeOnDisk(for: path) {
+ size = "\(appSize)"
+ } else {
+ print("Error calculating the total size on disk for item \(index).")
+ }
+ if let folderIcon = getIconForFileOrFolder(atPath: path) {
+ icon = folderIcon
+ }
+ itemDetails[index] = (size: size, icon: icon)
+ }
+ if let appSize = totalSizeOnDisk(for: appState.paths) {
+ self.appSize = "\(appSize)"
+ self.showDetails = true
+ } else {
+ print("Error calculating the total size on disk.")
+ }
+ }
+ } else {
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
+ calculateFileDetails()
+ }
+ }
+ }
+
+
+ func refreshAppList(_ appInfo: AppInfo) {
+ let sortedApps = getSortedApps()
+ appState.sortedApps.userApps = []
+ appState.sortedApps.systemApps = []
+ appState.sortedApps.userApps = sortedApps.userApps
+ appState.sortedApps.systemApps = sortedApps.systemApps
+ }
+}
+
+
+
+struct FileDetailsItem: View {
+ @EnvironmentObject var appState: AppState
+ let size: String
+ let icon: Image?
+ let path: URL
+
+ var body: some View {
+ HStack(alignment: .center, spacing: 20) {
+ Toggle("", isOn: Binding(
+ get: { self.appState.selectedItems.contains(self.path) },
+ set: { isChecked in
+ if isChecked {
+ self.appState.selectedItems.insert(self.path)
+ if self.path == appState.appInfo.path {
+ self.appState.paths.forEach { self.appState.selectedItems.insert($0) }
+ }
+ } else {
+ self.appState.selectedItems.remove(self.path)
+ if self.path == appState.appInfo.path {
+ self.appState.selectedItems.forEach { self.appState.selectedItems.remove($0) }
+ }
+ }
+ }
+ ))
+ .disabled(self.path.path.contains(".Trash"))
+
+ if let appIcon = icon {
+ appIcon
+ .resizable()
+ .aspectRatio(contentMode: .fit)
+ .frame(width: 30, height: 30)
+ }
+ VStack(alignment: .leading, spacing: 5) {
+ Text(path.lastPathComponent)
+ .font(.title3)
+ .lineLimit(1)
+ .truncationMode(.tail)
+ .help(path.lastPathComponent)
+ Text(path.path)
+ .font(.footnote)
+ .lineLimit(1)
+ .truncationMode(.tail)
+ .opacity(0.5)
+// .foregroundStyle(Color("AccentColor"))
+ .help(path.path)
+ }
+
+ Spacer()
+ if size.isEmpty {
+ ProgressView().controlSize(.small)
+ } else {
+ Text(size)
+ }
+
+
+ Button("") {
+ NSWorkspace.shared.selectFile(path.path, inFileViewerRootedAtPath: path.deletingLastPathComponent().path)
+ }
+ .buttonStyle(SimpleButtonStyle(icon: "folder.fill", help: "Show in Finder", color: Color("AccentColor")))
+
+ }
+ .background(
+ RoundedRectangle(cornerRadius: 10)
+ .fill(Color.white.opacity(0))
+ )
+ }
+}
+
+
+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) {
+ print("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 {
+ print("Error opening file for appending")
+ }
+ }
+}
+
+//struct FilesView: View {
+// @EnvironmentObject var appState: AppState
+// @State private var appSize: String = ""
+// @State private var showDetails: Bool = false
+// @State private var itemDetails: [(size: String, icon: Image?)] = []
+//
+// var body: some View {
+// VStack(alignment: .center) {
+// if !self.showDetails {
+// ProgressView("Finding application files..")
+// } else {
+// VStack() {
+// HStack() {
+// Text("\(appState.paths.count > 1 ? "\(appState.paths.count) files found" : "\(appState.paths.count) file found")")
+// Text("•")
+// Text("\(appSize)")
+// }
+// .padding()
+//
+// if appState.appInfo.appName.count < 5 {
+// Text("Short or common word app name, there might be unrelated files found")
+// .font(.footnote)
+// .foregroundStyle(.red)
+// }
+//
+// ScrollView {
+// ForEach(Array(zip(appState.paths, itemDetails)), id: \.0) { path, details in
+// FileDetailsItem(size: details.size, icon: details.icon, path: path)
+// }
+// }
+// .frame(width: 500)
+//
+// Button("Remove") {
+// Task {
+// updateOnMain {
+// appState.appInfo = AppInfo.empty
+// }
+// let selectedItemsArray = Array(appState.selectedItems)
+// killApp(appId: appState.appInfo.bundleIdentifier) {
+// moveFilesToTrash(at: selectedItemsArray) {
+// refreshAppList(appState.appInfo)
+// }
+// }
+// }
+// }
+// .disabled(appState.selectedItems.isEmpty)
+// }
+// .padding()
+//
+// }
+//
+// }
+// .frame(minWidth: 500)
+// .onAppear {
+// Task {
+// // appState.appInfo = appInfo
+// // findPathsForApp(appState: appState, appInfo: appState.appInfo)
+// calculateFileDetails()
+// }
+// }
+// }
+//
+//
+// func calculateFileDetails() {
+// if appState.paths.count != 0 {
+// itemDetails = Array(repeating: (size: "", icon: nil), count: appState.paths.count)
+//
+// Task {
+// for (index, path) in appState.paths.enumerated() {
+// var size = ""
+// var icon: Image? = nil
+//
+// if let appSize = totalSizeOnDisk(for: path) {
+// size = "\(appSize)"
+// } else {
+// print("Error calculating the total size on disk for item \(index).")
+// }
+//
+// if let folderIcon = getIconForFileOrFolder(atPath: path) {
+// icon = folderIcon
+// }
+//
+// itemDetails[index] = (size: size, icon: icon)
+//
+//
+//
+// }
+//
+// if let appSize = totalSizeOnDisk(for: appState.paths) {
+// self.appSize = "\(appSize)"
+// // updateOnMain {
+// // appState.doneLoading = true
+// // }
+// self.showDetails = true
+// // appState.doneLoading = true
+// } else {
+// print("Error calculating the total size on disk.")
+// }
+// }
+// } else {
+// DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
+// calculateFileDetails()
+// }
+// }
+// }
+//
+// func refreshAppList(_ appInfo: AppInfo) {
+// let sortedApps = getSortedApps()
+// appState.sortedApps.userApps = []
+// appState.sortedApps.systemApps = []
+// appState.sortedApps.userApps = sortedApps.userApps
+// appState.sortedApps.systemApps = sortedApps.systemApps
+// }
+//}
diff --git a/Pearcleaner/Views/TopBar.swift b/Pearcleaner/Views/TopBar.swift
new file mode 100644
index 0000000..8ff1c75
--- /dev/null
+++ b/Pearcleaner/Views/TopBar.swift
@@ -0,0 +1,104 @@
+//
+// TopBar.swift
+// Pearcleaner
+//
+// Created by Alin Lupascu on 11/10/23.
+//
+
+import Foundation
+import SwiftUI
+
+struct TopBar: View {
+// @Binding var sidebar: Bool
+ @Binding var reload: Bool
+ @AppStorage("displayMode") var displayMode: DisplayMode = .system
+ @AppStorage("settings.general.glass") private var glass: Bool = false
+ @EnvironmentObject var appState: AppState
+
+ var body: some View {
+ HStack(alignment: .center, spacing: 20) {
+
+ HStack(alignment: .center, spacing: 5) {
+// Button("") {
+// withAnimation(.easeInOut(duration: 0.5)) {
+// appState.sidebar.toggle()
+// if appState.sidebar {
+// appState.winWidth += 300
+// } else {
+// appState.winWidth -= 300
+// }
+// }
+// }
+// .buttonStyle(SimpleButtonStyle(icon: "sidebar.left", help: "Toggle Sidebar", color: Color("AccentColor")))
+
+
+ Button("") {
+ withAnimation(.easeInOut(duration: 0.5)) {
+ // Refresh Apps list
+ reload.toggle()
+ let sortedApps = getSortedApps()
+ DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
+ appState.sortedApps.userApps = sortedApps.userApps
+ appState.sortedApps.systemApps = sortedApps.systemApps
+ reload.toggle()
+ }
+
+ }
+ }
+ .buttonStyle(SimpleButtonStyle(icon: "arrow.circlepath", help: "Refresh app list", color: Color("AccentColor")))
+
+ Button("") {
+ withAnimation(.easeInOut(duration: 0.5)) {
+ openTrash()
+ }
+ }
+ .buttonStyle(SimpleButtonStyle(icon: "trash", help: "Open Trash", color: Color("AccentColor")))
+ }
+
+ Spacer()
+
+
+
+ if appState.currentView != .empty {
+ Button("") {
+ withAnimation(.easeInOut(duration: 0.5)) {
+ appState.currentView = .empty
+ appState.appInfo = AppInfo.empty
+ }
+ }
+ .buttonStyle(SimpleButtonStyle(icon: "house", help: "Home", color: Color("AccentColor")))
+ }
+
+
+
+
+ Spacer()
+
+ HStack(alignment: .center, spacing: 5) {
+ Button("") {
+ withAnimation(.easeInOut(duration: 0.5)) {
+ glass.toggle()
+ }
+ }
+ .buttonStyle(SimpleButtonStyle(icon: glass ? "circle.dashed" : "circle.dashed.inset.filled", help: "Toggle transparency", color: Color("AccentColor")))
+
+ Button("") {
+ withAnimation(.easeInOut(duration: 0.5)) {
+ if displayMode.colorScheme == .dark {
+ displayMode.colorScheme = .light
+ } else if displayMode.colorScheme == .light {
+ displayMode.colorScheme = .dark
+ } else {
+ displayMode.colorScheme = .dark
+ }
+ }
+ }
+ .buttonStyle(SimpleButtonStyle(icon: displayMode.colorScheme == .dark ? "sun.max.fill" : "moon.fill", help: "Toggle appearance", color: Color("AccentColor")))
+ }
+
+
+ }
+ .padding(.horizontal, 20)
+ .padding(.top, 30)
+ }
+}
diff --git a/Pearcleaner/Views/TrashView.swift b/Pearcleaner/Views/TrashView.swift
new file mode 100644
index 0000000..7affbcc
--- /dev/null
+++ b/Pearcleaner/Views/TrashView.swift
@@ -0,0 +1,19 @@
+//
+// TrashView.swift
+// Pearcleaner
+//
+// Created by Alin Lupascu on 11/11/23.
+//
+
+import Foundation
+import SwiftUI
+
+struct TrashView: View {
+ @EnvironmentObject var appState: AppState
+
+ var body: some View {
+ VStack() {
+ Text("Trash view my boi")
+ }
+ }
+}
diff --git a/PearcleanerSentinel/PearcleanerSentinel.entitlements b/PearcleanerSentinel/PearcleanerSentinel.entitlements
new file mode 100644
index 0000000..0c67376
--- /dev/null
+++ b/PearcleanerSentinel/PearcleanerSentinel.entitlements
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/PearcleanerSentinel/main.swift b/PearcleanerSentinel/main.swift
new file mode 100644
index 0000000..abe12d6
--- /dev/null
+++ b/PearcleanerSentinel/main.swift
@@ -0,0 +1,285 @@
+//
+// main.swift
+// PearcleanerSentinel
+//
+// Created by Alin Lupascu on 11/9/23.
+//
+
+import Foundation
+import FileWatcher
+import AppKit
+
+// Start Trash monitoring
+fileWatcher()
+
+// Keep alive indefinitely
+while true {
+ sleep(1)
+}
+
+func fileWatcher() {
+ let home = FileManager.default.homeDirectoryForCurrentUser.path
+ let filewatcher = FileWatcher(["\(home)/.Trash"])
+ filewatcher.queue = DispatchQueue.global()
+ filewatcher.callback = { event in
+ checkApp(file: event.path)
+ }
+ filewatcher.start()
+}
+
+func checkApp(file: String) {
+ let app = URL(fileURLWithPath: file)
+ let appExt = app.pathExtension
+ if appExt == "app" {
+ if FileManager.default.isInTrash(app) {
+ NSWorkspace.shared.open(URL(string: "pear://com.alienator88.Pearcleaner?path=\(file)")!)
+ }
+ }
+}
+
+// --- Trash Relationship ---
+extension FileManager {
+ public func isInTrash(_ file: URL) -> Bool {
+ var relationship: URLRelationship = .other
+ do {
+ try getRelationship(&relationship, of: .trashDirectory, in: .userDomainMask, toItemAt: file)
+ return relationship == .contains
+ } catch {
+ return false
+ }
+ }
+}
+
+
+
+
+
+
+// For testing and outputing logging to file from cmd line tool
+func writeLog(string: String) {
+ let fileManager = FileManager.default
+ let home = fileManager.homeDirectoryForCurrentUser.path
+ let logFilePath = "\(home)/Downloads/monitor.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) {
+ print("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 {
+ print("Error opening file for appending")
+ }
+ }
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+// let trashContents = getTrashContents()
+// if !trashContents.isEmpty && trashContents.contains(event.path) {
+// NSWorkspace.shared.open(URL(string: "pear://com.alienator88.Pearcleaner?path=\(event.path)")!)
+// }
+
+//func getTrashContents() -> [String] {
+// let fileManager = FileManager.default
+// let trashURLs = fileManager.urls(for: .trashDirectory, in: .userDomainMask)
+// do {
+// let trashContents = try fileManager.contentsOfDirectory(at: trashURLs.first!, includingPropertiesForKeys: nil, options: [])
+// let appFiles = trashContents.filter { $0.pathExtension == "app" }
+// writeLog(string: appFiles.first!.path)
+//
+// return appFiles.map { $0.path }
+// } catch {
+// print("Failed to get contents of trash directory: \(error)")
+// return []
+// }
+//}
+
+//func hasAccessToTrashFolder() -> Bool {
+// let fileManager = FileManager.default
+// let trashURLs = fileManager.urls(for: .trashDirectory, in: .userDomainMask)
+//
+// if let trashURL = trashURLs.first {
+// return fileManager.isReadableFile(atPath: trashURL.path) && fileManager.isWritableFile(atPath: trashURL.path)
+// }
+//
+// return false
+//}
+
+//if hasAccessToTrashFolder() {
+// writeLog(string: "Your tool has access to the Trash folder.")
+// print("Your tool has access to the Trash folder.")
+//} else {
+// if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles") {
+// NSWorkspace.shared.open(url)
+// }
+// writeLog(string: "Your tool does not have access to the Trash folder.")
+// print("Your tool does not have access to the Trash folder.")
+//}
+
+
+//var dirWatcher: DirMonitor?
+//func fileW2() {
+// let home = FileManager.default.homeDirectoryForCurrentUser.path
+// let url = URL(fileURLWithPath: "\(home)/.Trash")
+// dirWatcher = DirMonitor(dir: url, queue: .global())
+// if dirWatcher?.start() == true {
+// NSLog("Started directory monitoring")
+// } else {
+// NSLog("Failed to start directory monitoring")
+// }
+//}
+//
+//class DirMonitor {
+//
+// init(dir: URL, queue: DispatchQueue) {
+// self.dir = dir
+// self.queue = queue
+// }
+//
+// deinit {
+// // The stream has a reference to us via its `info` pointer. If the
+// // client releases their reference to us without calling `stop`, that
+// // results in a dangling pointer. We detect this as a programming error.
+// // There are other approaches to take here (for example, messing around
+// // with weak, or forming a retain cycle that’s broken on `stop`), but
+// // this approach:
+// //
+// // * Has clear rules
+// // * Is easy to implement
+// // * Generate a sensible debug message if the client gets things wrong
+// precondition(self.stream == nil, "released a running monitor")
+// // I added this log line as part of my testing of the deallocation path.
+// NSLog("did deinit")
+// }
+//
+// let dir: URL
+// let queue: DispatchQueue
+//
+// private var stream: FSEventStreamRef? = nil
+//
+// func start() -> Bool {
+// precondition(self.stream == nil, "started a running monitor")
+//
+// // Set up our context.
+// //
+// // `FSEventStreamCallback` is a C function, so we pass `self` to the
+// // `info` pointer so that it get call our `handleUnsafeEvents(…)`
+// // method. This involves the standard `Unmanaged` dance:
+// //
+// // * Here we set `info` to an unretained pointer to `self`.
+// // * Inside the function we extract that pointer as `obj` and then use
+// // that to call `handleUnsafeEvents(…)`.
+//
+// var context = FSEventStreamContext()
+// context.info = Unmanaged.passUnretained(self).toOpaque()
+//
+// // Create the stream.
+// //
+// // In this example I wanted to show how to deal with raw string paths,
+// // so I’m not taking advantage of `kFSEventStreamCreateFlagUseCFTypes`
+// // or the even cooler `kFSEventStreamCreateFlagUseExtendedData`.
+//
+// guard let stream = FSEventStreamCreate(nil, { (stream, info, numEvents, eventPaths, eventFlags, eventIds) in
+// let obj = Unmanaged.fromOpaque(info!).takeUnretainedValue()
+// obj.handleUnsafeEvents(numEvents: numEvents, eventPaths: eventPaths, eventFlags: eventFlags, eventIDs: eventIds)
+// },
+// &context,
+// [self.dir.path as NSString] as NSArray,
+// UInt64(kFSEventStreamEventIdSinceNow),
+// 1.0,
+// FSEventStreamCreateFlags(kFSEventStreamCreateFlagNone)
+// ) else {
+// return false
+// }
+// self.stream = stream
+//
+// // Now that we have a stream, schedule it on our target queue.
+//
+// FSEventStreamSetDispatchQueue(stream, queue)
+// guard FSEventStreamStart(stream) else {
+// FSEventStreamInvalidate(stream)
+// self.stream = nil
+// return false
+// }
+// return true
+// }
+//
+// private func handleUnsafeEvents(numEvents: Int, eventPaths: UnsafeMutableRawPointer, eventFlags: UnsafePointer, eventIDs: UnsafePointer) {
+// // This takes the low-level goo from the C callback, converts it to
+// // something that makes sense for Swift, and then passes that to
+// // `handle(events:…)`.
+// //
+// // Note that we don’t need to do any rebinding here because this data is
+// // coming C as the right type.
+// let pathsBase = eventPaths.assumingMemoryBound(to: UnsafePointer.self)
+// let pathsBuffer = UnsafeBufferPointer(start: pathsBase, count: numEvents)
+// let flagsBuffer = UnsafeBufferPointer(start: eventFlags, count: numEvents)
+// let eventIDsBuffer = UnsafeBufferPointer(start: eventIDs, count: numEvents)
+// // As `zip(_:_:)` only handles two sequences, I map over the index.
+// let events = (0.. (url: URL, flags: FSEventStreamEventFlags, eventIDs: FSEventStreamEventId) in
+// let path = pathsBuffer[i]
+// // We set `isDirectory` to true because we only generate directory
+// // events (that is, we don’t pass
+// // `kFSEventStreamCreateFlagFileEvents` to `FSEventStreamCreate`.
+// // This is generally the best way to use FSEvents, but if you decide
+// // to take advantage of `kFSEventStreamCreateFlagFileEvents` then
+// // you’ll need to code to `isDirectory` correctly.
+// let url: URL = URL(fileURLWithFileSystemRepresentation: path, isDirectory: true, relativeTo: nil)
+// return (url, flagsBuffer[i], eventIDsBuffer[i])
+// }
+// self.handle(events: events)
+// }
+//
+// private func handle(events: [(url: URL, flags: FSEventStreamEventFlags, eventIDs: FSEventStreamEventId)]) {
+// // In this example we just print the events with get, prefixed by a
+// // count so that we can see the batching in action.
+// NSLog("%d", events.count)
+// for (url, flags, eventID) in events {
+// NSLog("%16x %8x %@", eventID, flags, url.path)
+// }
+// for (url, flags, _) in events {
+// if flags & FSEventStreamEventFlags(kFSEventStreamEventFlagItemRemoved) != 0 {
+// NSLog("Removed: \(url.path)")
+// }
+// if flags & FSEventStreamEventFlags(kFSEventStreamEventFlagItemCreated) != 0 {
+// NSLog("Created: \(url.path)")
+// }
+// }
+// getTrashContents()
+// }
+//
+// func stop() {
+// guard let stream = self.stream else {
+// return // We accept redundant calls to `stop`.
+// }
+// FSEventStreamStop(stream)
+// FSEventStreamInvalidate(stream)
+// self.stream = nil
+// }
+//
+// func getTrashContents() {
+// let fileManager = FileManager.default
+// let trashURLs = fileManager.urls(for: .trashDirectory, in: .userDomainMask)
+// let trashContents = try? fileManager.contentsOfDirectory(at: trashURLs.first!, includingPropertiesForKeys: nil, options: [])
+// print(trashContents as Any)
+// }
+//}
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..708a6f1
--- /dev/null
+++ b/README.md
@@ -0,0 +1,2 @@
+# Pearcleaner
+ Open-source macOS app cleaner