mirror of
https://github.com/exituser/Pearcleaner.git
synced 2026-09-17 08:29:07 +00:00
initial
This commit is contained in:
@@ -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()
|
||||
}
|
||||
Reference in New Issue
Block a user