This commit is contained in:
Alin
2023-11-12 20:59:30 -07:00
parent 84fe1c48b6
commit fd652e4c64
56 changed files with 5711 additions and 0 deletions
+172
View File
@@ -0,0 +1,172 @@
//
// About.swift
// Pearcleaner
//
// Created by Alin Lupascu on 11/5/23.
//
import SwiftUI
struct AboutSettingsTab: View {
@EnvironmentObject var appState: AppState
var body: some View {
VStack {
HStack(alignment: .center) {
VStack(alignment: .center, spacing: 10){
Spacer()
Image(nsImage: NSApp.applicationIconImage)
.padding()
// .padding(.bottom, 5)
HStack(alignment: .center, spacing: 20) {
Button("") {
loadGithubReleases(appState: appState, manual: true)
}
.buttonStyle(SimpleButtonStyle(icon: "cloud", help: "Update application", color: Color("AccentColor")))
Button("") {
NSWorkspace.shared.open(URL(string: "https://github.com/ghosted-labs/Pearcleaner")!)
}
.buttonStyle(SimpleButtonStyle(icon: "paperplane", help: "View repository", color: Color("AccentColor")))
}
Spacer()
}
// .padding()
// .padding(.top, 10)
.frame(width: 250)
VStack(alignment: .center, spacing: 20) {
HStack(alignment: .firstTextBaseline) {
Text(Bundle.main.name)
.font(.title)
.bold()
Spacer()
HStack {
Text("v\(Bundle.main.version)")
Text(" (build \(Bundle.main.buildVersion))")
}
}
Divider()
.padding(.top, -8)
VStack(alignment: .leading) {
HStack{
Image(systemName: "applescript.fill")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 16, height: 16)
VStack(alignment: .leading){
Text("Privacy Guides")
Text("Inspired by open-source appcleaner script from Sun Knudsen").font(.footnote).foregroundStyle(.gray)
}
Spacer()
Button("") {
NSWorkspace.shared.open(URL(string: "https://sunknudsen.com/privacy-guides/how-to-clean-uninstall-macos-apps-using-appcleaner-open-source-alternative")!)
}
.buttonStyle(SimpleButtonStyle(icon: "paperplane", help: "View", color: Color("AccentColor")))
}
.padding()
.background(Color("mode").opacity(0.05))
.cornerRadius(8)
HStack{
Image(systemName: "trash.fill")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 16, height: 16)
VStack(alignment: .leading){
Text("AppCleaner")
Text("Inspired by AppCleaner from Freemacsoft").font(.footnote).foregroundStyle(.gray)
}
Spacer()
Button(""){
NSWorkspace.shared.open(URL(string: "https://freemacsoft.net/appcleaner/")!)
}
.buttonStyle(SimpleButtonStyle(icon: "paperplane", help: "View", color: Color("AccentColor")))
}
.padding()
.background(Color("mode").opacity(0.05))
.cornerRadius(8)
// HStack{
// Image(systemName: "paintbrush.fill")
// .resizable()
// .aspectRatio(contentMode: .fit)
// .frame(width: 16, height: 16)
// VStack(alignment: .leading){
// Text("Icon")
// Text("Created by Oviotti on DeviantArt").font(.footnote).foregroundStyle(.gray)
// }
// Spacer()
//
// Button("") {
// NSWorkspace.shared.open(URL(string: "https://www.deviantart.com/oviotti/art/AppCleaner-for-macOS-632260251")!)
// }
// .buttonStyle(SimpleButtonStyle(icon: "paperplane", help: "View", color: Color("AccentColor")))
//
// }
// .padding()
// .background(Color("mode").opacity(0.05))
// .cornerRadius(8)
}
Spacer()
}
.padding(.trailing)
// .padding()
// .padding(.top, 30)
}
Spacer()
Text("Made with ❤️ by Alin Lupascu").font(.footnote).padding(.bottom)
}
.padding(20)
.frame(width: 750, height: 325)
}
}
extension Bundle {
var name: String {
func string(for key: String) -> String? {
object(forInfoDictionaryKey: key) as? String
}
return string(for: "CFBundleDisplayName")
?? string(for: "CFBundleName")
?? "N/A"
}
var version: String {
infoDictionary?["CFBundleShortVersionString"] as? String ?? "N/A"
}
var buildVersion: String {
infoDictionary?["CFBundleVersion"] as? String ?? "N/A"
}
}
+64
View File
@@ -0,0 +1,64 @@
//
// General.swift
// Pearcleaner
//
// Created by Alin Lupascu on 11/5/23.
//
import Foundation
import SwiftUI
struct GeneralSettingsTab: View {
@EnvironmentObject var appState: AppState
@AppStorage("settings.general.glass") private var glass: Bool = true
// @AppStorage("settings.general.ants") private var ants: Bool = false
var body: some View {
Form {
VStack {
HStack {
VStack(alignment: .leading, spacing: 5) {
Text("Transparency").font(.title2)
Text("Toggles the sidebar material on the app list drawer")
.font(.footnote)
.foregroundStyle(.gray)
}
Spacer()
Toggle(isOn: $glass, label: {
})
.toggleStyle(.switch)
}
.padding()
.background(
RoundedRectangle(cornerRadius: 8)
.fill(Color("mode").opacity(0.05))
)
// HStack {
// VStack(alignment: .leading, spacing: 5) {
// Text("Animation").font(.title2)
// Text("Toggles the marching ants animation on the drop zone")
// .font(.footnote)
// .foregroundStyle(.gray)
// }
// Spacer()
// Toggle(isOn: $ants, label: {
// })
// .toggleStyle(.switch)
// }
// .padding()
// .background(
// RoundedRectangle(cornerRadius: 8)
// .fill(Color("mode").opacity(0.05))
// )
Spacer()
}
}
.padding(20)
.frame(width: 400, height: 100)
}
}
+104
View File
@@ -0,0 +1,104 @@
//
// Permissions.swift
// Pearcleaner
//
// Created by Alin Lupascu on 11/5/23.
//
import Foundation
import SwiftUI
struct PermissionsSettingsTab: View {
@EnvironmentObject var appState: AppState
@State private var diskStatus: Bool = false
@State private var accessStatus: Bool = false
var body: some View {
Form {
VStack(alignment: .leading, spacing: 10) {
HStack(spacing: 15) {
Image(systemName: "internaldrive")
.resizable()
.scaledToFit()
.frame(width: 20)
VStack(alignment: .leading, spacing: 5) {
Text("Full Disk")
Text(diskStatus ? "Permission granted" : "Permission not granted")
.font(.footnote)
.foregroundStyle(diskStatus ? .green : .red)
}
Spacer()
Button("") {
if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles") {
NSWorkspace.shared.open(url)
}
}
.buttonStyle(SimpleButtonStyle(icon: "paperplane", help: "View disk permission pane", color: Color("AccentColor")))
}
.padding()
.background(
RoundedRectangle(cornerRadius: 8)
.fill(Color("mode").opacity(0.05))
)
HStack(spacing: 15) {
Image(systemName: "accessibility")
.resizable()
.scaledToFit()
.frame(width: 20)
VStack(alignment: .leading, spacing: 5) {
Text("Accessibility")
Text(accessStatus ? "Permission granted" : "Permission not granted")
.font(.footnote)
.foregroundStyle(accessStatus ? .green : .red)
}
Spacer()
Button("") {
if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility") {
NSWorkspace.shared.open(url)
}
}
.buttonStyle(SimpleButtonStyle(icon: "paperplane", help: "View accessibility pane", color: Color("AccentColor")))
}
.padding()
.background(
RoundedRectangle(cornerRadius: 8)
.fill(Color("mode").opacity(0.05))
)
// HStack(alignment: .center) {
// Spacer()
// Button("") {
// relaunchApp()
// }
// .buttonStyle(SimpleButtonStyle(icon: "restart", help: "Restart application", color: Color("AccentColor")))
// Text("Restart")
// Spacer()
// }
// .padding()
}
}
.padding(20)
.frame(width: 400, height: 200)
.onAppear {
diskStatus = checkAndRequestFullDiskAccess(appState: appState, skipAlert: true)
accessStatus = checkAndRequestAccessibilityAccess(appState: appState)
}
}
}
+52
View File
@@ -0,0 +1,52 @@
//
// Sentinel.swift
// Pearcleaner
//
// Created by Alin Lupascu on 11/9/23.
//
import Foundation
import SwiftUI
struct SentinelSettingsTab: View {
@EnvironmentObject var appState: AppState
@AppStorage("settings.sentinel.enable") private var sentinel: Bool = false
var body: some View {
Form {
VStack {
HStack {
VStack(alignment: .leading, spacing: 5) {
Text("Sentinel Monitor").font(.title2)
Text("Detects when apps are moved to Trash for file removal")
.font(.footnote)
.foregroundStyle(.gray)
}
Spacer()
Toggle(isOn: $sentinel, label: {
})
.toggleStyle(.switch)
.onChange(of: sentinel) { newValue in
if newValue {
launchctl(load: true)
} else {
launchctl(load: false)
}
}
}
.padding()
.background(
RoundedRectangle(cornerRadius: 8)
.fill(Color("mode").opacity(0.05))
)
}
}
.padding(20)
.frame(width: 400, height: 100)
}
}
+57
View File
@@ -0,0 +1,57 @@
//
// SettingsWindow.swift
// Pearcleaner
//
// Created by Alin Lupascu on 10/31/23.
//
import SwiftUI
struct SettingsView: View {
@EnvironmentObject var appState: AppState
var body: some View {
TabView() {
GeneralSettingsTab()
.tabItem {
Label(CurrentTabView.general.title, systemImage: "gear")
}
.tag(CurrentTabView.general)
PermissionsSettingsTab()
.tabItem {
Label(CurrentTabView.permissions.title, systemImage: "lock")
}
.tag(CurrentTabView.permissions)
SentinelSettingsTab()
.tabItem {
Label(CurrentTabView.sentinel.title, systemImage: "eye.circle")
}
.tag(CurrentTabView.sentinel)
UpdateSettingsTab()
.tabItem {
Label(CurrentTabView.update.title, systemImage: "tray.and.arrow.down")
}
.tag(CurrentTabView.update)
AboutSettingsTab()
.tabItem {
Label(CurrentTabView.about.title, systemImage: "info.circle")
}
.tag(CurrentTabView.about)
}
}
}
+186
View File
@@ -0,0 +1,186 @@
//
// Update.swift
// Pearcleaner
//
// Created by Alin Lupascu on 11/5/23.
//
import SwiftUI
import Foundation
struct Release: Codable {
let id: Int
let tag_name: String
let body: String
let assets: [Asset]
}
struct Asset: Codable {
let name: String
let url: String
let browser_download_url: String
}
struct UpdateSettingsTab: View {
@EnvironmentObject var appState: AppState
@State private var showAlert = false
@State private var showDone = false
@AppStorage("settings.updater.updateTimeframe") private var updateTimeframe: Int = 1
var body: some View {
VStack {
HStack {
Text("Check for updates every ") +
Text("**\(updateTimeframe)**").foregroundColor(.red) +
Text(updateTimeframe == 1 ? " day" : " days")
Stepper(value: $updateTimeframe, in: 1...7) {
Text("")
}
}
ScrollView {
VStack() {
ForEach(appState.releases, id: \.id) { release in
VStack(alignment: .leading) {
LabeledDivider(label: "\(release.tag_name)")
Text(release.body)
}
}
}
.padding()
}
.frame(minHeight: 0, maxHeight: .infinity)
.frame(minWidth: 0, maxWidth: .infinity)
.padding()
Text("Showing last 3").opacity(0.5).font(.footnote)
HStack(alignment: .center, spacing: 5) {
Spacer()
Button("Check"){
loadGithubReleases(appState: appState)
}.buttonStyle(BorderedButtonStyle())
Button("Force Update"){
loadGithubReleases(appState: appState, manual: true)
}.buttonStyle(BorderedButtonStyle())
Button("GitHub"){
NSWorkspace.shared.open(URL(string: "https://github.com/alienator88/Pearcleaner/releases")!)
}.buttonStyle(BorderedButtonStyle())
Spacer()
}
.padding()
}
.padding(20)
.frame(width: 500, height: 400)
}
}
func loadGithubReleases(appState: AppState, manual: Bool = false) {
let url = URL(string: "https://api.github.com/repos/alienator88/Pearcleaner/releases")!
let request = URLRequest(url: url)
// request.setValue("token \(ghToken)", forHTTPHeaderField: "Authorization")
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
if let decodedResponse = try? JSONDecoder().decode([Release].self, from: data) {
DispatchQueue.main.async {
let lastFiveReleases = Array(decodedResponse.prefix(3)) // Get only the last 3 recent releases
appState.releases = lastFiveReleases
checkForUpdate(appState: appState, manual: manual)
}
return
}
}
print("Fetch failed: \(error?.localizedDescription ?? "Unknown error")")
}.resume()
}
func checkForUpdate(appState: AppState, manual: Bool = false) {
guard let latestRelease = appState.releases.first else { return }
let currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
if latestRelease.tag_name > currentVersion ?? "" {
appState.alertType = .update
appState.showAlert = true
} else {
if manual {
appState.alertType = .no_update
appState.showAlert = true
}
}
}
func downloadUpdate(appState: AppState) {
guard let latestRelease = appState.releases.first else { return }
guard let asset = latestRelease.assets.first else { return }
guard let url = URL(string: asset.url) else { return }
var request = URLRequest(url: url)
// request.setValue("token \(ghToken)", forHTTPHeaderField: "Authorization")
request.setValue("application/octet-stream", forHTTPHeaderField: "Accept")
let downloadTask = URLSession.shared.downloadTask(with: request) { localURL, urlResponse, error in
guard let localURL = localURL else { return }
let fileManager = FileManager.default
let destinationURL = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!.appendingPathComponent("Pearcleaner").appendingPathComponent("\(asset.name)")
do {
if fileManager.fileExists(atPath: destinationURL.path) {
try fileManager.removeItem(at: destinationURL)
}
try fileManager.moveItem(at: localURL, to: destinationURL)
UnzipAndReplace(DownloadedFileURL: destinationURL.path, appState: appState) {
updateOnMain {
// Restart App
relaunchApp(afterDelay: 1)
}
}
} catch {
print("Error moving downloaded file: \(error.localizedDescription)")
}
}
downloadTask.resume()
}
func UnzipAndReplace(DownloadedFileURL fileURL: String, appState: AppState, completion: @escaping () -> Void = {}) {
let appDirectory = Bundle.main.bundleURL.deletingLastPathComponent().path
let appBundle = Bundle.main.bundleURL.path
let fileManager = FileManager.default
do {
// Remove the old version of your app
try fileManager.removeItem(atPath: appBundle)
// Unzip the downloaded update file to your app's bundle path
let process = Process()
process.executableURL = URL(fileURLWithPath: "/usr/bin/unzip")
process.arguments = [fileURL, "-d", appDirectory]
try process.run()
process.waitUntilExit()
// After unzipping, remove the update file
try fileManager.removeItem(atPath: fileURL)
} catch {
print("Error updating the app: \(error)")
}
completion()
}