mirror of
https://github.com/exituser/Pearcleaner.git
synced 2026-09-17 10:48:52 +00:00
Updates
This commit is contained in:
@@ -6,17 +6,22 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import AlinFoundation
|
||||
|
||||
struct AppCommands: Commands {
|
||||
|
||||
let appState: AppState
|
||||
let locations: Locations
|
||||
let fsm: FolderSettingsManager
|
||||
let updater: Updater
|
||||
let themeManager: ThemeManager
|
||||
|
||||
init(appState: AppState, locations: Locations, fsm: FolderSettingsManager) {
|
||||
init(appState: AppState, locations: Locations, fsm: FolderSettingsManager, updater: Updater, themeManager: ThemeManager) {
|
||||
self.appState = appState
|
||||
self.locations = locations
|
||||
self.fsm = fsm
|
||||
self.updater = updater
|
||||
self.themeManager = themeManager
|
||||
}
|
||||
|
||||
var body: some Commands {
|
||||
@@ -25,7 +30,7 @@ struct AppCommands: Commands {
|
||||
CommandGroup(replacing: .appInfo) {
|
||||
|
||||
Button {
|
||||
loadGithubReleases(appState: appState, manual: true)
|
||||
updater.checkForUpdates()
|
||||
} label: {
|
||||
Text("Check for Updates")
|
||||
}
|
||||
@@ -55,12 +60,15 @@ struct AppCommands: Commands {
|
||||
|
||||
Button
|
||||
{
|
||||
undoTrash(appState: appState) {
|
||||
reloadAppsList(appState: appState, fsm: fsm)
|
||||
for app in appState.trashedFiles {
|
||||
AppPathFinder(appInfo: app, appState: appState, locations: locations, undo: true).findPaths()
|
||||
if appState.currentView != .zombie {
|
||||
undoTrash(appState: appState) {
|
||||
reloadAppsList(appState: appState, fsm: fsm)
|
||||
for app in appState.trashedFiles {
|
||||
AppPathFinder(appInfo: app, appState: appState, locations: locations, undo: true).findPaths()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} label: {
|
||||
Label("Undo Removal", systemImage: "clear")
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import AlinFoundation
|
||||
|
||||
class AppInfoFetcher {
|
||||
static let fileManager = FileManager.default
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import Foundation
|
||||
import AppKit
|
||||
import SwiftUI
|
||||
|
||||
import AlinFoundation
|
||||
|
||||
class AppPathFinder {
|
||||
private var appInfo: AppInfo
|
||||
|
||||
@@ -22,15 +22,14 @@ class AppState: ObservableObject {
|
||||
@Published var currentView = CurrentDetailsView.empty
|
||||
@Published var showAlert: Bool = false
|
||||
@Published var sidebar: Bool = true
|
||||
@Published var releases = [Release]()
|
||||
@Published var progressBar: (String, Double) = ("Ready", 0.0)
|
||||
@Published var reload: Bool = false
|
||||
@Published var showProgress: Bool = false
|
||||
@Published var finderExtensionEnabled: Bool = false
|
||||
@Published var updateAvailable: Bool = false
|
||||
// @Published var updateAvailable: Bool = false
|
||||
@Published var featureAvailable: Bool = false
|
||||
@Published var permissionsOkay: Bool = true
|
||||
@Published var permissionResults: PermissionsCheckResults?
|
||||
// @Published var permissionsOkay: Bool = true
|
||||
// @Published var permissionResults: PermissionsCheckResults?
|
||||
@Published var showUninstallAlert: Bool = false
|
||||
@Published var oneShotMode: Bool = false
|
||||
@Published var showConditionBuilder: Bool = false
|
||||
@@ -182,8 +181,6 @@ enum CurrentDetailsView:Int
|
||||
|
||||
enum NewWindow:Int
|
||||
{
|
||||
case update
|
||||
case no_update
|
||||
case perm
|
||||
case feature
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import AlinFoundation
|
||||
|
||||
class DeeplinkManager {
|
||||
@Binding var showPopover: Bool
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import AlinFoundation
|
||||
|
||||
|
||||
func getFeatures(appState: AppState, features: Binding<String>) {
|
||||
let url = URL(string: "https://api.github.com/repos/alienator88/Pearcleaner/contents/features.json")!
|
||||
@@ -78,7 +80,7 @@ struct FeatureNotificationView: View {
|
||||
.foregroundStyle(.white)
|
||||
|
||||
|
||||
Text("Show")
|
||||
Text("Check")
|
||||
.foregroundStyle(.white)
|
||||
|
||||
}
|
||||
@@ -103,7 +105,7 @@ struct FeatureNotificationView: View {
|
||||
}
|
||||
.frame(height: 30)
|
||||
.padding(5)
|
||||
.background(Color("mode").opacity(0.05))
|
||||
.background(.primary.opacity(0.05))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 6))
|
||||
.padding(.horizontal)
|
||||
.padding(.bottom)
|
||||
|
||||
@@ -26,7 +26,7 @@ class Locations: ObservableObject {
|
||||
self.tempDir = tempDir
|
||||
|
||||
self.apps = Category(name: "Apps", paths: [
|
||||
"\(home)",
|
||||
// "\(home)", //MARK: Disable searching for files in user's home folder
|
||||
"\(home)/Library",
|
||||
"\(home)/Library/Application Scripts",
|
||||
"\(home)/Library/Application Support",
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import AlinFoundation
|
||||
|
||||
|
||||
// Get all apps from /Applications and ~/Applications
|
||||
@@ -212,14 +213,14 @@ func moveFilesToTrash(appState: AppState, at fileURLs: [URL], completion: @escap
|
||||
if let scriptObject = NSAppleScript(source: scriptSource) {
|
||||
let output: NSAppleEventDescriptor = scriptObject.executeAndReturnError(&error)
|
||||
if let error = error {
|
||||
checkAllPermissions(appState: appState) { results in
|
||||
appState.permissionResults = results
|
||||
if !results.allPermissionsGranted {
|
||||
updateOnMain {
|
||||
appState.permissionsOkay = false
|
||||
}
|
||||
}
|
||||
}
|
||||
// checkAllPermissions(appState: appState) { results in
|
||||
// appState.permissionResults = results
|
||||
// if !results.allPermissionsGranted {
|
||||
// updateOnMain {
|
||||
// appState.permissionsOkay = false
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
printOS("Trash Error: \(error)")
|
||||
DispatchQueue.main.async {
|
||||
completion(false) // Indicate failure
|
||||
@@ -261,14 +262,14 @@ func undoTrash(appState: AppState, completion: @escaping () -> Void = {}) {
|
||||
if let scriptObject = NSAppleScript(source: scriptSource) {
|
||||
let output: NSAppleEventDescriptor = scriptObject.executeAndReturnError(&error)
|
||||
if let error = error {
|
||||
checkAllPermissions(appState: appState) { results in
|
||||
appState.permissionResults = results
|
||||
if !results.allPermissionsGranted {
|
||||
updateOnMain {
|
||||
appState.permissionsOkay = false
|
||||
}
|
||||
}
|
||||
}
|
||||
// checkAllPermissions(appState: appState) { results in
|
||||
// appState.permissionResults = results
|
||||
// if !results.allPermissionsGranted {
|
||||
// updateOnMain {
|
||||
// appState.permissionsOkay = false
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
printOS("Undo Trash Error: \(error)")
|
||||
} else if let outputString = output.stringValue {
|
||||
printOS(outputString)
|
||||
|
||||
@@ -1,176 +0,0 @@
|
||||
//
|
||||
// PermissionChecker.swift
|
||||
// Pearcleaner
|
||||
//
|
||||
// Created by Alin Lupascu on 5/3/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
//import EventKit
|
||||
|
||||
struct PermissionsCheckResults {
|
||||
var fullDiskAccess: Bool
|
||||
var accessibility: Bool
|
||||
var automation: Bool
|
||||
|
||||
// Computed property to check if all permissions are granted
|
||||
var allPermissionsGranted: Bool {
|
||||
return fullDiskAccess && accessibility && automation
|
||||
}
|
||||
}
|
||||
|
||||
func checkAllPermissions(appState: AppState, completion: @escaping (PermissionsCheckResults) -> Void) {
|
||||
let dispatchGroup = DispatchGroup()
|
||||
|
||||
// Check Full Disk Access
|
||||
var fullDiskAccess = false
|
||||
@AppStorage("settings.permissions.hasLaunched") var hasLaunched: 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 pipe = Pipe()
|
||||
process.standardOutput = pipe
|
||||
process.launch()
|
||||
process.waitUntilExit() // Ensure process completes
|
||||
|
||||
let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
||||
let output = String(data: data, encoding: .utf8)
|
||||
fullDiskAccess = (output?.contains("com.alienator88.Pearcleaner") ?? false)
|
||||
|
||||
// Check Accessibility
|
||||
let options: NSDictionary = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: true]
|
||||
let accessibilityEnabled = AXIsProcessTrustedWithOptions(options)
|
||||
|
||||
// Check Automation Permission
|
||||
var automationAccess = false
|
||||
dispatchGroup.enter()
|
||||
checkAutomationPermission(appState: appState) { success in
|
||||
automationAccess = success
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
|
||||
|
||||
// Wait for all async checks to complete
|
||||
dispatchGroup.notify(queue: .main) {
|
||||
let results = PermissionsCheckResults(
|
||||
fullDiskAccess: fullDiskAccess,
|
||||
accessibility: accessibilityEnabled,
|
||||
automation: automationAccess
|
||||
)
|
||||
|
||||
// Check if any permission is denied and show window
|
||||
if !(results.fullDiskAccess && results.accessibility && results.automation) {
|
||||
updateOnMain {
|
||||
appState.permissionsOkay = false
|
||||
}
|
||||
}
|
||||
|
||||
completion(results)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Check Finder Automation permission
|
||||
func checkAutomationPermission(appState: AppState, completion: @escaping (Bool) -> Void) {
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
let scriptText = "tell application \"Finder\" to return name of home"
|
||||
var error: NSDictionary?
|
||||
if let script = NSAppleScript(source: scriptText) {
|
||||
let _ = script.executeAndReturnError(&error)
|
||||
DispatchQueue.main.async {
|
||||
completion(error == nil)
|
||||
}
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
completion(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
struct PermissionsNotificationView: View {
|
||||
let appState: AppState
|
||||
@State private var hovered: Bool = false
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Text("Missing Permissions!")
|
||||
.font(.callout)
|
||||
.opacity(0.5)
|
||||
.padding(.leading, 7)
|
||||
|
||||
Spacer()
|
||||
|
||||
settingsLinkButton
|
||||
}
|
||||
.frame(height: 30)
|
||||
.padding(5)
|
||||
.background(Color("mode").opacity(0.05))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 6))
|
||||
.padding(.horizontal)
|
||||
.padding(.bottom)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var settingsLinkButton: some View {
|
||||
if #available(macOS 14.0, *) {
|
||||
SettingsLink {
|
||||
labelContent
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
.padding(4)
|
||||
.background(Color.red)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 6))
|
||||
.onHover { hover in
|
||||
withAnimation {
|
||||
hovered = hover
|
||||
}
|
||||
}
|
||||
.help("Check all permissions")
|
||||
} else {
|
||||
Button(action: {
|
||||
if #available(macOS 13.0, *) {
|
||||
NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil)
|
||||
}
|
||||
else {
|
||||
NSApp.sendAction(Selector(("showPreferencesWindow:")), to: nil, from: nil)
|
||||
}
|
||||
}) {
|
||||
labelContent
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
.padding(4)
|
||||
.background(Color.red)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 6))
|
||||
.onHover { hover in
|
||||
withAnimation {
|
||||
hovered = hover
|
||||
}
|
||||
}
|
||||
.help("Check all permissions")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var labelContent: some View {
|
||||
HStack(alignment: .center) {
|
||||
Image(systemName: !hovered ? "lock" : "lock.fill")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 14, height: 14)
|
||||
.foregroundStyle(.white)
|
||||
Text("Check")
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
.padding(3)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
import Foundation
|
||||
import AppKit
|
||||
import AlinFoundation
|
||||
|
||||
class ReversePathsSearcher {
|
||||
private let appState: AppState
|
||||
|
||||
+17
-651
@@ -7,88 +7,7 @@
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct SimpleButtonStyle: ButtonStyle {
|
||||
@State private var hovered = false
|
||||
let icon: String
|
||||
let iconFlip: String
|
||||
let label: String
|
||||
let help: String
|
||||
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, rotate: Bool = false) {
|
||||
self.icon = icon
|
||||
self.iconFlip = iconFlip
|
||||
self.label = label
|
||||
self.help = help
|
||||
self.color = color
|
||||
self.size = size
|
||||
self.padding = padding
|
||||
self.rotate = rotate
|
||||
}
|
||||
|
||||
func makeBody(configuration: Self.Configuration) -> some View {
|
||||
HStack(alignment: .center) {
|
||||
Image(systemName: (hovered && !iconFlip.isEmpty) ? iconFlip : icon)
|
||||
.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)
|
||||
}
|
||||
}
|
||||
.foregroundColor(hovered ? color : color.opacity(0.5))
|
||||
.padding(padding)
|
||||
.onHover { hovering in
|
||||
withAnimation() {
|
||||
hovered = hovering
|
||||
}
|
||||
}
|
||||
.scaleEffect(configuration.isPressed ? 0.95 : 1)
|
||||
.help(help)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct RegularButtonStyle: ButtonStyle {
|
||||
let icon: String
|
||||
let label: String
|
||||
let help: String
|
||||
@State private var hovered = false
|
||||
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
|
||||
HStack(alignment: .center) {
|
||||
Image(systemName: icon).foregroundColor(Color("mode")).padding(.leading, 2).padding(.trailing, 0)
|
||||
Text(label).textCase(.uppercase).foregroundColor(Color("mode")).fontWeight(.bold).padding(.trailing, 2)
|
||||
|
||||
}
|
||||
.padding(8)
|
||||
.background(!configuration.isPressed ? hovered ? Color("mode").opacity(0.4) : Color("mode").opacity(0) : Color("mode").opacity(0.5))
|
||||
.cornerRadius(6)
|
||||
.scaleEffect(configuration.isPressed ? 0.95 : 1)
|
||||
.animation(.easeOut(duration: 0.2), value: configuration.isPressed)
|
||||
.onHover { hovering in
|
||||
withAnimation(Animation.easeIn(duration: 0.15)) {
|
||||
hovered = hovering
|
||||
}
|
||||
}
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 6)
|
||||
.strokeBorder(Color("mode").opacity(0.5), lineWidth: 1)
|
||||
.scaleEffect(configuration.isPressed ? 0.95 : 1)
|
||||
)
|
||||
.help(help)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
import AlinFoundation
|
||||
|
||||
struct ResetSettingsButtonStyle: ButtonStyle {
|
||||
@Binding var isResetting: Bool
|
||||
@@ -105,13 +24,13 @@ struct ResetSettingsButtonStyle: ButtonStyle {
|
||||
.frame(width: 15)
|
||||
} else {
|
||||
Image(systemName: "gear")
|
||||
.foregroundColor(Color("mode")).padding(.leading, 2).padding(.trailing, 0)
|
||||
.foregroundColor(.primary).padding(.leading, 2).padding(.trailing, 0)
|
||||
}
|
||||
Text(label)
|
||||
.textCase(.uppercase).foregroundColor(Color("mode")).fontWeight(.bold).padding(.trailing, 2)
|
||||
.textCase(.uppercase).foregroundColor(.primary).fontWeight(.bold).padding(.trailing, 2)
|
||||
}
|
||||
.padding(8)
|
||||
.background(!configuration.isPressed ? hovered ? Color("mode").opacity(0.4) : Color("mode").opacity(0) : Color("mode").opacity(0.5))
|
||||
.background(!configuration.isPressed ? hovered ? Color.primary.opacity(0.4) : Color.primary.opacity(0) : Color.primary.opacity(0.5))
|
||||
.cornerRadius(6)
|
||||
.scaleEffect(configuration.isPressed ? 0.95 : 1)
|
||||
.animation(.easeOut(duration: 0.2), value: configuration.isPressed)
|
||||
@@ -122,7 +41,7 @@ struct ResetSettingsButtonStyle: ButtonStyle {
|
||||
}
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 6)
|
||||
.strokeBorder(Color("mode").opacity(0.5), lineWidth: 1)
|
||||
.strokeBorder(.primary.opacity(0.5), lineWidth: 1)
|
||||
.scaleEffect(configuration.isPressed ? 0.95 : 1)
|
||||
)
|
||||
.help(help)
|
||||
@@ -131,18 +50,18 @@ struct ResetSettingsButtonStyle: ButtonStyle {
|
||||
|
||||
|
||||
struct SimpleCheckboxToggleStyle: ToggleStyle {
|
||||
@EnvironmentObject private var themeSettings: ThemeSettings
|
||||
@EnvironmentObject private var themeManager: ThemeManager
|
||||
@State private var isHovered: Bool = false
|
||||
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
HStack {
|
||||
RoundedRectangle(cornerRadius: 4)
|
||||
.fill(themeSettings.themeColor.darker(by: 5))
|
||||
.fill(themeManager.pickerColor.adjustBrightness(5))
|
||||
.frame(width: 18, height: 18)
|
||||
.overlay {
|
||||
if configuration.isOn {
|
||||
Image(systemName: "checkmark")
|
||||
.foregroundStyle(Color("mode"))
|
||||
.foregroundStyle(.primary)
|
||||
.scaleEffect(isHovered ? 0.8 : 1.0)
|
||||
}
|
||||
}
|
||||
@@ -160,150 +79,6 @@ struct SimpleCheckboxToggleStyle: ToggleStyle {
|
||||
}
|
||||
|
||||
|
||||
struct InfoButton: View {
|
||||
@State private var isPopoverPresented: Bool = false
|
||||
let text: String
|
||||
let color: Color
|
||||
let label: String
|
||||
let warning: Bool
|
||||
let edge: Edge
|
||||
|
||||
init(text: String, color: Color = Color("mode"), label: String = "", warning: Bool = false, edge: Edge = .bottom) {
|
||||
self.text = text
|
||||
self.color = color
|
||||
self.label = label
|
||||
self.warning = warning
|
||||
self.edge = edge
|
||||
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Button(action: {
|
||||
self.isPopoverPresented.toggle()
|
||||
}) {
|
||||
HStack(alignment: .center, spacing: 5) {
|
||||
Image(systemName: !warning ? "info.circle.fill" : "exclamationmark.triangle.fill")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 14, height: 14)
|
||||
.foregroundColor(!warning ? color.opacity(0.7) : color)
|
||||
.frame(height: 16)
|
||||
if !label.isEmpty {
|
||||
Text(label)
|
||||
.font(.callout)
|
||||
.foregroundColor(color.opacity(0.7))
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
.onHover { isHovered in
|
||||
if isHovered {
|
||||
NSCursor.pointingHand.push()
|
||||
} else {
|
||||
NSCursor.pop()
|
||||
}
|
||||
}
|
||||
.popover(isPresented: $isPopoverPresented, arrowEdge: edge) {
|
||||
VStack {
|
||||
Spacer()
|
||||
Text(text)
|
||||
.font(.callout)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
Spacer()
|
||||
}
|
||||
.frame(width: 300)
|
||||
}
|
||||
.padding(.horizontal, 5)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
struct InfoButtonPerms: View {
|
||||
@State private var isPopoverPresented: Bool = false
|
||||
let color: Color
|
||||
let label: String
|
||||
let warning: Bool
|
||||
|
||||
init(color: Color = Color("mode"), label: String = "", warning: Bool = false) {
|
||||
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: !warning ? "info.circle.fill" : "exclamationmark.triangle.fill")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 14, height: 14)
|
||||
.foregroundColor(!warning ? color.opacity(0.7) : color)
|
||||
.frame(height: 16)
|
||||
if !label.isEmpty {
|
||||
Text(label)
|
||||
.font(.callout)
|
||||
.foregroundColor(color.opacity(0.7))
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
.onHover { isHovered in
|
||||
if isHovered {
|
||||
NSCursor.pointingHand.push()
|
||||
} else {
|
||||
NSCursor.pop()
|
||||
}
|
||||
}
|
||||
.popover(isPresented: $isPopoverPresented, arrowEdge: .bottom) {
|
||||
VStack(alignment: .leading, spacing: 15) {
|
||||
|
||||
HStack(alignment: .top, spacing: 20) {
|
||||
Image(systemName: "externaldrive")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 20, height: 20)
|
||||
.foregroundStyle(Color("mode").opacity(0.5))
|
||||
Text("Full Disk permission to access files/folders in system paths")
|
||||
.font(.callout)
|
||||
.foregroundStyle(Color("mode").opacity(0.5))
|
||||
}
|
||||
|
||||
HStack(alignment: .top, spacing: 20) {
|
||||
Image(systemName: "accessibility")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 20, height: 20)
|
||||
.foregroundStyle(Color("mode").opacity(0.5))
|
||||
Text("Accessibility permission to allow execution of AppleScript")
|
||||
.font(.callout)
|
||||
.foregroundStyle(Color("mode").opacity(0.5))
|
||||
}
|
||||
|
||||
HStack(alignment: .top, spacing: 20) {
|
||||
Image(systemName: "gearshape.2")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 20, height: 20)
|
||||
.foregroundStyle(Color("mode").opacity(0.5))
|
||||
Text("Automation permission to perform delete actions via Finder")
|
||||
.font(.callout)
|
||||
.foregroundStyle(Color("mode").opacity(0.5))
|
||||
}
|
||||
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.padding(.horizontal, 5)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct UninstallButton: ButtonStyle {
|
||||
@State private var hovered: Bool = false
|
||||
@@ -371,40 +146,6 @@ struct RescanButton: ButtonStyle {
|
||||
|
||||
|
||||
|
||||
struct WarningPopoverView: View {
|
||||
var label: String
|
||||
var bodyText: String
|
||||
|
||||
@Binding var isPresented: Bool
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Image(systemName: "exclamationmark.triangle.fill")
|
||||
.foregroundStyle(.red)
|
||||
.popover(isPresented: $isPresented, arrowEdge: .top) {
|
||||
VStack {
|
||||
Spacer()
|
||||
Text(bodyText)
|
||||
.font(.callout)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
Spacer()
|
||||
}
|
||||
.frame(width: 300)
|
||||
}
|
||||
|
||||
Text(label)
|
||||
.foregroundStyle(Color.red)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.onTapGesture {
|
||||
isPresented.toggle()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public struct NewFeatureView: View {
|
||||
var text: String
|
||||
var mini: Bool
|
||||
@@ -425,7 +166,7 @@ public struct NewFeatureView: View {
|
||||
showFeature = false
|
||||
}
|
||||
}
|
||||
.buttonStyle(SimpleButtonStyle(icon: "x.circle.fill", help: "Close", color: (Color("mode").opacity(0.5))))
|
||||
.buttonStyle(SimpleButtonStyle(icon: "x.circle.fill", help: "Close", color: (.primary.opacity(0.5))))
|
||||
.onHover { isHovered in
|
||||
if isHovered {
|
||||
NSCursor.pointingHand.push()
|
||||
@@ -451,7 +192,7 @@ public struct NewFeatureView: View {
|
||||
.cornerRadius(10)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.strokeBorder(Color("mode").opacity(0.2), lineWidth: 0.6)
|
||||
.strokeBorder(.primary.opacity(0.2), lineWidth: 0.6)
|
||||
)
|
||||
.padding()
|
||||
}
|
||||
@@ -459,7 +200,7 @@ public struct NewFeatureView: View {
|
||||
|
||||
|
||||
public struct SlideableDivider: View {
|
||||
@EnvironmentObject private var themeSettings: ThemeSettings
|
||||
@EnvironmentObject private var themeManager: ThemeManager
|
||||
@Binding var dimension: Double
|
||||
@State private var dimensionStart: Double?
|
||||
@State private var handleWidth: Double = 4
|
||||
@@ -471,7 +212,7 @@ public struct SlideableDivider: View {
|
||||
|
||||
public var body: some View {
|
||||
Rectangle()
|
||||
.foregroundStyle(Color("mode").opacity(0.0))
|
||||
.foregroundStyle(.primary.opacity(0.0))
|
||||
.frame(width: 10)
|
||||
.onHover { inside in
|
||||
if inside {
|
||||
@@ -518,117 +259,6 @@ public struct SlideableDivider: View {
|
||||
}
|
||||
|
||||
|
||||
struct LabeledDivider: View {
|
||||
let label: String
|
||||
@EnvironmentObject var appState: AppState
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 0) {
|
||||
Rectangle()
|
||||
.frame(height: 1)
|
||||
.foregroundColor(Color("mode").opacity(0.2))
|
||||
|
||||
Text(label)
|
||||
.textCase(.uppercase)
|
||||
.font(.title2)
|
||||
.foregroundColor(Color("mode").opacity(0.6))
|
||||
.padding(.horizontal, 10)
|
||||
.frame(minWidth: 80)
|
||||
|
||||
Rectangle()
|
||||
.frame(height: 1)
|
||||
.foregroundColor(Color("mode").opacity(0.2))
|
||||
}
|
||||
.frame(minHeight: 35)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct AnimatedSearchStyle: TextFieldStyle {
|
||||
@State private var isHovered = false
|
||||
@FocusState private var isFocused: Bool
|
||||
@Binding var text: String
|
||||
@EnvironmentObject var appState: AppState
|
||||
@EnvironmentObject var fsm: FolderSettingsManager
|
||||
|
||||
func _body(configuration: TextField<Self._Label>) -> some View {
|
||||
|
||||
HStack(spacing: 5) {
|
||||
|
||||
if isHovered || !text.isEmpty {
|
||||
|
||||
configuration
|
||||
.frame(height: 15)
|
||||
.font(.title3)
|
||||
.textFieldStyle(PlainTextFieldStyle())
|
||||
.padding(.leading, 50)
|
||||
|
||||
|
||||
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 {
|
||||
withAnimation {
|
||||
reloadAppsList(appState: appState, fsm: fsm)
|
||||
}
|
||||
|
||||
}
|
||||
.help("Refresh app list")
|
||||
|
||||
|
||||
if text != "" {
|
||||
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 {
|
||||
withAnimation {
|
||||
text = ""
|
||||
}
|
||||
}
|
||||
.help("Clear search")
|
||||
}
|
||||
|
||||
|
||||
} else {
|
||||
HStack {
|
||||
Spacer()
|
||||
Image(systemName: "magnifyingglass")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 18, height: 18)
|
||||
.foregroundColor(isHovered ? Color("mode") : Color("mode").opacity(0.5))
|
||||
}
|
||||
.frame(width: 150)
|
||||
|
||||
}
|
||||
}
|
||||
.padding(8)
|
||||
.overlay(
|
||||
Group {
|
||||
if isHovered || !text.isEmpty {
|
||||
RoundedRectangle(cornerRadius: 6)
|
||||
.strokeBorder(Color("mode").opacity(0.4), lineWidth: 1)
|
||||
.allowsHitTesting(false)
|
||||
.padding(.leading, 50)
|
||||
}
|
||||
}
|
||||
)
|
||||
.onHover { hovering in
|
||||
withAnimation(Animation.easeInOut(duration: 0.4)) {
|
||||
self.isHovered = hovering
|
||||
self.isFocused = hovering
|
||||
}
|
||||
}
|
||||
.focused($isFocused)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct RoundedTextFieldStyle: TextFieldStyle {
|
||||
func _body(configuration: TextField<Self._Label>) -> some View {
|
||||
@@ -638,7 +268,7 @@ struct RoundedTextFieldStyle: TextFieldStyle {
|
||||
.textFieldStyle(.plain)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.strokeBorder(Color("mode").opacity(0.4), lineWidth: 0.8)
|
||||
.strokeBorder(.primary.opacity(0.4), lineWidth: 0.8)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -661,54 +291,6 @@ struct GlassEffect: NSViewRepresentable {
|
||||
|
||||
|
||||
|
||||
struct VerticalDivider: View {
|
||||
|
||||
var color: Color = Color("AccentColor")
|
||||
var width: CGFloat = 1
|
||||
var height: CGFloat = 20
|
||||
var opacity: Double = 1
|
||||
|
||||
var body: some View {
|
||||
Divider()
|
||||
.foregroundColor(color)
|
||||
.frame(width: width, height: height)
|
||||
.opacity(opacity)
|
||||
.ignoresSafeArea(.all)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
struct WindowActionButton: ButtonStyle {
|
||||
enum UserAction {
|
||||
case accept
|
||||
case cancel
|
||||
}
|
||||
@State private var hovered = false
|
||||
let action: UserAction
|
||||
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
configuration.label
|
||||
.frame(width: 100)
|
||||
.padding(10)
|
||||
.background(
|
||||
!configuration.isPressed ?
|
||||
!hovered ?
|
||||
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)
|
||||
.onHover { hovering in
|
||||
withAnimation(Animation.easeIn(duration: 0.15)) {
|
||||
hovered = hovering
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct SimpleButtonBrightStyle: ButtonStyle {
|
||||
@State private var hovered = false
|
||||
@@ -748,165 +330,6 @@ struct SimpleButtonBrightStyle: ButtonStyle {
|
||||
|
||||
|
||||
|
||||
struct FilesViewActionButton: ButtonStyle {
|
||||
enum UserAction {
|
||||
case uninstall
|
||||
case close
|
||||
}
|
||||
@State private var hovered = false
|
||||
let action: UserAction
|
||||
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
configuration.label
|
||||
.frame(width: 70)
|
||||
.padding(6)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 6)
|
||||
.strokeBorder(
|
||||
configuration.isPressed ?
|
||||
Color("AccentColor").opacity(0.7) :
|
||||
hovered ? Color("AccentColor").opacity(0.8) : Color("AccentColor").opacity(0.9), lineWidth: 2
|
||||
)
|
||||
|
||||
|
||||
)
|
||||
.foregroundColor(
|
||||
action == .uninstall ? Color("AccentColor") : Color("mode")
|
||||
)
|
||||
.cornerRadius(6)
|
||||
.onHover { hovering in
|
||||
withAnimation(Animation.easeIn(duration: 0.15)) {
|
||||
hovered = hovering
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
struct SentinelToggleStyle: ToggleStyle {
|
||||
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
|
||||
HStack {
|
||||
|
||||
ZStack{
|
||||
RoundedRectangle(cornerRadius: 50)
|
||||
.fill(configuration.isOn ? greenBG : redBG)
|
||||
.frame(width: 70, height: 40)
|
||||
HStack{
|
||||
Image(systemName: "lock.shield")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 26, height: 26)
|
||||
.padding(.leading, 10)
|
||||
.foregroundColor(.white)
|
||||
Spacer()
|
||||
Image(systemName: "shield")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 26, height: 26)
|
||||
.padding(.trailing, 10)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
|
||||
}
|
||||
.frame(width: 70, height: 40, alignment: .center)
|
||||
.overlay(
|
||||
Circle()
|
||||
.foregroundColor(.white)
|
||||
.shadow(color: .black.opacity(0.5), radius: 3, x: 0, y: 0)
|
||||
.padding(.all, 4)
|
||||
.offset(x: configuration.isOn ? 15 : -15, y: 0)
|
||||
)
|
||||
.overlay(
|
||||
Image(systemName: "power")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 20, height: 20)
|
||||
.foregroundColor(.black.opacity(0.3))
|
||||
.offset(x: configuration.isOn ? 15 : -15, y: 0)
|
||||
)
|
||||
.onTapGesture {
|
||||
withAnimation(.easeInOut(duration: 0.5)) {
|
||||
configuration.isOn.toggle()
|
||||
}
|
||||
}
|
||||
.animation(.default, value: configuration.isOn)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
private let greenBG: AnyShapeStyle = AnyShapeStyle(
|
||||
.green.shadow(.inner(radius: 2, x: 0, y: 1))
|
||||
)
|
||||
|
||||
private let redBG: AnyShapeStyle = AnyShapeStyle(
|
||||
.red.shadow(.inner(radius: 2, x: 0, y: 1))
|
||||
)
|
||||
|
||||
|
||||
|
||||
struct PillPicker: View {
|
||||
@Binding var index: Int
|
||||
var onTapAction: () -> Void
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .center, spacing: 10) {
|
||||
ForEach(0..<3) { i in
|
||||
ZStack {
|
||||
Capsule()
|
||||
.fill(Color.white)
|
||||
.offset(x: (CGFloat(index - i) * 100))
|
||||
.animation(.default, value: index)
|
||||
.opacity(index == i ? 1 : 0)
|
||||
.scaleEffect(index == i ? 1 : 0.70)
|
||||
.shadow(color: Color.black.opacity(0.2), radius: 2, x: 0, y: 2)
|
||||
|
||||
HStack(spacing: 0) {
|
||||
|
||||
Text(i == 0 ? "Apps" : (i == 1 ? "Widgets" : "Plugins"))
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(index == i ? (colorScheme == .dark ? .black : Color("AccentColor")) : Color("mode"))
|
||||
}
|
||||
.padding(5)
|
||||
.frame(height: 25)
|
||||
.background(.white.opacity(0.0001))
|
||||
}
|
||||
.onTapGesture {
|
||||
withAnimation {
|
||||
index = i
|
||||
onTapAction()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(height: 25)
|
||||
.padding(5)
|
||||
.background(
|
||||
Capsule()
|
||||
.fill(colorScheme == .dark ? .white.opacity(0.1) : .black.opacity(0.06))
|
||||
.overlay(
|
||||
Capsule()
|
||||
.stroke(Color.black.opacity(colorScheme == .dark ? 0.8 : 0.2), lineWidth: 1)
|
||||
.blur(radius: 1)
|
||||
.offset(y: 1)
|
||||
)
|
||||
)
|
||||
.clipShape(Capsule())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
struct PearDropView: View {
|
||||
|
||||
var body: some View {
|
||||
@@ -914,9 +337,9 @@ struct PearDropView: View {
|
||||
HStack(spacing: 0) {
|
||||
LinearGradient(gradient: Gradient(colors: [.green, .orange]), startPoint: .leading, endPoint: .trailing)
|
||||
.frame(width: 220)
|
||||
LinearGradient(gradient: Gradient(colors: [.orange, Color("mode").opacity(0.5)]), startPoint: .leading, endPoint: .trailing)
|
||||
LinearGradient(gradient: Gradient(colors: [.orange, .primary.opacity(0.5)]), startPoint: .leading, endPoint: .trailing)
|
||||
.frame(width: 10)
|
||||
LinearGradient(gradient: Gradient(colors: [Color("mode").opacity(0.5), Color("mode").opacity(0.5)]), startPoint: .leading, endPoint: .trailing)
|
||||
LinearGradient(gradient: Gradient(colors: [.primary.opacity(0.5), .primary.opacity(0.5)]), startPoint: .leading, endPoint: .trailing)
|
||||
.frame(width: 300)
|
||||
}
|
||||
.mask(
|
||||
@@ -934,71 +357,14 @@ struct PearDropView: View {
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Used for testing UI bounds
|
||||
extension View {
|
||||
func bounds() -> some View {
|
||||
self.border(Color.red, width: 1)
|
||||
}
|
||||
}
|
||||
|
||||
// Background color/glass setter
|
||||
@ViewBuilder
|
||||
func backgroundView(themeSettings: ThemeSettings, darker: Bool = false, glass: Bool = false) -> some View {
|
||||
func backgroundView(themeManager: ThemeManager = ThemeManager.shared, darker: Bool = false, glass: Bool = false) -> some View {
|
||||
if glass {
|
||||
GlassEffect(material: .sidebar, blendingMode: .behindWindow)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
} else {
|
||||
darker ? themeSettings.themeColor.darker(by: 5).edgesIgnoringSafeArea(.all) : themeSettings.themeColor.edgesIgnoringSafeArea(.all)
|
||||
darker ? themeManager.pickerColor.adjustBrightness(5).edgesIgnoringSafeArea(.all) : themeManager.pickerColor.edgesIgnoringSafeArea(.all)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct PickerModifier: ViewModifier {
|
||||
@State private var isHovered: Bool = false
|
||||
let themeSettings: ThemeSettings
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.buttonStyle(.borderless)
|
||||
.padding(4)
|
||||
// .background {
|
||||
// backgroundView(themeSettings: themeSettings, darker: isHovered)
|
||||
// .clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
// }
|
||||
.onHover { hovering in
|
||||
isHovered = hovering
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
func pickerStyle(themeSettings: ThemeSettings) -> some View {
|
||||
self.modifier(PickerModifier(themeSettings: themeSettings))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Preset view elements
|
||||
struct PresetColor: ButtonStyle {
|
||||
var fillColor: Color
|
||||
var label: String
|
||||
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
HStack(alignment: .center, spacing: 10) {
|
||||
Circle()
|
||||
.frame(width: 20, height: 20)
|
||||
.fixedSize(horizontal: true, vertical: true)
|
||||
.foregroundColor(fillColor)
|
||||
.background(fillColor)
|
||||
.clipShape(Circle())
|
||||
.help(label)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 25)
|
||||
.strokeBorder(Color("mode").opacity(0.8), lineWidth: 1))
|
||||
// Text(label)
|
||||
}
|
||||
.onHover { inside in inside ? NSCursor.pointingHand.push() : NSCursor.pop() }
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
//
|
||||
// ThemeManager.swift
|
||||
// Pearcleaner
|
||||
//
|
||||
// Created by Alin Lupascu on 4/12/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import AppKit
|
||||
|
||||
class ThemeSettings: ObservableObject {
|
||||
static let shared = ThemeSettings()
|
||||
private let userDefaults = UserDefaults.standard
|
||||
private let colorKey = "themeColor"
|
||||
|
||||
@Published var themeColor: Color {
|
||||
didSet {
|
||||
saveThemeColor()
|
||||
}
|
||||
}
|
||||
|
||||
// light - Color(.sRGB, red: 1.0, green: 1.0, blue: 1.0, opacity: 1)
|
||||
// dark - Color(.sRGB, red: 0.149, green: 0.149, blue: 0.149, opacity: 1)
|
||||
// pearcleaner - Color(.sRGB, red: 0.188143, green: 0.208556, blue: 0.262679, opacity: 1)
|
||||
|
||||
init() {
|
||||
themeColor = userDefaults.color(forKey: colorKey) ?? .clear
|
||||
}
|
||||
|
||||
func setupInitialColor(forcedDarkMode: Bool? = nil) {
|
||||
// Determine dark mode either from forced setting or system setting
|
||||
let darkMode = forcedDarkMode ?? isDarkMode()
|
||||
|
||||
// Set the theme color based on the dark mode status
|
||||
themeColor = darkMode ? Color(.sRGB, red: 0.188143, green: 0.208556, blue: 0.262679, opacity: 1) :
|
||||
Color(.sRGB, red: 1.0, green: 1.0, blue: 1.0, opacity: 1)
|
||||
saveThemeColor()
|
||||
}
|
||||
|
||||
private func loadThemeColor() {
|
||||
if let components = UserDefaults.standard.array(forKey: colorKey) as? [CGFloat], components.count == 4 {
|
||||
themeColor = Color(.sRGB, red: components[0], green: components[1], blue: components[2], opacity: components[3])
|
||||
}
|
||||
}
|
||||
|
||||
func saveThemeColor() {
|
||||
let nsColor = NSColor(themeColor)
|
||||
if let components = nsColor.cgColor.components {
|
||||
UserDefaults.standard.set(components, forKey: colorKey)
|
||||
}
|
||||
}
|
||||
|
||||
func resetToDefault(dark: Bool = true) {
|
||||
themeColor = dark ? Color(.sRGB, red: 0.188143, green: 0.208556, blue: 0.262679, opacity: 1) : Color(.sRGB, red: 1.0, green: 1.0, blue: 1.0, opacity: 1)
|
||||
// themeColor = dark ? Color(.sRGB, red: 0.149, green: 0.149, blue: 0.149, opacity: 1) : Color(.sRGB, red: 1.0, green: 1.0, blue: 1.0, opacity: 1)
|
||||
saveThemeColor()
|
||||
}
|
||||
|
||||
func setPreset(preset: String, colorScheme: DisplayMode) {
|
||||
themeColor = getColorForPreset(preset: preset, colorScheme: colorScheme)
|
||||
saveThemeColor()
|
||||
}
|
||||
|
||||
func getColorForPreset(preset: String, colorScheme: DisplayMode) -> Color {
|
||||
let pearcleaner = colorScheme.colorScheme == .light ? Color(.sRGB, red: 0.499549, green: 0.545169, blue: 0.682028, opacity: 1) : Color(.sRGB, red: 0.188143, green: 0.208556, blue: 0.262679, opacity: 1)
|
||||
let solarized = colorScheme.colorScheme == .light ? Color(.sRGB, red: 0.554372, green: 0.6557, blue: 0.734336, opacity: 1) : Color(.sRGB, red: 0.117257, green: 0.22506, blue: 0.249171, opacity: 1)
|
||||
let dracula = colorScheme.colorScheme == .light ? Color(.sRGB, red: 0.567094, green: 0.562125, blue: 0.81285, opacity: 1) : Color(.sRGB, red: 0.268614, green: 0.264737, blue: 0.383503, opacity: 1)
|
||||
let macOS = colorScheme.colorScheme == .light ? Color(.sRGB, red: 1.0, green: 1.0, blue: 1.0, opacity: 1) : Color(.sRGB, red: 0.149, green: 0.149, blue: 0.149, opacity: 1)
|
||||
|
||||
switch preset {
|
||||
case "pearcleaner":
|
||||
return pearcleaner
|
||||
case "solarized":
|
||||
return solarized
|
||||
case "dracula":
|
||||
return dracula
|
||||
case "macOS":
|
||||
return macOS
|
||||
default:
|
||||
return themeColor
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Color {
|
||||
/// Darkens the color by a percentage.
|
||||
func darker(by percentage: CGFloat = 30.0) -> Color {
|
||||
var hsb = (hue: CGFloat(0), saturation: CGFloat(0), brightness: CGFloat(0), alpha: CGFloat(0))
|
||||
NSColor(self).getHue(&hsb.hue, saturation: &hsb.saturation, brightness: &hsb.brightness, alpha: &hsb.alpha)
|
||||
return Color(hue: hsb.hue, saturation: hsb.saturation, brightness: max(hsb.brightness - percentage/100, 0), opacity: hsb.alpha)
|
||||
}
|
||||
|
||||
/// Lightens the color by a percentage.
|
||||
func lighter(by percentage: CGFloat = 30.0) -> Color {
|
||||
var hsb = (hue: CGFloat(0), saturation: CGFloat(0), brightness: CGFloat(0), alpha: CGFloat(0))
|
||||
NSColor(self).getHue(&hsb.hue, saturation: &hsb.saturation, brightness: &hsb.brightness, alpha: &hsb.alpha)
|
||||
return Color(hue: hsb.hue, saturation: hsb.saturation, brightness: min(hsb.brightness + percentage / 100, 1.0), opacity: hsb.alpha)
|
||||
}
|
||||
}
|
||||
|
||||
extension Color {
|
||||
/// Checks if the current themeColor is of a light or dark variant to set the appropriate displayMode on launch
|
||||
func isLight() -> Bool? {
|
||||
guard let components = NSColor(self).cgColor.components, components.count >= 3 else {
|
||||
return nil
|
||||
}
|
||||
// Formula to calculate perceived luminance
|
||||
let brightness = 0.299 * components[0] + 0.587 * components[1] + 0.114 * components[2]
|
||||
return brightness > 0.5
|
||||
}
|
||||
}
|
||||
|
||||
extension UserDefaults {
|
||||
func color(forKey key: String) -> Color? {
|
||||
guard let components = array(forKey: key) as? [CGFloat], components.count == 4 else {
|
||||
return nil
|
||||
}
|
||||
return Color(.sRGB, red: components[0], green: components[1], blue: components[2], opacity: components[3])
|
||||
}
|
||||
|
||||
func setColor(_ color: Color, forKey key: String) {
|
||||
let nsColor = NSColor(color)
|
||||
if let components = nsColor.cgColor.components, components.count == 4 {
|
||||
set(components, forKey: key)
|
||||
} else {
|
||||
removeObject(forKey: key)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,292 +0,0 @@
|
||||
//
|
||||
// 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: "")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// --- Updater functionality
|
||||
|
||||
|
||||
func loadGithubReleases(appState: AppState, manual: Bool = false, releaseOnly: 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
|
||||
if !releaseOnly {
|
||||
checkForUpdate(appState: appState, manual: manual)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
printOS("Fetch failed: \(error?.localizedDescription ?? "Unknown error")")
|
||||
}.resume()
|
||||
}
|
||||
|
||||
func checkForUpdate(appState: AppState, manual: Bool) {
|
||||
guard let latestRelease = appState.releases.first else { return }
|
||||
let currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0.0"
|
||||
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 = "UPDATER: Getting update link"
|
||||
appState.progressBar.1 = 0.1
|
||||
}
|
||||
let fileManager = FileManager.default
|
||||
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 = "UPDATER: Starting download of update file"
|
||||
appState.progressBar.1 = 0.2
|
||||
}
|
||||
|
||||
guard let localURL = localURL else { return }
|
||||
|
||||
let destinationURL = FileManager.default.temporaryDirectory.appendingPathComponent("\(asset.name)")
|
||||
|
||||
do {
|
||||
if fileManager.fileExists(atPath: destinationURL.path) {
|
||||
try? fileManager.removeItem(at: destinationURL)
|
||||
}
|
||||
|
||||
updateOnMain {
|
||||
appState.progressBar.0 = "UPDATER: File downloaded to temp directory"
|
||||
appState.progressBar.1 = 0.3
|
||||
}
|
||||
try fileManager.moveItem(at: localURL, to: destinationURL)
|
||||
|
||||
updateOnMain {
|
||||
appState.progressBar.0 = "UPDATER: File renamed using asset name"
|
||||
appState.progressBar.1 = 0.4
|
||||
}
|
||||
|
||||
UnzipAndReplace(DownloadedFileURL: destinationURL.path, appState: appState)
|
||||
|
||||
} 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 = "UPDATER: Removing currently installed application bundle"
|
||||
appState.progressBar.1 = 0.5
|
||||
}
|
||||
|
||||
// Remove the old version of your app
|
||||
try fileManager.removeItem(atPath: appBundle)
|
||||
|
||||
updateOnMain {
|
||||
appState.progressBar.0 = "UPDATER: Unziping file to original install 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 = "UPDATER: Removing file from temp directory"
|
||||
appState.progressBar.1 = 0.8
|
||||
}
|
||||
|
||||
// After unzipping, remove the update file
|
||||
try fileManager.removeItem(atPath: fileURL)
|
||||
|
||||
updateOnMain {
|
||||
appState.progressBar.0 = "UPDATER: Completed, please restart!"
|
||||
appState.progressBar.1 = 1.0
|
||||
appState.updateAvailable = false
|
||||
}
|
||||
|
||||
} catch {
|
||||
printOS("Error updating the app: \(error)")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// --- Updater check frequency
|
||||
|
||||
enum UpdateFrequency: String, CaseIterable, Identifiable {
|
||||
case none = "Never"
|
||||
case daily = "Daily"
|
||||
case weekly = "Weekly"
|
||||
case monthly = "Monthly"
|
||||
|
||||
var id: String { self.rawValue }
|
||||
|
||||
var interval: TimeInterval? {
|
||||
switch self {
|
||||
case .none:
|
||||
return nil
|
||||
case .daily:
|
||||
return 86400 // 1 day in seconds
|
||||
case .weekly:
|
||||
return 604800 // 7 days in seconds
|
||||
case .monthly:
|
||||
return 2592000 // 30 days in seconds
|
||||
}
|
||||
}
|
||||
|
||||
func updateNextUpdateDate() {
|
||||
guard let updateInterval = self.interval else { return }
|
||||
let newUpdateDate = Calendar.current.startOfDay(for: Date().addingTimeInterval(updateInterval))
|
||||
UserDefaults.standard.set(newUpdateDate.timeIntervalSinceReferenceDate, forKey: "settings.updater.nextUpdateDate")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func checkAndUpdateIfNeeded(appState: AppState) {
|
||||
@AppStorage("settings.updater.updateFrequency") var updateFrequency: UpdateFrequency = .daily
|
||||
@AppStorage("settings.updater.nextUpdateDate") var nextUpdateDate = Date.now.timeIntervalSinceReferenceDate
|
||||
|
||||
guard let updateInterval = updateFrequency.interval else {
|
||||
printOS("Updater: no update frequency set, skipping check")
|
||||
return
|
||||
}
|
||||
|
||||
let now = Date()
|
||||
let nextUpdateDateLocal = Date(timeIntervalSinceReferenceDate: nextUpdateDate)
|
||||
|
||||
if !isSameDay(date1: nextUpdateDateLocal, date2: now) {
|
||||
printOS("Updater: next update date is in the future, skipping (\(nextUpdateDateLocal))")
|
||||
return
|
||||
}
|
||||
|
||||
updateApp(appState: appState)
|
||||
setNextUpdateDate(interval: updateInterval)
|
||||
}
|
||||
|
||||
func updateApp(appState: AppState) {
|
||||
// Perform your update logic here
|
||||
printOS("Updater: checking for update")
|
||||
loadGithubReleases(appState: appState)
|
||||
}
|
||||
|
||||
func setNextUpdateDate(interval: TimeInterval) {
|
||||
let newUpdateDate = Calendar.current.startOfDay(for: Date().addingTimeInterval(interval))
|
||||
UserDefaults.standard.set(newUpdateDate.timeIntervalSinceReferenceDate, forKey: "settings.updater.nextUpdateDate")
|
||||
}
|
||||
|
||||
func isSameDay(date1: Date, date2: Date) -> Bool {
|
||||
return Calendar.current.isDate(date1, inSameDayAs: date2)
|
||||
}
|
||||
|
||||
|
||||
// --- Updater Badge View
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
.onTapGesture {
|
||||
NewWin.show(appState: appState, width: 500, height: 440, newWin: .update)
|
||||
}
|
||||
.help("Download latest update")
|
||||
.padding(.horizontal, 5)
|
||||
.padding(.vertical, 4)
|
||||
.background(Color("pear"))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 6))
|
||||
|
||||
}
|
||||
.frame(height: 30)
|
||||
.padding(5)
|
||||
.background(Color("mode").opacity(0.05))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 6))
|
||||
.padding(.horizontal)
|
||||
.padding(.bottom)
|
||||
}
|
||||
}
|
||||
+318
-319
@@ -7,30 +7,29 @@
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import OSLog
|
||||
import CoreImage
|
||||
import AlinFoundation
|
||||
import AppKit
|
||||
|
||||
// Make updates on main thread
|
||||
func updateOnMain(after delay: Double? = nil, _ updates: @escaping () -> Void) {
|
||||
if let delay = delay {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
|
||||
updates()
|
||||
}
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
updates()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Execute functions on background thread
|
||||
func updateOnBackground(_ updates: @escaping () -> Void) {
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
updates()
|
||||
}
|
||||
}
|
||||
//func updateOnMain(after delay: Double? = nil, _ updates: @escaping () -> Void) {
|
||||
// if let delay = delay {
|
||||
// DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
|
||||
// updates()
|
||||
// }
|
||||
// } else {
|
||||
// DispatchQueue.main.async {
|
||||
// updates()
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//
|
||||
//// Execute functions on background thread
|
||||
//func updateOnBackground(_ updates: @escaping () -> Void) {
|
||||
// DispatchQueue.global(qos: .userInitiated).async {
|
||||
// updates()
|
||||
// }
|
||||
//}
|
||||
|
||||
// Reload apps list
|
||||
func reloadAppsList(appState: AppState, fsm: FolderSettingsManager) {
|
||||
@@ -57,57 +56,57 @@ func resizeWindowAuto(windowSettings: WindowSettings, title: String) {
|
||||
|
||||
|
||||
// Check app directory based on user permission
|
||||
func checkAppDirectoryAndUserRole(completion: @escaping ((isInCorrectDirectory: Bool, isAdmin: Bool)) -> Void) {
|
||||
isCurrentUserAdmin { isAdmin in
|
||||
let bundlePath = Bundle.main.bundlePath as NSString
|
||||
let applicationsDir = "/Applications"
|
||||
let userApplicationsDir = "\(home)/Applications"
|
||||
|
||||
var isInCorrectDirectory = false
|
||||
|
||||
if isAdmin {
|
||||
// Admins can have the app in either /Applications or ~/Applications
|
||||
isInCorrectDirectory = bundlePath.deletingLastPathComponent == applicationsDir ||
|
||||
bundlePath.deletingLastPathComponent == userApplicationsDir
|
||||
} else {
|
||||
// Standard users should only have the app in ~/Applications
|
||||
isInCorrectDirectory = bundlePath.deletingLastPathComponent == userApplicationsDir
|
||||
}
|
||||
|
||||
// Return both conditions: if the app is in the correct directory and if the user is an admin
|
||||
completion((isInCorrectDirectory, isAdmin))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Check if user is admin or standard user
|
||||
func isCurrentUserAdmin(completion: @escaping (Bool) -> Void) {
|
||||
let process = Process()
|
||||
process.executableURL = URL(fileURLWithPath: "/bin/zsh") // Using zsh, macOS default shell
|
||||
process.arguments = ["-c", "groups $(whoami) | grep -q ' admin '"]
|
||||
|
||||
process.terminationHandler = { process in
|
||||
// On macOS, a process's exit status of 0 indicates success (admin group found in this context)
|
||||
completion(process.terminationStatus == 0)
|
||||
}
|
||||
|
||||
do {
|
||||
try process.run()
|
||||
} catch {
|
||||
print("Failed to execute command: \(error)")
|
||||
completion(false)
|
||||
}
|
||||
}
|
||||
//func checkAppDirectoryAndUserRole(completion: @escaping ((isInCorrectDirectory: Bool, isAdmin: Bool)) -> Void) {
|
||||
// isCurrentUserAdmin { isAdmin in
|
||||
// let bundlePath = Bundle.main.bundlePath as NSString
|
||||
// let applicationsDir = "/Applications"
|
||||
// let userApplicationsDir = "\(home)/Applications"
|
||||
//
|
||||
// var isInCorrectDirectory = false
|
||||
//
|
||||
// if isAdmin {
|
||||
// // Admins can have the app in either /Applications or ~/Applications
|
||||
// isInCorrectDirectory = bundlePath.deletingLastPathComponent == applicationsDir ||
|
||||
// bundlePath.deletingLastPathComponent == userApplicationsDir
|
||||
// } else {
|
||||
// // Standard users should only have the app in ~/Applications
|
||||
// isInCorrectDirectory = bundlePath.deletingLastPathComponent == userApplicationsDir
|
||||
// }
|
||||
//
|
||||
// // Return both conditions: if the app is in the correct directory and if the user is an admin
|
||||
// completion((isInCorrectDirectory, isAdmin))
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//
|
||||
//// Check if user is admin or standard user
|
||||
//func isCurrentUserAdmin(completion: @escaping (Bool) -> Void) {
|
||||
// let process = Process()
|
||||
// process.executableURL = URL(fileURLWithPath: "/bin/zsh") // Using zsh, macOS default shell
|
||||
// process.arguments = ["-c", "groups $(whoami) | grep -q ' admin '"]
|
||||
//
|
||||
// process.terminationHandler = { process in
|
||||
// // On macOS, a process's exit status of 0 indicates success (admin group found in this context)
|
||||
// completion(process.terminationStatus == 0)
|
||||
// }
|
||||
//
|
||||
// do {
|
||||
// try process.run()
|
||||
// } catch {
|
||||
// print("Failed to execute command: \(error)")
|
||||
// completion(false)
|
||||
// }
|
||||
//}
|
||||
|
||||
// Check if appearance is dark mode
|
||||
func isDarkMode() -> Bool {
|
||||
return NSApp.effectiveAppearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua
|
||||
}
|
||||
|
||||
// Set app color mode
|
||||
func setAppearance(mode: NSAppearance.Name) {
|
||||
NSApp.appearance = NSAppearance(named: mode)
|
||||
}
|
||||
//func isDarkMode() -> Bool {
|
||||
// return NSApp.effectiveAppearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua
|
||||
//}
|
||||
//
|
||||
//// Set app color mode
|
||||
//func setAppearance(mode: NSAppearance.Name) {
|
||||
// NSApp.appearance = NSAppearance(named: mode)
|
||||
//}
|
||||
|
||||
// Check if Pearcleaner has any windows open
|
||||
func hasWindowOpen() -> Bool {
|
||||
@@ -118,13 +117,13 @@ func hasWindowOpen() -> Bool {
|
||||
}
|
||||
|
||||
// Find and hide/show main app window when using menubar item
|
||||
func findAndHideWindows(named titles: [String]) {
|
||||
for title in titles {
|
||||
if let window = NSApp.windows.first(where: { $0.title == title }) {
|
||||
window.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
//func findAndHideWindows(named titles: [String]) {
|
||||
// for title in titles {
|
||||
// if let window = NSApp.windows.first(where: { $0.title == title }) {
|
||||
// window.close()
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
func findAndSetWindowFrame(named titles: [String], windowSettings: WindowSettings) {
|
||||
windowSettings.registerDefaultWindowSettings() {
|
||||
@@ -139,20 +138,20 @@ func findAndSetWindowFrame(named titles: [String], windowSettings: WindowSetting
|
||||
}
|
||||
|
||||
|
||||
func findAndShowWindows(named titles: [String]) {
|
||||
for title in titles {
|
||||
if let window = NSApp.windows.first(where: { $0.title == title }) {
|
||||
window.makeKeyAndOrderFront(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
//func findAndShowWindows(named titles: [String]) {
|
||||
// for title in titles {
|
||||
// if let window = NSApp.windows.first(where: { $0.title == title }) {
|
||||
// window.makeKeyAndOrderFront(nil)
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
// Copy to clipboard
|
||||
func copyToClipboard(text: String) {
|
||||
let pasteboard = NSPasteboard.general
|
||||
pasteboard.clearContents()
|
||||
pasteboard.setString(text, forType: .string)
|
||||
}
|
||||
//func copyToClipboard(text: String) {
|
||||
// let pasteboard = NSPasteboard.general
|
||||
// pasteboard.clearContents()
|
||||
// pasteboard.setString(text, forType: .string)
|
||||
//}
|
||||
|
||||
|
||||
|
||||
@@ -224,15 +223,15 @@ func saveURLsToFile(urls: Set<URL>, appState: AppState) {
|
||||
|
||||
|
||||
// Check if symlink
|
||||
func isSymlink(atPath path: URL) -> Bool {
|
||||
do {
|
||||
let _ = try path.checkResourceIsReachable()
|
||||
let resourceValues = try path.resourceValues(forKeys: [.isSymbolicLinkKey])
|
||||
return resourceValues.isSymbolicLink == true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
//func isSymlink(atPath path: URL) -> Bool {
|
||||
// do {
|
||||
// let _ = try path.checkResourceIsReachable()
|
||||
// let resourceValues = try path.resourceValues(forKeys: [.isSymbolicLinkKey])
|
||||
// return resourceValues.isSymbolicLink == true
|
||||
// } catch {
|
||||
// return false
|
||||
// }
|
||||
//}
|
||||
|
||||
|
||||
// Open trash folder
|
||||
@@ -297,62 +296,62 @@ func checkAppBundleArchitecture(at appBundlePath: String) -> Arch {
|
||||
|
||||
|
||||
// Convert icon to png so colors render correctly
|
||||
func convertICNSToPNG(icon: NSImage, size: NSSize) -> NSImage? {
|
||||
// Resize the icon to the specified size
|
||||
let resizedIcon = NSImage(size: size)
|
||||
resizedIcon.lockFocus()
|
||||
icon.draw(in: NSRect(x: 0, y: 0, width: size.width, height: size.height))
|
||||
resizedIcon.unlockFocus()
|
||||
|
||||
// Convert the resized icon to PNG format
|
||||
if let resizedImageData = resizedIcon.tiffRepresentation,
|
||||
let resizedBitmap = NSBitmapImageRep(data: resizedImageData),
|
||||
let pngData = resizedBitmap.representation(using: .png, properties: [:]) {
|
||||
return NSImage(data: pngData)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
//func convertICNSToPNG(icon: NSImage, size: NSSize) -> NSImage? {
|
||||
// // Resize the icon to the specified size
|
||||
// let resizedIcon = NSImage(size: size)
|
||||
// resizedIcon.lockFocus()
|
||||
// icon.draw(in: NSRect(x: 0, y: 0, width: size.width, height: size.height))
|
||||
// resizedIcon.unlockFocus()
|
||||
//
|
||||
// // Convert the resized icon to PNG format
|
||||
// if let resizedImageData = resizedIcon.tiffRepresentation,
|
||||
// let resizedBitmap = NSBitmapImageRep(data: resizedImageData),
|
||||
// let pngData = resizedBitmap.representation(using: .png, properties: [:]) {
|
||||
// return NSImage(data: pngData)
|
||||
// }
|
||||
//
|
||||
// return nil
|
||||
//}
|
||||
|
||||
// Get icon for files and folders
|
||||
func getIconForFileOrFolder(atPath path: URL) -> Image? {
|
||||
return Image(nsImage: NSWorkspace.shared.icon(forFile: path.path))
|
||||
}
|
||||
|
||||
func getIconForFileOrFolderNS(atPath path: URL) -> NSImage? {
|
||||
return NSWorkspace.shared.icon(forFile: path.path)
|
||||
}
|
||||
//func getIconForFileOrFolder(atPath path: URL) -> Image? {
|
||||
// return Image(nsImage: NSWorkspace.shared.icon(forFile: path.path))
|
||||
//}
|
||||
//
|
||||
//func getIconForFileOrFolderNS(atPath path: URL) -> NSImage? {
|
||||
// return NSWorkspace.shared.icon(forFile: path.path)
|
||||
//}
|
||||
|
||||
|
||||
// Get average color from image
|
||||
extension NSImage {
|
||||
var averageColor: NSColor? {
|
||||
guard let tiffData = self.tiffRepresentation, let bitmapImage = NSBitmapImageRep(data: tiffData), let inputImage = CIImage(bitmapImageRep: bitmapImage) else { return nil }
|
||||
|
||||
let extentVector = CIVector(x: inputImage.extent.origin.x, y: inputImage.extent.origin.y, z: inputImage.extent.size.width, w: inputImage.extent.size.height)
|
||||
|
||||
guard let filter = CIFilter(name: "CIAreaAverage", parameters: [kCIInputImageKey: inputImage, kCIInputExtentKey: extentVector]) else { return nil }
|
||||
guard let outputImage = filter.outputImage else { return nil }
|
||||
|
||||
var bitmap = [UInt8](repeating: 0, count: 4)
|
||||
let context = CIContext(options: [.workingColorSpace: NSNull()])
|
||||
context.render(outputImage, toBitmap: &bitmap, rowBytes: 4, bounds: CGRect(x: 0, y: 0, width: 1, height: 1), format: CIFormat.RGBA8, colorSpace: nil)
|
||||
|
||||
return NSColor(red: CGFloat(bitmap[0]) / 255, green: CGFloat(bitmap[1]) / 255, blue: CGFloat(bitmap[2]) / 255, alpha: CGFloat(bitmap[3]) / 255)
|
||||
}
|
||||
}
|
||||
//extension NSImage {
|
||||
// var averageColor: NSColor? {
|
||||
// guard let tiffData = self.tiffRepresentation, let bitmapImage = NSBitmapImageRep(data: tiffData), let inputImage = CIImage(bitmapImageRep: bitmapImage) else { return nil }
|
||||
//
|
||||
// let extentVector = CIVector(x: inputImage.extent.origin.x, y: inputImage.extent.origin.y, z: inputImage.extent.size.width, w: inputImage.extent.size.height)
|
||||
//
|
||||
// guard let filter = CIFilter(name: "CIAreaAverage", parameters: [kCIInputImageKey: inputImage, kCIInputExtentKey: extentVector]) else { return nil }
|
||||
// guard let outputImage = filter.outputImage else { return nil }
|
||||
//
|
||||
// var bitmap = [UInt8](repeating: 0, count: 4)
|
||||
// let context = CIContext(options: [.workingColorSpace: NSNull()])
|
||||
// context.render(outputImage, toBitmap: &bitmap, rowBytes: 4, bounds: CGRect(x: 0, y: 0, width: 1, height: 1), format: CIFormat.RGBA8, colorSpace: nil)
|
||||
//
|
||||
// return NSColor(red: CGFloat(bitmap[0]) / 255, green: CGFloat(bitmap[1]) / 255, blue: CGFloat(bitmap[2]) / 255, alpha: CGFloat(bitmap[3]) / 255)
|
||||
// }
|
||||
//}
|
||||
|
||||
|
||||
// Relaunch app
|
||||
func relaunchApp(afterDelay seconds: TimeInterval = 0.5) -> Never {
|
||||
let task = Process()
|
||||
task.launchPath = "/bin/sh"
|
||||
task.arguments = ["-c", "sleep \(seconds); open \"\(Bundle.main.bundlePath)\""]
|
||||
task.launch()
|
||||
|
||||
NSApp.terminate(nil)
|
||||
exit(0)
|
||||
}
|
||||
//func relaunchApp(afterDelay seconds: TimeInterval = 0.5) -> Never {
|
||||
// let task = Process()
|
||||
// task.launchPath = "/bin/sh"
|
||||
// task.arguments = ["-c", "sleep \(seconds); open \"\(Bundle.main.bundlePath)\""]
|
||||
// task.launch()
|
||||
//
|
||||
// NSApp.terminate(nil)
|
||||
// exit(0)
|
||||
//}
|
||||
|
||||
// Check if app is running before deleting app files
|
||||
func killApp(appId: String, completion: @escaping () -> Void = {}) {
|
||||
@@ -416,7 +415,7 @@ func folderImages(for path: String) -> AnyView? {
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 13)
|
||||
.foregroundStyle(Color("mode").opacity(0.5))
|
||||
.foregroundStyle(.primary.opacity(0.5))
|
||||
.help("Container")
|
||||
)
|
||||
} else if path.contains("/Library/Application Scripts/") {
|
||||
@@ -425,7 +424,7 @@ func folderImages(for path: String) -> AnyView? {
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 13)
|
||||
.foregroundStyle(Color("mode").opacity(0.5))
|
||||
.foregroundStyle(.primary.opacity(0.5))
|
||||
.help("Application Script")
|
||||
)
|
||||
} else if path.contains(".plist") {
|
||||
@@ -434,7 +433,7 @@ func folderImages(for path: String) -> AnyView? {
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 13)
|
||||
.foregroundStyle(Color("mode").opacity(0.5))
|
||||
.foregroundStyle(.primary.opacity(0.5))
|
||||
.help("Plist File")
|
||||
)
|
||||
}
|
||||
@@ -461,22 +460,22 @@ func isNested(path: URL) -> Bool {
|
||||
}
|
||||
|
||||
// Check if macOS is 14.0 or higher
|
||||
func isMacOS14OrHigher() -> Bool {
|
||||
if #available(macOS 14, *) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
//func isMacOS14OrHigher() -> Bool {
|
||||
// if #available(macOS 14, *) {
|
||||
// return true
|
||||
// } else {
|
||||
// return false
|
||||
// }
|
||||
//}
|
||||
|
||||
|
||||
|
||||
// --- Extend Int to convert hours to seconds ---
|
||||
extension Int {
|
||||
var daysToSeconds: Double {
|
||||
return Double(self) * 24 * 60 * 60
|
||||
}
|
||||
}
|
||||
//extension Int {
|
||||
// var daysToSeconds: Double {
|
||||
// return Double(self) * 24 * 60 * 60
|
||||
// }
|
||||
//}
|
||||
|
||||
|
||||
// --- Extend String to remove periods, spaces and lowercase the string
|
||||
@@ -495,15 +494,15 @@ extension String {
|
||||
}
|
||||
|
||||
// --- Capitalize first letter of string only
|
||||
extension String {
|
||||
func capitalizingFirstLetter() -> String {
|
||||
return prefix(1).capitalized + dropFirst()
|
||||
}
|
||||
|
||||
mutating func capitalizeFirstLetter() {
|
||||
self = self.capitalizingFirstLetter()
|
||||
}
|
||||
}
|
||||
//extension String {
|
||||
// func capitalizingFirstLetter() -> String {
|
||||
// return prefix(1).capitalized + dropFirst()
|
||||
// }
|
||||
//
|
||||
// mutating func capitalizeFirstLetter() {
|
||||
// self = self.capitalizingFirstLetter()
|
||||
// }
|
||||
//}
|
||||
|
||||
// --- Returns comma separated string as array of strings
|
||||
extension String {
|
||||
@@ -517,18 +516,18 @@ extension String {
|
||||
|
||||
|
||||
// --- Overload the greater than operator ">" to do a semantic check on the string versions
|
||||
extension String {
|
||||
func versionStringToTuple() -> (Int, Int, Int) {
|
||||
let components = self.split(separator: ".").compactMap { Int($0) }
|
||||
return (components[0], components[1], components[2])
|
||||
}
|
||||
|
||||
static func > (lhs: String, rhs: String) -> Bool {
|
||||
let lhsVersion = lhs.versionStringToTuple()
|
||||
let rhsVersion = rhs.versionStringToTuple()
|
||||
return lhsVersion > rhsVersion
|
||||
}
|
||||
}
|
||||
//extension String {
|
||||
// func versionStringToTuple() -> (Int, Int, Int) {
|
||||
// let components = self.split(separator: ".").compactMap { Int($0) }
|
||||
// return (components[0], components[1], components[2])
|
||||
// }
|
||||
//
|
||||
// static func > (lhs: String, rhs: String) -> Bool {
|
||||
// let lhsVersion = lhs.versionStringToTuple()
|
||||
// let rhsVersion = rhs.versionStringToTuple()
|
||||
// return lhsVersion > rhsVersion
|
||||
// }
|
||||
//}
|
||||
|
||||
|
||||
// --- Trash Relationship ---
|
||||
@@ -541,103 +540,103 @@ extension String {
|
||||
//}
|
||||
|
||||
// --- Extend print command to also output to the Console ---
|
||||
func printOS(_ items: Any..., separator: String = " ", terminator: String = "\n") {
|
||||
let message = items.map { "\($0)" }.joined(separator: separator)
|
||||
let log = OSLog(subsystem: "pearcleaner", category: "Application")
|
||||
os_log("%@", log: log, type: .default, message)
|
||||
}
|
||||
//func printOS(_ items: Any..., separator: String = " ", terminator: String = "\n") {
|
||||
// let message = items.map { "\($0)" }.joined(separator: separator)
|
||||
// let log = OSLog(subsystem: "pearcleaner", category: "Application")
|
||||
// os_log("%@", log: log, type: .default, message)
|
||||
//}
|
||||
|
||||
|
||||
// Get size of files
|
||||
func totalSizeOnDisk(for paths: [URL]) -> (real: Int64, logical: Int64) {
|
||||
let fileManager = FileManager.default
|
||||
var totalAllocatedSize: Int64 = 0
|
||||
var totalFileSize: Int64 = 0
|
||||
|
||||
for url in paths {
|
||||
var isDirectory: ObjCBool = false
|
||||
if fileManager.fileExists(atPath: url.path, isDirectory: &isDirectory) {
|
||||
let keys: [URLResourceKey] = [.totalFileAllocatedSizeKey, .fileSizeKey]
|
||||
if isDirectory.boolValue {
|
||||
// It's a directory, recurse into it
|
||||
if let enumerator = fileManager.enumerator(at: url, includingPropertiesForKeys: keys, errorHandler: nil) {
|
||||
for case let fileURL as URL in enumerator {
|
||||
do {
|
||||
let fileAttributes = try fileURL.resourceValues(forKeys: Set(keys))
|
||||
if let allocatedSize = fileAttributes.totalFileAllocatedSize {
|
||||
totalAllocatedSize += Int64(allocatedSize)
|
||||
}
|
||||
if let fileSize = fileAttributes.fileSize {
|
||||
totalFileSize += Int64(fileSize)
|
||||
}
|
||||
} catch {
|
||||
print("Error getting file attributes for \(fileURL): \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// It's a file
|
||||
do {
|
||||
let fileAttributes = try url.resourceValues(forKeys: Set(keys))
|
||||
if let allocatedSize = fileAttributes.totalFileAllocatedSize {
|
||||
totalAllocatedSize += Int64(allocatedSize)
|
||||
}
|
||||
if let fileSize = fileAttributes.fileSize {
|
||||
totalFileSize += Int64(fileSize)
|
||||
}
|
||||
} catch {
|
||||
print("Error getting file attributes for \(url): \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (real: totalAllocatedSize, logical: totalFileSize)
|
||||
}
|
||||
|
||||
|
||||
|
||||
func totalSizeOnDisk(for path: URL) -> (real: Int64, logical: Int64) {
|
||||
return totalSizeOnDisk(for: [path])
|
||||
}
|
||||
|
||||
// ByteFormatter
|
||||
func formatByte(size: Int64) -> (human: String, byte: String) {
|
||||
let byteCountFormatter = ByteCountFormatter()
|
||||
byteCountFormatter.countStyle = .file
|
||||
byteCountFormatter.allowedUnits = [.useAll]
|
||||
let human = byteCountFormatter.string(fromByteCount: size)
|
||||
|
||||
let numberformatter = NumberFormatter()
|
||||
numberformatter.numberStyle = .decimal
|
||||
let formattedNumber = numberformatter.string(from: NSNumber(value: size)) ?? "\(size)"
|
||||
let byte = "\(formattedNumber)"
|
||||
|
||||
return (human: human, byte: byte)
|
||||
|
||||
}
|
||||
//func totalSizeOnDisk(for paths: [URL]) -> (real: Int64, logical: Int64) {
|
||||
// let fileManager = FileManager.default
|
||||
// var totalAllocatedSize: Int64 = 0
|
||||
// var totalFileSize: Int64 = 0
|
||||
//
|
||||
// for url in paths {
|
||||
// var isDirectory: ObjCBool = false
|
||||
// if fileManager.fileExists(atPath: url.path, isDirectory: &isDirectory) {
|
||||
// let keys: [URLResourceKey] = [.totalFileAllocatedSizeKey, .fileSizeKey]
|
||||
// if isDirectory.boolValue {
|
||||
// // It's a directory, recurse into it
|
||||
// if let enumerator = fileManager.enumerator(at: url, includingPropertiesForKeys: keys, errorHandler: nil) {
|
||||
// for case let fileURL as URL in enumerator {
|
||||
// do {
|
||||
// let fileAttributes = try fileURL.resourceValues(forKeys: Set(keys))
|
||||
// if let allocatedSize = fileAttributes.totalFileAllocatedSize {
|
||||
// totalAllocatedSize += Int64(allocatedSize)
|
||||
// }
|
||||
// if let fileSize = fileAttributes.fileSize {
|
||||
// totalFileSize += Int64(fileSize)
|
||||
// }
|
||||
// } catch {
|
||||
// print("Error getting file attributes for \(fileURL): \(error)")
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// } else {
|
||||
// // It's a file
|
||||
// do {
|
||||
// let fileAttributes = try url.resourceValues(forKeys: Set(keys))
|
||||
// if let allocatedSize = fileAttributes.totalFileAllocatedSize {
|
||||
// totalAllocatedSize += Int64(allocatedSize)
|
||||
// }
|
||||
// if let fileSize = fileAttributes.fileSize {
|
||||
// totalFileSize += Int64(fileSize)
|
||||
// }
|
||||
// } catch {
|
||||
// print("Error getting file attributes for \(url): \(error)")
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return (real: totalAllocatedSize, logical: totalFileSize)
|
||||
//}
|
||||
//
|
||||
//
|
||||
//
|
||||
//func totalSizeOnDisk(for path: URL) -> (real: Int64, logical: Int64) {
|
||||
// return totalSizeOnDisk(for: [path])
|
||||
//}
|
||||
//
|
||||
//// ByteFormatter
|
||||
//func formatByte(size: Int64) -> (human: String, byte: String) {
|
||||
// let byteCountFormatter = ByteCountFormatter()
|
||||
// byteCountFormatter.countStyle = .file
|
||||
// byteCountFormatter.allowedUnits = [.useAll]
|
||||
// let human = byteCountFormatter.string(fromByteCount: size)
|
||||
//
|
||||
// let numberformatter = NumberFormatter()
|
||||
// numberformatter.numberStyle = .decimal
|
||||
// let formattedNumber = numberformatter.string(from: NSNumber(value: size)) ?? "\(size)"
|
||||
// let byte = "\(formattedNumber)"
|
||||
//
|
||||
// return (human: human, byte: byte)
|
||||
//
|
||||
//}
|
||||
|
||||
|
||||
// Only process supported files
|
||||
func isSupportedFileType(at path: String) -> Bool {
|
||||
let fileManager = FileManager.default
|
||||
do {
|
||||
let attributes = try fileManager.attributesOfItem(atPath: path)
|
||||
if let fileType = attributes[FileAttributeKey.type] as? FileAttributeType {
|
||||
switch fileType {
|
||||
case .typeRegular, .typeDirectory, .typeSymbolicLink:
|
||||
// The file is a regular file, directory, or symbolic link
|
||||
return true
|
||||
default:
|
||||
// The file is a socket, pipe, or another type not supported
|
||||
return false
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
printOS("Error getting file attributes: \(error)")
|
||||
}
|
||||
return false
|
||||
}
|
||||
//func isSupportedFileType(at path: String) -> Bool {
|
||||
// let fileManager = FileManager.default
|
||||
// do {
|
||||
// let attributes = try fileManager.attributesOfItem(atPath: path)
|
||||
// if let fileType = attributes[FileAttributeKey.type] as? FileAttributeType {
|
||||
// switch fileType {
|
||||
// case .typeRegular, .typeDirectory, .typeSymbolicLink:
|
||||
// // The file is a regular file, directory, or symbolic link
|
||||
// return true
|
||||
// default:
|
||||
// // The file is a socket, pipe, or another type not supported
|
||||
// return false
|
||||
// }
|
||||
// }
|
||||
// } catch {
|
||||
// printOS("Error getting file attributes: \(error)")
|
||||
// }
|
||||
// return false
|
||||
//}
|
||||
|
||||
|
||||
|
||||
@@ -671,43 +670,43 @@ func uninstallPearcleaner(appState: AppState, locations: Locations) {
|
||||
|
||||
|
||||
// --- Create Application Support folder if it doesn't exist ---
|
||||
func ensureApplicationSupportFolderExists(appState: AppState) {
|
||||
let fileManager = FileManager.default
|
||||
let supportURL = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!.appendingPathComponent("com.alienator88.Pearcleaner")
|
||||
|
||||
// Check to make sure Application Support/Pearcleaner folder exists
|
||||
if !fileManager.fileExists(atPath: supportURL.path) {
|
||||
try! fileManager.createDirectory(at: supportURL, withIntermediateDirectories: true)
|
||||
printOS("Created Application Support/com.alienator88.Pearcleaner folder")
|
||||
}
|
||||
}
|
||||
//func ensureApplicationSupportFolderExists(appState: AppState) {
|
||||
// let fileManager = FileManager.default
|
||||
// let supportURL = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!.appendingPathComponent("com.alienator88.Pearcleaner")
|
||||
//
|
||||
// // Check to make sure Application Support/Pearcleaner folder exists
|
||||
// if !fileManager.fileExists(atPath: supportURL.path) {
|
||||
// try! fileManager.createDirectory(at: supportURL, withIntermediateDirectories: true)
|
||||
// printOS("Created Application Support/com.alienator88.Pearcleaner folder")
|
||||
// }
|
||||
//}
|
||||
|
||||
|
||||
// --- 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) {
|
||||
printOS("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 {
|
||||
printOS("Error opening file for appending")
|
||||
}
|
||||
}
|
||||
}
|
||||
//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) {
|
||||
// printOS("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 {
|
||||
// printOS("Error opening file for appending")
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
|
||||
// --- Load Plist file with launchctl ---
|
||||
@@ -752,20 +751,20 @@ func sendStopNotificationFW() {
|
||||
}
|
||||
|
||||
|
||||
func getCurrentTimestamp() -> String {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS"
|
||||
return dateFormatter.string(from: Date())
|
||||
}
|
||||
//func getCurrentTimestamp() -> String {
|
||||
// let dateFormatter = DateFormatter()
|
||||
// dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS"
|
||||
// return dateFormatter.string(from: Date())
|
||||
//}
|
||||
|
||||
|
||||
func formattedDate(_ date: Date?) -> String {
|
||||
guard let date = date else { return "N/A" }
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateStyle = .short
|
||||
formatter.timeStyle = .none
|
||||
return formatter.string(from: date)
|
||||
}
|
||||
//func formattedDate(_ date: Date?) -> String {
|
||||
// guard let date = date else { return "N/A" }
|
||||
// let formatter = DateFormatter()
|
||||
// formatter.dateStyle = .short
|
||||
// formatter.timeStyle = .none
|
||||
// return formatter.string(from: date)
|
||||
//}
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user