This commit is contained in:
Alin
2024-05-03 12:35:06 -06:00
parent 7ca691ee91
commit d303cb0729
15 changed files with 326 additions and 234 deletions
+1 -1
View File
@@ -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() {
+36 -21
View File
@@ -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
+218
View File
@@ -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)
}
}