vChewing-macOS/Packages/vChewing_UpdateSputnik/Sources/UpdateSputnik/UpdateSputnik.swift

164 lines
5.7 KiB
Swift

// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
// ... with NTL restriction stating that:
// No trademark license is granted to use the trade names, trademarks, service
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
import Cocoa
public class UpdateSputnik {
public static var shared: UpdateSputnik = .init()
public var varkUpdateInfoPageURLKey: String = "UpdateInfoSite"
public var varkUpdateCheckDateKeyPrevious: String = "PreviousUpdateCheckDate"
public var varkUpdateCheckDateKeyNext: String = "NextUpdateCheckDate"
public var varkUpdateCheckInterval: TimeInterval = 114_514
public var varCheckUpdateAutomatically = "CheckUpdateAutomatically"
public init() {}
public func checkForUpdate(forced: Bool = false, url: URL) {
guard !busy else { return }
if !forced {
if !UserDefaults.standard.bool(forKey: varCheckUpdateAutomatically) { return }
if let nextCheckDate = nextUpdateCheckDate, Date().compare(nextCheckDate) == .orderedAscending {
return
}
}
isCurrentCheckForced = forced //
let request = URLRequest(
url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 5
)
let task = URLSession.shared.dataTask(with: request) { data, _, error in
if let error = error {
DispatchQueue.main.async {
self.showError(message: error.localizedDescription)
self.currentTask = nil
}
return
}
self.data = data
}
task.resume()
currentTask = task
}
// MARK: - Private Properties
private var isCurrentCheckForced = false
var sessionConfiguration = URLSessionConfiguration.background(withIdentifier: Bundle.main.bundleIdentifier!)
private var busy: Bool { currentTask != nil }
private var currentTask: URLSessionDataTask?
private var data: Data? {
didSet {
if let data = data {
DispatchQueue.main.async {
self.dataDidSet(data: data)
self.currentTask = nil
}
}
}
}
private var nextUpdateCheckDate: Date? {
get {
UserDefaults.standard.object(forKey: varkUpdateCheckDateKeyNext) as? Date
}
set {
UserDefaults.standard.set(newValue, forKey: varkUpdateCheckDateKeyNext)
}
}
// MARK: - Private Functions.
internal func dataDidSet(data: Data) {
var plist: [AnyHashable: Any]?
plist = try? PropertyListSerialization.propertyList(from: data, options: [], format: nil) as? [AnyHashable: Any]
nextUpdateCheckDate = .init().addingTimeInterval(varkUpdateCheckInterval)
cleanUp()
guard let plist = plist else {
DispatchQueue.main.async {
self.showError(message: NSLocalizedString("Plist downloaded is nil.", comment: ""))
self.currentTask = nil
}
return
}
NSLog("update check plist: \(plist)")
guard let intRemoteVersion = Int(plist[kCFBundleVersionKey] as? String ?? ""),
let strRemoteVersionShortened = plist["CFBundleShortVersionString"] as? String
else {
DispatchQueue.main.async {
self.showError(message: NSLocalizedString("Plist downloaded cannot be parsed correctly.", comment: ""))
self.currentTask = nil
}
return
}
guard let dicMainBundle = Bundle.main.infoDictionary,
let intCurrentVersion = Int(dicMainBundle[kCFBundleVersionKey as String] as? String ?? ""),
let strCurrentVersionShortened = dicMainBundle["CFBundleShortVersionString"] as? String
else { return } // Shouldn't happen.
if intRemoteVersion <= intCurrentVersion {
guard isCurrentCheckForced else { return }
let alert = NSAlert()
alert.messageText = NSLocalizedString("Update Check Completed", comment: "")
alert.informativeText = NSLocalizedString("You are already using the latest version.", comment: "")
alert.addButton(withTitle: NSLocalizedString("OK", comment: ""))
alert.runModal()
NSApp.activate(ignoringOtherApps: true)
return
}
let content = String(
format: NSLocalizedString(
"You're currently using vChewing %@ (%@), a new version %@ (%@) is now available. Do you want to visit vChewing's website to download the version?",
comment: ""
),
strCurrentVersionShortened,
intCurrentVersion.description,
strRemoteVersionShortened,
intRemoteVersion.description
)
let alert = NSAlert()
alert.messageText = NSLocalizedString("New Version Available", comment: "")
alert.informativeText = content
alert.addButton(withTitle: NSLocalizedString("Visit Website", comment: ""))
alert.addButton(withTitle: NSLocalizedString("Not Now", comment: ""))
let result = alert.runModal()
NSApp.activate(ignoringOtherApps: true)
if result == NSApplication.ModalResponse.alertFirstButtonReturn {
if let siteInfoURLString = plist[varkUpdateInfoPageURLKey] as? String,
let siteURL = URL(string: siteInfoURLString)
{
DispatchQueue.main.async {
NSWorkspace.shared.open(siteURL)
}
}
}
}
private func cleanUp() {
currentTask = nil
data = nil
}
private func showError(message: String = "") {
NSLog("Update check: plist error, forced check: \(isCurrentCheckForced)")
if !isCurrentCheckForced { return }
let alert = NSAlert()
let content = message
alert.messageText = NSLocalizedString("Update Check Failed", comment: "")
alert.informativeText = content
alert.addButton(withTitle: NSLocalizedString("OK", comment: ""))
alert.runModal()
NSApp.activate(ignoringOtherApps: true)
}
}