initial
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
## New Features
|
||||||
|
|
||||||
|
- N/A
|
||||||
|
|
||||||
|
## Fixed Issues
|
||||||
|
|
||||||
|
- N/A
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- N/A
|
||||||
@@ -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 # exec > "/Users/alin/GitHub/PeakPurge/Builds/Export/Xcode.log" 2>&1 # Variables MAIN_PROJECT_PATH="/Users/alin/GitHub/Pearcleaner" EXPORT_PATH="/Users/alin/GitHub/Pearcleaner/Builds/Export" while [[ -z $MARKETING_VERSION ]]; do MARKETING_VERSION=$(xcodebuild -showBuildSettings 2>/dev/null | awk -F' = ' '/MARKETING_VERSION/{print $2}' | xargs) sleep 1 done # Export the app file xcodebuild -exportArchive -archivePath "${ARCHIVE_PATH}" -exportOptionsPlist "${MAIN_PROJECT_PATH}"/Builds/ExportOptions.plist -exportPath "${EXPORT_PATH}" # Zip up the app file cd "${EXPORT_PATH}" zip -r -X "Pearcleaner.zip" "Pearcleaner.app" # Push to GitHub /opt/homebrew/bin/gh release create "${MARKETING_VERSION}" PeakPurge.zip --latest -t "v${MARKETING_VERSION}" -F "${MAIN_PROJECT_PATH}/Builds/changes.md" # Clean out the archive and app file, zip, etc. in Export folder rm -rf "${ARCHIVE_PATH}" #rm -rf "${EXPORT_PATH}"/*.app rm -rf "${EXPORT_PATH}"/* # Open folder at Export location #open "${EXPORT_PATH}" "
|
||||||
|
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>
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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
|
||||||
|
//]
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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",
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
//}
|
||||||
@@ -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)
|
||||||
|
// )
|
||||||
|
//}
|
||||||
@@ -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)
|
||||||
@@ -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
|
||||||
|
//}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 662 B |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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) }
|
||||||
|
//
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
//
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
// }
|
||||||
|
//}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,285 @@
|
|||||||
|
//
|
||||||
|
// main.swift
|
||||||
|
// PearcleanerSentinel
|
||||||
|
//
|
||||||
|
// Created by Alin Lupascu on 11/9/23.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import FileWatcher
|
||||||
|
import AppKit
|
||||||
|
|
||||||
|
// Start Trash monitoring
|
||||||
|
fileWatcher()
|
||||||
|
|
||||||
|
// Keep alive indefinitely
|
||||||
|
while true {
|
||||||
|
sleep(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fileWatcher() {
|
||||||
|
let home = FileManager.default.homeDirectoryForCurrentUser.path
|
||||||
|
let filewatcher = FileWatcher(["\(home)/.Trash"])
|
||||||
|
filewatcher.queue = DispatchQueue.global()
|
||||||
|
filewatcher.callback = { event in
|
||||||
|
checkApp(file: event.path)
|
||||||
|
}
|
||||||
|
filewatcher.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkApp(file: String) {
|
||||||
|
let app = URL(fileURLWithPath: file)
|
||||||
|
let appExt = app.pathExtension
|
||||||
|
if appExt == "app" {
|
||||||
|
if FileManager.default.isInTrash(app) {
|
||||||
|
NSWorkspace.shared.open(URL(string: "pear://com.alienator88.Pearcleaner?path=\(file)")!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Trash Relationship ---
|
||||||
|
extension FileManager {
|
||||||
|
public func isInTrash(_ file: URL) -> Bool {
|
||||||
|
var relationship: URLRelationship = .other
|
||||||
|
do {
|
||||||
|
try getRelationship(&relationship, of: .trashDirectory, in: .userDomainMask, toItemAt: file)
|
||||||
|
return relationship == .contains
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// For testing and outputing logging to file from cmd line tool
|
||||||
|
func writeLog(string: String) {
|
||||||
|
let fileManager = FileManager.default
|
||||||
|
let home = fileManager.homeDirectoryForCurrentUser.path
|
||||||
|
let logFilePath = "\(home)/Downloads/monitor.txt"
|
||||||
|
|
||||||
|
// Check if the log file exists, and create it if it doesn't
|
||||||
|
if !fileManager.fileExists(atPath: logFilePath) {
|
||||||
|
if !fileManager.createFile(atPath: logFilePath, contents: nil, attributes: nil) {
|
||||||
|
print("Failed to create the log file.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
if let fileHandle = FileHandle(forWritingAtPath: logFilePath) {
|
||||||
|
let ns = "\(string)\n"
|
||||||
|
fileHandle.seekToEndOfFile()
|
||||||
|
fileHandle.write(ns.data(using: .utf8)!)
|
||||||
|
fileHandle.closeFile()
|
||||||
|
} else {
|
||||||
|
print("Error opening file for appending")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// let trashContents = getTrashContents()
|
||||||
|
// if !trashContents.isEmpty && trashContents.contains(event.path) {
|
||||||
|
// NSWorkspace.shared.open(URL(string: "pear://com.alienator88.Pearcleaner?path=\(event.path)")!)
|
||||||
|
// }
|
||||||
|
|
||||||
|
//func getTrashContents() -> [String] {
|
||||||
|
// let fileManager = FileManager.default
|
||||||
|
// let trashURLs = fileManager.urls(for: .trashDirectory, in: .userDomainMask)
|
||||||
|
// do {
|
||||||
|
// let trashContents = try fileManager.contentsOfDirectory(at: trashURLs.first!, includingPropertiesForKeys: nil, options: [])
|
||||||
|
// let appFiles = trashContents.filter { $0.pathExtension == "app" }
|
||||||
|
// writeLog(string: appFiles.first!.path)
|
||||||
|
//
|
||||||
|
// return appFiles.map { $0.path }
|
||||||
|
// } catch {
|
||||||
|
// print("Failed to get contents of trash directory: \(error)")
|
||||||
|
// return []
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
|
//func hasAccessToTrashFolder() -> Bool {
|
||||||
|
// let fileManager = FileManager.default
|
||||||
|
// let trashURLs = fileManager.urls(for: .trashDirectory, in: .userDomainMask)
|
||||||
|
//
|
||||||
|
// if let trashURL = trashURLs.first {
|
||||||
|
// return fileManager.isReadableFile(atPath: trashURL.path) && fileManager.isWritableFile(atPath: trashURL.path)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// return false
|
||||||
|
//}
|
||||||
|
|
||||||
|
//if hasAccessToTrashFolder() {
|
||||||
|
// writeLog(string: "Your tool has access to the Trash folder.")
|
||||||
|
// print("Your tool has access to the Trash folder.")
|
||||||
|
//} else {
|
||||||
|
// if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles") {
|
||||||
|
// NSWorkspace.shared.open(url)
|
||||||
|
// }
|
||||||
|
// writeLog(string: "Your tool does not have access to the Trash folder.")
|
||||||
|
// print("Your tool does not have access to the Trash folder.")
|
||||||
|
//}
|
||||||
|
|
||||||
|
|
||||||
|
//var dirWatcher: DirMonitor?
|
||||||
|
//func fileW2() {
|
||||||
|
// let home = FileManager.default.homeDirectoryForCurrentUser.path
|
||||||
|
// let url = URL(fileURLWithPath: "\(home)/.Trash")
|
||||||
|
// dirWatcher = DirMonitor(dir: url, queue: .global())
|
||||||
|
// if dirWatcher?.start() == true {
|
||||||
|
// NSLog("Started directory monitoring")
|
||||||
|
// } else {
|
||||||
|
// NSLog("Failed to start directory monitoring")
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//class DirMonitor {
|
||||||
|
//
|
||||||
|
// init(dir: URL, queue: DispatchQueue) {
|
||||||
|
// self.dir = dir
|
||||||
|
// self.queue = queue
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// deinit {
|
||||||
|
// // The stream has a reference to us via its `info` pointer. If the
|
||||||
|
// // client releases their reference to us without calling `stop`, that
|
||||||
|
// // results in a dangling pointer. We detect this as a programming error.
|
||||||
|
// // There are other approaches to take here (for example, messing around
|
||||||
|
// // with weak, or forming a retain cycle that’s broken on `stop`), but
|
||||||
|
// // this approach:
|
||||||
|
// //
|
||||||
|
// // * Has clear rules
|
||||||
|
// // * Is easy to implement
|
||||||
|
// // * Generate a sensible debug message if the client gets things wrong
|
||||||
|
// precondition(self.stream == nil, "released a running monitor")
|
||||||
|
// // I added this log line as part of my testing of the deallocation path.
|
||||||
|
// NSLog("did deinit")
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// let dir: URL
|
||||||
|
// let queue: DispatchQueue
|
||||||
|
//
|
||||||
|
// private var stream: FSEventStreamRef? = nil
|
||||||
|
//
|
||||||
|
// func start() -> Bool {
|
||||||
|
// precondition(self.stream == nil, "started a running monitor")
|
||||||
|
//
|
||||||
|
// // Set up our context.
|
||||||
|
// //
|
||||||
|
// // `FSEventStreamCallback` is a C function, so we pass `self` to the
|
||||||
|
// // `info` pointer so that it get call our `handleUnsafeEvents(…)`
|
||||||
|
// // method. This involves the standard `Unmanaged` dance:
|
||||||
|
// //
|
||||||
|
// // * Here we set `info` to an unretained pointer to `self`.
|
||||||
|
// // * Inside the function we extract that pointer as `obj` and then use
|
||||||
|
// // that to call `handleUnsafeEvents(…)`.
|
||||||
|
//
|
||||||
|
// var context = FSEventStreamContext()
|
||||||
|
// context.info = Unmanaged.passUnretained(self).toOpaque()
|
||||||
|
//
|
||||||
|
// // Create the stream.
|
||||||
|
// //
|
||||||
|
// // In this example I wanted to show how to deal with raw string paths,
|
||||||
|
// // so I’m not taking advantage of `kFSEventStreamCreateFlagUseCFTypes`
|
||||||
|
// // or the even cooler `kFSEventStreamCreateFlagUseExtendedData`.
|
||||||
|
//
|
||||||
|
// guard let stream = FSEventStreamCreate(nil, { (stream, info, numEvents, eventPaths, eventFlags, eventIds) in
|
||||||
|
// let obj = Unmanaged<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 don’t 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 don’t pass
|
||||||
|
// // `kFSEventStreamCreateFlagFileEvents` to `FSEventStreamCreate`.
|
||||||
|
// // This is generally the best way to use FSEvents, but if you decide
|
||||||
|
// // to take advantage of `kFSEventStreamCreateFlagFileEvents` then
|
||||||
|
// // you’ll need to code to `isDirectory` correctly.
|
||||||
|
// let url: URL = URL(fileURLWithFileSystemRepresentation: path, isDirectory: true, relativeTo: nil)
|
||||||
|
// return (url, flagsBuffer[i], eventIDsBuffer[i])
|
||||||
|
// }
|
||||||
|
// self.handle(events: events)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private func handle(events: [(url: URL, flags: FSEventStreamEventFlags, eventIDs: FSEventStreamEventId)]) {
|
||||||
|
// // In this example we just print the events with get, prefixed by a
|
||||||
|
// // count so that we can see the batching in action.
|
||||||
|
// NSLog("%d", events.count)
|
||||||
|
// for (url, flags, eventID) in events {
|
||||||
|
// NSLog("%16x %8x %@", eventID, flags, url.path)
|
||||||
|
// }
|
||||||
|
// for (url, flags, _) in events {
|
||||||
|
// if flags & FSEventStreamEventFlags(kFSEventStreamEventFlagItemRemoved) != 0 {
|
||||||
|
// NSLog("Removed: \(url.path)")
|
||||||
|
// }
|
||||||
|
// if flags & FSEventStreamEventFlags(kFSEventStreamEventFlagItemCreated) != 0 {
|
||||||
|
// NSLog("Created: \(url.path)")
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// getTrashContents()
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func stop() {
|
||||||
|
// guard let stream = self.stream else {
|
||||||
|
// return // We accept redundant calls to `stop`.
|
||||||
|
// }
|
||||||
|
// FSEventStreamStop(stream)
|
||||||
|
// FSEventStreamInvalidate(stream)
|
||||||
|
// self.stream = nil
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func getTrashContents() {
|
||||||
|
// let fileManager = FileManager.default
|
||||||
|
// let trashURLs = fileManager.urls(for: .trashDirectory, in: .userDomainMask)
|
||||||
|
// let trashContents = try? fileManager.contentsOfDirectory(at: trashURLs.first!, includingPropertiesForKeys: nil, options: [])
|
||||||
|
// print(trashContents as Any)
|
||||||
|
// }
|
||||||
|
//}
|
||||||