mirror of
https://github.com/exituser/Pearcleaner.git
synced 2026-09-17 09:38:51 +00:00
v3.6.1
This commit is contained in:
@@ -19,7 +19,6 @@ class AppState: ObservableObject
|
||||
@Published var zombieFile: ZombieFile
|
||||
@Published var sortedApps: [AppInfo] = []
|
||||
@Published var selectedItems = Set<URL>()
|
||||
// @Published var selectedZombieItems = Set<URL>()
|
||||
@Published var alertType = AlertType.off
|
||||
@Published var currentView = CurrentDetailsView.empty
|
||||
@Published var showAlert: Bool = false
|
||||
@@ -29,6 +28,7 @@ class AppState: ObservableObject
|
||||
@Published var reload: Bool = false
|
||||
@Published var showProgress: Bool = false
|
||||
@Published var finderExtensionEnabled: Bool = false
|
||||
@Published var updateAvailable: Bool = false
|
||||
|
||||
|
||||
init() {
|
||||
|
||||
@@ -17,8 +17,9 @@ struct SimpleButtonStyle: ButtonStyle {
|
||||
let color: Color
|
||||
let size: CGFloat
|
||||
let padding: CGFloat
|
||||
let rotate: Bool
|
||||
|
||||
init(icon: String, iconFlip: String = "", label: String = "", help: String, color: Color = Color("mode"), size: CGFloat = 20, padding: CGFloat = 5) {
|
||||
init(icon: String, iconFlip: String = "", label: String = "", help: String, color: Color = Color("mode"), size: CGFloat = 20, padding: CGFloat = 5, rotate: Bool = false) {
|
||||
self.icon = icon
|
||||
self.iconFlip = iconFlip
|
||||
self.label = label
|
||||
@@ -26,6 +27,7 @@ struct SimpleButtonStyle: ButtonStyle {
|
||||
self.color = color
|
||||
self.size = size
|
||||
self.padding = padding
|
||||
self.rotate = rotate
|
||||
}
|
||||
|
||||
func makeBody(configuration: Self.Configuration) -> some View {
|
||||
@@ -34,6 +36,9 @@ struct SimpleButtonStyle: ButtonStyle {
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: size, height: size)
|
||||
.scaleEffect(hovered ? 1.1 : 1.0)
|
||||
.rotationEffect(.degrees(rotate ? (hovered ? 90 : 0) : 0))
|
||||
.animation(.easeInOut(duration: 0.2), value: hovered)
|
||||
if !label.isEmpty {
|
||||
Text(label)
|
||||
}
|
||||
@@ -158,24 +163,33 @@ struct SimpleCheckboxToggleStyle: ToggleStyle {
|
||||
struct InfoButton: View {
|
||||
@State private var isPopoverPresented: Bool = false
|
||||
let text: String
|
||||
let color: Color?
|
||||
let label: String?
|
||||
let color: Color
|
||||
let label: String
|
||||
let warning: Bool
|
||||
|
||||
init(text: String, color: Color = Color("mode"), label: String = "", warning: Bool = false) {
|
||||
self.text = text
|
||||
self.color = color
|
||||
self.label = label
|
||||
self.warning = warning
|
||||
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Button(action: {
|
||||
self.isPopoverPresented.toggle()
|
||||
}) {
|
||||
HStack(alignment: .center, spacing: 5) {
|
||||
Image(systemName: "info.circle.fill")
|
||||
Image(systemName: !warning ? "info.circle.fill" : "exclamationmark.triangle.fill")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 14, height: 14)
|
||||
.foregroundColor(color?.opacity(0.7) ?? Color("mode").opacity(0.7))
|
||||
.foregroundColor(!warning ? color.opacity(0.7) : color)
|
||||
.frame(height: 16)
|
||||
if !label!.isEmpty {
|
||||
Text(label!)
|
||||
if !label.isEmpty {
|
||||
Text(label)
|
||||
.font(.callout)
|
||||
.foregroundColor(color?.opacity(0.7) ?? Color("mode").opacity(0.7))
|
||||
.foregroundColor(color.opacity(0.7))
|
||||
|
||||
}
|
||||
}
|
||||
@@ -205,12 +219,15 @@ struct InfoButton: View {
|
||||
|
||||
|
||||
struct UninstallButton: ButtonStyle {
|
||||
@State private var hovered: Bool = false
|
||||
var isEnabled: Bool
|
||||
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
HStack(alignment: .center, spacing: 10) {
|
||||
Image(systemName: "trash")
|
||||
Image(systemName: !hovered ? "trash" : "trash.fill")
|
||||
.foregroundColor(isEnabled ? .white.opacity(1) : .white.opacity(0.3))
|
||||
.scaleEffect(hovered ? 1.2 : 1.0)
|
||||
.animation(.easeInOut(duration: 0.2), value: hovered)
|
||||
|
||||
Divider()
|
||||
.frame(height: 24)
|
||||
@@ -227,11 +244,7 @@ struct UninstallButton: ButtonStyle {
|
||||
.background(configuration.isPressed ? Color("uninstall").opacity(0.8) : Color("uninstall"))
|
||||
.cornerRadius(8)
|
||||
.onHover { over in
|
||||
if over {
|
||||
NSCursor.pointingHand.push()
|
||||
} else {
|
||||
NSCursor.pop()
|
||||
}
|
||||
hovered = over
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -239,11 +252,14 @@ struct UninstallButton: ButtonStyle {
|
||||
|
||||
|
||||
struct RescanButton: ButtonStyle {
|
||||
@State private var hovered: Bool = false
|
||||
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
HStack(alignment: .center, spacing: 10) {
|
||||
Image(systemName: "arrow.counterclockwise.circle")
|
||||
Image(systemName: !hovered ? "arrow.counterclockwise.circle" : "arrow.counterclockwise.circle.fill")
|
||||
.foregroundColor(.white)
|
||||
.scaleEffect(hovered ? 1.2 : 1.0)
|
||||
.animation(.easeInOut(duration: 0.2), value: hovered)
|
||||
|
||||
Divider()
|
||||
.frame(height: 24)
|
||||
@@ -261,11 +277,7 @@ struct RescanButton: ButtonStyle {
|
||||
.background(configuration.isPressed ? Color("button").opacity(0.8) : Color("button"))
|
||||
.cornerRadius(8)
|
||||
.onHover { over in
|
||||
if over {
|
||||
NSCursor.pointingHand.push()
|
||||
} else {
|
||||
NSCursor.pop()
|
||||
}
|
||||
hovered = over
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -606,12 +618,14 @@ struct WindowActionButton: ButtonStyle {
|
||||
struct SimpleButtonBrightStyle: ButtonStyle {
|
||||
@State private var hovered = false
|
||||
let icon: String
|
||||
let label: String
|
||||
let help: String
|
||||
let color: Color
|
||||
let shield: Bool?
|
||||
|
||||
init(icon: String, help: String, color: Color, shield: Bool? = nil) {
|
||||
init(icon: String, label: String = "", help: String, color: Color, shield: Bool? = nil) {
|
||||
self.icon = icon
|
||||
self.label = label
|
||||
self.help = help
|
||||
self.color = color
|
||||
self.shield = shield
|
||||
@@ -624,6 +638,7 @@ struct SimpleButtonBrightStyle: ButtonStyle {
|
||||
.scaledToFit()
|
||||
.frame(width: 20)
|
||||
.foregroundColor(hovered ? color.opacity(0.5) : color)
|
||||
Text(label)
|
||||
}
|
||||
.padding(5)
|
||||
.onHover { hovering in
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
//
|
||||
// Updater.swift
|
||||
// Pearcleaner
|
||||
//
|
||||
// Created by Alin Lupascu on 5/3/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
extension Release {
|
||||
var modifiedBody: String {
|
||||
return body.replacingOccurrences(of: "- [x]", with: ">").replacingOccurrences(of: "###", with: "")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
func loadGithubReleases(appState: AppState, manual: Bool = false) {
|
||||
let url = URL(string: "https://api.github.com/repos/alienator88/Pearcleaner/releases")!
|
||||
let request = URLRequest(url: url)
|
||||
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 lastFewReleases = Array(decodedResponse.prefix(3)) // Get only the last 3 recent releases
|
||||
appState.releases = lastFewReleases
|
||||
checkForUpdate(appState: appState, manual: manual)
|
||||
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
printOS("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 ?? "" {
|
||||
updateOnMain {
|
||||
appState.updateAvailable = true
|
||||
}
|
||||
} else {
|
||||
if manual {
|
||||
NewWin.show(appState: appState, width: 500, height: 300, newWin: .no_update)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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 }
|
||||
var request = URLRequest(url: url)
|
||||
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
|
||||
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)
|
||||
}
|
||||
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 {
|
||||
appState.progressBar.0 = "Done, please restart!"
|
||||
appState.progressBar.1 = 1.0
|
||||
appState.updateAvailable = false
|
||||
}
|
||||
|
||||
|
||||
} catch {
|
||||
printOS("Error moving downloaded file: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
downloadTask.resume()
|
||||
}
|
||||
|
||||
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/ditto")
|
||||
process.arguments = ["-xk", fileURL, appDirectory]
|
||||
process.standardOutput = FileHandle.nullDevice
|
||||
process.standardError = FileHandle.nullDevice
|
||||
|
||||
try process.run()
|
||||
process.waitUntilExit()
|
||||
|
||||
updateOnMain {
|
||||
appState.progressBar.0 = "Deleting update file"
|
||||
appState.progressBar.1 = 0.8
|
||||
}
|
||||
|
||||
// After unzipping, remove the update file
|
||||
try fileManager.removeItem(atPath: fileURL)
|
||||
|
||||
|
||||
} catch {
|
||||
printOS("Error updating the app: \(error)")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
struct UpdateNotificationView: View {
|
||||
let appState: AppState
|
||||
@State private var hovered: Bool = false
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
|
||||
Text("Update Available")
|
||||
.font(.callout)
|
||||
.opacity(0.5)
|
||||
.padding(.leading, 7)
|
||||
|
||||
Spacer()
|
||||
|
||||
HStack(alignment: .center) {
|
||||
Image(systemName: !hovered ? "square.and.arrow.down" : "square.and.arrow.down.fill")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 14, height: 14)
|
||||
.animation(.easeInOut(duration: 0.2), value: hovered)
|
||||
.foregroundStyle(.white)
|
||||
|
||||
|
||||
Text("v\(appState.releases.first?.tag_name ?? "Update")")
|
||||
.foregroundStyle(.white)
|
||||
|
||||
}
|
||||
.padding(3)
|
||||
.onHover { hovering in
|
||||
withAnimation() {
|
||||
hovered = hovering
|
||||
}
|
||||
}
|
||||
.help("Download latest update")
|
||||
.padding(.horizontal, 5)
|
||||
.padding(.vertical, 4)
|
||||
.background(Color("pear"))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 6))
|
||||
.onTapGesture {
|
||||
NewWin.show(appState: appState, width: 500, height: 440, newWin: .update)
|
||||
}
|
||||
}
|
||||
.frame(height: 30)
|
||||
.padding(5)
|
||||
.background(Color("mode").opacity(0.05))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 6))
|
||||
.padding(.horizontal)
|
||||
.padding(.bottom)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user