diff --git a/Pareto Updater.xcodeproj/project.pbxproj b/Pareto Updater.xcodeproj/project.pbxproj index 326c7c7..0693dd0 100644 --- a/Pareto Updater.xcodeproj/project.pbxproj +++ b/Pareto Updater.xcodeproj/project.pbxproj @@ -59,6 +59,7 @@ 4F75DDD9287D512900FD4503 /* Version in Frameworks */ = {isa = PBXBuildFile; productRef = 4F75DDD8287D512900FD4503 /* Version */; }; 4F7D6FFE28327BC4007D6E8A /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F7D6FFD28327BC4007D6E8A /* Constants.swift */; }; 4F7D700028327FE4007D6E8A /* FileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F7D6FFF28327FE4007D6E8A /* FileManager.swift */; }; + 4F804F8E2BE0EAF300372B36 /* SettingsAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 4F804F8D2BE0EAF300372B36 /* SettingsAccess */; }; 4F868ED5286B224800383328 /* Menubar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F868ED4286B224800383328 /* Menubar.swift */; }; 4F91931C294BA7D000FB1DF7 /* Gramarly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F91931B294BA7D000FB1DF7 /* Gramarly.swift */; }; 4F9209DC282BBD7D007897A6 /* Docker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F9209DB282BBD7D007897A6 /* Docker.swift */; }; @@ -232,6 +233,7 @@ 4FE610E4282A88400060C002 /* Alamofire in Frameworks */, 4F3BE0A1286EF61B00DDC1AC /* XMLCoder in Frameworks */, 4FE610E7282A88540060C002 /* Regex in Frameworks */, + 4F804F8E2BE0EAF300372B36 /* SettingsAccess in Frameworks */, 4F0DEA7A286D923A00344B92 /* JWTDecode in Frameworks */, 4FE610DB282A87F40060C002 /* Defaults in Frameworks */, ); @@ -470,6 +472,7 @@ 4F3BE0A0286EF61B00DDC1AC /* XMLCoder */, 4FA755FF287709960064D0ED /* AppUpdater */, 4F75DDD8287D512900FD4503 /* Version */, + 4F804F8D2BE0EAF300372B36 /* SettingsAccess */, ); productName = "Pareto Updater"; productReference = 4F33FE31280808C000585E5A /* Pareto Updater.app */; @@ -554,6 +557,7 @@ 4F3BE09F286EF61B00DDC1AC /* XCRemoteSwiftPackageReference "XMLCoder" */, 4FA755FE287709960064D0ED /* XCRemoteSwiftPackageReference "AppUpdater" */, 4F75DDD7287D512900FD4503 /* XCRemoteSwiftPackageReference "Version" */, + 4F804F8C2BE0EAB300372B36 /* XCRemoteSwiftPackageReference "SettingsAccess" */, ); productRefGroup = 4F33FE32280808C000585E5A /* Products */; projectDirPath = ""; @@ -1087,6 +1091,14 @@ kind = branch; }; }; + 4F804F8C2BE0EAB300372B36 /* XCRemoteSwiftPackageReference "SettingsAccess" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/orchetect/SettingsAccess"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.4.0; + }; + }; 4FA755FE287709960064D0ED /* XCRemoteSwiftPackageReference "AppUpdater" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/teamniteo/AppUpdater"; @@ -1158,6 +1170,11 @@ package = 4F75DDD7287D512900FD4503 /* XCRemoteSwiftPackageReference "Version" */; productName = Version; }; + 4F804F8D2BE0EAF300372B36 /* SettingsAccess */ = { + isa = XCSwiftPackageProductDependency; + package = 4F804F8C2BE0EAB300372B36 /* XCRemoteSwiftPackageReference "SettingsAccess" */; + productName = SettingsAccess; + }; 4FA755FF287709960064D0ED /* AppUpdater */ = { isa = XCSwiftPackageProductDependency; package = 4FA755FE287709960064D0ED /* XCRemoteSwiftPackageReference "AppUpdater" */; diff --git a/Pareto Updater.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Pareto Updater.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index f1f7660..784005d 100644 --- a/Pareto Updater.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Pareto Updater.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,4 +1,5 @@ { + "originHash" : "4d2deb1ffbf29bdd6f26ea089e45f0ba037553dc39b785a7d7a240869d32cd57", "pins" : [ { "identity" : "alamofire", @@ -24,7 +25,7 @@ "location" : "https://github.com/hyperoslo/Cache", "state" : { "branch" : "master", - "revision" : "eeaf771d8d2e8247fbd6da2e27c986d99803fb1f" + "revision" : "f44a8f6b5ec27730198725ccc542fef0d1cc6b3d" } }, { @@ -81,13 +82,22 @@ "revision" : "4f95793b3acf6ec2a89ee2b635bca268bbf01315" } }, + { + "identity" : "settingsaccess", + "kind" : "remoteSourceControl", + "location" : "https://github.com/orchetect/SettingsAccess", + "state" : { + "revision" : "0fd73c8b5892e88acb13adb7f36a4ba9293a0061", + "version" : "1.4.0" + } + }, { "identity" : "version", "kind" : "remoteSourceControl", "location" : "https://github.com/ParetoSecurity/Version", "state" : { "branch" : "master", - "revision" : "9efa0d232cd886e78a6c59fd6eff337ebcbdafce" + "revision" : "88064e19f3c4efd3f7930017ce3ff77378fb22c5" } }, { @@ -100,5 +110,5 @@ } } ], - "version" : 2 + "version" : 3 } diff --git a/Pareto Updater/Extensions/AppUpdater.swift b/Pareto Updater/Extensions/AppUpdater.swift index 738651e..13c4fed 100644 --- a/Pareto Updater/Extensions/AppUpdater.swift +++ b/Pareto Updater/Extensions/AppUpdater.swift @@ -81,7 +81,9 @@ public class AppUpdater: Hashable, Identifiable, ObservableObject { if isInstalled { let weekAgo = Date().addingTimeInterval(-7 * 24 * 60 * 60) let attributes = NSMetadataItem(url: applicationPath) - guard let lastUse = attributes?.value(forAttribute: "kMDItemLastUsedDate") as? Date else { return false } + guard let lastUse = attributes?.value(forAttribute: "kMDItemLastUsedDate") as? Date else { + return false + } return lastUse >= weekAgo } return true @@ -90,7 +92,8 @@ public class AppUpdater: Hashable, Identifiable, ObservableObject { public var fromAppStore: Bool { if isInstalled { let attributes = NSMetadataItem(url: applicationPath) - guard let hasReceipt = attributes?.value(forAttribute: "kMDItemAppStoreHasReceipt") as? Bool else { return false } + guard let hasReceipt = attributes?.value(forAttribute: "kMDItemAppStoreHasReceipt") as? Bool + else { return false } return hasReceipt } return false @@ -99,13 +102,16 @@ public class AppUpdater: Hashable, Identifiable, ObservableObject { public var isSafariWebApp: Bool { if isInstalled { let attributes = Bundle.plistDict(path: applicationPath) - return (((attributes?.value(forKey: "CFBundleIdentifier") as? String)?.contains("com.apple.Safari.WebApp")) != nil) + return + ((attributes?.value(forKey: "CFBundleIdentifier") as? String)?.contains( + "com.apple.Safari.WebApp")) != nil } return false } - + func downloadLatest(completion: @escaping (URL, URL) -> Void) { - let cachedPath = Constants.cacheFolder.appendingPathComponent("\(appBundle)-\(latestVersion).\(latestURLExtension)") + let cachedPath = Constants.cacheFolder.appendingPathComponent( + "\(appBundle)-\(latestVersion).\(latestURLExtension)") if FileManager.default.fileExists(atPath: cachedPath.path), Constants.useCacheFolder { os_log("Update from cache at %{public}s", cachedPath.debugDescription) completion(latestURL, cachedPath) @@ -120,7 +126,10 @@ public class AppUpdater: Hashable, Identifiable, ObservableObject { try FileManager.default.removeItem(at: cachedPath) } try FileManager.default.moveItem(atPath: response.fileURL!.path, toPath: cachedPath.path) - os_log("Update downloadLatest: %{public} from %{public}s", cachedPath.debugDescription, self.latestURL.debugDescription) + os_log( + "Update downloadLatest: %{public} from %{public}s", cachedPath.debugDescription, + self.latestURL.debugDescription + ) completion(latestURL, cachedPath) return } catch { @@ -159,43 +168,72 @@ public class AppUpdater: Hashable, Identifiable, ObservableObject { switch appFile.pathExtension { case "dmg": let mountPoint = URL(string: "/Volumes/" + appBundle)! - os_log("Mount %{public}s is %{public}s", appFile.debugDescription, mountPoint.debugDescription) + os_log( + "Mount %{public}s is %{public}s", appFile.debugDescription, mountPoint.debugDescription + ) if DMGMounter.attach(diskImage: appFile, at: mountPoint) { do { - let app = try FileManager.default.contentsOfDirectory(at: mountPoint, includingPropertiesForKeys: nil).filter { $0.lastPathComponent.contains(".app") }.first + let app = try FileManager.default.contentsOfDirectory( + at: mountPoint, includingPropertiesForKeys: nil + ).filter { $0.lastPathComponent.contains(".app") }.first + let pkg = try FileManager.default.contentsOfDirectory( + at: mountPoint, includingPropertiesForKeys: nil + ).filter { $0.lastPathComponent.contains(".pkg") }.first + + if app == nil && pkg == nil { + os_log("Failed to find app bundle in DMG %{public}s", appFile.debugDescription) + return AppUpdaterStatus.Failed + } - let downloadedAppBundle = Bundle(url: app!)! - if let installedAppBundle = Bundle(url: applicationPath) { - if !validate(downloadedAppBundle, installedAppBundle) { - os_log("Failed to validate app bundle %{public}s", appBundle) - return AppUpdaterStatus.Failed - } + if app != nil { + os_log("Found app bundle in DMG %{public}s", app.debugDescription) - let localName = installedAppBundle.path.basename(dropExtension: true) + ".backup" - os_log("Archive installedAppBundle: \(installedAppBundle.description)") - try installedAppBundle.path.rename(to: localName) + let downloadedAppBundle = Bundle(url: app!)! + if let installedAppBundle = Bundle(url: applicationPath) { + if !validate(downloadedAppBundle, installedAppBundle) { + os_log("Failed to validate app bundle %{public}s", appBundle) + return AppUpdaterStatus.Failed + } - os_log("Update installedAppBundle: \(installedAppBundle.description) with \(downloadedAppBundle.description)") - try downloadedAppBundle.path.copy(to: installedAppBundle.path, overwrite: true) + let localName = installedAppBundle.path.basename(dropExtension: true) + ".backup" + os_log("Archive installedAppBundle: \(installedAppBundle.description)") + try installedAppBundle.path.rename(to: localName) - if needsStart { - installedAppBundle.launch() + os_log( + "Update installedAppBundle: \(installedAppBundle.description) with \(downloadedAppBundle.description)" + ) + try downloadedAppBundle.path.copy(to: installedAppBundle.path, overwrite: true) + + if needsStart { + installedAppBundle.launch() + } + + let oldBackup = installedAppBundle.path.parent.join(localName) + try? oldBackup.delete() + + } else { + os_log("Install AppBundle \(downloadedAppBundle.description)") + try downloadedAppBundle.path.copy(to: Path(url: applicationPath)!, overwrite: true) } + _ = DMGMounter.detach(mountPoint: mountPoint) - let oldBackup = installedAppBundle.path.parent.join(localName) - try? oldBackup.delete() + if let bundle = Bundle(url: applicationPath), needsStart { + bundle.launch() + } - } else { - os_log("Install AppBundle \(downloadedAppBundle.description)") - try downloadedAppBundle.path.copy(to: Path(url: applicationPath)!, overwrite: true) + return AppUpdaterStatus.Installed } - _ = DMGMounter.detach(mountPoint: mountPoint) - if let bundle = Bundle(url: applicationPath), needsStart { - bundle.launch() + if pkg != nil { + os_log("Found pkg bundle in DMG %{public}s", pkg.debugDescription) + let task = Process() + task.launchPath = "/usr/bin/open" + task.arguments = [pkg!.path] + task.launch() + _ = DMGMounter.detach(mountPoint: mountPoint) + return AppUpdaterStatus.Installed } - return AppUpdaterStatus.Installed } catch { _ = DMGMounter.detach(mountPoint: mountPoint) os_log("Failed to check for app bundle %{public}s", error.localizedDescription) @@ -216,7 +254,9 @@ public class AppUpdater: Hashable, Identifiable, ObservableObject { os_log("Archive installedAppBundle: \(installedAppBundle.description)") try installedAppBundle.path.rename(to: localName) - os_log("Update installedAppBundle: \(installedAppBundle.description) with \(downloadedAppBundle.description)") + os_log( + "Update installedAppBundle: \(installedAppBundle.description) with \(downloadedAppBundle.description)" + ) try downloadedAppBundle.path.copy(to: installedAppBundle.path, overwrite: true) if needsStart { installedAppBundle.launch() @@ -316,8 +356,8 @@ public class AppUpdater: Hashable, Identifiable, ObservableObject { } public var latestVersion: String { - if let found = try? Constants.versionStorage.existsObject(forKey: appBundle), found { - return latestVersionHook(try! Constants.versionStorage.object(forKey: appBundle)) + if let found = try? Constants.versionStorage.object(forKey: appBundle), !found.isEmpty { + return latestVersionHook(found) } else { let lock = DispatchSemaphore(value: 0) DispatchQueue.global(qos: .userInteractive).async { [self] in diff --git a/Pareto Updater/Extensions/Bundle.swift b/Pareto Updater/Extensions/Bundle.swift index 3090ff5..5d1fc05 100644 --- a/Pareto Updater/Extensions/Bundle.swift +++ b/Pareto Updater/Extensions/Bundle.swift @@ -23,12 +23,12 @@ extension Bundle { } return nil } - - static func plistDict (path: URL) -> NSDictionary? { + + static func plistDict(path: URL) -> NSDictionary? { let plist = path.appendingPathComponent("/Contents/Info.plist") return NSDictionary(contentsOf: plist) } - + static func appVersion(path: URL, key: String = "CFBundleShortVersionString") -> String? { plistDict(path: path)?.value(forKey: key) as? String } diff --git a/Pareto Updater/ParetoUpdater.swift b/Pareto Updater/ParetoUpdater.swift index bf01f01..a2f6d32 100644 --- a/Pareto Updater/ParetoUpdater.swift +++ b/Pareto Updater/ParetoUpdater.swift @@ -13,6 +13,7 @@ import Defaults import Foundation import os.log import Regex +import SettingsAccess import SwiftUI #if !DEBUG @@ -343,7 +344,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate, NSWindowDele popOver.behavior = .transient popOver.animates = true popOver.contentViewController = NSViewController() - popOver.contentViewController?.view = NSHostingView(rootView: AppList().environmentObject(appsStore)) + popOver.contentViewController?.view = NSHostingView(rootView: AppList().environmentObject(appsStore).openSettingsAccess()) statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) @@ -366,10 +367,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate, NSWindowDele statusMenu = NSMenu(title: "ParetoUpdater") - let preferencesItem = NSMenuItem(title: "Preferences", action: #selector(AppDelegate.preferences), keyEquivalent: ",") - preferencesItem.target = NSApp.delegate - statusMenu?.addItem(preferencesItem) - let contactItem = NSMenuItem(title: "Contact Support", action: #selector(AppDelegate.contact), keyEquivalent: "c") contactItem.target = NSApp.delegate statusMenu?.addItem(contactItem) @@ -442,16 +439,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate, NSWindowDele NSWorkspace.shared.open(Constants.bugReportURL) } - @objc - func preferences() { - if #available(macOS 13.0, *) { - NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil) - } else { - NSApp.sendAction(Selector(("showPreferencesWindow:")), to: nil, from: nil) - } - NSApp.activate(ignoringOtherApps: true) - } - func showInstallAppsWindow() { shouldTerminate = true statusItem?.isVisible = false diff --git a/Pareto Updater/Views/AppList.swift b/Pareto Updater/Views/AppList.swift index e9f79ed..39e45e0 100644 --- a/Pareto Updater/Views/AppList.swift +++ b/Pareto Updater/Views/AppList.swift @@ -5,10 +5,12 @@ // Created by Janez Troha on 14/04/2022. // +import SettingsAccess import SwiftUI struct AppList: View { @EnvironmentObject var viewModel: AppBundles + @Environment(\.openSettings) private var openSettings var body: some View { VStack(alignment: .leading) { @@ -45,11 +47,7 @@ struct AppList: View { .help("Refresh the status of the apps") Button { - if #available(macOS 13.0, *) { - NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil) - } else { - NSApp.sendAction(Selector(("showPreferencesWindow:")), to: nil, from: nil) - } + try? openSettings() NSApp.activate(ignoringOtherApps: true) } label: { Image(systemName: "gearshape") diff --git a/Pareto Updater/Views/AppRow.swift b/Pareto Updater/Views/AppRow.swift index b77d695..16eec33 100644 --- a/Pareto Updater/Views/AppRow.swift +++ b/Pareto Updater/Views/AppRow.swift @@ -78,7 +78,7 @@ struct AppRow: View { Button { onUpdate?() } - label: { + label: { Image(systemName: "arrow.down.app.fill") .resizable() .aspectRatio(contentMode: .fit)