mirror of
https://github.com/exituser/Pearcleaner.git
synced 2026-09-17 07:19:04 +00:00
v1.3 and v1.4
This commit is contained in:
@@ -23,7 +23,7 @@ struct AppCommands: Commands {
|
||||
Button {
|
||||
loadGithubReleases(appState: appState, manual: true)
|
||||
} label: {
|
||||
Text("Updates")
|
||||
Text("Check for Updates")
|
||||
}
|
||||
.keyboardShortcut("u", modifiers: .command)
|
||||
|
||||
|
||||
@@ -22,8 +22,11 @@ class AppState: ObservableObject
|
||||
// @Published var currentMode = CurrentMode.regular
|
||||
@Published var showAlert: Bool = false
|
||||
@Published var sidebar: Bool = true
|
||||
@Published var showPopover: Bool = false
|
||||
@Published var isReminderVisible: Bool = false
|
||||
@Published var releases = [Release]()
|
||||
@Published var progressBar: (String, Double) = ("Ready", 0.0)
|
||||
|
||||
|
||||
//Window
|
||||
// @Published var winWidth: CGFloat = 1020
|
||||
@@ -35,7 +38,8 @@ class AppState: ObservableObject
|
||||
bundleIdentifier: "",
|
||||
appName: "",
|
||||
appVersion: "",
|
||||
appIcon: nil
|
||||
appIcon: nil,
|
||||
webApp: false
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -47,8 +51,9 @@ struct AppInfo: Identifiable, Hashable {
|
||||
let appName: String
|
||||
let appVersion: String
|
||||
let appIcon: NSImage?
|
||||
let webApp: Bool
|
||||
|
||||
static let empty = AppInfo(id: UUID(), path: URL(fileURLWithPath: ""), bundleIdentifier: "", appName: "", appVersion: "", appIcon: nil)
|
||||
static let empty = AppInfo(id: UUID(), path: URL(fileURLWithPath: ""), bundleIdentifier: "", appName: "", appVersion: "", appIcon: nil, webApp: false)
|
||||
}
|
||||
|
||||
|
||||
@@ -78,11 +83,13 @@ enum CurrentDetailsView:Int
|
||||
case apps
|
||||
}
|
||||
|
||||
//enum CurrentMode:Int
|
||||
//{
|
||||
// case regular
|
||||
// case mini
|
||||
//}
|
||||
enum NewWindow:Int
|
||||
{
|
||||
case update
|
||||
case no_update
|
||||
case perm
|
||||
}
|
||||
|
||||
|
||||
enum AlertType:Int
|
||||
{
|
||||
|
||||
@@ -77,6 +77,7 @@ func getAppInfo(atPath path: URL) -> AppInfo? {
|
||||
var appIconFileName = bundle.infoDictionary?["CFBundleIconFile"] as? String {
|
||||
var appIcon: NSImage?
|
||||
var appName: String?
|
||||
var webApp: Bool?
|
||||
if let localizedName = bundle.localizedInfoDictionary?[kCFBundleNameKey as String] as? String {
|
||||
appName = localizedName
|
||||
} else if let bundleName = bundle.infoDictionary?["CFBundleName"] as? String {
|
||||
@@ -111,7 +112,13 @@ func getAppInfo(atPath path: URL) -> AppInfo? {
|
||||
print("App Icon not found for app at path: \(path)")
|
||||
}
|
||||
|
||||
return AppInfo(id: UUID(), path: path, bundleIdentifier: bundleIdentifier, appName: appName ?? "", appVersion: appVersion, appIcon: appIcon)
|
||||
if bundle.infoDictionary?["LSTemplateApplication"] is Bool {
|
||||
webApp = true
|
||||
} else {
|
||||
webApp = false
|
||||
}
|
||||
|
||||
return AppInfo(id: UUID(), path: path, bundleIdentifier: bundleIdentifier, appName: appName ?? "", appVersion: appVersion, appIcon: appIcon, webApp: webApp ?? false)
|
||||
|
||||
} else {
|
||||
print("One or more variables missing for app at path: \(path)")
|
||||
@@ -178,6 +185,9 @@ func findPathsForApp(appState: AppState, appInfo: AppInfo) {
|
||||
// appState.paths.insert(url, at: 0)
|
||||
collection.insert(url, at: 0)
|
||||
}
|
||||
|
||||
// Only search for extra files if not a webapp
|
||||
// if !appInfo.webApp {
|
||||
let fileManager = FileManager.default
|
||||
let dispatchGroup = DispatchGroup()
|
||||
let bundleComponents = appInfo.bundleIdentifier.components(separatedBy: ".")
|
||||
@@ -203,29 +213,43 @@ func findPathsForApp(appState: AppState, appInfo: AppInfo) {
|
||||
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)) {
|
||||
// Catch web app plist files
|
||||
if appInfo.webApp {
|
||||
if itemL.contains(bundleIdentifierL) {
|
||||
if collection.contains(itemURL) {
|
||||
return
|
||||
}
|
||||
collection.append(itemURL)
|
||||
}
|
||||
} else {
|
||||
if itemL.contains(bundleIdentifierL) || itemL.contains(bundle) || (nameL.count > 3 && itemL.contains(nameL)) {
|
||||
collection.append(itemURL)
|
||||
// Catch all the com.apple folders in the OS since there's a ton and probably unrelated
|
||||
if itemL.contains("comapple") {
|
||||
if itemL.contains(bundle) || itemL.contains(bundleIdentifierL) || (nameL.count > 4 && itemL.contains(nameL)) {
|
||||
collection.append(itemURL)
|
||||
}
|
||||
}
|
||||
// Catch Xcode files
|
||||
else if itemL.contains("xcode") {
|
||||
if itemL.contains(bundle) || itemL.contains(bundleIdentifierL) || (nameL.count > 4 && itemL.contains(nameL)) {
|
||||
collection.append(itemURL)
|
||||
}
|
||||
// Catch Logitech files since Logi is part of word login and can return random files
|
||||
} 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)
|
||||
}
|
||||
// Catch MS Office files since they have many random folder names and very short names
|
||||
} 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
|
||||
@@ -241,6 +265,14 @@ func findPathsForApp(appState: AppState, appInfo: AppInfo) {
|
||||
}
|
||||
|
||||
}
|
||||
// }
|
||||
|
||||
// if appInfo.webApp {
|
||||
// updateOnMain {
|
||||
// appState.paths = collection
|
||||
// appState.selectedItems = Set(collection)
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+596
-596
File diff suppressed because it is too large
Load Diff
@@ -143,7 +143,10 @@ struct SimpleSearchStyle: TextFieldStyle {
|
||||
@State private var isHovered = false
|
||||
@State var icon: Image?
|
||||
@State var trash: Bool = false
|
||||
@Binding var reload: Bool
|
||||
@Binding var text: String
|
||||
@EnvironmentObject var appState: AppState
|
||||
@AppStorage("settings.general.mini") private var mini: Bool = false
|
||||
|
||||
func _body(configuration: TextField<Self._Label>) -> some View {
|
||||
|
||||
@@ -156,21 +159,40 @@ struct SimpleSearchStyle: TextFieldStyle {
|
||||
}
|
||||
|
||||
configuration
|
||||
.frame(maxWidth: 300)
|
||||
// .frame(minWidth: mini ? 80 : 300)
|
||||
// .frame(maxWidth: mini ? 150 : 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 = ""
|
||||
|
||||
Image(systemName: "arrow.triangle.2.circlepath")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 15, height: 15)
|
||||
.padding(.trailing, 5)
|
||||
.foregroundStyle(isHovered ? Color("mode").opacity(0.8) : Color("mode").opacity(0.5))
|
||||
.onTapGesture {
|
||||
// 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()
|
||||
}
|
||||
.opacity(text.isEmpty ? 0 : 1)
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
// }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -242,9 +264,9 @@ struct WindowActionButton: ButtonStyle {
|
||||
.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)
|
||||
action == .accept ? Color("button") : Color.red :
|
||||
action == .accept ? Color("button").opacity(0.8) : Color.red.opacity(0.8) :
|
||||
action == .accept ? Color("button").opacity(0.5) : Color.red.opacity(0.5)
|
||||
)
|
||||
.foregroundColor(.white)
|
||||
.cornerRadius(8)
|
||||
|
||||
@@ -25,35 +25,98 @@ func updateOnBackground(_ updates: @escaping () -> Void) {
|
||||
|
||||
|
||||
// Resize window
|
||||
func resizeWindow() {
|
||||
func resizeWindow(width: CGFloat, height: CGFloat) {
|
||||
if let window = NSApplication.shared.windows.first {
|
||||
let newSize = NSSize(width: 500, height: 450)
|
||||
let newSize = NSSize(width: width, height: height)
|
||||
window.setContentSize(newSize)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Check FDA
|
||||
func checkFullDiskAccessForApp() -> Bool {
|
||||
let process = Process()
|
||||
process.launchPath = "/usr/bin/sqlite3"
|
||||
process.arguments = ["/Library/Application Support/com.apple.TCC/TCC.db", "select client from access where auth_value and service = \"kTCCServiceSystemPolicyAllFiles\" and client = \"com.alienator88.Pearcleaner\""]
|
||||
|
||||
let pipe = Pipe()
|
||||
let pipeErr = Pipe()
|
||||
process.standardOutput = pipe
|
||||
process.standardError = pipeErr
|
||||
process.launch()
|
||||
|
||||
// let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
||||
let dataErr = pipeErr.fileHandleForReading.readDataToEndOfFile()
|
||||
|
||||
let output = String(data: dataErr, encoding: .utf8)
|
||||
|
||||
// Check if the app is in the results
|
||||
if let result = output, result.isEmpty {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// Check for access to Full Disk access
|
||||
func checkAndRequestFullDiskAccess(appState: AppState, skipAlert: Bool = false) -> Bool {
|
||||
@AppStorage("settings.permissions.disk") var diskP: Bool = false
|
||||
|
||||
let process = Process()
|
||||
process.launchPath = "/usr/bin/sqlite3"
|
||||
process.arguments = ["/Library/Application Support/com.apple.TCC/TCC.db", "select client from access where auth_value and service = \"kTCCServiceSystemPolicyAllFiles\" and client = \"com.alienator88.Pearcleaner\""]
|
||||
|
||||
let fileURL = URL(fileURLWithPath: "\(home)/Library/Application Support/com.apple.TCC/TCC.db")
|
||||
let pipe = Pipe()
|
||||
let pipeErr = Pipe()
|
||||
process.standardOutput = pipe
|
||||
process.standardError = pipeErr
|
||||
process.launch()
|
||||
|
||||
let accessStatus = FileManager.default.isWritableFile(atPath: fileURL.path)
|
||||
if accessStatus {
|
||||
// let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
||||
let dataErr = pipeErr.fileHandleForReading.readDataToEndOfFile()
|
||||
|
||||
let output = String(data: dataErr, encoding: .utf8)
|
||||
|
||||
// Check if the app is in the results
|
||||
if let result = output, result.isEmpty {
|
||||
diskP = true
|
||||
_ = checkAndRequestAccessibilityAccess(appState: appState)
|
||||
|
||||
return true
|
||||
} else {
|
||||
diskP = false
|
||||
if !skipAlert {
|
||||
appState.alertType = .diskAccess
|
||||
appState.showAlert = true
|
||||
NewWin.show(appState: appState, width: 500, height: 350, newWin: .perm)
|
||||
// appState.alertType = .diskAccess
|
||||
// appState.showAlert = true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
// let fileURL = URL(fileURLWithPath: "/Library/Application Support/com.apple.TCC/TCC.db")
|
||||
//
|
||||
// let accessStatus = FileManager.default.isReadableFile(atPath: fileURL.path)
|
||||
// print(accessStatus)
|
||||
// if accessStatus {
|
||||
// diskP = true
|
||||
// _ = checkAndRequestAccessibilityAccess(appState: appState)
|
||||
//
|
||||
// return true
|
||||
// } else {
|
||||
// diskP = false
|
||||
// if !skipAlert {
|
||||
// appState.alertType = .diskAccess
|
||||
// appState.showAlert = true
|
||||
// }
|
||||
//
|
||||
// return false
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
@@ -74,6 +137,22 @@ func checkAndRequestAccessibilityAccess(appState: AppState) -> Bool {
|
||||
}
|
||||
|
||||
|
||||
// Check if appearance is dark mode
|
||||
func isDarkModeEnabled() -> Bool {
|
||||
let process = Process()
|
||||
process.executableURL = URL(fileURLWithPath: "/bin/zsh")
|
||||
process.arguments = ["-c", "defaults read -g AppleInterfaceStyle"]
|
||||
let pipe = Pipe()
|
||||
process.standardOutput = pipe
|
||||
process.standardError = pipe
|
||||
try? process.run()
|
||||
let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
||||
if !data.isEmpty{
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Check if symlink
|
||||
@@ -277,35 +356,28 @@ 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 check again later from Settings"), primaryButton: .default(Text("Open")) {
|
||||
// downloadUpdate(appState: appState)
|
||||
launchUpdate()
|
||||
return Alert(title: Text("Update Available 🥳"), message: Text("You may choose to install the update now, otherwise you may check again later from Settings"), primaryButton: .default(Text("Install")) {
|
||||
downloadUpdate(appState: appState)
|
||||
// launchUpdate()
|
||||
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")) {
|
||||
return Alert(title: Text("No Updates 😌"), message: Text("Pearcleaner 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. Drag the app into the Full Disk pane if not visible."), primaryButton: .default(Text("Allow in Settings")) {
|
||||
return Alert(title: Text("Permissions"), message: Text("Pearcleaner requires Full Disk and Accessibility permissions. Drag the app into the Full Disk and Accessibility pane to enable or toggle On if already present."), 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")) {
|
||||
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")) {
|
||||
appState.alertType = .off
|
||||
relaunchApp(afterDelay: 1)
|
||||
})
|
||||
relaunchApp()
|
||||
}, secondaryButton: .cancel(Text("Later")))
|
||||
case .off:
|
||||
return Alert(title: Text(""))
|
||||
}
|
||||
@@ -326,7 +398,34 @@ func ensureApplicationSupportFolderExists(appState: AppState) {
|
||||
}
|
||||
|
||||
|
||||
// --- Write Log to File for troubleshooting ---
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// --- Load Plist file with launchctl ---
|
||||
func launchctl(load: Bool) {
|
||||
let cmd = load ? "load" : "unload"
|
||||
if let plistPath = Bundle.main.path(forResource: "com.alienator88.PearcleanerSentinel", ofType: "plist") {
|
||||
|
||||
@@ -15,108 +15,66 @@ struct PearcleanerApp: App {
|
||||
@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
|
||||
@AppStorage("displayMode") var displayMode: DisplayMode = .system
|
||||
@AppStorage("settings.general.mini") private var mini: Bool = false
|
||||
@State private var search = ""
|
||||
|
||||
var body: some Scene {
|
||||
|
||||
|
||||
WindowGroup {
|
||||
if !mini {
|
||||
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)
|
||||
let fileManager = FileManager.default
|
||||
let destinationURL = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!.appendingPathComponent("Pearcleaner").appendingPathComponent("Pearcleaner.zip")
|
||||
try? fileManager.removeItem(at: destinationURL)
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
} else {
|
||||
MiniMode(search: $search)
|
||||
.environmentObject(appState)
|
||||
.frame(minWidth: 500, minHeight: 450)
|
||||
.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)
|
||||
let fileManager = FileManager.default
|
||||
let destinationURL = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!.appendingPathComponent("Pearcleaner").appendingPathComponent("Pearcleaner.zip")
|
||||
try? fileManager.removeItem(at: destinationURL)
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
Group {
|
||||
|
||||
if !mini {
|
||||
AppListView(search: $search)
|
||||
.frame(minWidth: 800, minHeight: 500)
|
||||
} else {
|
||||
MiniMode(search: $search)
|
||||
.frame(minWidth: 300, minHeight: 300)
|
||||
}
|
||||
|
||||
}
|
||||
.environmentObject(appState)
|
||||
.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)
|
||||
@@ -125,6 +83,7 @@ struct PearcleanerApp: App {
|
||||
.commands {
|
||||
AppCommands(appState: appState)
|
||||
CommandGroup(replacing: .newItem, addition: { })
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "display-p3",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xFF",
|
||||
"green" : "0x83",
|
||||
"red" : "0x24"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "display-p3",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xFF",
|
||||
"green" : "0x83",
|
||||
"red" : "0x24"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "display-p3",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xFF",
|
||||
"green" : "0xFF",
|
||||
"red" : "0xFF"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "display-p3",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x26",
|
||||
"green" : "0x26",
|
||||
"red" : "0x26"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -7,16 +7,18 @@
|
||||
<dict>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>Application Bundle</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Default</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Default</string>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>com.apple.application-bundle</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleGetInfoString</key>
|
||||
<string></string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
@@ -30,5 +32,13 @@
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>SUPublicEDKey</key>
|
||||
<string>cD156JWtfwtgFJH2RApyj9Bgc0fCqLBGEf9C9IRMCWE=</string>
|
||||
<key>SUFeedURL</key>
|
||||
<string>https://raw.githubusercontent.com/alienator88/Pearcleaner/main/appcast.xml</string>
|
||||
<key>SUEnableInstallerLauncherService</key>
|
||||
<true/>
|
||||
<key>SUEnableDownloaderService</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -139,7 +139,7 @@ struct AboutSettingsTab: View {
|
||||
|
||||
Spacer()
|
||||
|
||||
Text("Made with ❤️ by Alin Lupascu").font(.footnote).padding(.bottom)
|
||||
Text("Made with ❤️ by Alin Lupascu (dev@itsalin.com)").font(.footnote).padding(.bottom)
|
||||
}
|
||||
.padding(20)
|
||||
.frame(width: 750, height: 325)
|
||||
|
||||
@@ -13,8 +13,10 @@ struct GeneralSettingsTab: View {
|
||||
@AppStorage("settings.general.glass") private var glass: Bool = true
|
||||
@AppStorage("settings.general.mini") private var mini: Bool = false
|
||||
@AppStorage("settings.general.dark") var isDark: Bool = true
|
||||
@AppStorage("displayMode") var displayMode: DisplayMode = .dark
|
||||
|
||||
@AppStorage("displayMode") var displayMode: DisplayMode = .system
|
||||
@State private var selectedTheme = "Auto"
|
||||
private let themes = ["Auto", "Dark", "Light"]
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
VStack {
|
||||
@@ -38,18 +40,44 @@ struct GeneralSettingsTab: View {
|
||||
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
Text("Dark Mode").font(.title2)
|
||||
Text("Toggles between dark and light mode")
|
||||
Text("Appearance").font(.title2)
|
||||
Text("Toggles color mode")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.gray)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
Toggle(isOn: $isDark, label: {
|
||||
})
|
||||
.toggleStyle(.switch)
|
||||
.onChange(of: isDark) { newValue in
|
||||
displayMode.colorScheme = newValue ? .dark : .light
|
||||
|
||||
Picker("", selection: $selectedTheme) {
|
||||
ForEach(themes, id: \.self) { theme in
|
||||
Text(theme)
|
||||
}
|
||||
}
|
||||
.pickerStyle(SegmentedPickerStyle())
|
||||
// .padding()
|
||||
.onChange(of: selectedTheme) { newTheme in
|
||||
switch newTheme {
|
||||
case "Auto":
|
||||
displayMode.colorScheme = nil
|
||||
if isDarkModeEnabled() {
|
||||
displayMode.colorScheme = .dark
|
||||
} else {
|
||||
displayMode.colorScheme = .light
|
||||
}
|
||||
case "Dark":
|
||||
displayMode.colorScheme = .dark
|
||||
case "Light":
|
||||
displayMode.colorScheme = .light
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
// Toggle(isOn: $isDark, label: {
|
||||
// })
|
||||
// .toggleStyle(.switch)
|
||||
// .onChange(of: isDark) { newValue in
|
||||
// displayMode.colorScheme = newValue ? .dark : .light
|
||||
// }
|
||||
}
|
||||
.padding()
|
||||
.background(
|
||||
@@ -69,7 +97,16 @@ struct GeneralSettingsTab: View {
|
||||
})
|
||||
.toggleStyle(.switch)
|
||||
.onChange(of: mini) { newVal in
|
||||
resizeWindow()
|
||||
resizeWindow(width: 300, height: 300)
|
||||
if mini {
|
||||
appState.currentView = .apps
|
||||
} else {
|
||||
if appState.appInfo.appName.isEmpty {
|
||||
appState.currentView = .empty
|
||||
} else {
|
||||
appState.currentView = .files
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
|
||||
@@ -39,13 +39,17 @@ struct SentinelSettingsTab: View {
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.fill(Color("mode").opacity(0.05))
|
||||
)
|
||||
Text("If you uninstall Pearcleaner, make sure to disable Sentinel first").font(.footnote)
|
||||
.foregroundStyle(Color.red)
|
||||
.padding()
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
.padding(20)
|
||||
.frame(width: 450, height: 100)
|
||||
.frame(width: 450, height: 130)
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -93,7 +93,8 @@ struct UpdateSettingsTab: View {
|
||||
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")
|
||||
// Set the token for a private repo
|
||||
// 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) {
|
||||
@@ -114,22 +115,25 @@ 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
|
||||
NewWin.show(appState: appState, width: 500, height: 400, newWin: .update)
|
||||
// appState.alertType = .update
|
||||
// appState.showAlert = true
|
||||
} else {
|
||||
if manual {
|
||||
appState.alertType = .no_update
|
||||
appState.showAlert = true
|
||||
NewWin.show(appState: appState, width: 500, height: 300, newWin: .no_update)
|
||||
// appState.alertType = .no_update
|
||||
// appState.showAlert = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func launchUpdate() {
|
||||
NSWorkspace.shared.open(URL(string: "https://github.com/alienator88/Pearcleaner/releases")!)
|
||||
|
||||
}
|
||||
|
||||
func downloadUpdate(appState: AppState) {
|
||||
updateOnMain {
|
||||
appState.progressBar.0 = "Getting update file links ready"
|
||||
appState.progressBar.1 = 0.1
|
||||
}
|
||||
|
||||
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 }
|
||||
@@ -138,6 +142,11 @@ func downloadUpdate(appState: AppState) {
|
||||
request.setValue("application/octet-stream", forHTTPHeaderField: "Accept")
|
||||
|
||||
let downloadTask = URLSession.shared.downloadTask(with: request) { localURL, urlResponse, error in
|
||||
updateOnMain {
|
||||
appState.progressBar.0 = "Downloading update file"
|
||||
appState.progressBar.1 = 0.2
|
||||
}
|
||||
|
||||
guard let localURL = localURL else { return }
|
||||
|
||||
let fileManager = FileManager.default
|
||||
@@ -147,15 +156,21 @@ func downloadUpdate(appState: AppState) {
|
||||
if fileManager.fileExists(atPath: destinationURL.path) {
|
||||
try? fileManager.removeItem(at: destinationURL)
|
||||
}
|
||||
updateOnMain {
|
||||
appState.progressBar.0 = "Moving update file to Application Support"
|
||||
appState.progressBar.1 = 0.4
|
||||
}
|
||||
|
||||
try fileManager.moveItem(at: localURL, to: destinationURL)
|
||||
|
||||
UnzipAndReplace(DownloadedFileURL: destinationURL.path, appState: appState) {
|
||||
updateOnMain {
|
||||
// Restart App
|
||||
relaunchApp(afterDelay: 1)
|
||||
}
|
||||
UnzipAndReplace(DownloadedFileURL: destinationURL.path, appState: appState)
|
||||
|
||||
updateOnMain {
|
||||
appState.progressBar.0 = "Done, please restart!"
|
||||
appState.progressBar.1 = 1.0
|
||||
}
|
||||
|
||||
|
||||
} catch {
|
||||
print("Error moving downloaded file: \(error.localizedDescription)")
|
||||
}
|
||||
@@ -164,28 +179,61 @@ func downloadUpdate(appState: AppState) {
|
||||
downloadTask.resume()
|
||||
}
|
||||
|
||||
func UnzipAndReplace(DownloadedFileURL fileURL: String, appState: AppState, completion: @escaping () -> Void = {}) {
|
||||
func UnzipAndReplace(DownloadedFileURL fileURL: String, appState: AppState) {
|
||||
let appDirectory = Bundle.main.bundleURL.deletingLastPathComponent().path
|
||||
let appBundle = Bundle.main.bundleURL.path
|
||||
let fileManager = FileManager.default
|
||||
|
||||
do {
|
||||
updateOnMain {
|
||||
appState.progressBar.0 = "Deleting existing application"
|
||||
appState.progressBar.1 = 0.5
|
||||
}
|
||||
|
||||
// Remove the old version of your app
|
||||
try fileManager.removeItem(atPath: appBundle)
|
||||
|
||||
updateOnMain {
|
||||
appState.progressBar.0 = "Unziping new update file to original Pearcleaner location"
|
||||
appState.progressBar.1 = 0.6
|
||||
}
|
||||
|
||||
|
||||
// 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]
|
||||
// process.executableURL = URL(fileURLWithPath: "/usr/bin/unzip")
|
||||
// process.arguments = [fileURL, "-d", appDirectory]
|
||||
process.executableURL = URL(fileURLWithPath: "/usr/bin/ditto")
|
||||
process.arguments = ["-xk", fileURL, appDirectory]
|
||||
// let pipe = Pipe()
|
||||
process.standardOutput = FileHandle.nullDevice
|
||||
process.standardError = FileHandle.nullDevice
|
||||
|
||||
try process.run()
|
||||
process.waitUntilExit()
|
||||
|
||||
// let outputData = pipe.fileHandleForReading.readDataToEndOfFile()
|
||||
// let output = String(decoding: outputData, as: UTF8.self)
|
||||
//
|
||||
// writeLog(string: output)
|
||||
|
||||
updateOnMain {
|
||||
appState.progressBar.0 = "Deleting update file"
|
||||
appState.progressBar.1 = 0.8
|
||||
}
|
||||
|
||||
// After unzipping, remove the update file
|
||||
try fileManager.removeItem(atPath: fileURL)
|
||||
|
||||
// Show Restart dialog
|
||||
// updateOnMain {
|
||||
// appState.alertType = .restartApp
|
||||
// appState.showAlert = true
|
||||
// }
|
||||
|
||||
|
||||
} catch {
|
||||
print("Error updating the app: \(error)")
|
||||
}
|
||||
|
||||
completion()
|
||||
}
|
||||
|
||||
@@ -10,8 +10,12 @@ import SwiftUI
|
||||
|
||||
struct AppListItems: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
// @Binding var reload: Bool
|
||||
// @Binding var search: String
|
||||
@State private var isHovered = false
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@AppStorage("settings.general.mini") private var mini: Bool = false
|
||||
|
||||
let appInfo: AppInfo
|
||||
var isSelected: Bool {
|
||||
appState.appInfo == appInfo
|
||||
@@ -39,6 +43,16 @@ struct AppListItems: View {
|
||||
.truncationMode(.tail)
|
||||
.padding(.horizontal, 5)
|
||||
}
|
||||
if appInfo.webApp {
|
||||
Text("web")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(Color("mode").opacity(0.5))
|
||||
.frame(minWidth: 30, minHeight: 15)
|
||||
.padding(2)
|
||||
.background(Color("mode").opacity(0.1))
|
||||
.clipShape(.capsule)
|
||||
}
|
||||
|
||||
|
||||
Spacer()
|
||||
Text(appInfo.appVersion)
|
||||
@@ -82,9 +96,26 @@ struct AppListItems: View {
|
||||
appState.appInfo = appInfo
|
||||
findPathsForApp(appState: appState, appInfo: appState.appInfo)
|
||||
withAnimation(Animation.easeIn(duration: 0.4)) {
|
||||
appState.currentView = .files
|
||||
if mini {
|
||||
appState.showPopover.toggle()
|
||||
} else {
|
||||
appState.currentView = .files
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// .popover(isPresented: $appState.showPopover, arrowEdge: .trailing) {
|
||||
// VStack {
|
||||
// FilesView()
|
||||
// .id(appState.appInfo.id)
|
||||
// }
|
||||
// .background(
|
||||
// Rectangle()
|
||||
// .background(colorScheme == .dark ? .black.opacity(0.8) : .white)
|
||||
// .padding(-80)
|
||||
// )
|
||||
// .frame(minWidth: 500, minHeight: 500)
|
||||
//
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,14 +47,14 @@ struct AppListView: View {
|
||||
ProgressView("Refreshing applications")
|
||||
Spacer()
|
||||
}
|
||||
.frame(width: 300)
|
||||
.frame(width: 250)
|
||||
.padding(.vertical)
|
||||
} else {
|
||||
VStack(alignment: .center) {
|
||||
|
||||
VStack(alignment: .center, spacing: 20) {
|
||||
HStack {
|
||||
SearchBar(search: $search)
|
||||
SearchBar(search: $search, reload: $reload)
|
||||
|
||||
Button("") {
|
||||
withAnimation(.easeInOut(duration: 0.5)) {
|
||||
@@ -89,7 +89,7 @@ struct AppListView: View {
|
||||
Divider().padding(.horizontal, 5)
|
||||
}
|
||||
}
|
||||
.padding(.bottom)
|
||||
// .padding(.bottom)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -113,7 +113,7 @@ struct AppListView: View {
|
||||
.scrollIndicators(.never)
|
||||
|
||||
}
|
||||
.frame(width: 300)
|
||||
.frame(width: 250)
|
||||
.padding(.vertical)
|
||||
}
|
||||
|
||||
@@ -200,11 +200,12 @@ struct AppDetailsEmptyView: View {
|
||||
|
||||
struct SearchBar: View {
|
||||
@Binding var search: String
|
||||
@Binding var reload: Bool
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
TextField("Search", text: $search)
|
||||
.textFieldStyle(SimpleSearchStyle(icon: Image(systemName: "magnifyingglass"),trash: true, text: $search))
|
||||
.textFieldStyle(SimpleSearchStyle(icon: Image(systemName: "magnifyingglass"),trash: true, reload: $reload, text: $search))
|
||||
}
|
||||
.frame(height: 30)
|
||||
}
|
||||
@@ -220,11 +221,13 @@ struct Header: View {
|
||||
Text(title).opacity(0.5)
|
||||
Spacer()
|
||||
Text("\(count)")
|
||||
.frame(minWidth: 30, minHeight: 15)
|
||||
.font(.system(size: 10))
|
||||
.frame(minWidth: count > 99 ? 30 : 20, minHeight: 15)
|
||||
.padding(2)
|
||||
.background(Color("mode").opacity(0.1))
|
||||
.clipShape(.capsule)
|
||||
// .clipShape(RoundedRectangle(cornerRadius: 6, style: .continuous))
|
||||
}
|
||||
.padding(4)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
////
|
||||
//// 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) }
|
||||
//
|
||||
// }
|
||||
//}
|
||||
//
|
||||
@@ -16,7 +16,7 @@ struct DropTarget: View, DropDelegate {
|
||||
@State private var shouldFlash = false
|
||||
private var types: [UTType] = [.fileURL]
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@State private var phase: CGFloat = 0
|
||||
@State private var phase: CGFloat = 4
|
||||
|
||||
public init(appState: AppState) {
|
||||
self.appState = appState
|
||||
@@ -39,7 +39,9 @@ struct DropTarget: View, DropDelegate {
|
||||
Image(mini ? "pear" : "logo")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: mini ? 104 : 500)
|
||||
.frame(minWidth: mini ? 104 : 300)
|
||||
.frame(maxWidth: mini ? 104 : 500)
|
||||
.padding(mini ? 20 : 10)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -52,17 +54,17 @@ struct DropTarget: View, DropDelegate {
|
||||
.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))
|
||||
.strokeBorder(LinearGradient(gradient: Gradient(colors: [.pink, .orange]), startPoint: .leading, endPoint: .trailing), style: StrokeStyle(lineWidth: 3, dash: [11.0], dashPhase: phase))
|
||||
.onAppear {
|
||||
withAnimation(.linear.speed(0.5).repeat(while: ants, autoreverses: false)) {
|
||||
if ants {
|
||||
phase -= 20
|
||||
phase -= 22
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: ants) { newValue in
|
||||
withAnimation(.linear.speed(0.5).repeat(while: newValue, autoreverses: false)) {
|
||||
phase -= 20
|
||||
phase -= 22
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
|
||||
@@ -25,6 +25,7 @@ struct FilesView: View {
|
||||
ProgressView("Finding application files..")
|
||||
Spacer()
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.transition(.opacity)
|
||||
} else {
|
||||
VStack() {
|
||||
@@ -45,19 +46,19 @@ struct FilesView: View {
|
||||
HStack(alignment: .center) {
|
||||
VStack(alignment: .leading, spacing: 5){
|
||||
HStack {
|
||||
Text("\(appState.appInfo.appName)").font(mini ? .title2 :.title).fontWeight(.bold)
|
||||
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(mini ? .footnote :.title3)
|
||||
Text("\(appState.appInfo.bundleIdentifier)").font(.title3)
|
||||
.foregroundStyle((.gray.opacity(0.8)))
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Text("\(appSize)").font(mini ? .title2 :.title).fontWeight(.bold)
|
||||
Text("\(appSize)").font(.title).fontWeight(.bold)
|
||||
// .foregroundStyle(Color("AccentColor"))
|
||||
}
|
||||
|
||||
@@ -74,6 +75,19 @@ struct FilesView: View {
|
||||
// .foregroundStyle(Color("AccentColor").opacity(0.7))
|
||||
.underline()
|
||||
}
|
||||
if appState.appInfo.webApp {
|
||||
HStack {
|
||||
Text("web")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(Color("mode").opacity(0.5))
|
||||
.frame(minWidth: 30, minHeight: 15)
|
||||
.padding(2)
|
||||
.background(Color("mode").opacity(0.1))
|
||||
.clipShape(.capsule)
|
||||
Spacer()
|
||||
}
|
||||
|
||||
}
|
||||
if appState.appInfo.appName.count < 5 {
|
||||
HStack(alignment: .center) {
|
||||
Button(""){
|
||||
@@ -103,7 +117,7 @@ struct FilesView: View {
|
||||
}
|
||||
.padding(20)
|
||||
}
|
||||
.padding([.horizontal, .bottom])
|
||||
// .padding(.horizontal)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
// .strokeBorder(Color("AccentColor"), lineWidth: 0.5)
|
||||
@@ -141,17 +155,7 @@ struct FilesView: View {
|
||||
|
||||
HStack() {
|
||||
Spacer()
|
||||
|
||||
// if appState.trashLaunch {
|
||||
// Button("Close") {
|
||||
// Task {
|
||||
// NSApp.terminate(nil)
|
||||
// exit(0)
|
||||
// }
|
||||
// }
|
||||
// .buttonStyle(WindowActionButton(action: .cancel))
|
||||
// }
|
||||
|
||||
|
||||
|
||||
Button("Uninstall") {
|
||||
Task {
|
||||
@@ -182,7 +186,7 @@ struct FilesView: View {
|
||||
}
|
||||
|
||||
}
|
||||
// .frame(minWidth: 700)
|
||||
// .frame(minWidth: 500, minHeight: 500)
|
||||
.onAppear {
|
||||
Task {
|
||||
calculateFileDetails()
|
||||
@@ -302,144 +306,3 @@ struct FileDetailsItem: View {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
// }
|
||||
//}
|
||||
|
||||
@@ -15,7 +15,8 @@ struct MiniMode: View {
|
||||
@State private var showSys: Bool = true
|
||||
@State private var showUsr: Bool = true
|
||||
@State private var reload: Bool = false
|
||||
|
||||
@AppStorage("settings.general.glass") private var glass: Bool = false
|
||||
|
||||
|
||||
|
||||
var body: some View {
|
||||
@@ -26,7 +27,7 @@ struct MiniMode: View {
|
||||
VStack(spacing: 0) {
|
||||
if appState.currentView == .empty {
|
||||
TopBarMini(reload: $reload, search: $search)
|
||||
AppDetailsEmptyView()
|
||||
MiniEmptyView()
|
||||
} else if appState.currentView == .files {
|
||||
TopBarMini(reload: $reload, search: $search)
|
||||
FilesView()
|
||||
@@ -39,8 +40,11 @@ struct MiniMode: View {
|
||||
// .padding(.leading, appState.sidebar ? 0 : 10)
|
||||
.transition(.move(edge: .leading))
|
||||
|
||||
|
||||
}
|
||||
.frame(minWidth: 300, minHeight: 300)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
.background(glass ? GlassEffect(material: .sidebar, blendingMode: .behindWindow).edgesIgnoringSafeArea(.all) : nil)
|
||||
.onOpenURL(perform: { url in
|
||||
let deeplinkManager = DeeplinkManager()
|
||||
deeplinkManager.manage(url: url, appState: appState)
|
||||
@@ -70,6 +74,11 @@ struct MiniEmptyView: View {
|
||||
DropTarget(appState: appState)
|
||||
.frame(maxWidth: mini ? 300 : 500)
|
||||
|
||||
Text("Drop an app to begin")
|
||||
.font(.title3)
|
||||
.padding(.bottom, 25)
|
||||
.opacity(0.5)
|
||||
|
||||
Spacer()
|
||||
|
||||
if appState.isReminderVisible {
|
||||
@@ -87,11 +96,9 @@ struct MiniEmptyView: View {
|
||||
Spacer()
|
||||
}
|
||||
|
||||
Text("Drop an app above or select one from the list to begin")
|
||||
.font(.title3)
|
||||
.padding(.bottom, 25)
|
||||
.opacity(0.5)
|
||||
|
||||
}
|
||||
// .padding()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,14 +147,6 @@ struct MiniAppView: View {
|
||||
} else {
|
||||
VStack(alignment: .center) {
|
||||
|
||||
// VStack(alignment: .center, spacing: 20) {
|
||||
// HStack {
|
||||
// SearchBar(search: $search)
|
||||
// }
|
||||
// }
|
||||
// .padding(.horizontal, 5)
|
||||
// .padding(.bottom)
|
||||
|
||||
ScrollView {
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
@@ -161,7 +160,6 @@ struct MiniAppView: View {
|
||||
Divider().padding(.horizontal, 5)
|
||||
}
|
||||
}
|
||||
.padding(.bottom)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -183,15 +181,29 @@ struct MiniAppView: View {
|
||||
|
||||
}
|
||||
.scrollIndicators(.never)
|
||||
|
||||
|
||||
}
|
||||
.padding(.vertical)
|
||||
// .padding(.vertical)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
.transition(.move(edge: .leading))
|
||||
.popover(isPresented: $appState.showPopover, arrowEdge: .trailing) {
|
||||
VStack {
|
||||
FilesView()
|
||||
.id(appState.appInfo.id)
|
||||
}
|
||||
.background(
|
||||
Rectangle()
|
||||
.fill(Color("pop"))
|
||||
// .background(Color("pop"))
|
||||
.padding(-80)
|
||||
)
|
||||
.frame(minWidth: 600, minHeight: 500)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,20 +18,45 @@ struct TopBarMini: View {
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .center, spacing: 5) {
|
||||
|
||||
Spacer()
|
||||
|
||||
if appState.currentView == .apps {
|
||||
HStack(alignment: .center, spacing: 0) {
|
||||
Button("") {
|
||||
withAnimation(.easeInOut(duration: 0.5)) {
|
||||
appState.currentView = .empty
|
||||
appState.appInfo = AppInfo.empty
|
||||
}
|
||||
}
|
||||
.buttonStyle(SimpleButtonStyle(icon: "arrow.down.app", help: "Drop", color: Color("mode")))
|
||||
|
||||
// HStack {
|
||||
// Spacer()
|
||||
|
||||
SearchBarMini(search: $search, reload: $reload)
|
||||
.frame(width: 180)
|
||||
// .padding(.top, 5)
|
||||
|
||||
// Spacer()
|
||||
|
||||
// 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.triangle.2.circlepath", help: "Refresh app list", color: Color("mode")))
|
||||
// .padding(.leading, 5)
|
||||
// }
|
||||
}
|
||||
|
||||
if appState.currentView == .apps {
|
||||
Button("") {
|
||||
withAnimation(.easeInOut(duration: 0.5)) {
|
||||
appState.currentView = .empty
|
||||
appState.appInfo = AppInfo.empty
|
||||
}
|
||||
}
|
||||
|
||||
.buttonStyle(SimpleButtonStyle(icon: "arrow.down.app", help: "Drop", color: Color("mode")))
|
||||
} else if appState.currentView == .empty {
|
||||
Button("") {
|
||||
withAnimation(.easeInOut(duration: 0.5)) {
|
||||
@@ -55,56 +80,22 @@ struct TopBarMini: View {
|
||||
.buttonStyle(SimpleButtonStyle(icon: "list.triangle", help: "Apps", color: Color("mode")))
|
||||
}
|
||||
|
||||
if appState.currentView == .apps {
|
||||
HStack {
|
||||
Spacer()
|
||||
|
||||
SearchBarMini(search: $search)
|
||||
|
||||
Spacer()
|
||||
|
||||
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.triangle.2.circlepath", help: "Refresh app list", color: Color("mode")))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Spacer()
|
||||
|
||||
// if sentinel {
|
||||
// Button("") {
|
||||
// //
|
||||
// }
|
||||
// .buttonStyle(SimpleButtonStyle(icon: "lock.shield", help: "Sentinel enabled", color: .green, shield: true))
|
||||
// }
|
||||
|
||||
}
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
.padding(.horizontal)
|
||||
.padding(.top, 30)
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.top, 10)
|
||||
.padding(.bottom, 15)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct SearchBarMini: View {
|
||||
@Binding var search: String
|
||||
@Binding var reload: Bool
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
TextField("Search", text: $search)
|
||||
.textFieldStyle(SimpleSearchStyle(trash: true, text: $search))
|
||||
.textFieldStyle(SimpleSearchStyle(trash: true, reload: $reload, text: $search))
|
||||
}
|
||||
.frame(height: 30)
|
||||
}
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
//
|
||||
// TrashView.swift
|
||||
// Pearcleaner
|
||||
//
|
||||
// Created by Alin Lupascu on 11/11/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct TrashView: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
|
||||
var body: some View {
|
||||
VStack() {
|
||||
Text("Trash view my boi")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
//
|
||||
// NewWin.swift
|
||||
// Pearcleaner
|
||||
//
|
||||
// Created by Alin Lupascu on 11/16/23.
|
||||
//
|
||||
|
||||
|
||||
import AppKit
|
||||
import SwiftUI
|
||||
|
||||
class NewWin: NSWindowController {
|
||||
|
||||
static var shared = NewWin()
|
||||
|
||||
static func show(appState: AppState, width: CGFloat, height: CGFloat, newWin: NewWindow) {
|
||||
shared = NewWin(appState: appState, width: width, height: height, newWin: newWin)
|
||||
shared.window?.makeKeyAndOrderFront(nil)
|
||||
}
|
||||
|
||||
static func close() {
|
||||
shared.window?.close()
|
||||
}
|
||||
|
||||
convenience init(appState: AppState, width: CGFloat, height: CGFloat, newWin: NewWindow) {
|
||||
|
||||
let window = Self.makeWindow(width: width, height: height)
|
||||
|
||||
window.backgroundColor = NSColor.controlBackgroundColor
|
||||
|
||||
self.init(window: window)
|
||||
|
||||
// Using Visual Effect to make titlebar fully transparent
|
||||
let visualEffect = NSVisualEffectView()
|
||||
visualEffect.blendingMode = .behindWindow
|
||||
visualEffect.state = .active
|
||||
visualEffect.material = .underWindowBackground
|
||||
|
||||
let contentView = makeNewView(appState: appState, newWin: newWin)
|
||||
|
||||
let hostView = NSHostingView(rootView: contentView)
|
||||
|
||||
window.contentView = visualEffect
|
||||
|
||||
visualEffect.addSubview(hostView)
|
||||
hostView.frame = visualEffect.frame
|
||||
window.titleVisibility = .hidden
|
||||
window.titlebarAppearsTransparent = true
|
||||
window.center()
|
||||
window.title = "Support Admin"
|
||||
|
||||
}
|
||||
|
||||
private static func makeWindow(width: CGFloat, height: CGFloat) -> NSWindow {
|
||||
let contentRect = NSRect(x: 0, y: 0, width: width, height: height)
|
||||
let styleMask: NSWindow.StyleMask = [
|
||||
.titled,
|
||||
.fullSizeContentView,
|
||||
// .closable,
|
||||
]
|
||||
return NSWindow(contentRect: contentRect,
|
||||
styleMask: styleMask,
|
||||
backing: .buffered,
|
||||
defer: false)
|
||||
}
|
||||
|
||||
private func makeNewView(appState: AppState, newWin: NewWindow) -> some View {
|
||||
switch newWin {
|
||||
case .update:
|
||||
return AnyView(UpdateView()
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.environmentObject(appState))
|
||||
case .no_update:
|
||||
return AnyView(NoUpdateView()
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.environmentObject(appState))
|
||||
case .perm:
|
||||
return AnyView(PermView()
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.environmentObject(appState))
|
||||
// case .restart:
|
||||
// return AnyView(RestartView()
|
||||
// .frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
// .environmentObject(appState))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
//
|
||||
// PermView.swift
|
||||
// Pearcleaner
|
||||
//
|
||||
// Created by Alin Lupascu on 11/16/23.
|
||||
//
|
||||
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct PermView: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
HStack {
|
||||
|
||||
Spacer()
|
||||
|
||||
Text("Attention")
|
||||
.font(.title)
|
||||
.bold()
|
||||
.padding(.vertical)
|
||||
|
||||
Spacer()
|
||||
|
||||
}
|
||||
.background(.red)
|
||||
|
||||
Text("Pearcleaner requires the following permissions:")
|
||||
.font(.title3)
|
||||
.fontWeight(.semibold)
|
||||
// .padding(.horizontal, 20)
|
||||
.padding(.vertical)
|
||||
// .padding(.bottom)
|
||||
|
||||
|
||||
ScrollView {
|
||||
Text(" - Full Disk Access permission to find and delete files in non-user locations.\n - Accessibility permission to delete files via Finder which allows for the Undo function.\n\nAdd Pearcleaner in both Privacy panes via the + or by dragging the app over the pane. If the app is already pre-populated in the list, just toggle On. Restart app when both permissions are granted.")
|
||||
.font(.body)
|
||||
.multilineTextAlignment(.leading)
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
.frame(height: 130)
|
||||
.padding(.horizontal)
|
||||
|
||||
Spacer()
|
||||
|
||||
HStack {
|
||||
|
||||
Button(action: {
|
||||
if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles") {
|
||||
NSWorkspace.shared.open(url)
|
||||
}
|
||||
}) {
|
||||
Text("Full Disk Access")
|
||||
}
|
||||
.buttonStyle(WindowActionButton(action: .accept))
|
||||
.padding()
|
||||
|
||||
Button(action: {
|
||||
if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility") {
|
||||
NSWorkspace.shared.open(url)
|
||||
}
|
||||
}) {
|
||||
Text("Accessibility")
|
||||
}
|
||||
.buttonStyle(WindowActionButton(action: .accept))
|
||||
.padding()
|
||||
|
||||
Button(action: {
|
||||
// NewWin.close
|
||||
relaunchApp(afterDelay: 1)
|
||||
}) {
|
||||
Text("Restart")
|
||||
}
|
||||
.buttonStyle(WindowActionButton(action: .cancel))
|
||||
.padding()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
.padding(EdgeInsets(top: -25, leading: 0, bottom: 25, trailing: 0))
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
//
|
||||
// UpdateView.swift
|
||||
// Pearcleaner
|
||||
//
|
||||
// Created by Alin Lupascu on 11/16/23.
|
||||
//
|
||||
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct UpdateView: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
HStack {
|
||||
|
||||
Spacer()
|
||||
.frame(width: appState.progressBar.1 != 1.0 ? 150 : 180)
|
||||
|
||||
Text("\(appState.progressBar.1 != 1.0 ? "Update Available 🥳" : "Completed 🚀")")
|
||||
.font(.title)
|
||||
.bold()
|
||||
.padding(.vertical)
|
||||
|
||||
Spacer()
|
||||
|
||||
Text("v\(appState.releases.first?.tag_name ?? "")")
|
||||
.font(.title3)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundStyle(Color.white.opacity(0.5))
|
||||
.padding()
|
||||
|
||||
}
|
||||
.background(.blue)
|
||||
|
||||
Spacer()
|
||||
|
||||
ScrollView {
|
||||
Text("\(appState.releases.first?.body ?? "No release information")")
|
||||
.font(.body)
|
||||
.multilineTextAlignment(.leading)
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
.frame(height: 160)
|
||||
.padding()
|
||||
|
||||
Spacer()
|
||||
|
||||
|
||||
VStack() {
|
||||
ProgressView("\(appState.progressBar.0)", value: appState.progressBar.1, total: 1.0)
|
||||
.progressViewStyle(LinearProgressViewStyle(tint: .blue))
|
||||
.frame(height: 10)
|
||||
}
|
||||
.padding()
|
||||
|
||||
|
||||
HStack(alignment: .center, spacing: 20) {
|
||||
Button(action: NewWin.close) {
|
||||
Text("Cancel")
|
||||
}
|
||||
.buttonStyle(WindowActionButton(action: .cancel))
|
||||
|
||||
if appState.progressBar.1 != 1.0 {
|
||||
Button(action: {
|
||||
downloadUpdate(appState: appState)
|
||||
}) {
|
||||
Text("Update")
|
||||
}
|
||||
.buttonStyle(WindowActionButton(action: .accept))
|
||||
} else {
|
||||
Button(action: {
|
||||
NewWin.close()
|
||||
relaunchApp(afterDelay: 1)
|
||||
}) {
|
||||
Text("Restart")
|
||||
}
|
||||
.buttonStyle(WindowActionButton(action: .accept))
|
||||
}
|
||||
|
||||
}
|
||||
.padding(.bottom)
|
||||
|
||||
|
||||
|
||||
}
|
||||
.padding(EdgeInsets(top: -25, leading: 0, bottom: 25, trailing: 0))
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
struct NoUpdateView: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
HStack {
|
||||
|
||||
Spacer()
|
||||
.frame(width: appState.progressBar.1 != 1.0 ? 140 : 180)
|
||||
|
||||
Text("\(appState.progressBar.1 != 1.0 ? "No Update Available 😌" : "Completed 🚀")")
|
||||
.font(.title)
|
||||
.bold()
|
||||
.padding(.vertical)
|
||||
|
||||
Spacer()
|
||||
|
||||
Text("v\(appState.releases.first?.tag_name ?? "")")
|
||||
.font(.title3)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundStyle(Color.white.opacity(0.5))
|
||||
.padding()
|
||||
|
||||
}
|
||||
.background(.blue)
|
||||
|
||||
Spacer()
|
||||
|
||||
Text("Pearcleaner is on the most current release available, but you may force a re-download of the same version below.")
|
||||
.font(.body)
|
||||
.lineLimit(3)
|
||||
.multilineTextAlignment(.leading)
|
||||
.padding(20)
|
||||
|
||||
|
||||
|
||||
Spacer()
|
||||
|
||||
VStack() {
|
||||
ProgressView("\(appState.progressBar.0)", value: appState.progressBar.1, total: 1.0)
|
||||
.progressViewStyle(LinearProgressViewStyle(tint: .blue))
|
||||
.frame(height: 10)
|
||||
}
|
||||
.padding()
|
||||
|
||||
HStack(alignment: .center, spacing: 20) {
|
||||
if appState.progressBar.1 < 0.1 {
|
||||
Button(action: {
|
||||
downloadUpdate(appState: appState)
|
||||
}) {
|
||||
Text("Force Update")
|
||||
}
|
||||
.buttonStyle(WindowActionButton(action: .cancel))
|
||||
|
||||
Button(action: {
|
||||
NewWin.close()
|
||||
}) {
|
||||
Text("Okay")
|
||||
}
|
||||
.buttonStyle(WindowActionButton(action: .accept))
|
||||
|
||||
}
|
||||
|
||||
|
||||
if appState.progressBar.1 == 1.0 {
|
||||
Button(action: {
|
||||
NewWin.close()
|
||||
relaunchApp()
|
||||
}) {
|
||||
Text("Restart")
|
||||
}
|
||||
.buttonStyle(WindowActionButton(action: .accept))
|
||||
}
|
||||
}
|
||||
.padding(.bottom)
|
||||
|
||||
|
||||
|
||||
}
|
||||
.padding(EdgeInsets(top: -25, leading: 0, bottom: 25, trailing: 0))
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user