This commit is contained in:
Alin
2023-11-12 20:59:30 -07:00
parent 84fe1c48b6
commit fd652e4c64
56 changed files with 5711 additions and 0 deletions
+38
View File
@@ -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.
+20
View File
@@ -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.
+91
View File
@@ -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
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key>
<string>mac-application</string>
<key>teamID</key>
<string>lupascu.marius@gmail.com</string>
</dict>
</plist>
+11
View File
@@ -0,0 +1,11 @@
## New Features
- N/A
## Fixed Issues
- N/A
## Changes
- N/A
+631
View File
@@ -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 = "<group>"; };
C728930F2AFD51EA00C8C1CD /* PearcleanerSentinel */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = PearcleanerSentinel; sourceTree = BUILT_PRODUCTS_DIR; };
C72893112AFD51EA00C8C1CD /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
C76D08472AF83C3F00D07867 /* Update.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Update.swift; sourceTree = "<group>"; };
C76D08492AF83C6E00D07867 /* General.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = General.swift; sourceTree = "<group>"; };
C76D084B2AF83C9D00D07867 /* Permissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Permissions.swift; sourceTree = "<group>"; };
C76D084D2AF84D8C00D07867 /* Builds */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Builds; sourceTree = "<group>"; };
C76D08502AF850F300D07867 /* About.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = About.swift; sourceTree = "<group>"; };
C76D08542AF89CDE00D07867 /* AppListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppListView.swift; sourceTree = "<group>"; };
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 = "<group>"; };
C77B90052AF18E2E009CC655 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
C77B90072AF18E2F009CC655 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
C77B90142AF18E7C009CC655 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
C77B90152AF19377009CC655 /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = "<group>"; };
C77B90172AF19382009CC655 /* AppCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCommands.swift; sourceTree = "<group>"; };
C77B90192AF1938F009CC655 /* Logic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logic.swift; sourceTree = "<group>"; };
C77B901B2AF193A3009CC655 /* Styles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Styles.swift; sourceTree = "<group>"; };
C77B901D2AF1A9DB009CC655 /* SettingsWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsWindow.swift; sourceTree = "<group>"; };
C77B90222AF2D616009CC655 /* FilesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilesView.swift; sourceTree = "<group>"; };
C77B90242AF2D796009CC655 /* Pearcleaner.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Pearcleaner.entitlements; sourceTree = "<group>"; };
C7A27E7F2AFD70DD00166168 /* PearcleanerSentinel.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = PearcleanerSentinel.entitlements; sourceTree = "<group>"; };
C7A27E802AFD7C4600166168 /* com.alienator88.PearcleanerSentinel.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = com.alienator88.PearcleanerSentinel.plist; sourceTree = "<group>"; };
C7D31D452AFDA90C00C7ED9E /* Sentinel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sentinel.swift; sourceTree = "<group>"; };
C7D31D472AFEB23700C7ED9E /* TopBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopBar.swift; sourceTree = "<group>"; };
C7D31D492AFEB26700C7ED9E /* AppListItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppListItems.swift; sourceTree = "<group>"; };
C7D31D502AFF00F300C7ED9E /* Locations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Locations.swift; sourceTree = "<group>"; };
C7D31D522B0048BE00C7ED9E /* TrashView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrashView.swift; sourceTree = "<group>"; };
C7D31D542B008CB900C7ED9E /* Shapes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shapes.swift; sourceTree = "<group>"; };
C7D31D582B00A49300C7ED9E /* DropTarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropTarget.swift; sourceTree = "<group>"; };
C7F539372AF60865007DF1B2 /* Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utilities.swift; sourceTree = "<group>"; };
/* 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 = "<group>";
};
C72893102AFD51EA00C8C1CD /* PearcleanerSentinel */ = {
isa = PBXGroup;
children = (
C7A27E822AFD996100166168 /* Resources */,
C7A27E7F2AFD70DD00166168 /* PearcleanerSentinel.entitlements */,
C72893112AFD51EA00C8C1CD /* main.swift */,
);
path = PearcleanerSentinel;
sourceTree = "<group>";
};
C72893162AFD531500C8C1CD /* Frameworks */ = {
isa = PBXGroup;
children = (
);
name = Frameworks;
sourceTree = "<group>";
};
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 = "<group>";
};
C76D084F2AF84E0100D07867 /* Resources */ = {
isa = PBXGroup;
children = (
C7A27E802AFD7C4600166168 /* com.alienator88.PearcleanerSentinel.plist */,
C77B90142AF18E7C009CC655 /* Info.plist */,
C77B90072AF18E2F009CC655 /* Assets.xcassets */,
C77B90242AF2D796009CC655 /* Pearcleaner.entitlements */,
);
path = Resources;
sourceTree = "<group>";
};
C77B8FF72AF18E2E009CC655 = {
isa = PBXGroup;
children = (
C76D084D2AF84D8C00D07867 /* Builds */,
C77B90022AF18E2E009CC655 /* Pearcleaner */,
C72893102AFD51EA00C8C1CD /* PearcleanerSentinel */,
C77B90012AF18E2E009CC655 /* Products */,
C72893162AFD531500C8C1CD /* Frameworks */,
);
sourceTree = "<group>";
};
C77B90012AF18E2E009CC655 /* Products */ = {
isa = PBXGroup;
children = (
C77B90002AF18E2E009CC655 /* Pearcleaner.app */,
C728930F2AFD51EA00C8C1CD /* PearcleanerSentinel */,
);
name = Products;
sourceTree = "<group>";
};
C77B90022AF18E2E009CC655 /* Pearcleaner */ = {
isa = PBXGroup;
children = (
C77B90032AF18E2E009CC655 /* PearcleanerApp.swift */,
C76D084F2AF84E0100D07867 /* Resources */,
C76D08462AF83C2F00D07867 /* Settings */,
C77B901F2AF1B390009CC655 /* Views */,
C72893072AFD442600C8C1CD /* Logic */,
);
path = Pearcleaner;
sourceTree = "<group>";
};
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 = "<group>";
};
C7A27E822AFD996100166168 /* Resources */ = {
isa = PBXGroup;
children = (
);
path = Resources;
sourceTree = "<group>";
};
/* 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 */;
}
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
@@ -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
}
@@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1500"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C77B8FFF2AF18E2E009CC655"
BuildableName = "Pearcleaner.app"
BlueprintName = "Pearcleaner"
ReferencedContainer = "container:Pearcleaner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C77B8FFF2AF18E2E009CC655"
BuildableName = "Pearcleaner.app"
BlueprintName = "Pearcleaner"
ReferencedContainer = "container:Pearcleaner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Debug"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C77B8FFF2AF18E2E009CC655"
BuildableName = "Pearcleaner.app"
BlueprintName = "Pearcleaner"
ReferencedContainer = "container:Pearcleaner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Debug"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
@@ -0,0 +1,103 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1500"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C77B8FFF2AF18E2E009CC655"
BuildableName = "Pearcleaner.app"
BlueprintName = "Pearcleaner"
ReferencedContainer = "container:Pearcleaner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Release"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Release"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C77B8FFF2AF18E2E009CC655"
BuildableName = "Pearcleaner.app"
BlueprintName = "Pearcleaner"
ReferencedContainer = "container:Pearcleaner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<EnvironmentVariables>
<EnvironmentVariable
key = "OS_ACTIVITY_MODE"
value = "disable"
isEnabled = "YES">
</EnvironmentVariable>
</EnvironmentVariables>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C77B8FFF2AF18E2E009CC655"
BuildableName = "Pearcleaner.app"
BlueprintName = "Pearcleaner"
ReferencedContainer = "container:Pearcleaner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Release">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "NO">
<PostActions>
<ExecutionAction
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
<ActionContent
title = "Run Script"
scriptText = "# For troubleshooting, enable to get log with errors&#10;# exec &gt; &quot;/Users/alin/GitHub/PeakPurge/Builds/Export/Xcode.log&quot; 2&gt;&amp;1&#10;&#10;# Variables&#10;MAIN_PROJECT_PATH=&quot;/Users/alin/GitHub/Pearcleaner&quot;&#10;EXPORT_PATH=&quot;/Users/alin/GitHub/Pearcleaner/Builds/Export&quot;&#10;while [[ -z $MARKETING_VERSION ]]; do&#10; MARKETING_VERSION=$(xcodebuild -showBuildSettings 2&gt;/dev/null | awk -F&apos; = &apos; &apos;/MARKETING_VERSION/{print $2}&apos; | xargs)&#10; sleep 1&#10;done&#10;&#10;# Export the app file&#10;xcodebuild -exportArchive -archivePath &quot;${ARCHIVE_PATH}&quot; -exportOptionsPlist &quot;${MAIN_PROJECT_PATH}&quot;/Builds/ExportOptions.plist -exportPath &quot;${EXPORT_PATH}&quot;&#10;&#10;# Zip up the app file&#10;cd &quot;${EXPORT_PATH}&quot;&#10;zip -r -X &quot;Pearcleaner.zip&quot; &quot;Pearcleaner.app&quot;&#10;&#10;# Push to GitHub&#10;/opt/homebrew/bin/gh release create &quot;${MARKETING_VERSION}&quot; PeakPurge.zip --latest -t &quot;v${MARKETING_VERSION}&quot; -F &quot;${MAIN_PROJECT_PATH}/Builds/changes.md&quot;&#10;&#10;# Clean out the archive and app file, zip, etc. in Export folder&#10;rm -rf &quot;${ARCHIVE_PATH}&quot;&#10;#rm -rf &quot;${EXPORT_PATH}&quot;/*.app&#10;rm -rf &quot;${EXPORT_PATH}&quot;/*&#10;&#10;# Open folder at Export location&#10;#open &quot;${EXPORT_PATH}&quot;&#10;"
shellToInvoke = "/bin/bash">
<EnvironmentBuildable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C77B8FFF2AF18E2E009CC655"
BuildableName = "Pearcleaner.app"
BlueprintName = "Pearcleaner"
ReferencedContainer = "container:Pearcleaner.xcodeproj">
</BuildableReference>
</EnvironmentBuildable>
</ActionContent>
</ExecutionAction>
</PostActions>
</ArchiveAction>
</Scheme>
@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1500"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C728930E2AFD51EA00C8C1CD"
BuildableName = "PearcleanerSentinel"
BlueprintName = "PearcleanerSentinel"
ReferencedContainer = "container:Pearcleaner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES"
viewDebuggingEnabled = "No">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C728930E2AFD51EA00C8C1CD"
BuildableName = "PearcleanerSentinel"
BlueprintName = "PearcleanerSentinel"
ReferencedContainer = "container:Pearcleaner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C728930E2AFD51EA00C8C1CD"
BuildableName = "PearcleanerSentinel"
BlueprintName = "PearcleanerSentinel"
ReferencedContainer = "container:Pearcleaner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
+106
View File
@@ -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")
}
}
}
}
+169
View File
@@ -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<URL>()
@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
//]
+41
View File
@@ -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")
}
}
}
+105
View File
@@ -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",
])
}
}
+362
View File
@@ -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()
//}
+599
View File
@@ -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)
// )
//}
+418
View File
@@ -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<Self._Label>) -> 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<Self._Label>) -> 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)
+509
View File
@@ -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
//}
+92
View File
@@ -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
}
}
@@ -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
}
}
@@ -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
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 662 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -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
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

@@ -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
}
}
+19
View File
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>com.alienator88.Pearcleaner</string>
<key>CFBundleURLSchemes</key>
<array>
<string>pear</string>
</array>
</dict>
</array>
</dict>
</plist>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.automation.apple-events</key>
<true/>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.alienator88.PearcleanerSentinel</string>
<key>ProgramArguments</key>
<array>
<string>__EXECUTABLE_PATH__</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
</dict>
</plist>
+172
View File
@@ -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"
}
}
+64
View File
@@ -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)
}
}
+104
View File
@@ -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)
}
}
}
+52
View File
@@ -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)
}
}
+57
View File
@@ -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)
}
}
}
+186
View File
@@ -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()
}
+90
View File
@@ -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
}
}
}
}
}
+213
View File
@@ -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))
}
}
}
+42
View File
@@ -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) }
//
// }
//}
//
+168
View File
@@ -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
}
}
}
+443
View File
@@ -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
// }
//}
+104
View File
@@ -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)
}
}
+19
View File
@@ -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")
}
}
}
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>
+285
View File
@@ -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 thats 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 Im 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<DirMonitor>.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<FSEventStreamEventFlags>, eventIDs: UnsafePointer<FSEventStreamEventId>) {
// // 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 dont need to do any rebinding here because this data is
// // coming C as the right type.
// let pathsBase = eventPaths.assumingMemoryBound(to: UnsafePointer<CChar>.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..<numEvents).map { i -> (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 dont pass
// // `kFSEventStreamCreateFlagFileEvents` to `FSEventStreamCreate`.
// // This is generally the best way to use FSEvents, but if you decide
// // to take advantage of `kFSEventStreamCreateFlagFileEvents` then
// // youll 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)
// }
//}
+2
View File
@@ -0,0 +1,2 @@
# Pearcleaner
Open-source macOS app cleaner