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