diff --git a/ishare.xcodeproj/project.pbxproj b/ishare.xcodeproj/project.pbxproj index df591fc..52d1604 100644 --- a/ishare.xcodeproj/project.pbxproj +++ b/ishare.xcodeproj/project.pbxproj @@ -26,6 +26,7 @@ 3AB38D0B2A62F28700184E0D /* UploaderSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB38D0A2A62F28700184E0D /* UploaderSettings.swift */; }; 3AB38D0D2A62F7C500184E0D /* RecordingSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB38D0C2A62F7C500184E0D /* RecordingSettings.swift */; }; 3ADDAEDA2A659A0B00C42406 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3ADDAED92A659A0B00C42406 /* Assets.xcassets */; }; + 3ADDAEE32A65D00F00C42406 /* Zip in Frameworks */ = {isa = PBXBuildFile; productRef = 3ADDAEE22A65D00F00C42406 /* Zip */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -53,6 +54,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 3ADDAEE32A65D00F00C42406 /* Zip in Frameworks */, 3AB345BC2A5EBA5400AAEC0E /* Defaults in Frameworks */, 3A17C0792A61A20E0012F285 /* Alamofire in Frameworks */, 3AB345C22A5ED5B800AAEC0E /* KeyboardShortcuts in Frameworks */, @@ -157,6 +159,7 @@ 3AB345C12A5ED5B800AAEC0E /* KeyboardShortcuts */, 3A17C0782A61A20E0012F285 /* Alamofire */, 3A0679E02A648C0F0088DE6A /* SwiftyJSON */, + 3ADDAEE22A65D00F00C42406 /* Zip */, ); productName = ishare; productReference = 3A9A9FD42A5C84B5007BA5C9 /* ishare.app */; @@ -192,6 +195,7 @@ 3AB345C02A5ED5B800AAEC0E /* XCRemoteSwiftPackageReference "KeyboardShortcuts" */, 3A17C0772A61A20E0012F285 /* XCRemoteSwiftPackageReference "Alamofire" */, 3A0679DF2A648C0F0088DE6A /* XCRemoteSwiftPackageReference "SwiftyJSON" */, + 3ADDAEE12A65D00F00C42406 /* XCRemoteSwiftPackageReference "Zip" */, ); productRefGroup = 3A9A9FD52A5C84B5007BA5C9 /* Products */; projectDirPath = ""; @@ -484,6 +488,14 @@ minimumVersion = 1.0.0; }; }; + 3ADDAEE12A65D00F00C42406 /* XCRemoteSwiftPackageReference "Zip" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/marmelroy/Zip.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.0.0; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -512,6 +524,11 @@ package = 3AB345C02A5ED5B800AAEC0E /* XCRemoteSwiftPackageReference "KeyboardShortcuts" */; productName = KeyboardShortcuts; }; + 3ADDAEE22A65D00F00C42406 /* Zip */ = { + isa = XCSwiftPackageProductDependency; + package = 3ADDAEE12A65D00F00C42406 /* XCRemoteSwiftPackageReference "Zip" */; + productName = Zip; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 3A9A9FCC2A5C84B4007BA5C9 /* Project object */; diff --git a/ishare/Util/Constants.swift b/ishare/Util/Constants.swift index aa3dada..bbb9252 100644 --- a/ishare/Util/Constants.swift +++ b/ishare/Util/Constants.swift @@ -5,8 +5,11 @@ // Created by Adrian Castro on 12.07.23. // +import Zip import SwiftUI import Defaults +import Alamofire +import SwiftyJSON @testable import KeyboardShortcuts extension KeyboardShortcuts.Name { @@ -90,7 +93,7 @@ func checkAppInstallation(_ app: InstalledApp) -> Bool { let fileManager = FileManager.default let homebrewPath = utsname.isAppleSilicon ? "/opt/homebrew/bin/brew" : "/usr/local/bin/brew" let ffmpegPath = utsname.isAppleSilicon ? "/opt/homebrew/bin/ffmpeg" : "/usr/local/bin/ffmpeg" - + return fileManager.fileExists(atPath: app == InstalledApp.HOMEBREW ? homebrewPath : ffmpegPath) } @@ -193,11 +196,11 @@ func importFile(_ url: URL, completion: @escaping (Bool, Error?) -> Void) { let data = try Data(contentsOf: url) let decoder = JSONDecoder() let uploader = try decoder.decode(CustomUploader.self, from: data) - + @Default(.savedCustomUploaders) var savedCustomUploaders @Default(.activeCustomUploader) var activeCustomUploader @Default(.uploadType) var uploadType - + if var uploaders = savedCustomUploaders { uploaders.remove(uploader) uploaders.insert(uploader) @@ -205,12 +208,167 @@ func importFile(_ url: URL, completion: @escaping (Bool, Error?) -> Void) { } else { savedCustomUploaders = Set([uploader]) } - + activeCustomUploader = uploader uploadType = .CUSTOM - + completion(true, nil) // Success callback } catch { completion(false, error) // Error callback } } + +func selfUpdate() { + guard let releasesURL = URL(string: "https://api.github.com/repos/castdrian/ishare/releases") else { + print("Invalid releases URL") + return + } + + AF.request(releasesURL).responseDecodable(of: JSON.self) { response in + switch response.result { + case .success(let value): + let json = JSON(value) + + // Check if the response contains any releases + guard let releases = json.array else { + print("No releases found") + return + } + + // Check if there is at least one release + guard let latestRelease = releases.first else { + print("No latest release found") + return + } + + // Extract the creation date of the latest release + if let createdAtString = latestRelease["created_at"].string, + let releaseCreationDate = ISO8601DateFormatter().date(from: createdAtString) { + print("Latest release creation date: \(releaseCreationDate)") + + // Get the bundle's creation date + if let executableURL = Bundle.main.executableURL, + let bundleCreationDate = (try? executableURL.resourceValues(forKeys: [.creationDateKey]))?.creationDate { + print("Bundle creation date: \(bundleCreationDate)") + + // Compare the dates + let comparisonResult = releaseCreationDate.compare(bundleCreationDate) + + if comparisonResult == .orderedDescending { + print("Bundle is older than the latest release") + let alert = NSAlert() + alert.messageText = "Update Available" + alert.informativeText = "An update is available. Do you want to update ishare?" + alert.addButton(withTitle: "Yes") + alert.addButton(withTitle: "No") + + let modalResponse = alert.runModal() + if modalResponse == .alertFirstButtonReturn { + if let assetURL = latestRelease["assets"][0]["browser_download_url"].url { + print("Latest release asset URL: \(assetURL)") + downloadAndReplaceApp(assetURL: assetURL) + } else { + print("No assets found for the latest release") + } + } + } else if comparisonResult == .orderedAscending { + print("Bundle is newer than the latest release") + showAlert(title: "Up to Date", message: "Your version of ishare is up to date.") + } else { + print("Bundle and latest release have the same creation date") + showAlert(title: "Up to Date", message: "Your version of ishare is up to date.") + } + } + } else { + print("Failed to extract release creation date") + showAlert(title: "Error", message: "Failed to extract release creation date.") + } + + case .failure(let error): + print("Request failed with error: \(error)") + showAlert(title: "Error", message: "Request failed with error: \(error)") + } + } +} + +func showAlert(title: String, message: String) { + let alert = NSAlert() + alert.messageText = title + alert.informativeText = message + alert.addButton(withTitle: "OK") + alert.runModal() +} + +func downloadAndReplaceApp(assetURL: URL) { + let destinationURL = FileManager.default.temporaryDirectory.appendingPathComponent("ishare.zip") + + AF.download(assetURL, to: { _, _ in (destinationURL, [.removePreviousFile]) }) + .response { response in + if response.error == nil { + if response.fileURL != nil { + replaceAppWithDownloadedArchive(zipURL: destinationURL) + } else { + showAlert(title: "Download Failed", message: "Failed to download the update archive.") + } + } else { + showAlert(title: "Download Failed", message: "Failed to download the update: \(response.error!.localizedDescription)") + } + } +} + +func replaceAppWithDownloadedArchive(zipURL: URL) { + let fileManager = FileManager.default + let appSupportDirectoryURL = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first! + let extractedAppDirectoryURL = appSupportDirectoryURL.appendingPathComponent("ishare_extracted") + + do { + try fileManager.createDirectory(at: extractedAppDirectoryURL, withIntermediateDirectories: true, attributes: nil) + + try Zip.unzipFile(zipURL, destination: extractedAppDirectoryURL, overwrite: true, password: nil) + + let extractedAppURLs = try fileManager.contentsOfDirectory(at: extractedAppDirectoryURL, includingPropertiesForKeys: nil) + print(extractedAppURLs) + guard let extractedAppURL = extractedAppURLs.first(where: { $0.pathExtension == "app" }) else { + print("Failed to find extracted ishare.app") + showAlert(title: "Update Failed", message: "Failed to find the extracted iShare app.") + return + } + + let extractedAppBundleURL = extractedAppURL.appendingPathComponent("Contents").appendingPathComponent("MacOS") + let extractedAppExecutableURL = extractedAppBundleURL.appendingPathComponent("ishare") + + guard fileManager.fileExists(atPath: extractedAppExecutableURL.path) else { + print("Failed to find extracted ishare.app executable") + showAlert(title: "Update Failed", message: "Failed to find the extracted ishare app executable.") + return + } + + let currentAppURL = Bundle.main.bundleURL + let currentAppBundleURL = currentAppURL.appendingPathComponent("Contents").appendingPathComponent("MacOS") + let currentAppExecutableURL = currentAppBundleURL.appendingPathComponent("ishare") + + + do { + try fileManager.replaceItemAt(currentAppExecutableURL, withItemAt: extractedAppExecutableURL) + + showAlert(title: "Update Successful", message: "ishare has been updated successfully. The app will now restart.") + + let appURL = Bundle.main.bundleURL + let configuration = NSWorkspace.OpenConfiguration() + NSWorkspace.shared.open(appURL, configuration: configuration) { _, _ in + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + NSApplication.shared.terminate(nil) + } + } + + } catch { + print("Failed to replace the current ishare.app: \(error.localizedDescription)") + showAlert(title: "Update Failed", message: "Failed to replace the current ishare app: \(error.localizedDescription)") + } + + try fileManager.removeItem(at: extractedAppDirectoryURL) + } catch { + print("Failed to extract the update archive: \(error.localizedDescription)") + showAlert(title: "Update Failed", message: "Failed to extract the update archive: \(error.localizedDescription)") + } +} diff --git a/ishare/Views/MainMenuView.swift b/ishare/Views/MainMenuView.swift index a935d99..bc4ebf8 100644 --- a/ishare/Views/MainMenuView.swift +++ b/ishare/Views/MainMenuView.swift @@ -54,9 +54,9 @@ struct MainMenuView: View { } if let uploaders = savedCustomUploaders { if !uploaders.isEmpty { - // doesn"t work :( + // doesn't work :( // Picker("Custom", selection: $activeCustomUploader) { -// ForEach(CustomUploader.allCases) { uploader in +// ForEach(CustomUploader.allCases, id: \.self) { uploader in // Text(uploader.name).tag(uploader) // } // } @@ -111,6 +111,10 @@ struct MainMenuView: View { ) }.keyboardShortcut("a") + Button("Check for Updates") { + selfUpdate() + }.keyboardShortcut("u") + Button("Quit") { NSApplication.shared.terminate(nil) }.keyboardShortcut("q")