initial
@@ -0,0 +1,106 @@
|
||||
//
|
||||
// AppCommands.swift
|
||||
// Pearcleaner
|
||||
//
|
||||
// Created by Alin Lupascu on 10/31/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct AppCommands: Commands {
|
||||
|
||||
let appState: AppState
|
||||
|
||||
init(appState: AppState) {
|
||||
self.appState = appState
|
||||
}
|
||||
|
||||
var body: some Commands {
|
||||
|
||||
// Pearcleaner Menu
|
||||
CommandGroup(replacing: .appInfo) {
|
||||
|
||||
Button {
|
||||
loadGithubReleases(appState: appState, manual: true)
|
||||
} label: {
|
||||
Text("Updates")
|
||||
}
|
||||
.keyboardShortcut("u", modifiers: .command)
|
||||
|
||||
Button {
|
||||
withAnimation(.easeInOut(duration: 0.5)) {
|
||||
// Refresh Apps list
|
||||
let sortedApps = getSortedApps()
|
||||
appState.sortedApps.userApps = sortedApps.userApps
|
||||
appState.sortedApps.systemApps = sortedApps.systemApps
|
||||
}
|
||||
} label: {
|
||||
Text("Refresh Apps")
|
||||
}
|
||||
.keyboardShortcut("r", modifiers: .command)
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Edit Menu
|
||||
CommandGroup(replacing: .undoRedo) {
|
||||
Button
|
||||
{
|
||||
undoTrash(appState: appState) {
|
||||
let sortedApps = getSortedApps()
|
||||
appState.sortedApps.userApps = []
|
||||
appState.sortedApps.systemApps = []
|
||||
appState.sortedApps.userApps = sortedApps.userApps
|
||||
appState.sortedApps.systemApps = sortedApps.systemApps
|
||||
}
|
||||
} label: {
|
||||
Label("Undo Removal", systemImage: "clear")
|
||||
}
|
||||
.keyboardShortcut("z", modifiers: .command)
|
||||
}
|
||||
|
||||
|
||||
|
||||
// GitHub Menu
|
||||
CommandMenu(Text("GitHub", comment: "Github Repo")) {
|
||||
Button
|
||||
{
|
||||
NSWorkspace.shared.open(URL(string: "https://github.com/alienator88/Pearcleaner")!)
|
||||
} label: {
|
||||
Label("View Repository", systemImage: "paperplane")
|
||||
}
|
||||
|
||||
|
||||
Button
|
||||
{
|
||||
NSWorkspace.shared.open(URL(string: "https://github.com/alienator88/Pearcleaner/releases")!)
|
||||
} label: {
|
||||
Label("View Releases", systemImage: "paperplane")
|
||||
}
|
||||
|
||||
|
||||
Button
|
||||
{
|
||||
NSWorkspace.shared.open(URL(string: "https://github.com/alienator88/Pearcleaner/issues")!)
|
||||
} label: {
|
||||
Label("View Issues", systemImage: "paperplane")
|
||||
}
|
||||
|
||||
|
||||
Divider()
|
||||
|
||||
|
||||
Button
|
||||
{
|
||||
NSWorkspace.shared.open(URL(string: "https://github.com/alienator88/Pearcleaner/issues/new/choose")!)
|
||||
} label: {
|
||||
Label("Submit New Issue", systemImage: "paperplane")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
//
|
||||
// AppState.swift
|
||||
// Pearcleaner
|
||||
//
|
||||
// Created by Alin Lupascu on 10/31/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
let home = FileManager.default.homeDirectoryForCurrentUser.path
|
||||
|
||||
class AppState: ObservableObject
|
||||
{
|
||||
@Published var showFiles: Bool = false
|
||||
@Published var appInfo: AppInfo
|
||||
@Published var paths: [URL] = []
|
||||
@Published var sortedApps: (userApps: [AppInfo], systemApps: [AppInfo]) = ([], [])
|
||||
@Published var selectedItems = Set<URL>()
|
||||
@Published var alertType = AlertType.off
|
||||
@Published var currentView = CurrentDetailsView.empty
|
||||
@Published var showAlert: Bool = false
|
||||
@Published var sidebar: Bool = true
|
||||
@Published var isReminderVisible: Bool = false
|
||||
@Published var releases = [Release]()
|
||||
|
||||
//Window
|
||||
// @Published var winWidth: CGFloat = 1020
|
||||
|
||||
init() {
|
||||
self.appInfo = AppInfo(
|
||||
id: UUID(),
|
||||
path: URL(fileURLWithPath: ""),
|
||||
bundleIdentifier: "",
|
||||
appName: "",
|
||||
appVersion: "",
|
||||
appIcon: nil
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct AppInfo: Identifiable, Hashable {
|
||||
let id: UUID
|
||||
let path: URL
|
||||
let bundleIdentifier: String
|
||||
let appName: String
|
||||
let appVersion: String
|
||||
let appIcon: NSImage?
|
||||
|
||||
static let empty = AppInfo(id: UUID(), path: URL(fileURLWithPath: ""), bundleIdentifier: "", appName: "", appVersion: "", appIcon: nil)
|
||||
}
|
||||
|
||||
|
||||
enum CurrentTabView:Int
|
||||
{
|
||||
case general
|
||||
case permissions
|
||||
case sentinel
|
||||
case update
|
||||
case about
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .general: return "General"
|
||||
case .permissions: return "Permissions"
|
||||
case .sentinel: return "Sentinel"
|
||||
case .update: return "Update"
|
||||
case .about: return "About"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum CurrentDetailsView:Int
|
||||
{
|
||||
case empty
|
||||
case files
|
||||
}
|
||||
|
||||
enum AlertType:Int
|
||||
{
|
||||
case diskAccess
|
||||
case update
|
||||
case no_update
|
||||
case restartApp
|
||||
case off
|
||||
}
|
||||
|
||||
|
||||
|
||||
enum DisplayMode: Int, CaseIterable {
|
||||
case system, dark, light
|
||||
|
||||
var colorScheme: ColorScheme? {
|
||||
get {
|
||||
switch self {
|
||||
case .system: return nil
|
||||
case .dark: return ColorScheme.dark
|
||||
case .light: return ColorScheme.light
|
||||
}
|
||||
}
|
||||
set {
|
||||
switch newValue {
|
||||
case .none:
|
||||
self = .system
|
||||
case .dark?:
|
||||
self = .dark
|
||||
case .light?:
|
||||
self = .light
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .system: return "System"
|
||||
case .dark: return "Dark"
|
||||
case .light: return "Light"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
//let (cacheDir, tempDir) = darwinCT()
|
||||
//let locations: [String] = [
|
||||
// "\(home)/Library",
|
||||
// "\(home)/Library/Application Scripts",
|
||||
// "\(home)/Library/Application Support",
|
||||
// "\(home)/Library/Application Support/CrashReporter",
|
||||
// "\(home)/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.ApplicationRecentDocuments",
|
||||
// "\(home)/Library/Containers",
|
||||
// "\(home)/Library/Group Containers",
|
||||
// "\(home)/Library/Caches",
|
||||
// "\(home)/Library/HTTPStorages",
|
||||
// "\(home)/Library/Group Containers",
|
||||
// "\(home)/Library/Internet Plug-Ins",
|
||||
// "\(home)/Library/LaunchAgents",
|
||||
// "\(home)/Library/Logs",
|
||||
// "\(home)/Library/Preferences",
|
||||
// "\(home)/Library/Preferences/ByHost",
|
||||
// "\(home)/Library/Saved Application State",
|
||||
// "\(home)/Library/WebKit",
|
||||
// "/Users/Shared",
|
||||
// "/Users/Library",
|
||||
// "/Library",
|
||||
// "/Library/Application Support",
|
||||
// "/Library/Application Support/CrashReporter",
|
||||
// "/Library/Caches",
|
||||
// "/Library/Extensions",
|
||||
// "/Library/Internet Plug-Ins",
|
||||
// "/Library/LaunchAgents",
|
||||
// "/Library/LaunchDaemons",
|
||||
// "/Library/Logs",
|
||||
// "/Library/Logs/DiagnosticReports",
|
||||
// "/Library/Preferences",
|
||||
// "/Library/PrivilegedHelperTools",
|
||||
// "/private/var/db/receipts",
|
||||
// "/private/tmp",
|
||||
// "/usr/local/bin",
|
||||
// "/usr/local/etc",
|
||||
// "/usr/local/opt",
|
||||
// "/usr/local/sbin",
|
||||
// "/usr/local/share",
|
||||
// "/usr/local/var",
|
||||
// cacheDir,
|
||||
// tempDir
|
||||
//]
|
||||
@@ -0,0 +1,41 @@
|
||||
//
|
||||
// DeepLink.swift
|
||||
// Pearcleaner
|
||||
//
|
||||
// Created by Alin Lupascu on 11/9/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
class DeeplinkManager {
|
||||
|
||||
class DeepLinkConstants {
|
||||
static let scheme = "pear"
|
||||
static let host = "com.alienator88.Pearcleaner"
|
||||
static let query = "path"
|
||||
}
|
||||
|
||||
func manage(url: URL, appState: AppState) {
|
||||
guard url.scheme == DeepLinkConstants.scheme,
|
||||
url.host == DeepLinkConstants.host,
|
||||
let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
|
||||
let queryItems = components.queryItems
|
||||
else {
|
||||
print("URL does not match the expected scheme and host")
|
||||
return
|
||||
}
|
||||
|
||||
if let path = queryItems.first(where: { $0.name == DeepLinkConstants.query })?.value {
|
||||
let pathURL = URL(fileURLWithPath: path)
|
||||
let appInfo = getAppInfo(atPath: pathURL)
|
||||
updateOnMain {
|
||||
appState.appInfo = appInfo!
|
||||
findPathsForApp(appState: appState, appInfo: appState.appInfo)
|
||||
appState.currentView = .files
|
||||
}
|
||||
} else {
|
||||
print("No path query parameter found in the URL")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
//
|
||||
// Locations.swift
|
||||
// Pearcleaner
|
||||
//
|
||||
// Created by Alin Lupascu on 11/10/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
struct Locations {
|
||||
struct Category {
|
||||
let name: String
|
||||
var paths: [String]
|
||||
}
|
||||
|
||||
let home: String
|
||||
let cacheDir: String
|
||||
let tempDir: String
|
||||
|
||||
var apps: Category
|
||||
var widgets: Category
|
||||
var plugins: Category
|
||||
|
||||
init() {
|
||||
self.home = FileManager.default.homeDirectoryForCurrentUser.path
|
||||
let (cacheDir, tempDir) = darwinCT()
|
||||
self.cacheDir = cacheDir
|
||||
self.tempDir = tempDir
|
||||
|
||||
self.apps = Category(name: "Apps", paths: [
|
||||
"\(home)/Library",
|
||||
"\(home)/Library/Application Scripts",
|
||||
"\(home)/Library/Application Support",
|
||||
"\(home)/Library/Application Support/CrashReporter",
|
||||
"\(home)/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.ApplicationRecentDocuments",
|
||||
"\(home)/Library/Containers",
|
||||
"\(home)/Library/Group Containers",
|
||||
"\(home)/Library/Caches",
|
||||
"\(home)/Library/HTTPStorages",
|
||||
"\(home)/Library/Group Containers",
|
||||
"\(home)/Library/Internet Plug-Ins",
|
||||
"\(home)/Library/LaunchAgents",
|
||||
"\(home)/Library/Logs",
|
||||
"\(home)/Library/Preferences",
|
||||
"\(home)/Library/Preferences/ByHost",
|
||||
"\(home)/Library/Saved Application State",
|
||||
"\(home)/Library/WebKit",
|
||||
"/Users/Shared",
|
||||
"/Users/Library",
|
||||
"/Library",
|
||||
"/Library/Application Support",
|
||||
"/Library/Application Support/CrashReporter",
|
||||
"/Library/Caches",
|
||||
"/Library/Extensions",
|
||||
"/Library/Internet Plug-Ins",
|
||||
"/Library/LaunchAgents",
|
||||
"/Library/LaunchDaemons",
|
||||
"/Library/Logs",
|
||||
"/Library/Logs/DiagnosticReports",
|
||||
"/Library/Preferences",
|
||||
"/Library/PrivilegedHelperTools",
|
||||
"/private/var/db/receipts",
|
||||
"/private/tmp",
|
||||
"/usr/local/bin",
|
||||
"/usr/local/etc",
|
||||
"/usr/local/opt",
|
||||
"/usr/local/sbin",
|
||||
"/usr/local/share",
|
||||
"/usr/local/var",
|
||||
cacheDir,
|
||||
tempDir
|
||||
])
|
||||
|
||||
self.widgets = Category(name: "Widgets", paths: [
|
||||
// User
|
||||
"~/Library/Widgets",
|
||||
// System
|
||||
"/Library/Widgets"
|
||||
])
|
||||
|
||||
self.plugins = Category(name: "Plugins", paths: [
|
||||
// User
|
||||
"~/Library/Contextual Menu Items",
|
||||
"~/Library/InputManagers",
|
||||
"~/Library/Internet Plug-Ins",
|
||||
"~/Library/Mail/Bundles",
|
||||
"~/Library/PreferencePanes",
|
||||
"~/Library/QuickLook",
|
||||
"~/Library/QuickTime",
|
||||
"~/Library/Screen Savers",
|
||||
"~/Library/Spotlight",
|
||||
// System
|
||||
"/Library/Contextual Menu Items",
|
||||
"/Library/InputManagers",
|
||||
"/Library/Internet Plug-Ins",
|
||||
"/Library/Mail/Bundles",
|
||||
"/Library/PreferencePanes",
|
||||
"/Library/QuickLook",
|
||||
"/Library/QuickTime",
|
||||
"/Library/Screen Savers",
|
||||
"/Library/Spotlight",
|
||||
])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,362 @@
|
||||
//
|
||||
// Logic.swift
|
||||
// Pearcleaner
|
||||
//
|
||||
// Created by Alin Lupascu on 10/31/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
|
||||
// Get list of apps and sort it
|
||||
func getSortedApps() -> (systemApps: [AppInfo], userApps: [AppInfo]) {
|
||||
let apps = getApplications()
|
||||
let sortedSystemApps = apps.systemApps
|
||||
.compactMap { getAppInfo(atPath: $0) }
|
||||
.sorted { $0.appName.replacingOccurrences(of: ".", with: "").lowercased() < $1.appName.replacingOccurrences(of: ".", with: "").lowercased() }
|
||||
let sortedUserApps = apps.userApps
|
||||
.compactMap { getAppInfo(atPath: $0) }
|
||||
.sorted { $0.appName.replacingOccurrences(of: ".", with: "").lowercased() < $1.appName.replacingOccurrences(of: ".", with: "").lowercased() }
|
||||
|
||||
return (systemApps: sortedSystemApps, userApps: sortedUserApps)
|
||||
}
|
||||
|
||||
|
||||
// Get all applications from /Applications and ~/Applications
|
||||
func getApplications() -> (systemApps: [URL], userApps: [URL]) {
|
||||
let fileManager = FileManager.default
|
||||
|
||||
// Define the paths for system and user applications
|
||||
let systemAppsPath = "/Applications"
|
||||
let userAppsPath = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Applications").path
|
||||
|
||||
var systemApps: [URL] = []
|
||||
var userApps: [URL] = []
|
||||
|
||||
func collectAppPaths(at directoryPath: String, isSystem: Bool) {
|
||||
do {
|
||||
let appURLs = try fileManager.contentsOfDirectory(at: URL(fileURLWithPath: directoryPath), includingPropertiesForKeys: nil, options: [])
|
||||
|
||||
for appURL in appURLs {
|
||||
if appURL.pathExtension == "app" {
|
||||
// Add the path to the appropriate list
|
||||
if isSystem && !isRestricted(atPath: appURL) {
|
||||
systemApps.append(appURL)
|
||||
} else if !isSystem && !isRestricted(atPath: appURL) {
|
||||
userApps.append(appURL)
|
||||
}
|
||||
} else if appURL.hasDirectoryPath {
|
||||
// If it's a directory, recursively explore it
|
||||
collectAppPaths(at: appURL.path, isSystem: isSystem)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Handle any potential errors here
|
||||
print("Error: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
// Collect system applications
|
||||
collectAppPaths(at: systemAppsPath, isSystem: true)
|
||||
|
||||
// Collect user applications
|
||||
collectAppPaths(at: userAppsPath, isSystem: false)
|
||||
|
||||
// Filter out unwanted system folders like /Applications/Utilities
|
||||
systemApps = systemApps.filter { !$0.absoluteString.contains("/Applications/Utilities/") }
|
||||
return (systemApps, userApps)
|
||||
}
|
||||
|
||||
|
||||
// Get app bundle information from provided path
|
||||
func getAppInfo(atPath path: URL) -> AppInfo? {
|
||||
if let bundle = Bundle(url: path) {
|
||||
if let bundleIdentifier = bundle.bundleIdentifier,
|
||||
let appVersion = bundle.infoDictionary?["CFBundleShortVersionString"] as? String,
|
||||
var appIconFileName = bundle.infoDictionary?["CFBundleIconFile"] as? String {
|
||||
var appIcon: NSImage?
|
||||
var appName: String?
|
||||
if let localizedName = bundle.localizedInfoDictionary?[kCFBundleNameKey as String] as? String {
|
||||
appName = localizedName
|
||||
} else if let bundleName = bundle.infoDictionary?["CFBundleName"] as? String {
|
||||
appName = bundleName
|
||||
}
|
||||
// Check if the icon file name has an .icns extension, if not, add it
|
||||
if !appIconFileName.hasSuffix(".icns") {
|
||||
appIconFileName += ".icns"
|
||||
}
|
||||
|
||||
// Try to find the icon in the main bundle
|
||||
if let iconPath = bundle.path(forResource: appIconFileName, ofType: nil),
|
||||
let icon = NSImage(contentsOfFile: iconPath) {
|
||||
appIcon = icon
|
||||
}
|
||||
|
||||
// If not found, try to find it in the resources directory
|
||||
if appIcon == nil,
|
||||
let resourcesPath = bundle.resourcePath {
|
||||
let iconURL = URL(fileURLWithPath: resourcesPath).appendingPathComponent(appIconFileName)
|
||||
if let icon = NSImage(contentsOfFile: iconURL.path) {
|
||||
appIcon = icon
|
||||
}
|
||||
}
|
||||
|
||||
// Convert the icon to a 100x100 PNG image
|
||||
if let pngIcon = appIcon.flatMap({ convertICNSToPNG(icon: $0, size: NSSize(width: 100, height: 100)) }) {
|
||||
appIcon = pngIcon
|
||||
}
|
||||
|
||||
if appIcon == nil {
|
||||
print("App Icon not found for app at path: \(path)")
|
||||
}
|
||||
|
||||
return AppInfo(id: UUID(), path: path, bundleIdentifier: bundleIdentifier, appName: appName ?? "", appVersion: appVersion, appIcon: appIcon)
|
||||
|
||||
} else {
|
||||
print("One or more variables missing for app at path: \(path)")
|
||||
}
|
||||
} else {
|
||||
print("Bundle not found at path: \(path)")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Get directory path for darwin cache and temp directories
|
||||
func darwinCT() -> (String, String) {
|
||||
let task = Process()
|
||||
task.launchPath = "/bin/bash"
|
||||
task.arguments = ["-c", "getconf DARWIN_USER_CACHE_DIR; getconf DARWIN_USER_TEMP_DIR"]
|
||||
let pipe = Pipe()
|
||||
task.standardOutput = pipe
|
||||
task.launch()
|
||||
let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
||||
if let output = String(data: data, encoding: .utf8) {
|
||||
let components = output.components(separatedBy: "\n")
|
||||
if components.count >= 2 {
|
||||
var cacheDir = components[0].trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
var tempDir = components[1].trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if cacheDir.hasSuffix("/") {
|
||||
cacheDir.removeLast()
|
||||
}
|
||||
if tempDir.hasSuffix("/") {
|
||||
tempDir.removeLast()
|
||||
}
|
||||
return (cacheDir, tempDir)
|
||||
}
|
||||
}
|
||||
print("Could not get DARWIN_USER_CACHE_DIR or DARWIN_USER_TEMP_DIR")
|
||||
return ("", "")
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Check if app is running before deleting app files
|
||||
func killApp(appId: String, completion: @escaping () -> Void = {}) {
|
||||
let runningApps = NSWorkspace.shared.runningApplications
|
||||
for app in runningApps {
|
||||
if app.bundleIdentifier == appId {
|
||||
app.terminate()
|
||||
}
|
||||
}
|
||||
completion()
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// Find all possible paths for an app based on name/bundle id
|
||||
func findPathsForApp(appState: AppState, appInfo: AppInfo) {
|
||||
Task(priority: .high) {
|
||||
updateOnMain {
|
||||
appState.paths = []
|
||||
}
|
||||
var collection: [URL] = []
|
||||
if let url = URL(string: appInfo.path.absoluteString) {
|
||||
// appState.paths.insert(url, at: 0)
|
||||
collection.insert(url, at: 0)
|
||||
}
|
||||
let fileManager = FileManager.default
|
||||
let dispatchGroup = DispatchGroup()
|
||||
let bundleComponents = appInfo.bundleIdentifier.components(separatedBy: ".")
|
||||
var bundle: String = ""
|
||||
if bundleComponents.count >= 3 { // get last 2 or middle 2 components
|
||||
bundle = bundleComponents[1...2].joined(separator: "").lowercased()
|
||||
}
|
||||
let nameL = appInfo.appName.pearFormat()
|
||||
let bundleIdentifierL = appInfo.bundleIdentifier.pearFormat()
|
||||
let locations = Locations()
|
||||
|
||||
for location in locations.apps.paths {
|
||||
dispatchGroup.enter() // Enter the dispatch group
|
||||
|
||||
do {
|
||||
let contents = try fileManager.contentsOfDirectory(atPath: location)
|
||||
|
||||
for item in contents {
|
||||
|
||||
let itemURL = URL(fileURLWithPath: location).appendingPathComponent(item)
|
||||
let itemL = ("\(item)").replacingOccurrences(of: ".", with: "").replacingOccurrences(of: " ", with: "").lowercased()
|
||||
|
||||
if collection.contains(itemURL) {
|
||||
return
|
||||
}
|
||||
|
||||
if itemL.contains("comapple") {
|
||||
if itemL.contains(bundle) || itemL.contains(bundleIdentifierL) || (nameL.count > 4 && itemL.contains(nameL)) {
|
||||
collection.append(itemURL)
|
||||
}
|
||||
}
|
||||
else if itemL.contains("xcode") {
|
||||
if itemL.contains(bundle) || itemL.contains(bundleIdentifierL) || (nameL.count > 4 && itemL.contains(nameL)) {
|
||||
collection.append(itemURL)
|
||||
}
|
||||
} else if itemL.contains("logi") && !itemL.contains("login") {
|
||||
if itemL.contains(bundleComponents[1]) || itemL.contains(bundle) || itemL.contains(bundleIdentifierL) || (nameL.count > 3 && itemL.contains(nameL)) {
|
||||
collection.append(itemURL)
|
||||
}
|
||||
} else if itemL.contains("office") || itemL.contains("oneauth") || itemL.suffix(2).contains("ms") || itemL.contains("onenote") {
|
||||
if itemL.contains(bundle) || itemL.contains(bundleIdentifierL) || (nameL.count > 4 && itemL.contains(nameL)) {
|
||||
collection.append(itemURL)
|
||||
}
|
||||
} else {
|
||||
if itemL.contains(bundleIdentifierL) || itemL.contains(bundle) || (nameL.count > 3 && itemL.contains(nameL)) {
|
||||
collection.append(itemURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
|
||||
dispatchGroup.leave() // Leave the dispatch group
|
||||
|
||||
dispatchGroup.notify(queue: .main) {
|
||||
updateOnMain {
|
||||
appState.paths = collection
|
||||
appState.selectedItems = Set(collection)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// Move files to trash using applescript/Finder so it asks for user password if needed
|
||||
func moveFilesToTrash(at fileURLs: [URL], completion: @escaping () -> Void = {}) {
|
||||
updateOnBackground {
|
||||
let posixFiles = fileURLs.map { "POSIX file \"\($0.path)\", " }.joined().dropLast(3)
|
||||
let scriptSource = """
|
||||
tell application \"Finder\" to delete { \(posixFiles)" }
|
||||
"""
|
||||
var error: NSDictionary?
|
||||
if let scriptObject = NSAppleScript(source: scriptSource) {
|
||||
let output: NSAppleEventDescriptor = scriptObject.executeAndReturnError(&error)
|
||||
if let error = error {
|
||||
print("Error: \(error)")
|
||||
} else if let outputString = output.stringValue {
|
||||
print(outputString)
|
||||
}
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
completion()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Undo trash action
|
||||
func undoTrash(appState: AppState, completion: @escaping () -> Void = {}) {
|
||||
let scriptSource = """
|
||||
tell application "Finder"
|
||||
activate
|
||||
end tell
|
||||
|
||||
tell application "System Events"
|
||||
keystroke "z" using command down
|
||||
end tell
|
||||
|
||||
tell application "Pearcleaner"
|
||||
activate
|
||||
end tell
|
||||
"""
|
||||
var error: NSDictionary?
|
||||
|
||||
updateOnBackground {
|
||||
if let scriptObject = NSAppleScript(source: scriptSource) {
|
||||
let output: NSAppleEventDescriptor = scriptObject.executeAndReturnError(&error)
|
||||
if let error = error {
|
||||
if let value = error["NSAppleScriptErrorNumber"] {
|
||||
if value as! Int == 1002 {
|
||||
_ = checkAndRequestAccessibilityAccess(appState: appState)
|
||||
}
|
||||
}
|
||||
print("Error: \(error)")
|
||||
} else if let outputString = output.stringValue {
|
||||
print(outputString)
|
||||
}
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
completion()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
//func isAppRunning(appName: String) -> Bool {
|
||||
// let task = Process()
|
||||
// task.executableURL = URL(fileURLWithPath: "/usr/bin/pgrep")
|
||||
// task.arguments = ["-af", appName]
|
||||
//
|
||||
// let outputPipe = Pipe()
|
||||
// task.standardOutput = outputPipe
|
||||
//
|
||||
// do {
|
||||
// try task.run()
|
||||
// } catch {
|
||||
// print("Failed to run task: \(error)")
|
||||
// return false
|
||||
// }
|
||||
//
|
||||
// let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
|
||||
// let output = String(decoding: outputData, as: UTF8.self)
|
||||
//
|
||||
// if output.isEmpty {
|
||||
// return false
|
||||
// } else {
|
||||
// return true
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//// Kill app if running
|
||||
//func killApp2(appName: String, completion: @escaping () -> Void = {}) {
|
||||
// if isAppRunning(appName: appName) {
|
||||
// let task = Process()
|
||||
// task.executableURL = URL(fileURLWithPath: "/usr/bin/pkill")
|
||||
// task.arguments = ["-af", appName]
|
||||
//
|
||||
// do {
|
||||
// try task.run()
|
||||
// } catch {
|
||||
// print("Failed to run task: \(error)")
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// }
|
||||
// completion()
|
||||
//}
|
||||
@@ -0,0 +1,599 @@
|
||||
//
|
||||
// Shapes.swift
|
||||
// Pearcleaner
|
||||
//
|
||||
// Created by Alin Lupascu on 11/11/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
// Pear Logo
|
||||
struct Pear: Shape {
|
||||
func path(in rect: CGRect) -> Path {
|
||||
var path = Path()
|
||||
let width = rect.size.width
|
||||
let height = rect.size.height
|
||||
path.move(to: CGPoint(x: 0.5985*width, y: 0.045*height))
|
||||
path.addCurve(to: CGPoint(x: 0.508*width, y: 0.141*height), control1: CGPoint(x: 0.571*width, y: 0.0635*height), control2: CGPoint(x: 0.521*width, y: 0.116*height))
|
||||
path.addLine(to: CGPoint(x: 0.4985*width, y: 0.159*height))
|
||||
path.addLine(to: CGPoint(x: 0.4725*width, y: 0.1325*height))
|
||||
path.addCurve(to: CGPoint(x: 0.318*width, y: 0.0975*height), control1: CGPoint(x: 0.433*width, y: 0.092*height), control2: CGPoint(x: 0.383*width, y: 0.0805*height))
|
||||
path.addCurve(to: CGPoint(x: 0.2105*width, y: 0.15*height), control1: CGPoint(x: 0.2875*width, y: 0.1055*height), control2: CGPoint(x: 0.231*width, y: 0.133*height))
|
||||
path.addLine(to: CGPoint(x: 0.1985*width, y: 0.16*height))
|
||||
path.addLine(to: CGPoint(x: 0.2125*width, y: 0.18*height))
|
||||
path.addCurve(to: CGPoint(x: 0.316*width, y: 0.273*height), control1: CGPoint(x: 0.2335*width, y: 0.2105*height), control2: CGPoint(x: 0.287*width, y: 0.2585*height))
|
||||
path.addCurve(to: CGPoint(x: 0.371*width, y: 0.289*height), control1: CGPoint(x: 0.331*width, y: 0.2805*height), control2: CGPoint(x: 0.354*width, y: 0.287*height))
|
||||
path.addLine(to: CGPoint(x: 0.4*width, y: 0.2915*height))
|
||||
path.addLine(to: CGPoint(x: 0.375*width, y: 0.318*height))
|
||||
path.addCurve(to: CGPoint(x: 0.3195*width, y: 0.443*height), control1: CGPoint(x: 0.3415*width, y: 0.3535*height), control2: CGPoint(x: 0.326*width, y: 0.388*height))
|
||||
path.addCurve(to: CGPoint(x: 0.2635*width, y: 0.564*height), control1: CGPoint(x: 0.3125*width, y: 0.501*height), control2: CGPoint(x: 0.302*width, y: 0.5235*height))
|
||||
path.addCurve(to: CGPoint(x: 0.19*width, y: 0.7345*height), control1: CGPoint(x: 0.21*width, y: 0.62*height), control2: CGPoint(x: 0.19*width, y: 0.6665*height))
|
||||
path.addCurve(to: CGPoint(x: 0.3235*width, y: 0.949*height), control1: CGPoint(x: 0.19*width, y: 0.8305*height), control2: CGPoint(x: 0.2385*width, y: 0.9085*height))
|
||||
path.addCurve(to: CGPoint(x: 0.4125*width, y: 0.9675*height), control1: CGPoint(x: 0.3605*width, y: 0.9665*height), control2: CGPoint(x: 0.365*width, y: 0.9675*height))
|
||||
path.addCurve(to: CGPoint(x: 0.4875*width, y: 0.958*height), control1: CGPoint(x: 0.449*width, y: 0.9675*height), control2: CGPoint(x: 0.4695*width, y: 0.965*height))
|
||||
path.addCurve(to: CGPoint(x: 0.547*width, y: 0.9595*height), control1: CGPoint(x: 0.5125*width, y: 0.949*height), control2: CGPoint(x: 0.513*width, y: 0.949*height))
|
||||
path.addCurve(to: CGPoint(x: 0.759*width, y: 0.9135*height), control1: CGPoint(x: 0.6225*width, y: 0.983*height), control2: CGPoint(x: 0.702*width, y: 0.9655*height))
|
||||
path.addCurve(to: CGPoint(x: 0.7975*width, y: 0.817*height), control1: CGPoint(x: 0.8085*width, y: 0.8675*height), control2: CGPoint(x: 0.8225*width, y: 0.8335*height))
|
||||
path.addCurve(to: CGPoint(x: 0.7615*width, y: 0.817*height), control1: CGPoint(x: 0.786*width, y: 0.8095*height), control2: CGPoint(x: 0.7835*width, y: 0.8095*height))
|
||||
path.addCurve(to: CGPoint(x: 0.661*width, y: 0.8015*height), control1: CGPoint(x: 0.723*width, y: 0.83*height), control2: CGPoint(x: 0.6935*width, y: 0.8255*height))
|
||||
path.addCurve(to: CGPoint(x: 0.634*width, y: 0.6745*height), control1: CGPoint(x: 0.6245*width, y: 0.7745*height), control2: CGPoint(x: 0.611*width, y: 0.711*height))
|
||||
path.addCurve(to: CGPoint(x: 0.6545*width, y: 0.618*height), control1: CGPoint(x: 0.659*width, y: 0.6345*height), control2: CGPoint(x: 0.6605*width, y: 0.631*height))
|
||||
path.addCurve(to: CGPoint(x: 0.6325*width, y: 0.6*height), control1: CGPoint(x: 0.651*width, y: 0.611*height), control2: CGPoint(x: 0.6415*width, y: 0.603*height))
|
||||
path.addCurve(to: CGPoint(x: 0.575*width, y: 0.5175*height), control1: CGPoint(x: 0.603*width, y: 0.59*height), control2: CGPoint(x: 0.575*width, y: 0.55*height))
|
||||
path.addCurve(to: CGPoint(x: 0.6595*width, y: 0.4255*height), control1: CGPoint(x: 0.575*width, y: 0.4685*height), control2: CGPoint(x: 0.609*width, y: 0.4315*height))
|
||||
path.addCurve(to: CGPoint(x: 0.691*width, y: 0.4095*height), control1: CGPoint(x: 0.6755*width, y: 0.4235*height), control2: CGPoint(x: 0.684*width, y: 0.419*height))
|
||||
path.addCurve(to: CGPoint(x: 0.691*width, y: 0.3745*height), control1: CGPoint(x: 0.701*width, y: 0.3965*height), control2: CGPoint(x: 0.701*width, y: 0.396*height))
|
||||
path.addCurve(to: CGPoint(x: 0.565*width, y: 0.2645*height), control1: CGPoint(x: 0.667*width, y: 0.3215*height), control2: CGPoint(x: 0.6175*width, y: 0.278*height))
|
||||
path.addLine(to: CGPoint(x: 0.54*width, y: 0.258*height))
|
||||
path.addLine(to: CGPoint(x: 0.54*width, y: 0.2355*height))
|
||||
path.addCurve(to: CGPoint(x: 0.6215*width, y: 0.102*height), control1: CGPoint(x: 0.54*width, y: 0.192*height), control2: CGPoint(x: 0.5745*width, y: 0.1355*height))
|
||||
path.addCurve(to: CGPoint(x: 0.66*width, y: 0.0575*height), control1: CGPoint(x: 0.653*width, y: 0.0795*height), control2: CGPoint(x: 0.66*width, y: 0.0715*height))
|
||||
path.addCurve(to: CGPoint(x: 0.6315*width, y: 0.03*height), control1: CGPoint(x: 0.66*width, y: 0.044*height), control2: CGPoint(x: 0.6455*width, y: 0.03*height))
|
||||
path.addCurve(to: CGPoint(x: 0.5985*width, y: 0.045*height), control1: CGPoint(x: 0.626*width, y: 0.03*height), control2: CGPoint(x: 0.6115*width, y: 0.037*height))
|
||||
path.closeSubpath()
|
||||
path.move(to: CGPoint(x: 0.4155*width, y: 0.1615*height))
|
||||
path.addCurve(to: CGPoint(x: 0.444*width, y: 0.19*height), control1: CGPoint(x: 0.4245*width, y: 0.1675*height), control2: CGPoint(x: 0.4375*width, y: 0.1805*height))
|
||||
path.addLine(to: CGPoint(x: 0.4555*width, y: 0.2075*height))
|
||||
path.addLine(to: CGPoint(x: 0.445*width, y: 0.216*height))
|
||||
path.addCurve(to: CGPoint(x: 0.369*width, y: 0.23*height), control1: CGPoint(x: 0.4265*width, y: 0.231*height), control2: CGPoint(x: 0.397*width, y: 0.2365*height))
|
||||
path.addCurve(to: CGPoint(x: 0.285*width, y: 0.175*height), control1: CGPoint(x: 0.345*width, y: 0.224*height), control2: CGPoint(x: 0.285*width, y: 0.185*height))
|
||||
path.addCurve(to: CGPoint(x: 0.2865*width, y: 0.17*height), control1: CGPoint(x: 0.285*width, y: 0.1725*height), control2: CGPoint(x: 0.2855*width, y: 0.17*height))
|
||||
path.addCurve(to: CGPoint(x: 0.315*width, y: 0.16*height), control1: CGPoint(x: 0.287*width, y: 0.17*height), control2: CGPoint(x: 0.3*width, y: 0.1655*height))
|
||||
path.addCurve(to: CGPoint(x: 0.4155*width, y: 0.1615*height), control1: CGPoint(x: 0.3535*width, y: 0.1465*height), control2: CGPoint(x: 0.394*width, y: 0.147*height))
|
||||
path.closeSubpath()
|
||||
path.move(to: CGPoint(x: 0.553*width, y: 0.3225*height))
|
||||
path.addCurve(to: CGPoint(x: 0.62*width, y: 0.369*height), control1: CGPoint(x: 0.5755*width, y: 0.329*height), control2: CGPoint(x: 0.62*width, y: 0.3605*height))
|
||||
path.addCurve(to: CGPoint(x: 0.599*width, y: 0.382*height), control1: CGPoint(x: 0.62*width, y: 0.371*height), control2: CGPoint(x: 0.6105*width, y: 0.377*height))
|
||||
path.addCurve(to: CGPoint(x: 0.5295*width, y: 0.453*height), control1: CGPoint(x: 0.572*width, y: 0.3945*height), control2: CGPoint(x: 0.5445*width, y: 0.4225*height))
|
||||
path.addCurve(to: CGPoint(x: 0.5315*width, y: 0.5815*height), control1: CGPoint(x: 0.512*width, y: 0.4895*height), control2: CGPoint(x: 0.5125*width, y: 0.5425*height))
|
||||
path.addCurve(to: CGPoint(x: 0.565*width, y: 0.626*height), control1: CGPoint(x: 0.5395*width, y: 0.597*height), control2: CGPoint(x: 0.5545*width, y: 0.6175*height))
|
||||
path.addCurve(to: CGPoint(x: 0.576*width, y: 0.6575*height), control1: CGPoint(x: 0.584*width, y: 0.642*height), control2: CGPoint(x: 0.584*width, y: 0.6425*height))
|
||||
path.addCurve(to: CGPoint(x: 0.572*width, y: 0.7725*height), control1: CGPoint(x: 0.5645*width, y: 0.68*height), control2: CGPoint(x: 0.562*width, y: 0.744*height))
|
||||
path.addCurve(to: CGPoint(x: 0.6785*width, y: 0.8755*height), control1: CGPoint(x: 0.5875*width, y: 0.8195*height), control2: CGPoint(x: 0.632*width, y: 0.862*height))
|
||||
path.addCurve(to: CGPoint(x: 0.6665*width, y: 0.902*height), control1: CGPoint(x: 0.7045*width, y: 0.883*height), control2: CGPoint(x: 0.701*width, y: 0.8905*height))
|
||||
path.addCurve(to: CGPoint(x: 0.546*width, y: 0.897*height), control1: CGPoint(x: 0.6305*width, y: 0.9145*height), control2: CGPoint(x: 0.585*width, y: 0.9125*height))
|
||||
path.addCurve(to: CGPoint(x: 0.498*width, y: 0.89*height), control1: CGPoint(x: 0.517*width, y: 0.885*height), control2: CGPoint(x: 0.5115*width, y: 0.8845*height))
|
||||
path.addCurve(to: CGPoint(x: 0.405*width, y: 0.909*height), control1: CGPoint(x: 0.451*width, y: 0.909*height), control2: CGPoint(x: 0.4415*width, y: 0.911*height))
|
||||
path.addCurve(to: CGPoint(x: 0.2925*width, y: 0.8535*height), control1: CGPoint(x: 0.3585*width, y: 0.9065*height), control2: CGPoint(x: 0.3245*width, y: 0.8895*height))
|
||||
path.addCurve(to: CGPoint(x: 0.2485*width, y: 0.734*height), control1: CGPoint(x: 0.2625*width, y: 0.8205*height), control2: CGPoint(x: 0.2485*width, y: 0.7825*height))
|
||||
path.addCurve(to: CGPoint(x: 0.3115*width, y: 0.597*height), control1: CGPoint(x: 0.2485*width, y: 0.6825*height), control2: CGPoint(x: 0.267*width, y: 0.642*height))
|
||||
path.addCurve(to: CGPoint(x: 0.375*width, y: 0.465*height), control1: CGPoint(x: 0.3535*width, y: 0.5545*height), control2: CGPoint(x: 0.3695*width, y: 0.5215*height))
|
||||
path.addCurve(to: CGPoint(x: 0.385*width, y: 0.4075*height), control1: CGPoint(x: 0.3775*width, y: 0.441*height), control2: CGPoint(x: 0.382*width, y: 0.415*height))
|
||||
path.addCurve(to: CGPoint(x: 0.438*width, y: 0.3395*height), control1: CGPoint(x: 0.394*width, y: 0.3835*height), control2: CGPoint(x: 0.4195*width, y: 0.351*height))
|
||||
path.addCurve(to: CGPoint(x: 0.553*width, y: 0.3225*height), control1: CGPoint(x: 0.4785*width, y: 0.3145*height), control2: CGPoint(x: 0.5105*width, y: 0.31*height))
|
||||
path.closeSubpath()
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
// Pear text
|
||||
struct Pearcleaner: Shape {
|
||||
func path(in rect: CGRect) -> Path {
|
||||
var path = Path()
|
||||
let width = rect.size.width
|
||||
let height = rect.size.height
|
||||
path.move(to: CGPoint(x: 1.10767*width, y: -0.23519*height))
|
||||
path.addCurve(to: CGPoint(x: 1.10292*width, y: -0.22248*height), control1: CGPoint(x: 1.10519*width, y: -0.23943*height), control2: CGPoint(x: 1.10375*width, y: -0.23519*height))
|
||||
path.addCurve(to: CGPoint(x: 1.09362*width, y: -0.14302*height), control1: CGPoint(x: 1.10044*width, y: -0.1854*height), control2: CGPoint(x: 1.09755*width, y: -0.16315*height))
|
||||
path.addCurve(to: CGPoint(x: 1.0744*width, y: -0.09005*height), control1: CGPoint(x: 1.08783*width, y: -0.11442*height), control2: CGPoint(x: 1.07977*width, y: -0.09005*height))
|
||||
path.addCurve(to: CGPoint(x: 1.09052*width, y: -0.24684*height), control1: CGPoint(x: 1.07977*width, y: -0.10594*height), control2: CGPoint(x: 1.08928*width, y: -0.16103*height))
|
||||
path.addCurve(to: CGPoint(x: 1.05869*width, y: -0.43648*height), control1: CGPoint(x: 1.09217*width, y: -0.36126*height), control2: CGPoint(x: 1.0775*width, y: -0.43648*height))
|
||||
path.addCurve(to: CGPoint(x: 1.02955*width, y: -0.36868*height), control1: CGPoint(x: 1.04546*width, y: -0.43648*height), control2: CGPoint(x: 1.03513*width, y: -0.39834*height))
|
||||
path.addLine(to: CGPoint(x: 1.03058*width, y: -0.40364*height))
|
||||
path.addCurve(to: CGPoint(x: 1.02686*width, y: -0.42377*height), control1: CGPoint(x: 1.031*width, y: -0.41529*height), control2: CGPoint(x: 1.02934*width, y: -0.42377*height))
|
||||
path.addLine(to: CGPoint(x: 1.01054*width, y: -0.42377*height))
|
||||
path.addCurve(to: CGPoint(x: 1.00578*width, y: -0.40258*height), control1: CGPoint(x: 1.00805*width, y: -0.42377*height), control2: CGPoint(x: 1.00619*width, y: -0.41529*height))
|
||||
path.addLine(to: CGPoint(x: 0.98532*width, y: 0.26062*height))
|
||||
path.addCurve(to: CGPoint(x: 0.98883*width, y: 0.2818*height), control1: CGPoint(x: 0.98491*width, y: 0.27227*height), control2: CGPoint(x: 0.98656*width, y: 0.2818*height))
|
||||
path.addLine(to: CGPoint(x: 1.00516*width, y: 0.2818*height))
|
||||
path.addCurve(to: CGPoint(x: 1.01012*width, y: 0.25956*height), control1: CGPoint(x: 1.00805*width, y: 0.2818*height), control2: CGPoint(x: 1.00971*width, y: 0.27333*height))
|
||||
path.addLine(to: CGPoint(x: 1.02087*width, y: -0.09111*height))
|
||||
path.addCurve(to: CGPoint(x: 1.05146*width, y: -0.31888*height), control1: CGPoint(x: 1.02583*width, y: -0.2532*height), control2: CGPoint(x: 1.03926*width, y: -0.31888*height))
|
||||
path.addCurve(to: CGPoint(x: 1.06282*width, y: -0.20023*height), control1: CGPoint(x: 1.06262*width, y: -0.31888*height), control2: CGPoint(x: 1.06572*width, y: -0.26379*height))
|
||||
path.addCurve(to: CGPoint(x: 1.04298*width, y: -0.09429*height), control1: CGPoint(x: 1.06034*width, y: -0.14302*height), control2: CGPoint(x: 1.05352*width, y: -0.09429*height))
|
||||
path.addCurve(to: CGPoint(x: 1.02976*width, y: -0.09111*height), control1: CGPoint(x: 1.03203*width, y: -0.10594*height), control2: CGPoint(x: 1.03058*width, y: -0.1017*height))
|
||||
path.addLine(to: CGPoint(x: 1.02562*width, y: -0.04132*height))
|
||||
path.addCurve(to: CGPoint(x: 1.03017*width, y: -0.00848*height), control1: CGPoint(x: 1.02438*width, y: -0.02649*height), control2: CGPoint(x: 1.02542*width, y: -0.01801*height))
|
||||
path.addCurve(to: CGPoint(x: 1.05538*width, y: 0.01271*height), control1: CGPoint(x: 1.03348*width, y: -0.00106*height), control2: CGPoint(x: 1.04154*width, y: 0.01271*height))
|
||||
path.addCurve(to: CGPoint(x: 1.11801*width, y: -0.19917*height), control1: CGPoint(x: 1.08411*width, y: 0.01271*height), control2: CGPoint(x: 1.10891*width, y: -0.05615*height))
|
||||
path.addCurve(to: CGPoint(x: 1.11573*width, y: -0.22248*height), control1: CGPoint(x: 1.11883*width, y: -0.21188*height), control2: CGPoint(x: 1.1178*width, y: -0.2193*height))
|
||||
path.closeSubpath()
|
||||
path.move(to: CGPoint(x: 1.18208*width, y: -0.214*height))
|
||||
path.addCurve(to: CGPoint(x: 1.14695*width, y: -0.09641*height), control1: CGPoint(x: 1.17423*width, y: -0.13137*height), control2: CGPoint(x: 1.15956*width, y: -0.09641*height))
|
||||
path.addCurve(to: CGPoint(x: 1.12959*width, y: -0.14302*height), control1: CGPoint(x: 1.13806*width, y: -0.09641*height), control2: CGPoint(x: 1.13248*width, y: -0.11442*height))
|
||||
path.addCurve(to: CGPoint(x: 1.17712*width, y: -0.28604*height), control1: CGPoint(x: 1.14757*width, y: -0.1409*height), control2: CGPoint(x: 1.17237*width, y: -0.18434*height))
|
||||
path.addCurve(to: CGPoint(x: 1.14984*width, y: -0.43648*height), control1: CGPoint(x: 1.18126*width, y: -0.37186*height), control2: CGPoint(x: 1.16844*width, y: -0.43648*height))
|
||||
path.addCurve(to: CGPoint(x: 1.10148*width, y: -0.23095*height), control1: CGPoint(x: 1.12918*width, y: -0.43648*height), control2: CGPoint(x: 1.10644*width, y: -0.35702*height))
|
||||
path.addCurve(to: CGPoint(x: 1.1424*width, y: 0.01271*height), control1: CGPoint(x: 1.0959*width, y: -0.09323*height), control2: CGPoint(x: 1.11533*width, y: 0.01271*height))
|
||||
path.addCurve(to: CGPoint(x: 1.198*width, y: -0.19917*height), control1: CGPoint(x: 1.16824*width, y: 0.01271*height), control2: CGPoint(x: 1.18973*width, y: -0.08263*height))
|
||||
path.addCurve(to: CGPoint(x: 1.19573*width, y: -0.22248*height), control1: CGPoint(x: 1.19883*width, y: -0.21082*height), control2: CGPoint(x: 1.19821*width, y: -0.21824*height))
|
||||
path.addLine(to: CGPoint(x: 1.18766*width, y: -0.23519*height))
|
||||
path.addCurve(to: CGPoint(x: 1.18208*width, y: -0.214*height), control1: CGPoint(x: 1.18518*width, y: -0.23943*height), control2: CGPoint(x: 1.18415*width, y: -0.23519*height))
|
||||
path.closeSubpath()
|
||||
path.move(to: CGPoint(x: 1.12773*width, y: -0.20659*height))
|
||||
path.addCurve(to: CGPoint(x: 1.12794*width, y: -0.214*height), control1: CGPoint(x: 1.12773*width, y: -0.20871*height), control2: CGPoint(x: 1.12794*width, y: -0.21188*height))
|
||||
path.addCurve(to: CGPoint(x: 1.14922*width, y: -0.32736*height), control1: CGPoint(x: 1.13021*width, y: -0.27863*height), control2: CGPoint(x: 1.14137*width, y: -0.32736*height))
|
||||
path.addCurve(to: CGPoint(x: 1.15563*width, y: -0.2977*height), control1: CGPoint(x: 1.15336*width, y: -0.32736*height), control2: CGPoint(x: 1.15563*width, y: -0.31465*height))
|
||||
path.addCurve(to: CGPoint(x: 1.12773*width, y: -0.20659*height), control1: CGPoint(x: 1.15563*width, y: -0.25426*height), control2: CGPoint(x: 1.14178*width, y: -0.20976*height))
|
||||
path.closeSubpath()
|
||||
path.move(to: CGPoint(x: 1.21075*width, y: 0.01271*height))
|
||||
path.addCurve(to: CGPoint(x: 1.24237*width, y: -0.0678*height), control1: CGPoint(x: 1.22604*width, y: 0.01271*height), control2: CGPoint(x: 1.23782*width, y: -0.0339*height))
|
||||
path.addCurve(to: CGPoint(x: 1.26552*width, y: 0.01271*height), control1: CGPoint(x: 1.24547*width, y: -0.01907*height), control2: CGPoint(x: 1.25373*width, y: 0.01271*height))
|
||||
path.addCurve(to: CGPoint(x: 1.30644*width, y: -0.19917*height), control1: CGPoint(x: 1.28866*width, y: 0.01271*height), control2: CGPoint(x: 1.30189*width, y: -0.11018*height))
|
||||
path.addCurve(to: CGPoint(x: 1.30416*width, y: -0.22248*height), control1: CGPoint(x: 1.30706*width, y: -0.21082*height), control2: CGPoint(x: 1.30664*width, y: -0.21824*height))
|
||||
path.addLine(to: CGPoint(x: 1.2961*width, y: -0.23519*height))
|
||||
path.addCurve(to: CGPoint(x: 1.29135*width, y: -0.22248*height), control1: CGPoint(x: 1.29362*width, y: -0.23943*height), control2: CGPoint(x: 1.29218*width, y: -0.23519*height))
|
||||
path.addCurve(to: CGPoint(x: 1.27192*width, y: -0.09429*height), control1: CGPoint(x: 1.28846*width, y: -0.18328*height), control2: CGPoint(x: 1.28143*width, y: -0.09429*height))
|
||||
path.addCurve(to: CGPoint(x: 1.26717*width, y: -0.18434*height), control1: CGPoint(x: 1.26614*width, y: -0.09429*height), control2: CGPoint(x: 1.26552*width, y: -0.12819*height))
|
||||
path.addLine(to: CGPoint(x: 1.27378*width, y: -0.40258*height))
|
||||
path.addCurve(to: CGPoint(x: 1.27027*width, y: -0.42377*height), control1: CGPoint(x: 1.2742*width, y: -0.41423*height), control2: CGPoint(x: 1.27275*width, y: -0.42377*height))
|
||||
path.addLine(to: CGPoint(x: 1.25373*width, y: -0.42377*height))
|
||||
path.addCurve(to: CGPoint(x: 1.24898*width, y: -0.40258*height), control1: CGPoint(x: 1.25125*width, y: -0.42377*height), control2: CGPoint(x: 1.24939*width, y: -0.41529*height))
|
||||
path.addLine(to: CGPoint(x: 1.24877*width, y: -0.39622*height))
|
||||
path.addCurve(to: CGPoint(x: 1.22769*width, y: -0.43648*height), control1: CGPoint(x: 1.24547*width, y: -0.41105*height), control2: CGPoint(x: 1.23885*width, y: -0.43648*height))
|
||||
path.addCurve(to: CGPoint(x: 1.18264*width, y: -0.24473*height), control1: CGPoint(x: 1.20765*width, y: -0.43648*height), control2: CGPoint(x: 1.18925*width, y: -0.35596*height))
|
||||
path.addCurve(to: CGPoint(x: 1.21075*width, y: 0.01271*height), control1: CGPoint(x: 1.1752*width, y: -0.11865*height), control2: CGPoint(x: 1.18512*width, y: 0.01271*height))
|
||||
path.closeSubpath()
|
||||
path.move(to: CGPoint(x: 1.24567*width, y: -0.29346*height))
|
||||
path.addCurve(to: CGPoint(x: 1.21901*width, y: -0.09429*height), control1: CGPoint(x: 1.24278*width, y: -0.18858*height), control2: CGPoint(x: 1.23183*width, y: -0.09429*height))
|
||||
path.addCurve(to: CGPoint(x: 1.20806*width, y: -0.21294*height), control1: CGPoint(x: 1.2093*width, y: -0.09429*height), control2: CGPoint(x: 1.20496*width, y: -0.15044*height))
|
||||
path.addCurve(to: CGPoint(x: 1.23245*width, y: -0.3263*height), control1: CGPoint(x: 1.21178*width, y: -0.28604*height), control2: CGPoint(x: 1.22232*width, y: -0.3263*height))
|
||||
path.addCurve(to: CGPoint(x: 1.24567*width, y: -0.29346*height), control1: CGPoint(x: 1.23968*width, y: -0.3263*height), control2: CGPoint(x: 1.24361*width, y: -0.30511*height))
|
||||
path.closeSubpath()
|
||||
path.move(to: CGPoint(x: 1.36936*width, y: -0.22248*height))
|
||||
path.addCurve(to: CGPoint(x: 1.34807*width, y: -0.09429*height), control1: CGPoint(x: 1.36626*width, y: -0.17904*height), control2: CGPoint(x: 1.35799*width, y: -0.09429*height))
|
||||
path.addCurve(to: CGPoint(x: 1.34228*width, y: -0.16527*height), control1: CGPoint(x: 1.34331*width, y: -0.09429*height), control2: CGPoint(x: 1.34001*width, y: -0.11442*height))
|
||||
path.addCurve(to: CGPoint(x: 1.34972*width, y: -0.31253*height), control1: CGPoint(x: 1.34497*width, y: -0.22777*height), control2: CGPoint(x: 1.34931*width, y: -0.30405*height))
|
||||
path.addCurve(to: CGPoint(x: 1.34393*width, y: -0.38245*height), control1: CGPoint(x: 1.35117*width, y: -0.34749*height), control2: CGPoint(x: 1.35075*width, y: -0.36656*height))
|
||||
path.addCurve(to: CGPoint(x: 1.315*width, y: -0.44813*height), control1: CGPoint(x: 1.3336*width, y: -0.40576*height), control2: CGPoint(x: 1.32099*width, y: -0.42589*height))
|
||||
path.addCurve(to: CGPoint(x: 1.30322*width, y: -0.52017*height), control1: CGPoint(x: 1.31541*width, y: -0.48945*height), control2: CGPoint(x: 1.31128*width, y: -0.52441*height))
|
||||
path.addCurve(to: CGPoint(x: 1.29495*width, y: -0.40258*height), control1: CGPoint(x: 1.29392*width, y: -0.51594*height), control2: CGPoint(x: 1.28999*width, y: -0.46508*height))
|
||||
path.addCurve(to: CGPoint(x: 1.29061*width, y: -0.21506*height), control1: CGPoint(x: 1.29619*width, y: -0.3655*height), control2: CGPoint(x: 1.29537*width, y: -0.27969*height))
|
||||
path.addCurve(to: CGPoint(x: 1.29206*width, y: -0.19493*height), control1: CGPoint(x: 1.28979*width, y: -0.20553*height), control2: CGPoint(x: 1.2902*width, y: -0.19811*height))
|
||||
path.addLine(to: CGPoint(x: 1.30177*width, y: -0.17586*height))
|
||||
path.addCurve(to: CGPoint(x: 1.30549*width, y: -0.18434*height), control1: CGPoint(x: 1.30363*width, y: -0.17268*height), control2: CGPoint(x: 1.30487*width, y: -0.17374*height))
|
||||
path.addCurve(to: CGPoint(x: 1.31211*width, y: -0.35279*height), control1: CGPoint(x: 1.30859*width, y: -0.23519*height), control2: CGPoint(x: 1.31087*width, y: -0.29346*height))
|
||||
path.addLine(to: CGPoint(x: 1.32017*width, y: -0.33372*height))
|
||||
path.addCurve(to: CGPoint(x: 1.32347*width, y: -0.29664*height), control1: CGPoint(x: 1.32347*width, y: -0.3263*height), control2: CGPoint(x: 1.32451*width, y: -0.31783*height))
|
||||
path.addCurve(to: CGPoint(x: 1.31583*width, y: -0.14408*height), control1: CGPoint(x: 1.32079*width, y: -0.23625*height), control2: CGPoint(x: 1.31748*width, y: -0.19387*height))
|
||||
path.addCurve(to: CGPoint(x: 1.3429*width, y: 0.01271*height), control1: CGPoint(x: 1.31335*width, y: -0.06145*height), control2: CGPoint(x: 1.32554*width, y: 0.01271*height))
|
||||
path.addCurve(to: CGPoint(x: 1.38444*width, y: -0.19917*height), control1: CGPoint(x: 1.36543*width, y: 0.01271*height), control2: CGPoint(x: 1.3799*width, y: -0.11018*height))
|
||||
path.addCurve(to: CGPoint(x: 1.38217*width, y: -0.22248*height), control1: CGPoint(x: 1.38506*width, y: -0.21082*height), control2: CGPoint(x: 1.38465*width, y: -0.21824*height))
|
||||
path.addLine(to: CGPoint(x: 1.37411*width, y: -0.23519*height))
|
||||
path.addCurve(to: CGPoint(x: 1.36936*width, y: -0.22248*height), control1: CGPoint(x: 1.37163*width, y: -0.23943*height), control2: CGPoint(x: 1.37018*width, y: -0.23519*height))
|
||||
path.closeSubpath()
|
||||
path.move(to: CGPoint(x: 1.45862*width, y: -0.22248*height))
|
||||
path.addLine(to: CGPoint(x: 1.45056*width, y: -0.23519*height))
|
||||
path.addCurve(to: CGPoint(x: 1.44539*width, y: -0.21824*height), control1: CGPoint(x: 1.44787*width, y: -0.23943*height), control2: CGPoint(x: 1.44705*width, y: -0.23413*height))
|
||||
path.addCurve(to: CGPoint(x: 1.41253*width, y: -0.09429*height), control1: CGPoint(x: 1.4363*width, y: -0.13666*height), control2: CGPoint(x: 1.42514*width, y: -0.09429*height))
|
||||
path.addCurve(to: CGPoint(x: 1.39455*width, y: -0.22036*height), control1: CGPoint(x: 1.39703*width, y: -0.09429*height), control2: CGPoint(x: 1.39166*width, y: -0.15573*height))
|
||||
path.addCurve(to: CGPoint(x: 1.41956*width, y: -0.32948*height), control1: CGPoint(x: 1.39744*width, y: -0.28498*height), control2: CGPoint(x: 1.40778*width, y: -0.32948*height))
|
||||
path.addCurve(to: CGPoint(x: 1.42968*width, y: -0.31783*height), control1: CGPoint(x: 1.42348*width, y: -0.32948*height), control2: CGPoint(x: 1.427*width, y: -0.32418*height))
|
||||
path.addCurve(to: CGPoint(x: 1.43506*width, y: -0.32312*height), control1: CGPoint(x: 1.43216*width, y: -0.31253*height), control2: CGPoint(x: 1.4332*width, y: -0.31253*height))
|
||||
path.addLine(to: CGPoint(x: 1.44126*width, y: -0.3602*height))
|
||||
path.addCurve(to: CGPoint(x: 1.44043*width, y: -0.38987*height), control1: CGPoint(x: 1.44271*width, y: -0.36974*height), control2: CGPoint(x: 1.44312*width, y: -0.37609*height))
|
||||
path.addCurve(to: CGPoint(x: 1.4148*width, y: -0.43648*height), control1: CGPoint(x: 1.43568*width, y: -0.41211*height), control2: CGPoint(x: 1.42576*width, y: -0.43648*height))
|
||||
path.addCurve(to: CGPoint(x: 1.36768*width, y: -0.2246*height), control1: CGPoint(x: 1.39455*width, y: -0.43648*height), control2: CGPoint(x: 1.37202*width, y: -0.3549*height))
|
||||
path.addCurve(to: CGPoint(x: 1.40592*width, y: 0.01271*height), control1: CGPoint(x: 1.36355*width, y: -0.09535*height), control2: CGPoint(x: 1.37926*width, y: 0.01271*height))
|
||||
path.addCurve(to: CGPoint(x: 1.46089*width, y: -0.19917*height), control1: CGPoint(x: 1.42968*width, y: 0.01271*height), control2: CGPoint(x: 1.45139*width, y: -0.0731*height))
|
||||
path.addCurve(to: CGPoint(x: 1.45862*width, y: -0.22248*height), control1: CGPoint(x: 1.46172*width, y: -0.21082*height), control2: CGPoint(x: 1.4611*width, y: -0.21824*height))
|
||||
path.closeSubpath()
|
||||
path.move(to: CGPoint(x: 1.52396*width, y: -0.22354*height))
|
||||
path.addLine(to: CGPoint(x: 1.5159*width, y: -0.23625*height))
|
||||
path.addCurve(to: CGPoint(x: 1.51114*width, y: -0.22566*height), control1: CGPoint(x: 1.51362*width, y: -0.23943*height), control2: CGPoint(x: 1.51218*width, y: -0.23731*height))
|
||||
path.addCurve(to: CGPoint(x: 1.48428*width, y: -0.09641*height), control1: CGPoint(x: 1.50164*width, y: -0.12395*height), control2: CGPoint(x: 1.4911*width, y: -0.09641*height))
|
||||
path.addCurve(to: CGPoint(x: 1.46692*width, y: -0.25214*height), control1: CGPoint(x: 1.47312*width, y: -0.09641*height), control2: CGPoint(x: 1.46754*width, y: -0.17374*height))
|
||||
path.addCurve(to: CGPoint(x: 1.51301*width, y: -0.44284*height), control1: CGPoint(x: 1.48325*width, y: -0.27333*height), control2: CGPoint(x: 1.50267*width, y: -0.33372*height))
|
||||
path.addCurve(to: CGPoint(x: 1.49813*width, y: -0.70133*height), control1: CGPoint(x: 1.52231*width, y: -0.54242*height), control2: CGPoint(x: 1.52603*width, y: -0.70133*height))
|
||||
path.addCurve(to: CGPoint(x: 1.44584*width, y: -0.36762*height), control1: CGPoint(x: 1.47705*width, y: -0.70133*height), control2: CGPoint(x: 1.45452*width, y: -0.57314*height))
|
||||
path.addCurve(to: CGPoint(x: 1.47829*width, y: 0.01271*height), control1: CGPoint(x: 1.43922*width, y: -0.21188*height), control2: CGPoint(x: 1.44294*width, y: 0.01271*height))
|
||||
path.addCurve(to: CGPoint(x: 1.52499*width, y: -0.18964*height), control1: CGPoint(x: 1.49854*width, y: 0.01271*height), control2: CGPoint(x: 1.51569*width, y: -0.06145*height))
|
||||
path.addCurve(to: CGPoint(x: 1.52396*width, y: -0.22354*height), control1: CGPoint(x: 1.52665*width, y: -0.21082*height), control2: CGPoint(x: 1.52685*width, y: -0.21824*height))
|
||||
path.closeSubpath()
|
||||
path.move(to: CGPoint(x: 1.46878*width, y: -0.34961*height))
|
||||
path.addCurve(to: CGPoint(x: 1.49523*width, y: -0.59221*height), control1: CGPoint(x: 1.47394*width, y: -0.47462*height), control2: CGPoint(x: 1.488*width, y: -0.5901*height))
|
||||
path.addCurve(to: CGPoint(x: 1.49296*width, y: -0.46508*height), control1: CGPoint(x: 1.5006*width, y: -0.59327*height), control2: CGPoint(x: 1.49957*width, y: -0.52759*height))
|
||||
path.addCurve(to: CGPoint(x: 1.46878*width, y: -0.34961*height), control1: CGPoint(x: 1.48655*width, y: -0.40258*height), control2: CGPoint(x: 1.47684*width, y: -0.36444*height))
|
||||
path.closeSubpath()
|
||||
path.move(to: CGPoint(x: 1.58615*width, y: -0.214*height))
|
||||
path.addCurve(to: CGPoint(x: 1.55101*width, y: -0.09641*height), control1: CGPoint(x: 1.5783*width, y: -0.13137*height), control2: CGPoint(x: 1.56362*width, y: -0.09641*height))
|
||||
path.addCurve(to: CGPoint(x: 1.53366*width, y: -0.14302*height), control1: CGPoint(x: 1.54213*width, y: -0.09641*height), control2: CGPoint(x: 1.53655*width, y: -0.11442*height))
|
||||
path.addCurve(to: CGPoint(x: 1.58119*width, y: -0.28604*height), control1: CGPoint(x: 1.55164*width, y: -0.1409*height), control2: CGPoint(x: 1.57644*width, y: -0.18434*height))
|
||||
path.addCurve(to: CGPoint(x: 1.55391*width, y: -0.43648*height), control1: CGPoint(x: 1.58533*width, y: -0.37186*height), control2: CGPoint(x: 1.57251*width, y: -0.43648*height))
|
||||
path.addCurve(to: CGPoint(x: 1.50555*width, y: -0.23095*height), control1: CGPoint(x: 1.53324*width, y: -0.43648*height), control2: CGPoint(x: 1.51051*width, y: -0.35702*height))
|
||||
path.addCurve(to: CGPoint(x: 1.54647*width, y: 0.01271*height), control1: CGPoint(x: 1.49997*width, y: -0.09323*height), control2: CGPoint(x: 1.5194*width, y: 0.01271*height))
|
||||
path.addCurve(to: CGPoint(x: 1.60207*width, y: -0.19917*height), control1: CGPoint(x: 1.57231*width, y: 0.01271*height), control2: CGPoint(x: 1.5938*width, y: -0.08263*height))
|
||||
path.addCurve(to: CGPoint(x: 1.59979*width, y: -0.22248*height), control1: CGPoint(x: 1.60289*width, y: -0.21082*height), control2: CGPoint(x: 1.60227*width, y: -0.21824*height))
|
||||
path.addLine(to: CGPoint(x: 1.59173*width, y: -0.23519*height))
|
||||
path.addCurve(to: CGPoint(x: 1.58615*width, y: -0.214*height), control1: CGPoint(x: 1.58925*width, y: -0.23943*height), control2: CGPoint(x: 1.58822*width, y: -0.23519*height))
|
||||
path.closeSubpath()
|
||||
path.move(to: CGPoint(x: 1.5318*width, y: -0.20659*height))
|
||||
path.addCurve(to: CGPoint(x: 1.532*width, y: -0.214*height), control1: CGPoint(x: 1.5318*width, y: -0.20871*height), control2: CGPoint(x: 1.532*width, y: -0.21188*height))
|
||||
path.addCurve(to: CGPoint(x: 1.55329*width, y: -0.32736*height), control1: CGPoint(x: 1.53428*width, y: -0.27863*height), control2: CGPoint(x: 1.54544*width, y: -0.32736*height))
|
||||
path.addCurve(to: CGPoint(x: 1.5597*width, y: -0.2977*height), control1: CGPoint(x: 1.55742*width, y: -0.32736*height), control2: CGPoint(x: 1.5597*width, y: -0.31465*height))
|
||||
path.addCurve(to: CGPoint(x: 1.5318*width, y: -0.20659*height), control1: CGPoint(x: 1.5597*width, y: -0.25426*height), control2: CGPoint(x: 1.54585*width, y: -0.20976*height))
|
||||
path.closeSubpath()
|
||||
path.move(to: CGPoint(x: 1.61481*width, y: 0.01271*height))
|
||||
path.addCurve(to: CGPoint(x: 1.64643*width, y: -0.0678*height), control1: CGPoint(x: 1.63011*width, y: 0.01271*height), control2: CGPoint(x: 1.64189*width, y: -0.0339*height))
|
||||
path.addCurve(to: CGPoint(x: 1.66958*width, y: 0.01271*height), control1: CGPoint(x: 1.64953*width, y: -0.01907*height), control2: CGPoint(x: 1.6578*width, y: 0.01271*height))
|
||||
path.addCurve(to: CGPoint(x: 1.7105*width, y: -0.19917*height), control1: CGPoint(x: 1.69273*width, y: 0.01271*height), control2: CGPoint(x: 1.70596*width, y: -0.11018*height))
|
||||
path.addCurve(to: CGPoint(x: 1.70823*width, y: -0.22248*height), control1: CGPoint(x: 1.71112*width, y: -0.21082*height), control2: CGPoint(x: 1.71071*width, y: -0.21824*height))
|
||||
path.addLine(to: CGPoint(x: 1.70017*width, y: -0.23519*height))
|
||||
path.addCurve(to: CGPoint(x: 1.69542*width, y: -0.22248*height), control1: CGPoint(x: 1.69769*width, y: -0.23943*height), control2: CGPoint(x: 1.69624*width, y: -0.23519*height))
|
||||
path.addCurve(to: CGPoint(x: 1.67599*width, y: -0.09429*height), control1: CGPoint(x: 1.69252*width, y: -0.18328*height), control2: CGPoint(x: 1.68549*width, y: -0.09429*height))
|
||||
path.addCurve(to: CGPoint(x: 1.67123*width, y: -0.18434*height), control1: CGPoint(x: 1.6702*width, y: -0.09429*height), control2: CGPoint(x: 1.66958*width, y: -0.12819*height))
|
||||
path.addLine(to: CGPoint(x: 1.67785*width, y: -0.40258*height))
|
||||
path.addCurve(to: CGPoint(x: 1.67433*width, y: -0.42377*height), control1: CGPoint(x: 1.67826*width, y: -0.41423*height), control2: CGPoint(x: 1.67681*width, y: -0.42377*height))
|
||||
path.addLine(to: CGPoint(x: 1.6578*width, y: -0.42377*height))
|
||||
path.addCurve(to: CGPoint(x: 1.65305*width, y: -0.40258*height), control1: CGPoint(x: 1.65532*width, y: -0.42377*height), control2: CGPoint(x: 1.65346*width, y: -0.41529*height))
|
||||
path.addLine(to: CGPoint(x: 1.65284*width, y: -0.39622*height))
|
||||
path.addCurve(to: CGPoint(x: 1.63176*width, y: -0.43648*height), control1: CGPoint(x: 1.64953*width, y: -0.41105*height), control2: CGPoint(x: 1.64292*width, y: -0.43648*height))
|
||||
path.addCurve(to: CGPoint(x: 1.5867*width, y: -0.24473*height), control1: CGPoint(x: 1.61171*width, y: -0.43648*height), control2: CGPoint(x: 1.59332*width, y: -0.35596*height))
|
||||
path.addCurve(to: CGPoint(x: 1.61481*width, y: 0.01271*height), control1: CGPoint(x: 1.57926*width, y: -0.11865*height), control2: CGPoint(x: 1.58918*width, y: 0.01271*height))
|
||||
path.closeSubpath()
|
||||
path.move(to: CGPoint(x: 1.64974*width, y: -0.29346*height))
|
||||
path.addCurve(to: CGPoint(x: 1.62308*width, y: -0.09429*height), control1: CGPoint(x: 1.64685*width, y: -0.18858*height), control2: CGPoint(x: 1.63589*width, y: -0.09429*height))
|
||||
path.addCurve(to: CGPoint(x: 1.61212*width, y: -0.21294*height), control1: CGPoint(x: 1.61336*width, y: -0.09429*height), control2: CGPoint(x: 1.60902*width, y: -0.15044*height))
|
||||
path.addCurve(to: CGPoint(x: 1.63651*width, y: -0.3263*height), control1: CGPoint(x: 1.61585*width, y: -0.28604*height), control2: CGPoint(x: 1.62639*width, y: -0.3263*height))
|
||||
path.addCurve(to: CGPoint(x: 1.64974*width, y: -0.29346*height), control1: CGPoint(x: 1.64375*width, y: -0.3263*height), control2: CGPoint(x: 1.64767*width, y: -0.30511*height))
|
||||
path.closeSubpath()
|
||||
path.move(to: CGPoint(x: 1.68931*width, y: -0.02119*height))
|
||||
path.addCurve(to: CGPoint(x: 1.69282*width, y: 0), control1: CGPoint(x: 1.6891*width, y: -0.00953*height), control2: CGPoint(x: 1.69055*width, y: 0))
|
||||
path.addLine(to: CGPoint(x: 1.70935*width, y: 0))
|
||||
path.addCurve(to: CGPoint(x: 1.71411*width, y: -0.02119*height), control1: CGPoint(x: 1.71225*width, y: 0), control2: CGPoint(x: 1.71369*width, y: -0.00848*height))
|
||||
path.addLine(to: CGPoint(x: 1.71597*width, y: -0.08263*height))
|
||||
path.addCurve(to: CGPoint(x: 1.74366*width, y: -0.31677*height), control1: CGPoint(x: 1.71886*width, y: -0.17798*height), control2: CGPoint(x: 1.73292*width, y: -0.31677*height))
|
||||
path.addCurve(to: CGPoint(x: 1.748*width, y: -0.2532*height), control1: CGPoint(x: 1.74821*width, y: -0.31677*height), control2: CGPoint(x: 1.74924*width, y: -0.29346*height))
|
||||
path.addLine(to: CGPoint(x: 1.7447*width, y: -0.14832*height))
|
||||
path.addCurve(to: CGPoint(x: 1.76888*width, y: 0.01271*height), control1: CGPoint(x: 1.74201*width, y: -0.05933*height), control2: CGPoint(x: 1.7511*width, y: 0.01271*height))
|
||||
path.addCurve(to: CGPoint(x: 1.8098*width, y: -0.19917*height), control1: CGPoint(x: 1.79202*width, y: 0.01271*height), control2: CGPoint(x: 1.80525*width, y: -0.11018*height))
|
||||
path.addCurve(to: CGPoint(x: 1.80753*width, y: -0.22248*height), control1: CGPoint(x: 1.81042*width, y: -0.21082*height), control2: CGPoint(x: 1.81001*width, y: -0.21824*height))
|
||||
path.addLine(to: CGPoint(x: 1.79946*width, y: -0.23519*height))
|
||||
path.addCurve(to: CGPoint(x: 1.79471*width, y: -0.22248*height), control1: CGPoint(x: 1.79698*width, y: -0.23943*height), control2: CGPoint(x: 1.79574*width, y: -0.23519*height))
|
||||
path.addCurve(to: CGPoint(x: 1.77528*width, y: -0.09429*height), control1: CGPoint(x: 1.79161*width, y: -0.18328*height), control2: CGPoint(x: 1.78479*width, y: -0.09429*height))
|
||||
path.addCurve(to: CGPoint(x: 1.77053*width, y: -0.18434*height), control1: CGPoint(x: 1.7695*width, y: -0.09429*height), control2: CGPoint(x: 1.76888*width, y: -0.12819*height))
|
||||
path.addLine(to: CGPoint(x: 1.7728*width, y: -0.26274*height))
|
||||
path.addCurve(to: CGPoint(x: 1.75358*width, y: -0.43648*height), control1: CGPoint(x: 1.7759*width, y: -0.35914*height), control2: CGPoint(x: 1.77032*width, y: -0.43648*height))
|
||||
path.addCurve(to: CGPoint(x: 1.72403*width, y: -0.34749*height), control1: CGPoint(x: 1.74056*width, y: -0.43648*height), control2: CGPoint(x: 1.73044*width, y: -0.39092*height))
|
||||
path.addLine(to: CGPoint(x: 1.72568*width, y: -0.40258*height))
|
||||
path.addCurve(to: CGPoint(x: 1.72217*width, y: -0.42377*height), control1: CGPoint(x: 1.7261*width, y: -0.41423*height), control2: CGPoint(x: 1.72465*width, y: -0.42377*height))
|
||||
path.addLine(to: CGPoint(x: 1.70584*width, y: -0.42377*height))
|
||||
path.addCurve(to: CGPoint(x: 1.70109*width, y: -0.40258*height), control1: CGPoint(x: 1.70336*width, y: -0.42377*height), control2: CGPoint(x: 1.7015*width, y: -0.41529*height))
|
||||
path.closeSubpath()
|
||||
path.move(to: CGPoint(x: 1.87397*width, y: -0.214*height))
|
||||
path.addCurve(to: CGPoint(x: 1.83883*width, y: -0.09641*height), control1: CGPoint(x: 1.86611*width, y: -0.13137*height), control2: CGPoint(x: 1.85144*width, y: -0.09641*height))
|
||||
path.addCurve(to: CGPoint(x: 1.82147*width, y: -0.14302*height), control1: CGPoint(x: 1.82994*width, y: -0.09641*height), control2: CGPoint(x: 1.82436*width, y: -0.11442*height))
|
||||
path.addCurve(to: CGPoint(x: 1.86901*width, y: -0.28604*height), control1: CGPoint(x: 1.83945*width, y: -0.1409*height), control2: CGPoint(x: 1.86425*width, y: -0.18434*height))
|
||||
path.addCurve(to: CGPoint(x: 1.84172*width, y: -0.43648*height), control1: CGPoint(x: 1.87314*width, y: -0.37186*height), control2: CGPoint(x: 1.86032*width, y: -0.43648*height))
|
||||
path.addCurve(to: CGPoint(x: 1.79336*width, y: -0.23095*height), control1: CGPoint(x: 1.82105*width, y: -0.43648*height), control2: CGPoint(x: 1.79832*width, y: -0.35702*height))
|
||||
path.addCurve(to: CGPoint(x: 1.83428*width, y: 0.01271*height), control1: CGPoint(x: 1.78778*width, y: -0.09323*height), control2: CGPoint(x: 1.80721*width, y: 0.01271*height))
|
||||
path.addCurve(to: CGPoint(x: 1.88988*width, y: -0.19917*height), control1: CGPoint(x: 1.86012*width, y: 0.01271*height), control2: CGPoint(x: 1.88161*width, y: -0.08263*height))
|
||||
path.addCurve(to: CGPoint(x: 1.8876*width, y: -0.22248*height), control1: CGPoint(x: 1.8907*width, y: -0.21082*height), control2: CGPoint(x: 1.89008*width, y: -0.21824*height))
|
||||
path.addLine(to: CGPoint(x: 1.87954*width, y: -0.23519*height))
|
||||
path.addCurve(to: CGPoint(x: 1.87396*width, y: -0.214*height), control1: CGPoint(x: 1.87706*width, y: -0.23943*height), control2: CGPoint(x: 1.87603*width, y: -0.23519*height))
|
||||
path.closeSubpath()
|
||||
path.move(to: CGPoint(x: 1.81961*width, y: -0.20659*height))
|
||||
path.addCurve(to: CGPoint(x: 1.81982*width, y: -0.214*height), control1: CGPoint(x: 1.81961*width, y: -0.20871*height), control2: CGPoint(x: 1.81982*width, y: -0.21188*height))
|
||||
path.addCurve(to: CGPoint(x: 1.8411*width, y: -0.32736*height), control1: CGPoint(x: 1.82209*width, y: -0.27863*height), control2: CGPoint(x: 1.83325*width, y: -0.32736*height))
|
||||
path.addCurve(to: CGPoint(x: 1.84751*width, y: -0.2977*height), control1: CGPoint(x: 1.84524*width, y: -0.32736*height), control2: CGPoint(x: 1.84751*width, y: -0.31465*height))
|
||||
path.addCurve(to: CGPoint(x: 1.81961*width, y: -0.20659*height), control1: CGPoint(x: 1.84751*width, y: -0.25426*height), control2: CGPoint(x: 1.83366*width, y: -0.20976*height))
|
||||
path.closeSubpath()
|
||||
path.move(to: CGPoint(x: 1.95305*width, y: -0.22248*height))
|
||||
path.addCurve(to: CGPoint(x: 1.93177*width, y: -0.09429*height), control1: CGPoint(x: 1.94995*width, y: -0.17904*height), control2: CGPoint(x: 1.94169*width, y: -0.09429*height))
|
||||
path.addCurve(to: CGPoint(x: 1.92598*width, y: -0.16527*height), control1: CGPoint(x: 1.92701*width, y: -0.09429*height), control2: CGPoint(x: 1.92371*width, y: -0.11442*height))
|
||||
path.addCurve(to: CGPoint(x: 1.93342*width, y: -0.31253*height), control1: CGPoint(x: 1.92867*width, y: -0.22777*height), control2: CGPoint(x: 1.93301*width, y: -0.30405*height))
|
||||
path.addCurve(to: CGPoint(x: 1.92763*width, y: -0.38245*height), control1: CGPoint(x: 1.93487*width, y: -0.34749*height), control2: CGPoint(x: 1.93445*width, y: -0.36656*height))
|
||||
path.addCurve(to: CGPoint(x: 1.8987*width, y: -0.44813*height), control1: CGPoint(x: 1.9173*width, y: -0.40576*height), control2: CGPoint(x: 1.90469*width, y: -0.42589*height))
|
||||
path.addCurve(to: CGPoint(x: 1.88692*width, y: -0.52017*height), control1: CGPoint(x: 1.89911*width, y: -0.48945*height), control2: CGPoint(x: 1.89498*width, y: -0.52441*height))
|
||||
path.addCurve(to: CGPoint(x: 1.87865*width, y: -0.40258*height), control1: CGPoint(x: 1.87762*width, y: -0.51594*height), control2: CGPoint(x: 1.87369*width, y: -0.46508*height))
|
||||
path.addCurve(to: CGPoint(x: 1.87431*width, y: -0.21506*height), control1: CGPoint(x: 1.87989*width, y: -0.3655*height), control2: CGPoint(x: 1.87906*width, y: -0.27969*height))
|
||||
path.addCurve(to: CGPoint(x: 1.87576*width, y: -0.19493*height), control1: CGPoint(x: 1.87348*width, y: -0.20553*height), control2: CGPoint(x: 1.8739*width, y: -0.19811*height))
|
||||
path.addLine(to: CGPoint(x: 1.88547*width, y: -0.17586*height))
|
||||
path.addCurve(to: CGPoint(x: 1.88919*width, y: -0.18434*height), control1: CGPoint(x: 1.88733*width, y: -0.17268*height), control2: CGPoint(x: 1.88857*width, y: -0.17374*height))
|
||||
path.addCurve(to: CGPoint(x: 1.8958*width, y: -0.35279*height), control1: CGPoint(x: 1.89229*width, y: -0.23519*height), control2: CGPoint(x: 1.89456*width, y: -0.29346*height))
|
||||
path.addLine(to: CGPoint(x: 1.90386*width, y: -0.33372*height))
|
||||
path.addCurve(to: CGPoint(x: 1.90717*width, y: -0.29664*height), control1: CGPoint(x: 1.90717*width, y: -0.3263*height), control2: CGPoint(x: 1.9082*width, y: -0.31783*height))
|
||||
path.addCurve(to: CGPoint(x: 1.89952*width, y: -0.14408*height), control1: CGPoint(x: 1.90448*width, y: -0.23625*height), control2: CGPoint(x: 1.90118*width, y: -0.19387*height))
|
||||
path.addCurve(to: CGPoint(x: 1.9266*width, y: 0.01271*height), control1: CGPoint(x: 1.89704*width, y: -0.06145*height), control2: CGPoint(x: 1.90924*width, y: 0.01271*height))
|
||||
path.addCurve(to: CGPoint(x: 1.96814*width, y: -0.19917*height), control1: CGPoint(x: 1.94913*width, y: 0.01271*height), control2: CGPoint(x: 1.96359*width, y: -0.11018*height))
|
||||
path.addCurve(to: CGPoint(x: 1.96587*width, y: -0.22248*height), control1: CGPoint(x: 1.96876*width, y: -0.21082*height), control2: CGPoint(x: 1.96835*width, y: -0.21824*height))
|
||||
path.addLine(to: CGPoint(x: 1.95781*width, y: -0.23519*height))
|
||||
path.addCurve(to: CGPoint(x: 1.95305*width, y: -0.22248*height), control1: CGPoint(x: 1.95533*width, y: -0.23943*height), control2: CGPoint(x: 1.95388*width, y: -0.23519*height))
|
||||
path.closeSubpath()
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
// Ghost
|
||||
struct Ghost: Shape {
|
||||
func path(in rect: CGRect) -> Path {
|
||||
var path = Path()
|
||||
let width = rect.size.width
|
||||
let height = rect.size.height
|
||||
path.move(to: CGPoint(x: 0.47*width, y: 0.0815*height))
|
||||
path.addCurve(to: CGPoint(x: 0.3148*width, y: 0.1708*height), control1: CGPoint(x: 0.4054*width, y: 0.0893*height), control2: CGPoint(x: 0.353*width, y: 0.1195*height))
|
||||
path.addCurve(to: CGPoint(x: 0.2539*width, y: 0.3318*height), control1: CGPoint(x: 0.2841*width, y: 0.2119*height), control2: CGPoint(x: 0.2634*width, y: 0.2668*height))
|
||||
path.addCurve(to: CGPoint(x: 0.2517*width, y: 0.4035*height), control1: CGPoint(x: 0.2515*width, y: 0.3483*height), control2: CGPoint(x: 0.2504*width, y: 0.3846*height))
|
||||
path.addCurve(to: CGPoint(x: 0.2525*width, y: 0.4167*height), control1: CGPoint(x: 0.2522*width, y: 0.4106*height), control2: CGPoint(x: 0.2526*width, y: 0.4166*height))
|
||||
path.addCurve(to: CGPoint(x: 0.2447*width, y: 0.4189*height), control1: CGPoint(x: 0.2524*width, y: 0.4169*height), control2: CGPoint(x: 0.2489*width, y: 0.4179*height))
|
||||
path.addCurve(to: CGPoint(x: 0.1841*width, y: 0.4547*height), control1: CGPoint(x: 0.2245*width, y: 0.4241*height), control2: CGPoint(x: 0.2051*width, y: 0.4355*height))
|
||||
path.addCurve(to: CGPoint(x: 0.15*width, y: 0.513*height), control1: CGPoint(x: 0.1646*width, y: 0.4723*height), control2: CGPoint(x: 0.15*width, y: 0.4974*height))
|
||||
path.addCurve(to: CGPoint(x: 0.1569*width, y: 0.5309*height), control1: CGPoint(x: 0.15*width, y: 0.5195*height), control2: CGPoint(x: 0.1528*width, y: 0.5268*height))
|
||||
path.addCurve(to: CGPoint(x: 0.2085*width, y: 0.5309*height), control1: CGPoint(x: 0.165*width, y: 0.539*height), control2: CGPoint(x: 0.1775*width, y: 0.539*height))
|
||||
path.addCurve(to: CGPoint(x: 0.264*width, y: 0.5252*height), control1: CGPoint(x: 0.2538*width, y: 0.5191*height), control2: CGPoint(x: 0.2539*width, y: 0.5191*height))
|
||||
path.addCurve(to: CGPoint(x: 0.272*width, y: 0.5335*height), control1: CGPoint(x: 0.2701*width, y: 0.529*height), control2: CGPoint(x: 0.2711*width, y: 0.53*height))
|
||||
path.addCurve(to: CGPoint(x: 0.4178*width, y: 0.7853*height), control1: CGPoint(x: 0.3036*width, y: 0.6544*height), control2: CGPoint(x: 0.355*width, y: 0.7433*height))
|
||||
path.addCurve(to: CGPoint(x: 0.5902*width, y: 0.7895*height), control1: CGPoint(x: 0.4685*width, y: 0.8193*height), control2: CGPoint(x: 0.5271*width, y: 0.8207*height))
|
||||
path.addCurve(to: CGPoint(x: 0.6492*width, y: 0.7534*height), control1: CGPoint(x: 0.6081*width, y: 0.7807*height), control2: CGPoint(x: 0.6235*width, y: 0.7713*height))
|
||||
path.addCurve(to: CGPoint(x: 0.7453*width, y: 0.709*height), control1: CGPoint(x: 0.7026*width, y: 0.7165*height), control2: CGPoint(x: 0.7188*width, y: 0.709*height))
|
||||
path.addCurve(to: CGPoint(x: 0.8012*width, y: 0.7341*height), control1: CGPoint(x: 0.7664*width, y: 0.709*height), control2: CGPoint(x: 0.7842*width, y: 0.717*height))
|
||||
path.addCurve(to: CGPoint(x: 0.816*width, y: 0.744*height), control1: CGPoint(x: 0.81*width, y: 0.7429*height), control2: CGPoint(x: 0.8116*width, y: 0.744*height))
|
||||
path.addCurve(to: CGPoint(x: 0.8269*width, y: 0.7335*height), control1: CGPoint(x: 0.8201*width, y: 0.744*height), control2: CGPoint(x: 0.8254*width, y: 0.7389*height))
|
||||
path.addCurve(to: CGPoint(x: 0.8265*width, y: 0.707*height), control1: CGPoint(x: 0.8284*width, y: 0.7278*height), control2: CGPoint(x: 0.8283*width, y: 0.7154*height))
|
||||
path.addCurve(to: CGPoint(x: 0.7937*width, y: 0.6544*height), control1: CGPoint(x: 0.8223*width, y: 0.687*height), control2: CGPoint(x: 0.8115*width, y: 0.6696*height))
|
||||
path.addCurve(to: CGPoint(x: 0.7576*width, y: 0.6304*height), control1: CGPoint(x: 0.7808*width, y: 0.6433*height), control2: CGPoint(x: 0.7711*width, y: 0.6369*height))
|
||||
path.addCurve(to: CGPoint(x: 0.6647*width, y: 0.6285*height), control1: CGPoint(x: 0.725*width, y: 0.6149*height), control2: CGPoint(x: 0.6977*width, y: 0.6143*height))
|
||||
path.addCurve(to: CGPoint(x: 0.6694*width, y: 0.6162*height), control1: CGPoint(x: 0.6633*width, y: 0.6291*height), control2: CGPoint(x: 0.6646*width, y: 0.6257*height))
|
||||
path.addCurve(to: CGPoint(x: 0.7549*width, y: 0.3918*height), control1: CGPoint(x: 0.7139*width, y: 0.5273*height), control2: CGPoint(x: 0.7443*width, y: 0.4478*height))
|
||||
path.addCurve(to: CGPoint(x: 0.759*width, y: 0.31*height), control1: CGPoint(x: 0.76*width, y: 0.3651*height), control2: CGPoint(x: 0.7616*width, y: 0.3337*height))
|
||||
path.addCurve(to: CGPoint(x: 0.6846*width, y: 0.1554*height), control1: CGPoint(x: 0.7528*width, y: 0.252*height), control2: CGPoint(x: 0.7264*width, y: 0.1972*height))
|
||||
path.addCurve(to: CGPoint(x: 0.5329*width, y: 0.0815*height), control1: CGPoint(x: 0.6431*width, y: 0.114*height), control2: CGPoint(x: 0.5913*width, y: 0.0888*height))
|
||||
path.addCurve(to: CGPoint(x: 0.47*width, y: 0.0815*height), control1: CGPoint(x: 0.518*width, y: 0.0797*height), control2: CGPoint(x: 0.4851*width, y: 0.0797*height))
|
||||
path.closeSubpath()
|
||||
path.move(to: CGPoint(x: 0.534*width, y: 0.0985*height))
|
||||
path.addCurve(to: CGPoint(x: 0.6525*width, y: 0.1488*height), control1: CGPoint(x: 0.577*width, y: 0.1042*height), control2: CGPoint(x: 0.6175*width, y: 0.1214*height))
|
||||
path.addCurve(to: CGPoint(x: 0.6948*width, y: 0.1922*height), control1: CGPoint(x: 0.663*width, y: 0.157*height), control2: CGPoint(x: 0.6865*width, y: 0.1812*height))
|
||||
path.addCurve(to: CGPoint(x: 0.741*width, y: 0.374*height), control1: CGPoint(x: 0.7344*width, y: 0.245*height), control2: CGPoint(x: 0.7506*width, y: 0.3087*height))
|
||||
path.addCurve(to: CGPoint(x: 0.6504*width, y: 0.6166*height), control1: CGPoint(x: 0.732*width, y: 0.435*height), control2: CGPoint(x: 0.7061*width, y: 0.5045*height))
|
||||
path.addCurve(to: CGPoint(x: 0.6311*width, y: 0.6609*height), control1: CGPoint(x: 0.6311*width, y: 0.6556*height), control2: CGPoint(x: 0.6301*width, y: 0.6579*height))
|
||||
path.addCurve(to: CGPoint(x: 0.6403*width, y: 0.6664*height), control1: CGPoint(x: 0.6324*width, y: 0.6646*height), control2: CGPoint(x: 0.6371*width, y: 0.6674*height))
|
||||
path.addCurve(to: CGPoint(x: 0.6505*width, y: 0.6588*height), control1: CGPoint(x: 0.6415*width, y: 0.666*height), control2: CGPoint(x: 0.6461*width, y: 0.6626*height))
|
||||
path.addCurve(to: CGPoint(x: 0.704*width, y: 0.6353*height), control1: CGPoint(x: 0.6679*width, y: 0.6436*height), control2: CGPoint(x: 0.685*width, y: 0.6362*height))
|
||||
path.addCurve(to: CGPoint(x: 0.7833*width, y: 0.6673*height), control1: CGPoint(x: 0.7305*width, y: 0.634*height), control2: CGPoint(x: 0.757*width, y: 0.6447*height))
|
||||
path.addCurve(to: CGPoint(x: 0.8108*width, y: 0.7143*height), control1: CGPoint(x: 0.7983*width, y: 0.6803*height), control2: CGPoint(x: 0.8081*width, y: 0.6969*height))
|
||||
path.addCurve(to: CGPoint(x: 0.8101*width, y: 0.719*height), control1: CGPoint(x: 0.8116*width, y: 0.7193*height), control2: CGPoint(x: 0.8115*width, y: 0.7198*height))
|
||||
path.addCurve(to: CGPoint(x: 0.8065*width, y: 0.716*height), control1: CGPoint(x: 0.8092*width, y: 0.7186*height), control2: CGPoint(x: 0.8076*width, y: 0.7172*height))
|
||||
path.addCurve(to: CGPoint(x: 0.7675*width, y: 0.6948*height), control1: CGPoint(x: 0.8*width, y: 0.7088*height), control2: CGPoint(x: 0.7812*width, y: 0.6986*height))
|
||||
path.addCurve(to: CGPoint(x: 0.7209*width, y: 0.6953*height), control1: CGPoint(x: 0.7554*width, y: 0.6916*height), control2: CGPoint(x: 0.7341*width, y: 0.6918*height))
|
||||
path.addCurve(to: CGPoint(x: 0.6629*width, y: 0.724*height), control1: CGPoint(x: 0.7013*width, y: 0.7005*height), control2: CGPoint(x: 0.6868*width, y: 0.7076*height))
|
||||
path.addCurve(to: CGPoint(x: 0.5548*width, y: 0.7864*height), control1: CGPoint(x: 0.5998*width, y: 0.7672*height), control2: CGPoint(x: 0.583*width, y: 0.7769*height))
|
||||
path.addCurve(to: CGPoint(x: 0.508*width, y: 0.7947*height), control1: CGPoint(x: 0.5376*width, y: 0.7921*height), control2: CGPoint(x: 0.5265*width, y: 0.7941*height))
|
||||
path.addCurve(to: CGPoint(x: 0.4626*width, y: 0.7888*height), control1: CGPoint(x: 0.489*width, y: 0.7953*height), control2: CGPoint(x: 0.4788*width, y: 0.794*height))
|
||||
path.addCurve(to: CGPoint(x: 0.3776*width, y: 0.725*height), control1: CGPoint(x: 0.4334*width, y: 0.7796*height), control2: CGPoint(x: 0.4033*width, y: 0.757*height))
|
||||
path.addCurve(to: CGPoint(x: 0.3451*width, y: 0.6763*height), control1: CGPoint(x: 0.3696*width, y: 0.715*height), control2: CGPoint(x: 0.3525*width, y: 0.6894*height))
|
||||
path.addCurve(to: CGPoint(x: 0.2886*width, y: 0.5293*height), control1: CGPoint(x: 0.3234*width, y: 0.638*height), control2: CGPoint(x: 0.3017*width, y: 0.5813*height))
|
||||
path.addCurve(to: CGPoint(x: 0.2683*width, y: 0.3864*height), control1: CGPoint(x: 0.2758*width, y: 0.4785*height), control2: CGPoint(x: 0.2692*width, y: 0.4321*height))
|
||||
path.addCurve(to: CGPoint(x: 0.2726*width, y: 0.3205*height), control1: CGPoint(x: 0.2677*width, y: 0.3559*height), control2: CGPoint(x: 0.2684*width, y: 0.3444*height))
|
||||
path.addCurve(to: CGPoint(x: 0.4818*width, y: 0.097*height), control1: CGPoint(x: 0.2961*width, y: 0.1862*height), control2: CGPoint(x: 0.3708*width, y: 0.1063*height))
|
||||
path.addCurve(to: CGPoint(x: 0.534*width, y: 0.0985*height), control1: CGPoint(x: 0.4951*width, y: 0.0959*height), control2: CGPoint(x: 0.5201*width, y: 0.0966*height))
|
||||
path.closeSubpath()
|
||||
path.move(to: CGPoint(x: 0.2544*width, y: 0.4372*height))
|
||||
path.addCurve(to: CGPoint(x: 0.2575*width, y: 0.4589*height), control1: CGPoint(x: 0.2547*width, y: 0.439*height), control2: CGPoint(x: 0.2561*width, y: 0.4488*height))
|
||||
path.addCurve(to: CGPoint(x: 0.2656*width, y: 0.5051*height), control1: CGPoint(x: 0.2597*width, y: 0.4749*height), control2: CGPoint(x: 0.2642*width, y: 0.5003*height))
|
||||
path.addCurve(to: CGPoint(x: 0.2618*width, y: 0.5053*height), control1: CGPoint(x: 0.266*width, y: 0.5067*height), control2: CGPoint(x: 0.2657*width, y: 0.5067*height))
|
||||
path.addCurve(to: CGPoint(x: 0.2233*width, y: 0.5101*height), control1: CGPoint(x: 0.2554*width, y: 0.503*height), control2: CGPoint(x: 0.246*width, y: 0.5042*height))
|
||||
path.addCurve(to: CGPoint(x: 0.1775*width, y: 0.5205*height), control1: CGPoint(x: 0.1947*width, y: 0.5175*height), control2: CGPoint(x: 0.1846*width, y: 0.5198*height))
|
||||
path.addCurve(to: CGPoint(x: 0.169*width, y: 0.519*height), control1: CGPoint(x: 0.1718*width, y: 0.5211*height), control2: CGPoint(x: 0.171*width, y: 0.521*height))
|
||||
path.addCurve(to: CGPoint(x: 0.1791*width, y: 0.4849*height), control1: CGPoint(x: 0.1641*width, y: 0.5141*height), control2: CGPoint(x: 0.1686*width, y: 0.499*height))
|
||||
path.addCurve(to: CGPoint(x: 0.2285*width, y: 0.4427*height), control1: CGPoint(x: 0.1894*width, y: 0.4709*height), control2: CGPoint(x: 0.2135*width, y: 0.4504*height))
|
||||
path.addCurve(to: CGPoint(x: 0.2527*width, y: 0.434*height), control1: CGPoint(x: 0.2342*width, y: 0.4398*height), control2: CGPoint(x: 0.2501*width, y: 0.4341*height))
|
||||
path.addCurve(to: CGPoint(x: 0.2544*width, y: 0.4372*height), control1: CGPoint(x: 0.2534*width, y: 0.434*height), control2: CGPoint(x: 0.2541*width, y: 0.4355*height))
|
||||
path.closeSubpath()
|
||||
path.move(to: CGPoint(x: 0.4813*width, y: 0.2149*height))
|
||||
path.addCurve(to: CGPoint(x: 0.4432*width, y: 0.2634*height), control1: CGPoint(x: 0.4651*width, y: 0.2208*height), control2: CGPoint(x: 0.4513*width, y: 0.2383*height))
|
||||
path.addCurve(to: CGPoint(x: 0.4356*width, y: 0.312*height), control1: CGPoint(x: 0.4376*width, y: 0.2805*height), control2: CGPoint(x: 0.4356*width, y: 0.2933*height))
|
||||
path.addCurve(to: CGPoint(x: 0.4404*width, y: 0.3387*height), control1: CGPoint(x: 0.4356*width, y: 0.3288*height), control2: CGPoint(x: 0.4366*width, y: 0.3342*height))
|
||||
path.addCurve(to: CGPoint(x: 0.4655*width, y: 0.3364*height), control1: CGPoint(x: 0.4439*width, y: 0.3427*height), control2: CGPoint(x: 0.4488*width, y: 0.3423*height))
|
||||
path.addCurve(to: CGPoint(x: 0.492*width, y: 0.3315*height), control1: CGPoint(x: 0.4793*width, y: 0.3316*height), control2: CGPoint(x: 0.4796*width, y: 0.3316*height))
|
||||
path.addCurve(to: CGPoint(x: 0.5195*width, y: 0.3324*height), control1: CGPoint(x: 0.4989*width, y: 0.3315*height), control2: CGPoint(x: 0.5113*width, y: 0.3319*height))
|
||||
path.addCurve(to: CGPoint(x: 0.5466*width, y: 0.3276*height), control1: CGPoint(x: 0.5369*width, y: 0.3334*height), control2: CGPoint(x: 0.5416*width, y: 0.3326*height))
|
||||
path.addCurve(to: CGPoint(x: 0.552*width, y: 0.2905*height), control1: CGPoint(x: 0.5526*width, y: 0.3216*height), control2: CGPoint(x: 0.5542*width, y: 0.3104*height))
|
||||
path.addCurve(to: CGPoint(x: 0.516*width, y: 0.22*height), control1: CGPoint(x: 0.5484*width, y: 0.2589*height), control2: CGPoint(x: 0.5346*width, y: 0.2319*height))
|
||||
path.addCurve(to: CGPoint(x: 0.4813*width, y: 0.2149*height), control1: CGPoint(x: 0.5055*width, y: 0.2133*height), control2: CGPoint(x: 0.4916*width, y: 0.2112*height))
|
||||
path.closeSubpath()
|
||||
path.move(to: CGPoint(x: 0.3452*width, y: 0.2165*height))
|
||||
path.addCurve(to: CGPoint(x: 0.306*width, y: 0.2848*height), control1: CGPoint(x: 0.3275*width, y: 0.2247*height), control2: CGPoint(x: 0.3104*width, y: 0.2544*height))
|
||||
path.addCurve(to: CGPoint(x: 0.3052*width, y: 0.3052*height), control1: CGPoint(x: 0.3053*width, y: 0.2892*height), control2: CGPoint(x: 0.305*width, y: 0.2984*height))
|
||||
path.addCurve(to: CGPoint(x: 0.3129*width, y: 0.3277*height), control1: CGPoint(x: 0.3056*width, y: 0.3193*height), control2: CGPoint(x: 0.3072*width, y: 0.3239*height))
|
||||
path.addCurve(to: CGPoint(x: 0.3367*width, y: 0.3279*height), control1: CGPoint(x: 0.3171*width, y: 0.3306*height), control2: CGPoint(x: 0.3204*width, y: 0.3306*height))
|
||||
path.addCurve(to: CGPoint(x: 0.3708*width, y: 0.3305*height), control1: CGPoint(x: 0.3529*width, y: 0.3252*height), control2: CGPoint(x: 0.3592*width, y: 0.3257*height))
|
||||
path.addCurve(to: CGPoint(x: 0.3942*width, y: 0.3371*height), control1: CGPoint(x: 0.3862*width, y: 0.3369*height), control2: CGPoint(x: 0.3912*width, y: 0.3383*height))
|
||||
path.addCurve(to: CGPoint(x: 0.3985*width, y: 0.3331*height), control1: CGPoint(x: 0.3957*width, y: 0.3366*height), control2: CGPoint(x: 0.3976*width, y: 0.3347*height))
|
||||
path.addCurve(to: CGPoint(x: 0.3964*width, y: 0.2647*height), control1: CGPoint(x: 0.4036*width, y: 0.3231*height), control2: CGPoint(x: 0.4026*width, y: 0.2895*height))
|
||||
path.addCurve(to: CGPoint(x: 0.3657*width, y: 0.2155*height), control1: CGPoint(x: 0.39*width, y: 0.2389*height), control2: CGPoint(x: 0.3788*width, y: 0.221*height))
|
||||
path.addCurve(to: CGPoint(x: 0.3452*width, y: 0.2165*height), control1: CGPoint(x: 0.3601*width, y: 0.2132*height), control2: CGPoint(x: 0.3514*width, y: 0.2136*height))
|
||||
path.closeSubpath()
|
||||
path.move(to: CGPoint(x: 0.4847*width, y: 0.4141*height))
|
||||
path.addCurve(to: CGPoint(x: 0.4028*width, y: 0.4568*height), control1: CGPoint(x: 0.4575*width, y: 0.417*height), control2: CGPoint(x: 0.4287*width, y: 0.432*height))
|
||||
path.addCurve(to: CGPoint(x: 0.372*width, y: 0.5039*height), control1: CGPoint(x: 0.3864*width, y: 0.4726*height), control2: CGPoint(x: 0.3753*width, y: 0.4895*height))
|
||||
path.addCurve(to: CGPoint(x: 0.3808*width, y: 0.5335*height), control1: CGPoint(x: 0.3692*width, y: 0.5161*height), control2: CGPoint(x: 0.3726*width, y: 0.5277*height))
|
||||
path.addCurve(to: CGPoint(x: 0.4297*width, y: 0.5298*height), control1: CGPoint(x: 0.389*width, y: 0.5392*height), control2: CGPoint(x: 0.4035*width, y: 0.5381*height))
|
||||
path.addCurve(to: CGPoint(x: 0.463*width, y: 0.521*height), control1: CGPoint(x: 0.4497*width, y: 0.5235*height), control2: CGPoint(x: 0.4592*width, y: 0.521*height))
|
||||
path.addCurve(to: CGPoint(x: 0.4835*width, y: 0.5305*height), control1: CGPoint(x: 0.4655*width, y: 0.521*height), control2: CGPoint(x: 0.4712*width, y: 0.5237*height))
|
||||
path.addLine(to: CGPoint(x: 0.5004*width, y: 0.5399*height))
|
||||
path.addLine(to: CGPoint(x: 0.5042*width, y: 0.5332*height))
|
||||
path.addCurve(to: CGPoint(x: 0.508*width, y: 0.5258*height), control1: CGPoint(x: 0.5063*width, y: 0.5295*height), control2: CGPoint(x: 0.508*width, y: 0.5262*height))
|
||||
path.addCurve(to: CGPoint(x: 0.4681*width, y: 0.5044*height), control1: CGPoint(x: 0.508*width, y: 0.5246*height), control2: CGPoint(x: 0.4716*width, y: 0.505*height))
|
||||
path.addCurve(to: CGPoint(x: 0.4329*width, y: 0.5114*height), control1: CGPoint(x: 0.4626*width, y: 0.5033*height), control2: CGPoint(x: 0.4517*width, y: 0.5055*height))
|
||||
path.addCurve(to: CGPoint(x: 0.3905*width, y: 0.5196*height), control1: CGPoint(x: 0.4037*width, y: 0.5206*height), control2: CGPoint(x: 0.3946*width, y: 0.5223*height))
|
||||
path.addCurve(to: CGPoint(x: 0.388*width, y: 0.5122*height), control1: CGPoint(x: 0.3883*width, y: 0.5182*height), control2: CGPoint(x: 0.388*width, y: 0.5172*height))
|
||||
path.addCurve(to: CGPoint(x: 0.4404*width, y: 0.448*height), control1: CGPoint(x: 0.3881*width, y: 0.4946*height), control2: CGPoint(x: 0.4088*width, y: 0.4693*height))
|
||||
path.addCurve(to: CGPoint(x: 0.4875*width, y: 0.4309*height), control1: CGPoint(x: 0.4554*width, y: 0.438*height), control2: CGPoint(x: 0.4691*width, y: 0.433*height))
|
||||
path.addCurve(to: CGPoint(x: 0.5648*width, y: 0.4481*height), control1: CGPoint(x: 0.5106*width, y: 0.4283*height), control2: CGPoint(x: 0.5396*width, y: 0.4347*height))
|
||||
path.addCurve(to: CGPoint(x: 0.5766*width, y: 0.453*height), control1: CGPoint(x: 0.5699*width, y: 0.4508*height), control2: CGPoint(x: 0.5752*width, y: 0.453*height))
|
||||
path.addCurve(to: CGPoint(x: 0.584*width, y: 0.445*height), control1: CGPoint(x: 0.5801*width, y: 0.453*height), control2: CGPoint(x: 0.584*width, y: 0.4488*height))
|
||||
path.addCurve(to: CGPoint(x: 0.5658*width, y: 0.43*height), control1: CGPoint(x: 0.584*width, y: 0.4402*height), control2: CGPoint(x: 0.5797*width, y: 0.4367*height))
|
||||
path.addCurve(to: CGPoint(x: 0.5108*width, y: 0.414*height), control1: CGPoint(x: 0.5463*width, y: 0.4206*height), control2: CGPoint(x: 0.5309*width, y: 0.4161*height))
|
||||
path.addCurve(to: CGPoint(x: 0.4847*width, y: 0.4141*height), control1: CGPoint(x: 0.5003*width, y: 0.4129*height), control2: CGPoint(x: 0.4958*width, y: 0.4129*height))
|
||||
path.closeSubpath()
|
||||
path.move(to: CGPoint(x: 0.4665*width, y: 0.893*height))
|
||||
path.addCurve(to: CGPoint(x: 0.3071*width, y: 0.9064*height), control1: CGPoint(x: 0.3762*width, y: 0.8947*height), control2: CGPoint(x: 0.3136*width, y: 0.8999*height))
|
||||
path.addCurve(to: CGPoint(x: 0.3093*width, y: 0.9095*height), control1: CGPoint(x: 0.3058*width, y: 0.9078*height), control2: CGPoint(x: 0.306*width, y: 0.9081*height))
|
||||
path.addCurve(to: CGPoint(x: 0.6915*width, y: 0.9164*height), control1: CGPoint(x: 0.3396*width, y: 0.9221*height), control2: CGPoint(x: 0.5731*width, y: 0.9264*height))
|
||||
path.addCurve(to: CGPoint(x: 0.7394*width, y: 0.9075*height), control1: CGPoint(x: 0.7204*width, y: 0.914*height), control2: CGPoint(x: 0.7389*width, y: 0.9105*height))
|
||||
path.addCurve(to: CGPoint(x: 0.4665*width, y: 0.893*height), control1: CGPoint(x: 0.7408*width, y: 0.8978*height), control2: CGPoint(x: 0.604*width, y: 0.8905*height))
|
||||
path.closeSubpath()
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
// Aurora
|
||||
struct AuroraView: View {
|
||||
|
||||
private enum AnimationProperties {
|
||||
static let animationSpeed: Double = 4
|
||||
static let timerDuration: TimeInterval = 3
|
||||
static let blurRadius: CGFloat = 130
|
||||
}
|
||||
|
||||
@State private var timer = Timer.publish(every: AnimationProperties.timerDuration, on: .main, in: .common).autoconnect()
|
||||
@ObservedObject private var animator = CircleAnimator(colors: AuroraColors.all)
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
ZStack {
|
||||
ForEach(animator.circles) { circle in
|
||||
MovingCircle(originOffset: circle.position)
|
||||
.foregroundColor(circle.color)
|
||||
}
|
||||
}.blur(radius: AnimationProperties.blurRadius)
|
||||
}
|
||||
.onDisappear {
|
||||
timer.upstream.connect().cancel()
|
||||
}
|
||||
.onAppear {
|
||||
animateCircles()
|
||||
timer = Timer.publish(every: AnimationProperties.timerDuration, on: .main, in: .common).autoconnect()
|
||||
}
|
||||
.onReceive(timer) { _ in
|
||||
animateCircles()
|
||||
}
|
||||
}
|
||||
|
||||
private func animateCircles() {
|
||||
withAnimation(.easeInOut(duration: AnimationProperties.animationSpeed)) {
|
||||
animator.animate()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private enum AuroraColors {
|
||||
static var all: [Color] {
|
||||
[
|
||||
// Color(red: 0/255, green: 0/255, blue: 128/255), // Dark Blue
|
||||
Color.purple, // Purple
|
||||
Color.pink, // Pink
|
||||
Color.orange, // Orange
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
private struct MovingCircle: Shape {
|
||||
|
||||
var originOffset: CGPoint
|
||||
|
||||
var animatableData: CGPoint.AnimatableData {
|
||||
get {
|
||||
originOffset.animatableData
|
||||
}
|
||||
set {
|
||||
originOffset.animatableData = newValue
|
||||
}
|
||||
}
|
||||
|
||||
func path(in rect: CGRect) -> Path {
|
||||
var path = Path()
|
||||
|
||||
let adjustedX = rect.width * originOffset.x
|
||||
let adjustedY = rect.height * originOffset.y
|
||||
let smallestDimension = min(rect.width, rect.height)
|
||||
path.addArc(center: CGPoint(x: adjustedX, y: adjustedY), radius: smallestDimension/2, startAngle: .zero, endAngle: .degrees(360), clockwise: true)
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
private class CircleAnimator: ObservableObject {
|
||||
class Circle: Identifiable {
|
||||
internal init(position: CGPoint, color: Color) {
|
||||
self.position = position
|
||||
self.color = color
|
||||
}
|
||||
var position: CGPoint
|
||||
let id = UUID().uuidString
|
||||
let color: Color
|
||||
}
|
||||
|
||||
@Published private(set) var circles: [Circle] = []
|
||||
|
||||
|
||||
init(colors: [Color]) {
|
||||
circles = colors.map({ color in
|
||||
Circle(position: CircleAnimator.generateRandomPosition(), color: color)
|
||||
})
|
||||
}
|
||||
|
||||
func animate() {
|
||||
objectWillChange.send()
|
||||
for circle in circles {
|
||||
circle.position = CircleAnimator.generateRandomPosition()
|
||||
}
|
||||
}
|
||||
|
||||
static func generateRandomPosition() -> CGPoint {
|
||||
CGPoint(x: CGFloat.random(in: 0 ... 1), y: CGFloat.random(in: 0 ... 1))
|
||||
}
|
||||
}
|
||||
|
||||
// Ghost View
|
||||
// ZStack {
|
||||
// LinearGradient(colors: [.pink, .orange], startPoint: .topLeading, endPoint: .bottomTrailing)
|
||||
// .edgesIgnoringSafeArea(.all)
|
||||
// .hueRotation(.degrees(animateGradient ? 45 : 0))
|
||||
// .onAppear {
|
||||
// withAnimation(.easeInOut(duration: 3).repeatForever(autoreverses: true)) {
|
||||
// animateGradient.toggle()
|
||||
// }
|
||||
// }
|
||||
// .mask(
|
||||
// Ghost()
|
||||
// .frame(width: 200, height: 200)
|
||||
// )
|
||||
// }
|
||||
|
||||
// Gradient Background
|
||||
//ZStack {
|
||||
// LinearGradient(colors: [.pink, .orange], startPoint: .topLeading, endPoint: .bottomTrailing)
|
||||
// .edgesIgnoringSafeArea(.all)
|
||||
// .hueRotation(.degrees(animateGradient ? 45 : 0))
|
||||
// .onAppear {
|
||||
// withAnimation(.easeInOut(duration: 3).repeatForever(autoreverses: true)) {
|
||||
// animateGradient.toggle()
|
||||
// }
|
||||
// }
|
||||
// .mask(
|
||||
// Ghost()
|
||||
// .frame(width: 200, height: 200)
|
||||
// )
|
||||
//}
|
||||
@@ -0,0 +1,418 @@
|
||||
//
|
||||
// Styles.swift
|
||||
// Pearcleaner
|
||||
//
|
||||
// Created by Alin Lupascu on 10/31/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct SimpleButtonStyle: ButtonStyle {
|
||||
@State private var hovered = false
|
||||
let icon: String
|
||||
let help: String
|
||||
let color: Color
|
||||
|
||||
func makeBody(configuration: Self.Configuration) -> some View {
|
||||
HStack {
|
||||
Image(systemName: icon)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 20)
|
||||
.foregroundColor(color)
|
||||
}
|
||||
.padding(8)
|
||||
.background {
|
||||
if hovered {
|
||||
// Circle()
|
||||
// .strokeBorder(Color("AccentColor"), lineWidth: 1)
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.strokeBorder(Color("AccentColor"), lineWidth: 1)
|
||||
}
|
||||
|
||||
}
|
||||
.onHover { hovering in
|
||||
withAnimation() {
|
||||
hovered = hovering
|
||||
}
|
||||
}
|
||||
.help(help)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct LabeledDivider: View {
|
||||
let label: String
|
||||
@EnvironmentObject var appState: AppState
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 0) {
|
||||
Rectangle()
|
||||
.frame(height: 1)
|
||||
.foregroundColor(Color("AccentColor").opacity(0.2))
|
||||
|
||||
Text(label)
|
||||
.textCase(.uppercase)
|
||||
.font(.title2)
|
||||
.foregroundColor(Color("AccentColor").opacity(0.6))
|
||||
.padding(.horizontal, 10)
|
||||
.frame(minWidth: 80)
|
||||
|
||||
Rectangle()
|
||||
.frame(height: 1)
|
||||
.foregroundColor(Color("AccentColor").opacity(0.2))
|
||||
}
|
||||
.frame(minHeight: 35)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct AnimatedSearchStyle: TextFieldStyle {
|
||||
@State private var isHovered = false
|
||||
@State var icon: Image?
|
||||
@State var trash: Bool = false
|
||||
@Binding var text: String
|
||||
|
||||
func _body(configuration: TextField<Self._Label>) -> some View {
|
||||
|
||||
HStack(spacing: 5) {
|
||||
|
||||
if isHovered || !text.isEmpty {
|
||||
HStack(spacing: 5) {
|
||||
if icon != nil {
|
||||
icon
|
||||
.foregroundColor(Color("AccentColor").opacity(0.5))
|
||||
Image(systemName: "chevron.right")
|
||||
.foregroundColor(Color("AccentColor").opacity(0.3))
|
||||
.controlSize(.small)
|
||||
|
||||
}
|
||||
|
||||
configuration
|
||||
.frame(maxWidth: 300)
|
||||
.font(.title3)
|
||||
.textFieldStyle(PlainTextFieldStyle())
|
||||
if trash {
|
||||
Image(systemName: "xmark")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 10, height: 10)
|
||||
.padding(.trailing, 5)
|
||||
.foregroundStyle(text.isEmpty ? Color.clear : (isHovered ? Color("AccentColor").opacity(0.8) : Color("AccentColor").opacity(0.5)))
|
||||
.onTapGesture {
|
||||
text = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Image(systemName: "magnifyingglass")
|
||||
.foregroundColor(Color("AccentColor").opacity(0.5))
|
||||
.padding(.trailing, 150)
|
||||
|
||||
}
|
||||
}
|
||||
.padding(6)
|
||||
.overlay(
|
||||
Group {
|
||||
if isHovered || !text.isEmpty {
|
||||
RoundedRectangle(cornerRadius: 6)
|
||||
.strokeBorder(Color("AccentColor").opacity(0.4), lineWidth: 1)
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
}
|
||||
)
|
||||
.onHover { hovering in
|
||||
withAnimation(Animation.easeIn(duration: 0.15)) {
|
||||
self.isHovered = hovering
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct SimpleSearchStyle: TextFieldStyle {
|
||||
@State private var isHovered = false
|
||||
@State var icon: Image?
|
||||
@State var trash: Bool = false
|
||||
@Binding var text: String
|
||||
|
||||
func _body(configuration: TextField<Self._Label>) -> some View {
|
||||
|
||||
HStack(spacing: 5) {
|
||||
|
||||
HStack(spacing: 5) {
|
||||
if icon != nil {
|
||||
icon
|
||||
.foregroundColor(Color("mode").opacity(0.5))
|
||||
}
|
||||
|
||||
configuration
|
||||
.frame(maxWidth: 300)
|
||||
.font(.title3)
|
||||
.textFieldStyle(PlainTextFieldStyle())
|
||||
if trash {
|
||||
Image(systemName: "xmark")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 10, height: 10)
|
||||
.padding(.trailing, 5)
|
||||
.foregroundStyle(isHovered ? Color("mode").opacity(0.8) : Color("mode").opacity(0.5))
|
||||
.onTapGesture {
|
||||
text = ""
|
||||
}
|
||||
.opacity(text.isEmpty ? 0 : 1)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
.padding(6)
|
||||
.overlay(
|
||||
Group {
|
||||
RoundedRectangle(cornerRadius: 6)
|
||||
.strokeBorder(Color("mode").opacity(0.2), lineWidth: 1)
|
||||
.allowsHitTesting(false)
|
||||
|
||||
}
|
||||
)
|
||||
.onHover { hovering in
|
||||
withAnimation(Animation.easeIn(duration: 0.15)) {
|
||||
self.isHovered = hovering
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct GlassEffect: NSViewRepresentable {
|
||||
var material: NSVisualEffectView.Material // Choose the material you want
|
||||
var blendingMode: NSVisualEffectView.BlendingMode // Choose the blending mode
|
||||
|
||||
func makeNSView(context: Self.Context) -> NSView {
|
||||
let visualEffectView = NSVisualEffectView()
|
||||
visualEffectView.material = material
|
||||
visualEffectView.blendingMode = blendingMode
|
||||
return visualEffectView
|
||||
}
|
||||
|
||||
func updateNSView(_ nsView: NSView, context: Context) { }
|
||||
}
|
||||
|
||||
|
||||
|
||||
struct VerticalDivider: View {
|
||||
|
||||
var color: Color = Color("AccentColor")
|
||||
var width: CGFloat = 1
|
||||
var height: CGFloat = 20
|
||||
var opacity: Double = 1
|
||||
|
||||
var body: some View {
|
||||
Divider()
|
||||
.foregroundColor(color)
|
||||
.frame(width: width, height: height)
|
||||
.opacity(opacity)
|
||||
.ignoresSafeArea(.all)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
struct WindowActionButton: ButtonStyle {
|
||||
enum UserAction {
|
||||
case accept
|
||||
case cancel
|
||||
}
|
||||
@State private var hovered = false
|
||||
let action: UserAction
|
||||
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
configuration.label
|
||||
.frame(width: 100)
|
||||
.padding(10)
|
||||
.background(
|
||||
!configuration.isPressed ?
|
||||
!hovered ?
|
||||
action == .accept ? Color("AccentColor") : Color.red :
|
||||
action == .accept ? Color("AccentColor").opacity(0.8) : Color.red.opacity(0.8) :
|
||||
action == .accept ? Color("AccentColor").opacity(0.5) : Color.red.opacity(0.5)
|
||||
)
|
||||
.foregroundColor(.white)
|
||||
.cornerRadius(8)
|
||||
.onHover { hovering in
|
||||
withAnimation(Animation.easeIn(duration: 0.15)) {
|
||||
hovered = hovering
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// Picker
|
||||
//struct PillPicker: View {
|
||||
// @Binding var index: Int
|
||||
// var onTapAction: () -> Void
|
||||
//
|
||||
// var body: some View {
|
||||
//
|
||||
// HStack(alignment: .center, spacing: 20) {
|
||||
// ForEach(0..<3) { i in
|
||||
// HStack(spacing: 4) {
|
||||
// Image(systemName: i == 0 ? "appclip" : "ellipsis.circle")
|
||||
// .resizable()
|
||||
// .scaledToFit()
|
||||
// .frame(width: 16)
|
||||
// .foregroundColor(index == i ? .black : .gray)
|
||||
//
|
||||
// Text(i == 0 ? "Apps" : (i == 1 ? "Widgets" : "Plugins"))
|
||||
// .font(.system(size: 12))
|
||||
// .foregroundColor(index == i ? .black : .gray)
|
||||
// }
|
||||
// .padding(8)
|
||||
// .background((Color.white).opacity(index == i ? 1 : 0))
|
||||
// .clipShape(Capsule())
|
||||
// .onTapGesture {
|
||||
// withAnimation {
|
||||
// index = i
|
||||
// onTapAction()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// .background(Color.black.opacity(0.06))
|
||||
// .clipShape(Capsule())
|
||||
// }
|
||||
//}
|
||||
|
||||
struct PillPicker: View {
|
||||
@Binding var index: Int
|
||||
var onTapAction: () -> Void
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .center, spacing: 10) {
|
||||
ForEach(0..<3) { i in
|
||||
ZStack {
|
||||
Capsule()
|
||||
.fill(Color.white)
|
||||
.offset(x: (CGFloat(index - i) * 100))
|
||||
.animation(.default, value: index)
|
||||
.opacity(index == i ? 1 : 0)
|
||||
.scaleEffect(index == i ? 1 : 0.70)
|
||||
.shadow(color: Color.black.opacity(0.2), radius: 2, x: 0, y: 2)
|
||||
|
||||
HStack(spacing: 0) {
|
||||
// Uncomment below for icon next to labels
|
||||
// Image(systemName: i == 0 ? "appclip" : (i == 1 ? "macwindow" : "cube.box.fill"))
|
||||
// .resizable()
|
||||
// .scaledToFit()
|
||||
// .frame(width: 16)
|
||||
// .foregroundColor(index == i ? .black : .gray)
|
||||
|
||||
Text(i == 0 ? "Apps" : (i == 1 ? "Widgets" : "Plugins"))
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(index == i ? (colorScheme == .dark ? .black : Color("AccentColor")) : .gray)
|
||||
}
|
||||
.padding(5)
|
||||
.frame(height: 25)
|
||||
.background(.white.opacity(0.0001))
|
||||
}
|
||||
.onTapGesture {
|
||||
withAnimation {
|
||||
index = i
|
||||
onTapAction()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(height: 25)
|
||||
.padding(5)
|
||||
.background(
|
||||
Capsule()
|
||||
.fill(colorScheme == .dark ? .white.opacity(0.1) : .black.opacity(0.06))
|
||||
.overlay(
|
||||
Capsule()
|
||||
.stroke(Color.black.opacity(colorScheme == .dark ? 0.8 : 0.2), lineWidth: 1)
|
||||
.blur(radius: 1)
|
||||
.offset(y: 1)
|
||||
)
|
||||
)
|
||||
// .background(Color.black.opacity(0.06))
|
||||
.clipShape(Capsule())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// .frame(height: 50)
|
||||
|
||||
// HStack(spacing: 10) {
|
||||
// HStack(spacing: 2) {
|
||||
// Image(systemName: "appclip")
|
||||
// .foregroundColor(index == 0 ? .black : .gray)
|
||||
// Text("Apps")
|
||||
// .font(.footnote)
|
||||
// .foregroundColor(index == 0 ? .black : .gray)
|
||||
// }
|
||||
// .padding(8)
|
||||
// .background((Color.white).opacity(index == 0 ? 1 : 0))
|
||||
// .clipShape(Capsule())
|
||||
// .onTapGesture {
|
||||
// withAnimation {
|
||||
// index = 0
|
||||
// onTapAction()
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// HStack(spacing: 2) {
|
||||
// Image(systemName: "ellipsis.circle")
|
||||
// .foregroundColor(index == 1 ? .black : .gray)
|
||||
// Text("Widgets")
|
||||
// .foregroundColor(index == 1 ? .black : .gray)
|
||||
//
|
||||
//
|
||||
// }
|
||||
// .padding(8)
|
||||
// .background((Color.white).opacity(index == 1 ? 1 : 0))
|
||||
// .clipShape(Capsule())
|
||||
// .onTapGesture {
|
||||
// withAnimation {
|
||||
// index = 1
|
||||
// onTapAction()
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// HStack(spacing: 2) {
|
||||
// Image(systemName: "ellipsis.circle")
|
||||
// .foregroundColor(index == 2 ? .black : .gray)
|
||||
// Text("Plugins")
|
||||
// .foregroundColor(index == 2 ? .black : .gray)
|
||||
//
|
||||
//
|
||||
// }
|
||||
// .padding(8)
|
||||
// .background((Color.white).opacity(index == 2 ? 2 : 0))
|
||||
// .clipShape(Capsule())
|
||||
// .onTapGesture {
|
||||
// withAnimation {
|
||||
// index = 2
|
||||
// onTapAction()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// .background(Color.black.opacity(0.06))
|
||||
// .clipShape(Capsule())
|
||||
// .frame(height: 50)
|
||||
@@ -0,0 +1,509 @@
|
||||
//
|
||||
// Utilities.swift
|
||||
// Pearcleaner
|
||||
//
|
||||
// Created by Alin Lupascu on 11/3/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
// Make updates on main thread
|
||||
func updateOnMain(_ updates: @escaping () -> Void) {
|
||||
DispatchQueue.main.async {
|
||||
updates()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Execute functions on background thread
|
||||
func updateOnBackground(_ updates: @escaping () -> Void) {
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
updates()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Check for access to Full Disk access
|
||||
func checkAndRequestFullDiskAccess(appState: AppState, skipAlert: Bool = false) -> Bool {
|
||||
@AppStorage("settings.permissions.disk") var diskP: Bool = false
|
||||
|
||||
let fileURL = URL(fileURLWithPath: "\(home)/Library/Application Support/com.apple.TCC/TCC.db")
|
||||
|
||||
let accessStatus = FileManager.default.isWritableFile(atPath: fileURL.path)
|
||||
if accessStatus {
|
||||
diskP = true
|
||||
_ = checkAndRequestAccessibilityAccess(appState: appState)
|
||||
|
||||
return true
|
||||
} else {
|
||||
diskP = false
|
||||
if !skipAlert {
|
||||
appState.alertType = .diskAccess
|
||||
appState.showAlert = true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Check for access to System Events
|
||||
func checkAndRequestAccessibilityAccess(appState: AppState) -> Bool {
|
||||
@AppStorage("settings.permissions.events") var diskE: Bool = false
|
||||
|
||||
let options: NSDictionary = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: true]
|
||||
let accessEnabled = AXIsProcessTrustedWithOptions(options)
|
||||
if accessEnabled {
|
||||
diskE = true
|
||||
return accessEnabled
|
||||
} else {
|
||||
diskE = false
|
||||
return false
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// Check if symlink
|
||||
func isSymlink(atPath path: URL) -> Bool {
|
||||
do {
|
||||
let _ = try path.checkResourceIsReachable()
|
||||
let resourceValues = try path.resourceValues(forKeys: [.isSymbolicLinkKey])
|
||||
return resourceValues.isSymbolicLink == true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Open trash folder
|
||||
func openTrash() {
|
||||
if let trashURL = try? FileManager.default.url(for: .trashDirectory, in: .userDomainMask, appropriateFor: nil, create: false) {
|
||||
NSWorkspace.shared.open(trashURL)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if restricted app
|
||||
func isRestricted(atPath path: URL) -> Bool {
|
||||
if path.path.contains("Safari") || path.path.contains(Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String ?? "") {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Convert icon to png so colors render correctly
|
||||
func convertICNSToPNG(icon: NSImage, size: NSSize) -> NSImage? {
|
||||
// Resize the icon to the specified size
|
||||
let resizedIcon = NSImage(size: size)
|
||||
resizedIcon.lockFocus()
|
||||
icon.draw(in: NSRect(x: 0, y: 0, width: size.width, height: size.height))
|
||||
resizedIcon.unlockFocus()
|
||||
|
||||
// Convert the resized icon to PNG format
|
||||
if let resizedImageData = resizedIcon.tiffRepresentation,
|
||||
let resizedBitmap = NSBitmapImageRep(data: resizedImageData),
|
||||
let pngData = resizedBitmap.representation(using: .png, properties: [:]) {
|
||||
return NSImage(data: pngData)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get icon for files and folders
|
||||
func getIconForFileOrFolder(atPath path: URL) -> Image? {
|
||||
let icon = NSWorkspace.shared.icon(forFile: path.path)
|
||||
let nsImage = icon
|
||||
return Image(nsImage: nsImage)
|
||||
}
|
||||
|
||||
|
||||
// Relaunch app
|
||||
func relaunchApp(afterDelay seconds: TimeInterval = 0.5) -> Never {
|
||||
let task = Process()
|
||||
task.launchPath = "/bin/sh"
|
||||
task.arguments = ["-c", "sleep \(seconds); open \"\(Bundle.main.bundlePath)\""]
|
||||
task.launch()
|
||||
|
||||
NSApp.terminate(nil)
|
||||
exit(0)
|
||||
}
|
||||
|
||||
|
||||
// --- Extend Int to convert hours to seconds ---
|
||||
extension Int {
|
||||
var daysToSeconds: Double {
|
||||
return Double(self) * 24 * 60 * 60
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// --- Extend String to remove periods, spaces and lowercase the string
|
||||
extension String {
|
||||
func pearFormat() -> String {
|
||||
return self.replacingOccurrences(of: ".", with: "").replacingOccurrences(of: " ", with: "").lowercased()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// --- Trash Relationship ---
|
||||
extension FileManager {
|
||||
public func isInTrash(_ file: URL) -> Bool {
|
||||
var relationship: URLRelationship = .other
|
||||
try? getRelationship(&relationship, of: .trashDirectory, in: .userDomainMask, toItemAt: file)
|
||||
return relationship == .contains
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// --- Gradient ---
|
||||
func schemeGradient(for colorScheme: ColorScheme) -> LinearGradient {
|
||||
return LinearGradient(gradient: Gradient(colors: [.pink, .orange]), startPoint: .leading, endPoint: .trailing)
|
||||
// switch colorScheme {
|
||||
// case .light:
|
||||
// return LinearGradient(gradient: Gradient(colors: [Color("AccentColor")]), startPoint: .leading, endPoint: .trailing)
|
||||
// case .dark:
|
||||
// return LinearGradient(gradient: Gradient(colors: [.pink, .orange]), startPoint: .leading, endPoint: .trailing)
|
||||
// @unknown default:
|
||||
// return LinearGradient(gradient: Gradient(colors: [Color("AccentColor")]), startPoint: .leading, endPoint: .trailing)
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Get total size of folders and files using DU cli command
|
||||
func totalSizeOnDisk(for paths: [URL]) -> String? {
|
||||
var totalSize = 0
|
||||
|
||||
let process = Process()
|
||||
process.launchPath = "/usr/bin/du"
|
||||
process.arguments = ["-sk"] + paths.map { (url: URL) -> String in
|
||||
return url.path
|
||||
}
|
||||
let pipe = Pipe()
|
||||
process.standardOutput = pipe
|
||||
|
||||
try? process.run()
|
||||
process.waitUntilExit()
|
||||
|
||||
let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
||||
if let output = String(data: data, encoding: .utf8) {
|
||||
let lines = output.components(separatedBy: .newlines)
|
||||
for line in lines {
|
||||
let components = line.components(separatedBy: "\t")
|
||||
if let sizeString = components.first, let size = Int(sizeString) {
|
||||
totalSize += size * 1024 // Convert the size from kilobytes to bytes
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let byteCountFormatter = ByteCountFormatter()
|
||||
byteCountFormatter.countStyle = .file
|
||||
byteCountFormatter.allowedUnits = [.useAll]
|
||||
return byteCountFormatter.string(fromByteCount: Int64(totalSize))
|
||||
}
|
||||
|
||||
func totalSizeOnDisk(for path: URL) -> String? {
|
||||
return totalSizeOnDisk(for: [path])
|
||||
}
|
||||
|
||||
|
||||
// Get total size of folders and files using straight swift
|
||||
extension URL {
|
||||
func totalAllocatedSize(includingSubfolders: Bool = false) throws -> Int? {
|
||||
return try [self].totalAllocatedSize(includingSubfolders: includingSubfolders)
|
||||
}
|
||||
|
||||
func totalSizeOnDisk(includingSubfolders: Bool = false) throws -> String? {
|
||||
return try [self].totalSizeOnDisk(includingSubfolders: includingSubfolders)
|
||||
}
|
||||
}
|
||||
|
||||
extension Array where Element == URL {
|
||||
func totalAllocatedSize(includingSubfolders: Bool = false) throws -> Int? {
|
||||
var totalSize = 0
|
||||
|
||||
for path in self {
|
||||
// if path.absoluteString.contains(".app") {
|
||||
let resourceValues = try path.resourceValues(forKeys: [.isDirectoryKey, .isPackageKey, .totalFileAllocatedSizeKey])
|
||||
|
||||
if resourceValues.isDirectory == true || path.pathExtension == "app" {
|
||||
if includingSubfolders {
|
||||
let filePaths = FileManager.default.subpaths(atPath: path.path) ?? []
|
||||
for filePath in filePaths {
|
||||
let fileUrl = path.appendingPathComponent(filePath)
|
||||
let fileAttributes = try FileManager.default.attributesOfItem(atPath: fileUrl.path)
|
||||
let fileSize = fileAttributes[.size] as? Int64 ?? 0
|
||||
totalSize += Int(fileSize)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
totalSize += resourceValues.totalFileAllocatedSize ?? 0
|
||||
}
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
return totalSize
|
||||
}
|
||||
|
||||
func totalSizeOnDisk(includingSubfolders: Bool = false) throws -> String? {
|
||||
if let totalSize = try self.totalAllocatedSize(includingSubfolders: includingSubfolders) {
|
||||
let byteCountFormatter = ByteCountFormatter()
|
||||
byteCountFormatter.countStyle = .file
|
||||
byteCountFormatter.allowedUnits = [.useBytes, .useKB, .useMB, .useTB]
|
||||
return byteCountFormatter.string(fromByteCount: Int64(totalSize))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Alerts
|
||||
func presentAlert(appState: AppState) -> Alert {
|
||||
|
||||
switch appState.alertType {
|
||||
case .update:
|
||||
return Alert(title: Text("Update Available 🥳"), message: Text("You may choose to install the update now, otherwise you may start the update later from Settings"), primaryButton: .default(Text("Install")) {
|
||||
downloadUpdate(appState: appState)
|
||||
appState.alertType = .off
|
||||
}, secondaryButton: .cancel())
|
||||
case .no_update:
|
||||
return Alert(title: Text("No Updates 😌"), message: Text("The application is on the latest release available"), primaryButton: .cancel(Text("Okay")), secondaryButton: .default(Text("Force Update")) {
|
||||
downloadUpdate(appState: appState)
|
||||
appState.alertType = .off
|
||||
})
|
||||
case .diskAccess:
|
||||
return Alert(title: Text("Permissions"), message: Text("This application requires Full Disk and Accessibility permissions. Once you grant full disk, don't restart the app yet. Wait a second for a second prompt for accessibility and then restart"), primaryButton: .default(Text("Allow in Settings")) {
|
||||
if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles") {
|
||||
NSWorkspace.shared.open(url)
|
||||
}
|
||||
appState.alertType = .off
|
||||
}, secondaryButton: .cancel(Text("Later")))
|
||||
// case .eventsAccess:
|
||||
// return Alert(title: Text("Accessibility Permission"), message: Text("This application requires Accessibility permission"), primaryButton: .default(Text("Allow in Settings")) {
|
||||
// if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility") {
|
||||
// NSWorkspace.shared.open(url)
|
||||
// }
|
||||
// appState.alertType = .off
|
||||
// }, secondaryButton: .cancel(Text("Later")))
|
||||
case .restartApp:
|
||||
return Alert(title: Text("Update Completed!"), message: Text("The application has been updated to the latest version, would you like to restart now?"), primaryButton: .default(Text("Restart")), secondaryButton: .cancel(Text("Later")) {
|
||||
appState.alertType = .off
|
||||
relaunchApp(afterDelay: 1)
|
||||
})
|
||||
case .off:
|
||||
return Alert(title: Text(""))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// --- Create Application Support folder if it doesn't exist ---
|
||||
func ensureApplicationSupportFolderExists(appState: AppState) {
|
||||
let fileManager = FileManager.default
|
||||
let supportURL = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!.appendingPathComponent("Pearcleaner")
|
||||
|
||||
// Check to make sure Application Support/Support Admin folder exists
|
||||
if !fileManager.fileExists(atPath: supportURL.path) {
|
||||
try! fileManager.createDirectory(at: supportURL, withIntermediateDirectories: true)
|
||||
print("Created Application Support/Pearcleaner folder")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
func launchctl(load: Bool) {
|
||||
let cmd = load ? "load" : "unload"
|
||||
if let plistPath = Bundle.main.path(forResource: "com.alienator88.PearcleanerSentinel", ofType: "plist") {
|
||||
var plistContent = try! String(contentsOfFile: plistPath)
|
||||
let executableURL = Bundle.main.bundleURL.appendingPathComponent("Contents/MacOS/PearcleanerSentinel")
|
||||
|
||||
// Replace the placeholder with the actual executable path
|
||||
plistContent = plistContent.replacingOccurrences(of: "__EXECUTABLE_PATH__", with: executableURL.path)
|
||||
|
||||
// Create a temporary plist file with the updated content
|
||||
let temporaryPlistURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("com.alienator88.PearcleanerSentinel.plist")
|
||||
|
||||
do {
|
||||
try plistContent.write(to: temporaryPlistURL, atomically: false, encoding: .utf8)
|
||||
} catch {
|
||||
print("Error writing the temporary plist file: \(error)")
|
||||
return
|
||||
}
|
||||
let task = Process()
|
||||
task.launchPath = "/bin/launchctl"
|
||||
task.arguments = [cmd, "-w", temporaryPlistURL.path]
|
||||
|
||||
let pipe = Pipe()
|
||||
task.standardOutput = pipe
|
||||
task.standardError = pipe
|
||||
|
||||
task.launch()
|
||||
|
||||
// let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
||||
// if let output = String(data: data, encoding: .utf8) {
|
||||
// print("Output: \(output)")
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
//func unloadAgent() {
|
||||
// if let plistPath = Bundle.main.path(forResource: "com.alienator88.PearcleanerMonitor", ofType: "plist") {
|
||||
// var plistContent = try! String(contentsOfFile: plistPath)
|
||||
// let executableURL = Bundle.main.bundleURL.appendingPathComponent("Contents/MacOS/PearcleanerMonitor")
|
||||
//
|
||||
// // Replace the placeholder with the actual executable path
|
||||
// plistContent = plistContent.replacingOccurrences(of: "__EXECUTABLE_PATH__", with: executableURL.path)
|
||||
//
|
||||
// // Create a temporary plist file with the updated content
|
||||
// let temporaryPlistURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("com.alienator88.PearcleanerMonitor.plist")
|
||||
//
|
||||
// do {
|
||||
// try plistContent.write(to: temporaryPlistURL, atomically: false, encoding: .utf8)
|
||||
// } catch {
|
||||
// print("Error writing the temporary plist file: \(error)")
|
||||
// return
|
||||
// }
|
||||
// let task = Process()
|
||||
// task.launchPath = "/bin/launchctl"
|
||||
// task.arguments = ["unload", "-w", temporaryPlistURL.path]
|
||||
//
|
||||
// let pipe = Pipe()
|
||||
// task.standardOutput = pipe
|
||||
// task.standardError = pipe
|
||||
//
|
||||
// task.launch()
|
||||
//
|
||||
//// let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
||||
//// if let output = String(data: data, encoding: .utf8) {
|
||||
//// print("Output: \(output)")
|
||||
//// }
|
||||
// }
|
||||
//}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
//import FileWatcher
|
||||
//
|
||||
//// MARK: https://github.com/eonist/FileWatcher
|
||||
//
|
||||
//func fileW() {
|
||||
// let filewatcher = FileWatcher([NSString(string: "~/.Trash").expandingTildeInPath])
|
||||
// filewatcher.queue = DispatchQueue.global()
|
||||
// filewatcher.callback = { event in
|
||||
// print("Something happened here: " + event.path)
|
||||
// }
|
||||
//
|
||||
// filewatcher.start()
|
||||
//}
|
||||
|
||||
|
||||
|
||||
//extension Array where Element == URL {
|
||||
// // DU shell way of getting sizes
|
||||
// func totalAllocatedSize2(includingSubfolders: Bool = false) throws -> Int? {
|
||||
// var totalSize = 0
|
||||
//
|
||||
// for path in self {
|
||||
// let process = Process()
|
||||
// process.launchPath = "/usr/bin/du"
|
||||
// process.arguments = ["-sk", path.path]
|
||||
//
|
||||
// let pipe = Pipe()
|
||||
// process.standardOutput = pipe
|
||||
//
|
||||
// try process.run()
|
||||
// process.waitUntilExit()
|
||||
//
|
||||
// let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
||||
// if let output = String(data: data, encoding: .utf8) {
|
||||
// let sizeString = output.components(separatedBy: "\t").first ?? ""
|
||||
// if let size = Int(sizeString) {
|
||||
// totalSize += size * 1024
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// return totalSize
|
||||
// }
|
||||
//
|
||||
// func totalAllocatedSize(includingSubfolders: Bool = false) throws -> Int? {
|
||||
// var totalSize = 0
|
||||
//
|
||||
// for path in self {
|
||||
// if includingSubfolders {
|
||||
// guard
|
||||
// let urls = FileManager.default.enumerator(at: path, includingPropertiesForKeys: nil)?.allObjects as? [URL] else { return nil }
|
||||
// let pathSize = try urls.lazy.reduce(0) {
|
||||
// (try $1.resourceValues(forKeys: [.totalFileAllocatedSizeKey]).totalFileAllocatedSize ?? 0) + $0
|
||||
// }
|
||||
// totalSize += pathSize
|
||||
// } else {
|
||||
// let pathSize = try FileManager.default.contentsOfDirectory(at: path, includingPropertiesForKeys: nil).lazy.reduce(0) {
|
||||
// (try $1.resourceValues(forKeys: [.totalFileAllocatedSizeKey])
|
||||
// .totalFileAllocatedSize ?? 0) + $0
|
||||
// }
|
||||
// totalSize += pathSize
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return totalSize
|
||||
// }
|
||||
//
|
||||
// func totalSizeOnDisk(includingSubfolders: Bool = false) throws -> String? {
|
||||
// if let totalSize = try self.totalAllocatedSize(includingSubfolders: includingSubfolders) {
|
||||
// let byteCountFormatter = ByteCountFormatter()
|
||||
// byteCountFormatter.countStyle = .file
|
||||
// byteCountFormatter.allowedUnits = [.useBytes, .useKB, .useMB, .useTB]
|
||||
// return byteCountFormatter.string(fromByteCount: Int64(totalSize))
|
||||
// }
|
||||
// return nil
|
||||
// }
|
||||
//}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
//func isSymbolicLink(atPath path: URL) -> Bool {
|
||||
//// let fileManager = FileManager.default
|
||||
//
|
||||
// do {
|
||||
//// let path = "/Applications/Safari.app"
|
||||
//// let url = URL(fileURLWithPath: path)
|
||||
// let destinationPath = path.resolvingSymlinksInPath().path
|
||||
// print("The symlink at \(path) points to \(destinationPath)")
|
||||
// return true
|
||||
// } catch {
|
||||
// print("An error occurred: \(error)")
|
||||
// return false
|
||||
// }
|
||||
// return false
|
||||
//// var isDirectory = false
|
||||
//// let exists = FileManager.default.fileExists(atPath: path, isDirectory: isDirectory)
|
||||
//// if exists && isDirectory {
|
||||
//// do {
|
||||
//// let _ = try FileManager.default.destinationOfSymbolicLink(atPath: path)
|
||||
//// return true
|
||||
//// } catch {
|
||||
//// return false
|
||||
//// }
|
||||
//// }
|
||||
//// return false
|
||||
//}
|
||||
@@ -0,0 +1,92 @@
|
||||
//
|
||||
// PearcleanerApp.swift
|
||||
// Pearcleaner
|
||||
//
|
||||
// Created by Alin Lupascu on 10/31/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import AppKit
|
||||
|
||||
@main
|
||||
struct PearcleanerApp: App {
|
||||
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
|
||||
@StateObject var appState = AppState()
|
||||
@AppStorage("settings.updater.updateTimeframe") private var updateTimeframe: Int = 1
|
||||
@AppStorage("settings.permissions.disk") private var diskP: Bool = false
|
||||
@AppStorage("settings.permissions.events") private var diskE: Bool = false
|
||||
@AppStorage("displayMode") var displayMode: DisplayMode = .dark
|
||||
@State private var search = ""
|
||||
|
||||
var body: some Scene {
|
||||
|
||||
WindowGroup {
|
||||
AppListView(search: $search)
|
||||
.environmentObject(appState)
|
||||
.frame(minWidth: 1020, minHeight: 700)
|
||||
.preferredColorScheme(displayMode.colorScheme)
|
||||
.alert(isPresented: $appState.showAlert) { presentAlert(appState: appState) }
|
||||
.handlesExternalEvents(preferring: Set(arrayLiteral: "pear"), allowing: Set(arrayLiteral: "*"))
|
||||
.onAppear {
|
||||
NSWindow.allowsAutomaticWindowTabbing = false
|
||||
|
||||
// Get Apps
|
||||
let sortedApps = getSortedApps()
|
||||
appState.sortedApps.userApps = sortedApps.userApps
|
||||
appState.sortedApps.systemApps = sortedApps.systemApps
|
||||
|
||||
Task {
|
||||
|
||||
#if !DEBUG
|
||||
|
||||
// Make sure App Support folder exists in the future if needed for storage
|
||||
// ensureApplicationSupportFolderExists(appState: appState)
|
||||
|
||||
// Check for updates
|
||||
if diskP && diskE {
|
||||
loadGithubReleases(appState: appState)
|
||||
}
|
||||
|
||||
// Check for disk/accessibility permissions
|
||||
_ = checkAndRequestFullDiskAccess(appState: appState)
|
||||
|
||||
|
||||
// TIMERS ////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Check for app updates every 8 hours or whatever user saved setting. Also refresh autosuggestion list
|
||||
let updateSeconds = updateTimeframe.daysToSeconds
|
||||
_ = Timer.scheduledTimer(withTimeInterval: updateSeconds, repeats: true) { _ in
|
||||
DispatchQueue.main.async {
|
||||
loadGithubReleases(appState: appState)
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
.windowStyle(.hiddenTitleBar)
|
||||
.windowToolbarStyle(.unified)
|
||||
.windowResizability(.contentMinSize)
|
||||
.commands {
|
||||
AppCommands(appState: appState)
|
||||
CommandGroup(replacing: .newItem, addition: { })
|
||||
}
|
||||
|
||||
Settings {
|
||||
SettingsView()
|
||||
.environmentObject(appState)
|
||||
.toolbarBackground(.clear)
|
||||
.preferredColorScheme(displayMode.colorScheme)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "display-p3",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x59",
|
||||
"green" : "0x48",
|
||||
"red" : "0xFF"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "display-p3",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x59",
|
||||
"green" : "0x48",
|
||||
"red" : "0xFF"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "icon_16x16.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "16x16"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_16x16@2x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "16x16"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_32x32.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "32x32"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_32x32@2x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "32x32"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_128x128.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "128x128"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_128x128@2x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "128x128"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_256x256.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "256x256"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_256x256@2x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "256x256"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_512x512.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "512x512"
|
||||
},
|
||||
{
|
||||
"filename" : "icon_512x512@2x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "512x512"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 662 B |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 94 KiB |
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "logo_combo.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 20 KiB |
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "display-p3",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x00",
|
||||
"green" : "0x00",
|
||||
"red" : "0x00"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xFF",
|
||||
"green" : "0xFF",
|
||||
"red" : "0xFE"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>com.alienator88.Pearcleaner</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>pear</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.automation.apple-events</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-only</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>com.alienator88.PearcleanerSentinel</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>__EXECUTABLE_PATH__</string>
|
||||
</array>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,172 @@
|
||||
//
|
||||
// About.swift
|
||||
// Pearcleaner
|
||||
//
|
||||
// Created by Alin Lupascu on 11/5/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct AboutSettingsTab: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
|
||||
var body: some View {
|
||||
|
||||
VStack {
|
||||
|
||||
HStack(alignment: .center) {
|
||||
VStack(alignment: .center, spacing: 10){
|
||||
Spacer()
|
||||
|
||||
Image(nsImage: NSApp.applicationIconImage)
|
||||
.padding()
|
||||
// .padding(.bottom, 5)
|
||||
|
||||
HStack(alignment: .center, spacing: 20) {
|
||||
Button("") {
|
||||
loadGithubReleases(appState: appState, manual: true)
|
||||
}
|
||||
.buttonStyle(SimpleButtonStyle(icon: "cloud", help: "Update application", color: Color("AccentColor")))
|
||||
|
||||
Button("") {
|
||||
NSWorkspace.shared.open(URL(string: "https://github.com/ghosted-labs/Pearcleaner")!)
|
||||
}
|
||||
.buttonStyle(SimpleButtonStyle(icon: "paperplane", help: "View repository", color: Color("AccentColor")))
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
|
||||
}
|
||||
// .padding()
|
||||
// .padding(.top, 10)
|
||||
.frame(width: 250)
|
||||
|
||||
VStack(alignment: .center, spacing: 20) {
|
||||
HStack(alignment: .firstTextBaseline) {
|
||||
Text(Bundle.main.name)
|
||||
.font(.title)
|
||||
.bold()
|
||||
Spacer()
|
||||
HStack {
|
||||
Text("v\(Bundle.main.version)")
|
||||
Text(" (build \(Bundle.main.buildVersion))")
|
||||
}
|
||||
|
||||
}
|
||||
Divider()
|
||||
.padding(.top, -8)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
|
||||
HStack{
|
||||
Image(systemName: "applescript.fill")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 16, height: 16)
|
||||
|
||||
VStack(alignment: .leading){
|
||||
Text("Privacy Guides")
|
||||
Text("Inspired by open-source appcleaner script from Sun Knudsen").font(.footnote).foregroundStyle(.gray)
|
||||
|
||||
}
|
||||
Spacer()
|
||||
|
||||
Button("") {
|
||||
NSWorkspace.shared.open(URL(string: "https://sunknudsen.com/privacy-guides/how-to-clean-uninstall-macos-apps-using-appcleaner-open-source-alternative")!)
|
||||
}
|
||||
.buttonStyle(SimpleButtonStyle(icon: "paperplane", help: "View", color: Color("AccentColor")))
|
||||
|
||||
}
|
||||
.padding()
|
||||
.background(Color("mode").opacity(0.05))
|
||||
.cornerRadius(8)
|
||||
|
||||
|
||||
HStack{
|
||||
Image(systemName: "trash.fill")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 16, height: 16)
|
||||
|
||||
VStack(alignment: .leading){
|
||||
Text("AppCleaner")
|
||||
Text("Inspired by AppCleaner from Freemacsoft").font(.footnote).foregroundStyle(.gray)
|
||||
|
||||
}
|
||||
Spacer()
|
||||
Button(""){
|
||||
NSWorkspace.shared.open(URL(string: "https://freemacsoft.net/appcleaner/")!)
|
||||
}
|
||||
.buttonStyle(SimpleButtonStyle(icon: "paperplane", help: "View", color: Color("AccentColor")))
|
||||
|
||||
}
|
||||
.padding()
|
||||
.background(Color("mode").opacity(0.05))
|
||||
.cornerRadius(8)
|
||||
|
||||
|
||||
// HStack{
|
||||
// Image(systemName: "paintbrush.fill")
|
||||
// .resizable()
|
||||
// .aspectRatio(contentMode: .fit)
|
||||
// .frame(width: 16, height: 16)
|
||||
// VStack(alignment: .leading){
|
||||
// Text("Icon")
|
||||
// Text("Created by Oviotti on DeviantArt").font(.footnote).foregroundStyle(.gray)
|
||||
// }
|
||||
// Spacer()
|
||||
//
|
||||
// Button("") {
|
||||
// NSWorkspace.shared.open(URL(string: "https://www.deviantart.com/oviotti/art/AppCleaner-for-macOS-632260251")!)
|
||||
// }
|
||||
// .buttonStyle(SimpleButtonStyle(icon: "paperplane", help: "View", color: Color("AccentColor")))
|
||||
//
|
||||
// }
|
||||
// .padding()
|
||||
// .background(Color("mode").opacity(0.05))
|
||||
// .cornerRadius(8)
|
||||
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
}
|
||||
.padding(.trailing)
|
||||
// .padding()
|
||||
// .padding(.top, 30)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Text("Made with ❤️ by Alin Lupascu").font(.footnote).padding(.bottom)
|
||||
}
|
||||
.padding(20)
|
||||
.frame(width: 750, height: 325)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
extension Bundle {
|
||||
|
||||
var name: String {
|
||||
func string(for key: String) -> String? {
|
||||
object(forInfoDictionaryKey: key) as? String
|
||||
}
|
||||
return string(for: "CFBundleDisplayName")
|
||||
?? string(for: "CFBundleName")
|
||||
?? "N/A"
|
||||
}
|
||||
|
||||
var version: String {
|
||||
infoDictionary?["CFBundleShortVersionString"] as? String ?? "N/A"
|
||||
}
|
||||
|
||||
var buildVersion: String {
|
||||
infoDictionary?["CFBundleVersion"] as? String ?? "N/A"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
//
|
||||
// General.swift
|
||||
// Pearcleaner
|
||||
//
|
||||
// Created by Alin Lupascu on 11/5/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct GeneralSettingsTab: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
@AppStorage("settings.general.glass") private var glass: Bool = true
|
||||
// @AppStorage("settings.general.ants") private var ants: Bool = false
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
VStack {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
Text("Transparency").font(.title2)
|
||||
Text("Toggles the sidebar material on the app list drawer")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.gray)
|
||||
}
|
||||
Spacer()
|
||||
Toggle(isOn: $glass, label: {
|
||||
})
|
||||
.toggleStyle(.switch)
|
||||
}
|
||||
.padding()
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.fill(Color("mode").opacity(0.05))
|
||||
)
|
||||
|
||||
// HStack {
|
||||
// VStack(alignment: .leading, spacing: 5) {
|
||||
// Text("Animation").font(.title2)
|
||||
// Text("Toggles the marching ants animation on the drop zone")
|
||||
// .font(.footnote)
|
||||
// .foregroundStyle(.gray)
|
||||
// }
|
||||
// Spacer()
|
||||
// Toggle(isOn: $ants, label: {
|
||||
// })
|
||||
// .toggleStyle(.switch)
|
||||
// }
|
||||
// .padding()
|
||||
// .background(
|
||||
// RoundedRectangle(cornerRadius: 8)
|
||||
// .fill(Color("mode").opacity(0.05))
|
||||
// )
|
||||
|
||||
Spacer()
|
||||
}
|
||||
|
||||
}
|
||||
.padding(20)
|
||||
.frame(width: 400, height: 100)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
//
|
||||
// Permissions.swift
|
||||
// Pearcleaner
|
||||
//
|
||||
// Created by Alin Lupascu on 11/5/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct PermissionsSettingsTab: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
@State private var diskStatus: Bool = false
|
||||
@State private var accessStatus: Bool = false
|
||||
|
||||
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
|
||||
HStack(spacing: 15) {
|
||||
Image(systemName: "internaldrive")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 20)
|
||||
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
Text("Full Disk")
|
||||
Text(diskStatus ? "Permission granted" : "Permission not granted")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(diskStatus ? .green : .red)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Button("") {
|
||||
if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles") {
|
||||
NSWorkspace.shared.open(url)
|
||||
}
|
||||
}
|
||||
.buttonStyle(SimpleButtonStyle(icon: "paperplane", help: "View disk permission pane", color: Color("AccentColor")))
|
||||
}
|
||||
.padding()
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.fill(Color("mode").opacity(0.05))
|
||||
)
|
||||
|
||||
|
||||
HStack(spacing: 15) {
|
||||
Image(systemName: "accessibility")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 20)
|
||||
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
Text("Accessibility")
|
||||
Text(accessStatus ? "Permission granted" : "Permission not granted")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(accessStatus ? .green : .red)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Button("") {
|
||||
if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility") {
|
||||
NSWorkspace.shared.open(url)
|
||||
}
|
||||
}
|
||||
.buttonStyle(SimpleButtonStyle(icon: "paperplane", help: "View accessibility pane", color: Color("AccentColor")))
|
||||
}
|
||||
.padding()
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.fill(Color("mode").opacity(0.05))
|
||||
)
|
||||
|
||||
|
||||
|
||||
// HStack(alignment: .center) {
|
||||
// Spacer()
|
||||
// Button("") {
|
||||
// relaunchApp()
|
||||
// }
|
||||
// .buttonStyle(SimpleButtonStyle(icon: "restart", help: "Restart application", color: Color("AccentColor")))
|
||||
// Text("Restart")
|
||||
// Spacer()
|
||||
// }
|
||||
// .padding()
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
.padding(20)
|
||||
.frame(width: 400, height: 200)
|
||||
.onAppear {
|
||||
diskStatus = checkAndRequestFullDiskAccess(appState: appState, skipAlert: true)
|
||||
accessStatus = checkAndRequestAccessibilityAccess(appState: appState)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
//
|
||||
// Sentinel.swift
|
||||
// Pearcleaner
|
||||
//
|
||||
// Created by Alin Lupascu on 11/9/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct SentinelSettingsTab: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
@AppStorage("settings.sentinel.enable") private var sentinel: Bool = false
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
VStack {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
Text("Sentinel Monitor").font(.title2)
|
||||
Text("Detects when apps are moved to Trash for file removal")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.gray)
|
||||
}
|
||||
Spacer()
|
||||
Toggle(isOn: $sentinel, label: {
|
||||
})
|
||||
.toggleStyle(.switch)
|
||||
.onChange(of: sentinel) { newValue in
|
||||
if newValue {
|
||||
launchctl(load: true)
|
||||
} else {
|
||||
launchctl(load: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.fill(Color("mode").opacity(0.05))
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
.padding(20)
|
||||
.frame(width: 400, height: 100)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
//
|
||||
// SettingsWindow.swift
|
||||
// Pearcleaner
|
||||
//
|
||||
// Created by Alin Lupascu on 10/31/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SettingsView: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
|
||||
var body: some View {
|
||||
|
||||
TabView() {
|
||||
GeneralSettingsTab()
|
||||
.tabItem {
|
||||
Label(CurrentTabView.general.title, systemImage: "gear")
|
||||
}
|
||||
.tag(CurrentTabView.general)
|
||||
|
||||
PermissionsSettingsTab()
|
||||
.tabItem {
|
||||
Label(CurrentTabView.permissions.title, systemImage: "lock")
|
||||
}
|
||||
.tag(CurrentTabView.permissions)
|
||||
|
||||
SentinelSettingsTab()
|
||||
.tabItem {
|
||||
Label(CurrentTabView.sentinel.title, systemImage: "eye.circle")
|
||||
}
|
||||
.tag(CurrentTabView.sentinel)
|
||||
|
||||
UpdateSettingsTab()
|
||||
.tabItem {
|
||||
Label(CurrentTabView.update.title, systemImage: "tray.and.arrow.down")
|
||||
}
|
||||
.tag(CurrentTabView.update)
|
||||
|
||||
AboutSettingsTab()
|
||||
.tabItem {
|
||||
Label(CurrentTabView.about.title, systemImage: "info.circle")
|
||||
}
|
||||
.tag(CurrentTabView.about)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
//
|
||||
// Update.swift
|
||||
// Pearcleaner
|
||||
//
|
||||
// Created by Alin Lupascu on 11/5/23.
|
||||
//
|
||||
|
||||
|
||||
import SwiftUI
|
||||
import Foundation
|
||||
|
||||
struct Release: Codable {
|
||||
let id: Int
|
||||
let tag_name: String
|
||||
let body: String
|
||||
let assets: [Asset]
|
||||
}
|
||||
|
||||
struct Asset: Codable {
|
||||
let name: String
|
||||
let url: String
|
||||
let browser_download_url: String
|
||||
}
|
||||
|
||||
|
||||
struct UpdateSettingsTab: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
@State private var showAlert = false
|
||||
@State private var showDone = false
|
||||
@AppStorage("settings.updater.updateTimeframe") private var updateTimeframe: Int = 1
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
|
||||
HStack {
|
||||
Text("Check for updates every ") +
|
||||
Text("**\(updateTimeframe)**").foregroundColor(.red) +
|
||||
Text(updateTimeframe == 1 ? " day" : " days")
|
||||
Stepper(value: $updateTimeframe, in: 1...7) {
|
||||
Text("")
|
||||
}
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
VStack() {
|
||||
ForEach(appState.releases, id: \.id) { release in
|
||||
VStack(alignment: .leading) {
|
||||
LabeledDivider(label: "\(release.tag_name)")
|
||||
Text(release.body)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.frame(minHeight: 0, maxHeight: .infinity)
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
.padding()
|
||||
|
||||
Text("Showing last 3").opacity(0.5).font(.footnote)
|
||||
|
||||
|
||||
|
||||
HStack(alignment: .center, spacing: 5) {
|
||||
Spacer()
|
||||
Button("Check"){
|
||||
loadGithubReleases(appState: appState)
|
||||
}.buttonStyle(BorderedButtonStyle())
|
||||
|
||||
Button("Force Update"){
|
||||
loadGithubReleases(appState: appState, manual: true)
|
||||
}.buttonStyle(BorderedButtonStyle())
|
||||
|
||||
Button("GitHub"){
|
||||
NSWorkspace.shared.open(URL(string: "https://github.com/alienator88/Pearcleaner/releases")!)
|
||||
}.buttonStyle(BorderedButtonStyle())
|
||||
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
|
||||
|
||||
|
||||
}
|
||||
.padding(20)
|
||||
.frame(width: 500, height: 400)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
func loadGithubReleases(appState: AppState, manual: Bool = false) {
|
||||
let url = URL(string: "https://api.github.com/repos/alienator88/Pearcleaner/releases")!
|
||||
let request = URLRequest(url: url)
|
||||
// request.setValue("token \(ghToken)", forHTTPHeaderField: "Authorization")
|
||||
URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
if let data = data {
|
||||
if let decodedResponse = try? JSONDecoder().decode([Release].self, from: data) {
|
||||
DispatchQueue.main.async {
|
||||
let lastFiveReleases = Array(decodedResponse.prefix(3)) // Get only the last 3 recent releases
|
||||
appState.releases = lastFiveReleases
|
||||
checkForUpdate(appState: appState, manual: manual)
|
||||
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
print("Fetch failed: \(error?.localizedDescription ?? "Unknown error")")
|
||||
}.resume()
|
||||
}
|
||||
|
||||
func checkForUpdate(appState: AppState, manual: Bool = false) {
|
||||
guard let latestRelease = appState.releases.first else { return }
|
||||
let currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
|
||||
if latestRelease.tag_name > currentVersion ?? "" {
|
||||
appState.alertType = .update
|
||||
appState.showAlert = true
|
||||
} else {
|
||||
if manual {
|
||||
appState.alertType = .no_update
|
||||
appState.showAlert = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func downloadUpdate(appState: AppState) {
|
||||
guard let latestRelease = appState.releases.first else { return }
|
||||
guard let asset = latestRelease.assets.first else { return }
|
||||
guard let url = URL(string: asset.url) else { return }
|
||||
var request = URLRequest(url: url)
|
||||
// request.setValue("token \(ghToken)", forHTTPHeaderField: "Authorization")
|
||||
request.setValue("application/octet-stream", forHTTPHeaderField: "Accept")
|
||||
|
||||
let downloadTask = URLSession.shared.downloadTask(with: request) { localURL, urlResponse, error in
|
||||
guard let localURL = localURL else { return }
|
||||
|
||||
let fileManager = FileManager.default
|
||||
let destinationURL = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!.appendingPathComponent("Pearcleaner").appendingPathComponent("\(asset.name)")
|
||||
|
||||
do {
|
||||
if fileManager.fileExists(atPath: destinationURL.path) {
|
||||
try fileManager.removeItem(at: destinationURL)
|
||||
}
|
||||
try fileManager.moveItem(at: localURL, to: destinationURL)
|
||||
|
||||
UnzipAndReplace(DownloadedFileURL: destinationURL.path, appState: appState) {
|
||||
updateOnMain {
|
||||
// Restart App
|
||||
relaunchApp(afterDelay: 1)
|
||||
}
|
||||
}
|
||||
|
||||
} catch {
|
||||
print("Error moving downloaded file: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
downloadTask.resume()
|
||||
}
|
||||
|
||||
func UnzipAndReplace(DownloadedFileURL fileURL: String, appState: AppState, completion: @escaping () -> Void = {}) {
|
||||
let appDirectory = Bundle.main.bundleURL.deletingLastPathComponent().path
|
||||
let appBundle = Bundle.main.bundleURL.path
|
||||
let fileManager = FileManager.default
|
||||
|
||||
do {
|
||||
// Remove the old version of your app
|
||||
try fileManager.removeItem(atPath: appBundle)
|
||||
|
||||
// Unzip the downloaded update file to your app's bundle path
|
||||
let process = Process()
|
||||
process.executableURL = URL(fileURLWithPath: "/usr/bin/unzip")
|
||||
process.arguments = [fileURL, "-d", appDirectory]
|
||||
try process.run()
|
||||
process.waitUntilExit()
|
||||
|
||||
// After unzipping, remove the update file
|
||||
try fileManager.removeItem(atPath: fileURL)
|
||||
|
||||
} catch {
|
||||
print("Error updating the app: \(error)")
|
||||
}
|
||||
|
||||
completion()
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
//
|
||||
// AppListDetails.swift
|
||||
// Pearcleaner
|
||||
//
|
||||
// Created by Alin Lupascu on 11/10/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct AppListItems: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
@State private var isHovered = false
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
let appInfo: AppInfo
|
||||
var isSelected: Bool {
|
||||
appState.appInfo == appInfo
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
HStack(alignment: .center) {
|
||||
// if isHovered || isSelected {
|
||||
// Circle()
|
||||
// .fill(Color("AccentColor"))
|
||||
// .frame(width: 10)
|
||||
// .padding(5)
|
||||
// }
|
||||
if let appIcon = appInfo.appIcon {
|
||||
Image(nsImage: appIcon)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 30, height: 30)
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
Text(appInfo.appName)
|
||||
.font(.system(size: 12))
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
.padding(.horizontal, 5)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
Text(appInfo.appVersion)
|
||||
.font(.footnote)
|
||||
.foregroundStyle(Color("mode").opacity(0.5))
|
||||
|
||||
if isHovered || isSelected {
|
||||
RoundedRectangle(cornerRadius: 50)
|
||||
.fill(isSelected ? Color("AccentColor") : Color("mode").opacity(0.5))
|
||||
.frame(width: isSelected ? 4 : 2, height: 35)
|
||||
.padding(.leading, 5)
|
||||
.offset(x: 20)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// .background(
|
||||
// RoundedRectangle(cornerRadius: 6)
|
||||
// .fill(isSelected ? Color("AccentColor").opacity(0.15) : (isHovered ? Color("AccentColor").opacity(0.1) : Color.white.opacity(colorScheme == .dark ? 0.05 : 0.8)))
|
||||
// .shadow(color: .black.opacity(0.2), radius: 2, x: 0, y: 1)
|
||||
// .overlay(
|
||||
// RoundedRectangle(cornerRadius: 6)
|
||||
// .strokeBorder(Color("AccentColor").opacity(isHovered ? 0.7 : 0.1), lineWidth: 1)
|
||||
// )
|
||||
// )
|
||||
|
||||
// Rectangle()
|
||||
// .frame(maxWidth: isSelected ? .infinity : (isHovered ? .infinity : 30), maxHeight: 2)
|
||||
// .foregroundStyle(Color("AccentColor"))
|
||||
|
||||
}
|
||||
.padding(.horizontal, 5)
|
||||
.help(appInfo.appName)
|
||||
.onHover { hovering in
|
||||
withAnimation(Animation.easeIn(duration: 0.2)) {
|
||||
self.isHovered = hovering
|
||||
}
|
||||
}
|
||||
.onTapGesture {
|
||||
updateOnMain {
|
||||
appState.appInfo = appInfo
|
||||
findPathsForApp(appState: appState, appInfo: appState.appInfo)
|
||||
withAnimation(Animation.easeIn(duration: 0.4)) {
|
||||
appState.currentView = .files
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
//
|
||||
// AppListH.swift
|
||||
// Pearcleaner
|
||||
//
|
||||
// Created by Alin Lupascu on 11/5/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct AppListView: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
@AppStorage("settings.general.glass") private var glass: Bool = false
|
||||
@Binding var search: String
|
||||
@State private var showSys: Bool = true
|
||||
@State private var showUsr: Bool = true
|
||||
// @State private var sidebar: Bool = true
|
||||
@State private var reload: Bool = false
|
||||
|
||||
var filteredUserApps: [AppInfo] {
|
||||
if search.isEmpty {
|
||||
return appState.sortedApps.userApps
|
||||
} else {
|
||||
return appState.sortedApps.userApps.filter { $0.appName.localizedCaseInsensitiveContains(search) }
|
||||
}
|
||||
}
|
||||
|
||||
var filteredSystemApps: [AppInfo] {
|
||||
if search.isEmpty {
|
||||
return appState.sortedApps.systemApps
|
||||
} else {
|
||||
return appState.sortedApps.systemApps.filter { $0.appName.localizedCaseInsensitiveContains(search) }
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
|
||||
HStack(alignment: .center, spacing: 0) {
|
||||
// App List
|
||||
if appState.sidebar {
|
||||
ZStack {
|
||||
HStack(spacing: 0){
|
||||
|
||||
if reload {
|
||||
VStack {
|
||||
Spacer()
|
||||
ProgressView("Refreshing applications")
|
||||
Spacer()
|
||||
}
|
||||
.frame(width: 300)
|
||||
.padding(.vertical)
|
||||
} else {
|
||||
VStack(alignment: .center) {
|
||||
|
||||
VStack(alignment: .center, spacing: 20) {
|
||||
SearchBar(search: $search)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.top, 20)
|
||||
.padding(.bottom)
|
||||
|
||||
ScrollView {
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
|
||||
if filteredUserApps.count > 0 {
|
||||
VStack {
|
||||
Header(title: "User", count: filteredUserApps.count)
|
||||
ForEach(filteredUserApps, id: \.self) { appInfo in
|
||||
AppListItems(appInfo: appInfo)
|
||||
if appInfo != filteredUserApps.last {
|
||||
Divider().padding(.horizontal, 5)
|
||||
}
|
||||
}
|
||||
.padding(.bottom)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if filteredSystemApps.count > 0 {
|
||||
VStack {
|
||||
Header(title: "System", count: filteredSystemApps.count)
|
||||
ForEach(filteredSystemApps, id: \.self) { appInfo in
|
||||
AppListItems(appInfo: appInfo)
|
||||
if appInfo != filteredSystemApps.last {
|
||||
Divider().padding(.horizontal, 5)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
.padding(.horizontal)
|
||||
|
||||
}
|
||||
.scrollIndicators(.never)
|
||||
|
||||
}
|
||||
.frame(width: 300)
|
||||
.padding(.vertical)
|
||||
}
|
||||
|
||||
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
.background(glass ? GlassEffect(material: .sidebar, blendingMode: .behindWindow).edgesIgnoringSafeArea(.all) : nil)
|
||||
.transition(.move(edge: .leading))
|
||||
|
||||
Spacer()
|
||||
}
|
||||
|
||||
|
||||
// Details View
|
||||
VStack(spacing: 0) {
|
||||
if appState.currentView == .empty {
|
||||
TopBar(reload: $reload)
|
||||
AppDetailsEmptyView()
|
||||
} else if appState.currentView == .files {
|
||||
TopBar(reload: $reload)
|
||||
FilesView()
|
||||
.id(appState.appInfo.id)
|
||||
}
|
||||
}
|
||||
// .padding(.leading, appState.sidebar ? 0 : 10)
|
||||
.transition(.move(edge: .leading))
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
.onOpenURL(perform: { url in
|
||||
let deeplinkManager = DeeplinkManager()
|
||||
deeplinkManager.manage(url: url, appState: appState)
|
||||
})
|
||||
// MARK: Background for whole app
|
||||
// .background(Color("bg").opacity(1))
|
||||
// .background(VisualEffect(material: .sidebar, blendingMode: .behindWindow).edgesIgnoringSafeArea(.all))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
struct AppDetailsEmptyView: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@EnvironmentObject var appState: AppState
|
||||
@State private var animateGradient: Bool = false
|
||||
|
||||
var body: some View {
|
||||
VStack() {
|
||||
|
||||
Spacer()
|
||||
|
||||
DropTarget(appState: appState)
|
||||
|
||||
Spacer()
|
||||
|
||||
if appState.isReminderVisible {
|
||||
Text(" + Z to undo")
|
||||
.font(.title2)
|
||||
.foregroundStyle(Color("AccentColor").opacity(0.5))
|
||||
.fontWeight(.medium)
|
||||
.onAppear {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
|
||||
withAnimation {
|
||||
appState.isReminderVisible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
|
||||
Text("Drop an app above or select one from the list to begin")
|
||||
.font(.title3)
|
||||
.padding(.bottom, 25)
|
||||
.opacity(0.5)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct SearchBar: View {
|
||||
@Binding var search: String
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
TextField("Search", text: $search)
|
||||
.textFieldStyle(SimpleSearchStyle(icon: Image(systemName: "magnifyingglass"),trash: true, text: $search))
|
||||
}
|
||||
.frame(height: 30)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct Header: View {
|
||||
let title: String
|
||||
let count: Int
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Text(title).opacity(0.5)
|
||||
Spacer()
|
||||
Text("\(count)")
|
||||
.frame(minWidth: 30, minHeight: 15)
|
||||
.padding(2)
|
||||
.background(Color("mode").opacity(0.1))
|
||||
.clipShape(.capsule)
|
||||
// .clipShape(RoundedRectangle(cornerRadius: 6, style: .continuous))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
////
|
||||
//// ContentView.swift
|
||||
//// Pearcleaner
|
||||
////
|
||||
//// Created by Alin Lupascu on 10/31/23.
|
||||
////
|
||||
//
|
||||
//import SwiftUI
|
||||
//
|
||||
//struct ContentView: View {
|
||||
// @EnvironmentObject var appState: AppState
|
||||
// @State private var search = ""
|
||||
//
|
||||
// var body: some View {
|
||||
//
|
||||
// VStack(alignment: .leading) {
|
||||
//// AppListNav(search: $search)
|
||||
// AppListH(search: $search)
|
||||
//// HStack {
|
||||
//// Image(nsImage: NSApp.applicationIconImage)
|
||||
//// .imageScale(.large)
|
||||
////
|
||||
//// Text("Pearcleaner")
|
||||
//// .font(.custom("MontserratAlt1-Light", size: 30, relativeTo: .body))
|
||||
//// }
|
||||
//// .padding()
|
||||
//
|
||||
//
|
||||
//
|
||||
//// VStack() {
|
||||
////// AppListView(search: $search)
|
||||
////// AppListNav(search: $search)
|
||||
//// }
|
||||
// }
|
||||
//// .sheet(isPresented: $appState.showFiles, content: {
|
||||
//// FilesView()
|
||||
//// })
|
||||
// .alert(isPresented: $appState.showAlert) { presentAlert(appState: appState) }
|
||||
//
|
||||
// }
|
||||
//}
|
||||
//
|
||||
@@ -0,0 +1,168 @@
|
||||
//
|
||||
// DropTarget.swift
|
||||
// Pearcleaner
|
||||
//
|
||||
// Created by Alin Lupascu on 11/11/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
struct DropTarget: View, DropDelegate {
|
||||
@AppStorage("settings.general.ants") private var ants: Bool = false
|
||||
@ObservedObject var appState: AppState
|
||||
@State private var shouldFlash = false
|
||||
private var types: [UTType] = [.fileURL]
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@State private var phase: CGFloat = 0
|
||||
|
||||
public init(appState: AppState) {
|
||||
self.appState = appState
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
|
||||
FlashingBackground(shouldFlash: $shouldFlash)
|
||||
|
||||
HStack {
|
||||
Spacer()
|
||||
|
||||
HStack {
|
||||
Spacer()
|
||||
|
||||
ZStack {
|
||||
LinearGradient(gradient: Gradient(colors: [.pink, .orange]), startPoint: .leading, endPoint: .trailing)
|
||||
.mask(Image("logo")
|
||||
.resizable()
|
||||
.scaledToFit())
|
||||
}
|
||||
.frame(width: 500, height: 200)
|
||||
|
||||
Spacer()
|
||||
|
||||
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.strokeBorder(LinearGradient(gradient: Gradient(colors: [.pink, .orange]), startPoint: .leading, endPoint: .trailing), style: StrokeStyle(lineWidth: 3, dash: [10], dashPhase: phase))
|
||||
.onAppear {
|
||||
withAnimation(.linear.speed(0.5).repeat(while: ants, autoreverses: false)) {
|
||||
if ants {
|
||||
phase -= 20
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: ants) { newValue in
|
||||
withAnimation(.linear.speed(0.5).repeat(while: newValue, autoreverses: false)) {
|
||||
phase -= 20
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
.onDrop(of: types, delegate: self)
|
||||
.padding(20)
|
||||
|
||||
}
|
||||
|
||||
func dropEntered(info: DropInfo) {
|
||||
DispatchQueue.main.async {
|
||||
self.ants = true
|
||||
}
|
||||
}
|
||||
|
||||
func dropExited(info: DropInfo) {
|
||||
DispatchQueue.main.async {
|
||||
self.ants = false
|
||||
}
|
||||
}
|
||||
|
||||
func performDrop(info: DropInfo) -> Bool {
|
||||
|
||||
let itemProviders = info.itemProviders(for: [UTType.fileURL])
|
||||
|
||||
guard itemProviders.count == 1 else {
|
||||
return false
|
||||
}
|
||||
for itemProvider in itemProviders {
|
||||
itemProvider.loadItem(forTypeIdentifier: UTType.fileURL.identifier, options: nil) { item, error in
|
||||
guard let data = item as? Data else {
|
||||
dump(error)
|
||||
return
|
||||
}
|
||||
guard let url = URL(dataRepresentation: data, relativeTo: nil) else {
|
||||
print("Error: Not a valid URL.")
|
||||
return
|
||||
}
|
||||
if url.pathExtension == "app" {
|
||||
let appInfo = getAppInfo(atPath: url)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.ants = false
|
||||
appState.appInfo = appInfo!
|
||||
findPathsForApp(appState: appState, appInfo: appState.appInfo)
|
||||
appState.currentView = .files
|
||||
}
|
||||
} else {
|
||||
print("Error: Dropped file is not an application bundle")
|
||||
DispatchQueue.main.async {
|
||||
self.shouldFlash = true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self.ants = false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
struct FlashingBackground: View {
|
||||
@Binding var shouldFlash: Bool
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geometry in
|
||||
Rectangle()
|
||||
.fill(shouldFlash ? Color.red : Color.clear)
|
||||
.cornerRadius(8)
|
||||
.padding()
|
||||
.opacity(shouldFlash ? 0.5 : 0)
|
||||
.animation(.easeInOut(duration: 0.2).repeatCount(1, autoreverses: true), value: shouldFlash)
|
||||
.onChange(of: shouldFlash) { newValue in
|
||||
if newValue {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
|
||||
shouldFlash = false
|
||||
}
|
||||
}
|
||||
}
|
||||
.ignoresSafeArea()
|
||||
.frame(width: geometry.size.width, height: geometry.size.height)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
extension Animation {
|
||||
func `repeat`(while expression: Bool, autoreverses: Bool = true) -> Animation {
|
||||
if expression {
|
||||
return self.repeatForever(autoreverses: autoreverses)
|
||||
} else {
|
||||
return self
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,443 @@
|
||||
//
|
||||
// FilesView.swift
|
||||
// Pearcleaner
|
||||
//
|
||||
// Created by Alin Lupascu on 11/1/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct FilesView: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
@State private var appSize: String = ""
|
||||
@State private var showDetails: Bool = false
|
||||
@State private var showPop: Bool = false
|
||||
@State private var itemDetails: [(size: String, icon: Image?)] = []
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .center) {
|
||||
if !self.showDetails {
|
||||
VStack {
|
||||
Spacer()
|
||||
ProgressView("Finding application files..")
|
||||
Spacer()
|
||||
}
|
||||
.transition(.opacity)
|
||||
} else {
|
||||
VStack() {
|
||||
|
||||
// Main Group
|
||||
HStack() {
|
||||
//icon
|
||||
if let appIcon = appState.appInfo.appIcon {
|
||||
Image(nsImage: appIcon)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 100, height: 100)
|
||||
.padding(.leading)
|
||||
}
|
||||
//app title, size and items
|
||||
VStack(alignment: .center) {
|
||||
HStack(alignment: .center) {
|
||||
VStack(alignment: .leading, spacing: 5){
|
||||
HStack {
|
||||
Text("\(appState.appInfo.appName)").font(.title).fontWeight(.bold)
|
||||
// .foregroundStyle(Color("AccentColor"))
|
||||
Text("•").foregroundStyle(Color("AccentColor"))
|
||||
Text("\(appState.appInfo.appVersion)").font(.title3)
|
||||
// .foregroundStyle(.gray.opacity(0.8))
|
||||
}
|
||||
Text("\(appState.appInfo.bundleIdentifier)").font(.title3)
|
||||
.foregroundStyle((.gray.opacity(0.8)))
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Text("\(appSize)").font(.title).fontWeight(.bold)
|
||||
// .foregroundStyle(Color("AccentColor"))
|
||||
}
|
||||
|
||||
|
||||
Divider().padding(.bottom, 7)
|
||||
|
||||
HStack{
|
||||
Text("Application files and folders")
|
||||
.font(.callout)
|
||||
// .foregroundStyle(Color("AccentColor"))
|
||||
.opacity(0.8)
|
||||
Spacer()
|
||||
Text("\(appState.paths.count > 1 ? "\(appState.paths.count) items" : "\(appState.paths.count) item")").font(.callout)
|
||||
// .foregroundStyle(Color("AccentColor").opacity(0.7))
|
||||
.underline()
|
||||
}
|
||||
if appState.appInfo.appName.count < 5 {
|
||||
HStack(alignment: .center) {
|
||||
Button(""){
|
||||
showPop = true
|
||||
}
|
||||
.buttonStyle(SimpleButtonStyle(icon: "exclamationmark.triangle.fill", help: "Warning", color: .red))
|
||||
.popover(isPresented: $showPop, arrowEdge: .top) {
|
||||
VStack() {
|
||||
Text("Pearcleaner searches for files with a combination of bundle id and app name.\n**\(appState.appInfo.appName)** has a common or short app name so there might be unrelated files found.\nPlease check the list thoroughly before uninstalling.")
|
||||
.padding(20)
|
||||
.font(.title2)
|
||||
}
|
||||
|
||||
}
|
||||
Text("Read caution message")
|
||||
.foregroundStyle(Color.red)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(.top)
|
||||
.onTapGesture {
|
||||
showPop = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
.padding(20)
|
||||
}
|
||||
.padding([.horizontal, .bottom])
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
// .strokeBorder(Color("AccentColor"), lineWidth: 0.5)
|
||||
.fill(Color("mode").opacity(colorScheme == .dark ? 0.05 : 0.05))
|
||||
// .background(
|
||||
// RoundedRectangle(cornerRadius: 8)
|
||||
// .strokeBorder(Color("AccentColor").opacity(colorScheme == .dark ? 0.1 : 0.1), lineWidth: 1)
|
||||
// )
|
||||
)
|
||||
|
||||
|
||||
|
||||
ScrollView {
|
||||
VStack {
|
||||
ForEach(Array(zip(appState.paths, itemDetails)), id: \.0) { path, details in
|
||||
if let firstPath = appState.paths.first, path == firstPath {
|
||||
FileDetailsItem(size: details.size, icon: details.icon, path: path)
|
||||
.padding(.trailing)
|
||||
Divider().padding(.leading, 40).padding(.trailing)
|
||||
} else {
|
||||
VStack {
|
||||
FileDetailsItem(size: details.size, icon: details.icon, path: path)
|
||||
if path != appState.paths.last {
|
||||
Divider().padding(.leading, 40)
|
||||
}
|
||||
}
|
||||
.padding(.leading, 47).padding(.trailing)
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
|
||||
HStack() {
|
||||
Spacer()
|
||||
|
||||
// if appState.trashLaunch {
|
||||
// Button("Close") {
|
||||
// Task {
|
||||
// NSApp.terminate(nil)
|
||||
// exit(0)
|
||||
// }
|
||||
// }
|
||||
// .buttonStyle(WindowActionButton(action: .cancel))
|
||||
// }
|
||||
|
||||
|
||||
Button("Uninstall") {
|
||||
Task {
|
||||
updateOnMain {
|
||||
appState.appInfo = AppInfo.empty
|
||||
appState.currentView = .empty
|
||||
}
|
||||
let selectedItemsArray = Array(appState.selectedItems).filter { !$0.path.contains(".Trash") }
|
||||
killApp(appId: appState.appInfo.bundleIdentifier) {
|
||||
moveFilesToTrash(at: selectedItemsArray) {
|
||||
withAnimation {
|
||||
appState.isReminderVisible.toggle()
|
||||
}
|
||||
refreshAppList(appState.appInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.disabled(appState.selectedItems.isEmpty)
|
||||
.buttonStyle(WindowActionButton(action: .accept))
|
||||
}
|
||||
// .padding(.top)
|
||||
|
||||
}
|
||||
.transition(.opacity)
|
||||
.padding(20)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
.frame(minWidth: 700)
|
||||
.onAppear {
|
||||
Task {
|
||||
calculateFileDetails()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func calculateFileDetails() {
|
||||
if appState.paths.count != 0 {
|
||||
itemDetails = Array(repeating: (size: "", icon: nil), count: appState.paths.count)
|
||||
Task {
|
||||
for (index, path) in appState.paths.enumerated() {
|
||||
var size = ""
|
||||
var icon: Image? = nil
|
||||
|
||||
if let appSize = totalSizeOnDisk(for: path) {
|
||||
size = "\(appSize)"
|
||||
} else {
|
||||
print("Error calculating the total size on disk for item \(index).")
|
||||
}
|
||||
if let folderIcon = getIconForFileOrFolder(atPath: path) {
|
||||
icon = folderIcon
|
||||
}
|
||||
itemDetails[index] = (size: size, icon: icon)
|
||||
}
|
||||
if let appSize = totalSizeOnDisk(for: appState.paths) {
|
||||
self.appSize = "\(appSize)"
|
||||
self.showDetails = true
|
||||
} else {
|
||||
print("Error calculating the total size on disk.")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
calculateFileDetails()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func refreshAppList(_ appInfo: AppInfo) {
|
||||
let sortedApps = getSortedApps()
|
||||
appState.sortedApps.userApps = []
|
||||
appState.sortedApps.systemApps = []
|
||||
appState.sortedApps.userApps = sortedApps.userApps
|
||||
appState.sortedApps.systemApps = sortedApps.systemApps
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
struct FileDetailsItem: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
let size: String
|
||||
let icon: Image?
|
||||
let path: URL
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .center, spacing: 20) {
|
||||
Toggle("", isOn: Binding(
|
||||
get: { self.appState.selectedItems.contains(self.path) },
|
||||
set: { isChecked in
|
||||
if isChecked {
|
||||
self.appState.selectedItems.insert(self.path)
|
||||
if self.path == appState.appInfo.path {
|
||||
self.appState.paths.forEach { self.appState.selectedItems.insert($0) }
|
||||
}
|
||||
} else {
|
||||
self.appState.selectedItems.remove(self.path)
|
||||
if self.path == appState.appInfo.path {
|
||||
self.appState.selectedItems.forEach { self.appState.selectedItems.remove($0) }
|
||||
}
|
||||
}
|
||||
}
|
||||
))
|
||||
.disabled(self.path.path.contains(".Trash"))
|
||||
|
||||
if let appIcon = icon {
|
||||
appIcon
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 30, height: 30)
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
Text(path.lastPathComponent)
|
||||
.font(.title3)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
.help(path.lastPathComponent)
|
||||
Text(path.path)
|
||||
.font(.footnote)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
.opacity(0.5)
|
||||
// .foregroundStyle(Color("AccentColor"))
|
||||
.help(path.path)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
if size.isEmpty {
|
||||
ProgressView().controlSize(.small)
|
||||
} else {
|
||||
Text(size)
|
||||
}
|
||||
|
||||
|
||||
Button("") {
|
||||
NSWorkspace.shared.selectFile(path.path, inFileViewerRootedAtPath: path.deletingLastPathComponent().path)
|
||||
}
|
||||
.buttonStyle(SimpleButtonStyle(icon: "folder.fill", help: "Show in Finder", color: Color("AccentColor")))
|
||||
|
||||
}
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.fill(Color.white.opacity(0))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func writeLog(string: String) {
|
||||
let fileManager = FileManager.default
|
||||
let home = fileManager.homeDirectoryForCurrentUser.path
|
||||
let logFilePath = "\(home)/Downloads/log.txt"
|
||||
|
||||
// Check if the log file exists, and create it if it doesn't
|
||||
if !fileManager.fileExists(atPath: logFilePath) {
|
||||
if !fileManager.createFile(atPath: logFilePath, contents: nil, attributes: nil) {
|
||||
print("Failed to create the log file.")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
do {
|
||||
if let fileHandle = FileHandle(forWritingAtPath: logFilePath) {
|
||||
let ns = "\(string)\n"
|
||||
fileHandle.seekToEndOfFile()
|
||||
fileHandle.write(ns.data(using: .utf8)!)
|
||||
fileHandle.closeFile()
|
||||
} else {
|
||||
print("Error opening file for appending")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//struct FilesView: View {
|
||||
// @EnvironmentObject var appState: AppState
|
||||
// @State private var appSize: String = ""
|
||||
// @State private var showDetails: Bool = false
|
||||
// @State private var itemDetails: [(size: String, icon: Image?)] = []
|
||||
//
|
||||
// var body: some View {
|
||||
// VStack(alignment: .center) {
|
||||
// if !self.showDetails {
|
||||
// ProgressView("Finding application files..")
|
||||
// } else {
|
||||
// VStack() {
|
||||
// HStack() {
|
||||
// Text("\(appState.paths.count > 1 ? "\(appState.paths.count) files found" : "\(appState.paths.count) file found")")
|
||||
// Text("•")
|
||||
// Text("\(appSize)")
|
||||
// }
|
||||
// .padding()
|
||||
//
|
||||
// if appState.appInfo.appName.count < 5 {
|
||||
// Text("Short or common word app name, there might be unrelated files found")
|
||||
// .font(.footnote)
|
||||
// .foregroundStyle(.red)
|
||||
// }
|
||||
//
|
||||
// ScrollView {
|
||||
// ForEach(Array(zip(appState.paths, itemDetails)), id: \.0) { path, details in
|
||||
// FileDetailsItem(size: details.size, icon: details.icon, path: path)
|
||||
// }
|
||||
// }
|
||||
// .frame(width: 500)
|
||||
//
|
||||
// Button("Remove") {
|
||||
// Task {
|
||||
// updateOnMain {
|
||||
// appState.appInfo = AppInfo.empty
|
||||
// }
|
||||
// let selectedItemsArray = Array(appState.selectedItems)
|
||||
// killApp(appId: appState.appInfo.bundleIdentifier) {
|
||||
// moveFilesToTrash(at: selectedItemsArray) {
|
||||
// refreshAppList(appState.appInfo)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// .disabled(appState.selectedItems.isEmpty)
|
||||
// }
|
||||
// .padding()
|
||||
//
|
||||
// }
|
||||
//
|
||||
// }
|
||||
// .frame(minWidth: 500)
|
||||
// .onAppear {
|
||||
// Task {
|
||||
// // appState.appInfo = appInfo
|
||||
// // findPathsForApp(appState: appState, appInfo: appState.appInfo)
|
||||
// calculateFileDetails()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
// func calculateFileDetails() {
|
||||
// if appState.paths.count != 0 {
|
||||
// itemDetails = Array(repeating: (size: "", icon: nil), count: appState.paths.count)
|
||||
//
|
||||
// Task {
|
||||
// for (index, path) in appState.paths.enumerated() {
|
||||
// var size = ""
|
||||
// var icon: Image? = nil
|
||||
//
|
||||
// if let appSize = totalSizeOnDisk(for: path) {
|
||||
// size = "\(appSize)"
|
||||
// } else {
|
||||
// print("Error calculating the total size on disk for item \(index).")
|
||||
// }
|
||||
//
|
||||
// if let folderIcon = getIconForFileOrFolder(atPath: path) {
|
||||
// icon = folderIcon
|
||||
// }
|
||||
//
|
||||
// itemDetails[index] = (size: size, icon: icon)
|
||||
//
|
||||
//
|
||||
//
|
||||
// }
|
||||
//
|
||||
// if let appSize = totalSizeOnDisk(for: appState.paths) {
|
||||
// self.appSize = "\(appSize)"
|
||||
// // updateOnMain {
|
||||
// // appState.doneLoading = true
|
||||
// // }
|
||||
// self.showDetails = true
|
||||
// // appState.doneLoading = true
|
||||
// } else {
|
||||
// print("Error calculating the total size on disk.")
|
||||
// }
|
||||
// }
|
||||
// } else {
|
||||
// DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||
// calculateFileDetails()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// func refreshAppList(_ appInfo: AppInfo) {
|
||||
// let sortedApps = getSortedApps()
|
||||
// appState.sortedApps.userApps = []
|
||||
// appState.sortedApps.systemApps = []
|
||||
// appState.sortedApps.userApps = sortedApps.userApps
|
||||
// appState.sortedApps.systemApps = sortedApps.systemApps
|
||||
// }
|
||||
//}
|
||||
@@ -0,0 +1,104 @@
|
||||
//
|
||||
// TopBar.swift
|
||||
// Pearcleaner
|
||||
//
|
||||
// Created by Alin Lupascu on 11/10/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct TopBar: View {
|
||||
// @Binding var sidebar: Bool
|
||||
@Binding var reload: Bool
|
||||
@AppStorage("displayMode") var displayMode: DisplayMode = .system
|
||||
@AppStorage("settings.general.glass") private var glass: Bool = false
|
||||
@EnvironmentObject var appState: AppState
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .center, spacing: 20) {
|
||||
|
||||
HStack(alignment: .center, spacing: 5) {
|
||||
// Button("") {
|
||||
// withAnimation(.easeInOut(duration: 0.5)) {
|
||||
// appState.sidebar.toggle()
|
||||
// if appState.sidebar {
|
||||
// appState.winWidth += 300
|
||||
// } else {
|
||||
// appState.winWidth -= 300
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// .buttonStyle(SimpleButtonStyle(icon: "sidebar.left", help: "Toggle Sidebar", color: Color("AccentColor")))
|
||||
|
||||
|
||||
Button("") {
|
||||
withAnimation(.easeInOut(duration: 0.5)) {
|
||||
// Refresh Apps list
|
||||
reload.toggle()
|
||||
let sortedApps = getSortedApps()
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
appState.sortedApps.userApps = sortedApps.userApps
|
||||
appState.sortedApps.systemApps = sortedApps.systemApps
|
||||
reload.toggle()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
.buttonStyle(SimpleButtonStyle(icon: "arrow.circlepath", help: "Refresh app list", color: Color("AccentColor")))
|
||||
|
||||
Button("") {
|
||||
withAnimation(.easeInOut(duration: 0.5)) {
|
||||
openTrash()
|
||||
}
|
||||
}
|
||||
.buttonStyle(SimpleButtonStyle(icon: "trash", help: "Open Trash", color: Color("AccentColor")))
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
|
||||
|
||||
if appState.currentView != .empty {
|
||||
Button("") {
|
||||
withAnimation(.easeInOut(duration: 0.5)) {
|
||||
appState.currentView = .empty
|
||||
appState.appInfo = AppInfo.empty
|
||||
}
|
||||
}
|
||||
.buttonStyle(SimpleButtonStyle(icon: "house", help: "Home", color: Color("AccentColor")))
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
Spacer()
|
||||
|
||||
HStack(alignment: .center, spacing: 5) {
|
||||
Button("") {
|
||||
withAnimation(.easeInOut(duration: 0.5)) {
|
||||
glass.toggle()
|
||||
}
|
||||
}
|
||||
.buttonStyle(SimpleButtonStyle(icon: glass ? "circle.dashed" : "circle.dashed.inset.filled", help: "Toggle transparency", color: Color("AccentColor")))
|
||||
|
||||
Button("") {
|
||||
withAnimation(.easeInOut(duration: 0.5)) {
|
||||
if displayMode.colorScheme == .dark {
|
||||
displayMode.colorScheme = .light
|
||||
} else if displayMode.colorScheme == .light {
|
||||
displayMode.colorScheme = .dark
|
||||
} else {
|
||||
displayMode.colorScheme = .dark
|
||||
}
|
||||
}
|
||||
}
|
||||
.buttonStyle(SimpleButtonStyle(icon: displayMode.colorScheme == .dark ? "sun.max.fill" : "moon.fill", help: "Toggle appearance", color: Color("AccentColor")))
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.top, 30)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// TrashView.swift
|
||||
// Pearcleaner
|
||||
//
|
||||
// Created by Alin Lupascu on 11/11/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct TrashView: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
|
||||
var body: some View {
|
||||
VStack() {
|
||||
Text("Trash view my boi")
|
||||
}
|
||||
}
|
||||
}
|
||||