From 59b0cc1c45d3dfd1fb1019021f9ef4539537c19b Mon Sep 17 00:00:00 2001 From: ShikiSuen Date: Thu, 8 Sep 2022 19:07:23 +0800 Subject: [PATCH] Repo // Introducing UpdateSputnik. --- Source/Modules/IMEModules/UpdateSputnik.swift | 159 ++++++++++++++++++ Source/Modules/main.swift | 9 + .../Resources/Base.lproj/Localizable.strings | 6 +- Source/Resources/en.lproj/Localizable.strings | 6 +- Source/Resources/ja.lproj/Localizable.strings | 6 +- .../zh-Hans.lproj/Localizable.strings | 6 +- .../zh-Hant.lproj/Localizable.strings | 6 +- vChewing.xcodeproj/project.pbxproj | 4 + 8 files changed, 197 insertions(+), 5 deletions(-) create mode 100644 Source/Modules/IMEModules/UpdateSputnik.swift diff --git a/Source/Modules/IMEModules/UpdateSputnik.swift b/Source/Modules/IMEModules/UpdateSputnik.swift new file mode 100644 index 00000000..dfaa4c05 --- /dev/null +++ b/Source/Modules/IMEModules/UpdateSputnik.swift @@ -0,0 +1,159 @@ +// (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 + +class UpdateSputnik: NSObject, URLSessionDataDelegate { + static let kUpdateInfoPageURLKey: String = "UpdateInfoSite" + static let kUpdateCheckDateKeyPrevious: String = "PreviousUpdateCheckDate" + static let kUpdateCheckDateKeyNext: String = "NextUpdateCheckDate" + static let kUpdateCheckInterval: TimeInterval = 114_514 + static var shared = UpdateSputnik() + + func checkForUpdate(forced: Bool = false) { + guard !busy else { return } + + if !forced { + if !mgrPrefs.checkUpdateAutomatically { return } + if let nextCheckDate = nextUpdateCheckDate, Date().compare(nextCheckDate) == .orderedAscending { + return + } + } + isCurrentCheckForced = forced // 留著用來生成錯誤報告 + let request = URLRequest( + url: kUpdateInfoSourceURL, 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: UpdateSputnik.kUpdateCheckDateKeyNext) as? Date + } + set { + UserDefaults.standard.set(newValue, forKey: UpdateSputnik.kUpdateCheckDateKeyNext) + } + } + + // 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(UpdateSputnik.kUpdateCheckInterval) + cleanUp() + + guard let plist = plist else { + DispatchQueue.main.async { + self.showError(message: "Plist downloaded is nil.") + 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: "Plist downloaded cannot be parsed correctly.") + 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, isCurrentCheckForced { + 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.setActivationPolicy(.accessory) + 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: "")) + NSApp.setActivationPolicy(.accessory) + let result = alert.runModal() + if result == NSApplication.ModalResponse.alertFirstButtonReturn { + if let siteInfoURLString = plist[UpdateSputnik.kUpdateInfoPageURLKey] 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 = NSLocalizedString(message, comment: "") + alert.messageText = NSLocalizedString("Update Check Failed", comment: "") + alert.informativeText = content + alert.addButton(withTitle: NSLocalizedString("Dismiss", comment: "")) + alert.runModal() + NSApp.setActivationPolicy(.accessory) + } +} diff --git a/Source/Modules/main.swift b/Source/Modules/main.swift index 3fb131c1..8daae0ba 100644 --- a/Source/Modules/main.swift +++ b/Source/Modules/main.swift @@ -55,6 +55,15 @@ else { exit(-1) } +guard let mainBundleInfoDict = Bundle.main.infoDictionary, + let strUpdateInfoSource = mainBundleInfoDict["UpdateInfoEndpoint"] as? String, + let urlUpdateInfoSource = URL(string: strUpdateInfoSource) +else { + NSLog("Fatal error: Info.plist wrecked It needs to have correct 'UpdateInfoEndpoint' value.") + exit(-1) +} + public let theServer = server +public let kUpdateInfoSourceURL = urlUpdateInfoSource NSApp.run() diff --git a/Source/Resources/Base.lproj/Localizable.strings b/Source/Resources/Base.lproj/Localizable.strings index 0bb504a4..653b8306 100644 --- a/Source/Resources/Base.lproj/Localizable.strings +++ b/Source/Resources/Base.lproj/Localizable.strings @@ -1,4 +1,8 @@ "vChewing" = "vChewing"; +"Update Check Completed" = "Update Check Completed"; +"You are already using the latest version." = "You are already using the latest version."; +"Plist downloaded is nil." = "Plist downloaded is nil."; +"Plist downloaded cannot be parsed correctly." = "Plist downloaded cannot be parsed correctly."; "vChewing crashed while handling previously loaded UOM observation data. These data files are cleaned now to ensure the usability." = "vChewing crashed while handling previously loaded UOM observation data. These data files are cleaned now to ensure the usability."; "About vChewing…" = "About vChewing…"; "vChewing Preferences…" = "vChewing Preferences…"; @@ -17,7 +21,7 @@ "New Version Available" = "New Version Available"; "Not Now" = "Not Now"; "Visit Website" = "Visit Website"; -"You're currently using vChewing %@ (%@), a new version %@ (%@) is now available. Do you want to visit vChewing's website to download the version?%@" = "You're currently using vChewing %@ (%@), a new version %@ (%@) is now available. Do you want to visit vChewing's website to download the version?%@"; +"You're currently using vChewing %@ (%@), a new version %@ (%@) is now available. Do you want to visit vChewing's website to download the version?" = "You're currently using vChewing %@ (%@), a new version %@ (%@) is now available. Do you want to visit vChewing's website to download the version?"; "Force KangXi Writing" = "Force KangXi Writing"; "NotificationSwitchON" = "✔ ON"; "NotificationSwitchOFF" = "✘ OFF"; diff --git a/Source/Resources/en.lproj/Localizable.strings b/Source/Resources/en.lproj/Localizable.strings index 0bb504a4..653b8306 100644 --- a/Source/Resources/en.lproj/Localizable.strings +++ b/Source/Resources/en.lproj/Localizable.strings @@ -1,4 +1,8 @@ "vChewing" = "vChewing"; +"Update Check Completed" = "Update Check Completed"; +"You are already using the latest version." = "You are already using the latest version."; +"Plist downloaded is nil." = "Plist downloaded is nil."; +"Plist downloaded cannot be parsed correctly." = "Plist downloaded cannot be parsed correctly."; "vChewing crashed while handling previously loaded UOM observation data. These data files are cleaned now to ensure the usability." = "vChewing crashed while handling previously loaded UOM observation data. These data files are cleaned now to ensure the usability."; "About vChewing…" = "About vChewing…"; "vChewing Preferences…" = "vChewing Preferences…"; @@ -17,7 +21,7 @@ "New Version Available" = "New Version Available"; "Not Now" = "Not Now"; "Visit Website" = "Visit Website"; -"You're currently using vChewing %@ (%@), a new version %@ (%@) is now available. Do you want to visit vChewing's website to download the version?%@" = "You're currently using vChewing %@ (%@), a new version %@ (%@) is now available. Do you want to visit vChewing's website to download the version?%@"; +"You're currently using vChewing %@ (%@), a new version %@ (%@) is now available. Do you want to visit vChewing's website to download the version?" = "You're currently using vChewing %@ (%@), a new version %@ (%@) is now available. Do you want to visit vChewing's website to download the version?"; "Force KangXi Writing" = "Force KangXi Writing"; "NotificationSwitchON" = "✔ ON"; "NotificationSwitchOFF" = "✘ OFF"; diff --git a/Source/Resources/ja.lproj/Localizable.strings b/Source/Resources/ja.lproj/Localizable.strings index 487857b0..43af0b74 100644 --- a/Source/Resources/ja.lproj/Localizable.strings +++ b/Source/Resources/ja.lproj/Localizable.strings @@ -1,4 +1,8 @@ "vChewing" = "威注音入力アプリ"; +"Update Check Completed" = "新バージョンチェック完了"; +"You are already using the latest version." = "現在稼働中のは最新バージョンである。"; +"Plist downloaded is nil." = "受けた新バージョンお知らせ情報データは Plist ではないため、失敗とみなす。"; +"Plist downloaded cannot be parsed correctly." = "受けた新バージョンお知らせ情報 Plist データは解読できないため、失敗とみなす。"; "vChewing crashed while handling previously loaded UOM observation data. These data files are cleaned now to ensure the usability." = "臨時記憶モジュールの観測行為による威注音入力アプリの意外中止は発生した。威注音入力アプリの無事利用のために、既存臨時記憶データは全てお消しした。"; "About vChewing…" = "威注音について…"; "vChewing Preferences…" = "入力機能設定…"; @@ -17,7 +21,7 @@ "New Version Available" = "最新版利用可能"; "Not Now" = "後で"; "Visit Website" = "公式サイト"; -"You're currently using vChewing %@ (%@), a new version %@ (%@) is now available. Do you want to visit vChewing's website to download the version?%@" = "今のご使用していた威注音入力アプリのバージョンは「%1$@ (%2$@)」であり、ネットでもっと新しいバージョン「%3$@ (%4$@)」の下載せはできるらしい。公式サイトへバージョン「%5$@」を下載せますか?"; +"You're currently using vChewing %@ (%@), a new version %@ (%@) is now available. Do you want to visit vChewing's website to download the version?" = "今のご使用していた威注音入力アプリのバージョンは「%1$@ (%2$@)」であり、ネットでもっと新しいバージョン「%3$@ (%4$@)」の下載せはできるらしい。お下載せしますか?"; "Force KangXi Writing" = "康熙文字変換モード"; "NotificationSwitchON" = "✔ 機能起動"; "NotificationSwitchOFF" = "✘ 機能停止"; diff --git a/Source/Resources/zh-Hans.lproj/Localizable.strings b/Source/Resources/zh-Hans.lproj/Localizable.strings index 4049ebb1..af36d847 100644 --- a/Source/Resources/zh-Hans.lproj/Localizable.strings +++ b/Source/Resources/zh-Hans.lproj/Localizable.strings @@ -1,4 +1,8 @@ "vChewing" = "威注音输入法"; +"Update Check Completed" = "更新检查完毕"; +"You are already using the latest version." = "您正在使用目前最新的发行版。"; +"Plist downloaded is nil." = "下载来的更新资讯并非 Plist 档案。"; +"Plist downloaded cannot be parsed correctly." = "下载来的更新资讯 Plist 档案无法正常解析。"; "vChewing crashed while handling previously loaded UOM observation data. These data files are cleaned now to ensure the usability." = "威注音输入法的使用者半衰记忆模组在观测时崩溃,相关半衰记忆资料档案内容已全部清空。"; "About vChewing…" = "关于威注音…"; "vChewing Preferences…" = "威注音偏好设定…"; @@ -17,7 +21,7 @@ "New Version Available" = "有新版可下载"; "Not Now" = "以后再说"; "Visit Website" = "前往网站"; -"You're currently using vChewing %@ (%@), a new version %@ (%@) is now available. Do you want to visit vChewing's website to download the version?%@" = "目前使用的威注音官方版本是 %1$@ (%2$@),网路上有更新版本 %3$@ (%4$@) 可供下载。是否要前往威注音网站下载新版来安装?%5$@"; +"You're currently using vChewing %@ (%@), a new version %@ (%@) is now available. Do you want to visit vChewing's website to download the version?" = "目前使用的威注音官方版本是 %1$@ (%2$@),网路上有更新版本 %3$@ (%4$@) 可供下载。是否要前往威注音网站下载新版来安装?"; "Force KangXi Writing" = "康熙正体字模式"; "NotificationSwitchON" = "✔ 已启用"; "NotificationSwitchOFF" = "✘ 已停用"; diff --git a/Source/Resources/zh-Hant.lproj/Localizable.strings b/Source/Resources/zh-Hant.lproj/Localizable.strings index e05cae2d..dfa732b0 100644 --- a/Source/Resources/zh-Hant.lproj/Localizable.strings +++ b/Source/Resources/zh-Hant.lproj/Localizable.strings @@ -1,4 +1,8 @@ "vChewing" = "威注音輸入法"; +"Update Check Completed" = "更新檢查完畢"; +"You are already using the latest version." = "您正在使用目前最新的發行版。"; +"Plist downloaded is nil." = "下載來的更新資訊並非 Plist 檔案。"; +"Plist downloaded cannot be parsed correctly." = "下載來的更新資訊 Plist 檔案無法正常解析。"; "vChewing crashed while handling previously loaded UOM observation data. These data files are cleaned now to ensure the usability." = "威注音輸入法的使用者半衰記憶模組在觀測時崩潰,相關半衰記憶資料檔案內容已全部清空。"; "About vChewing…" = "關於威注音…"; "vChewing Preferences…" = "威注音偏好設定…"; @@ -17,7 +21,7 @@ "New Version Available" = "有新版可下載"; "Not Now" = "以後再說"; "Visit Website" = "前往網站"; -"You're currently using vChewing %@ (%@), a new version %@ (%@) is now available. Do you want to visit vChewing's website to download the version?%@" = "目前使用的威注音官方版本是 %1$@ (%2$@),網路上有更新版本 %3$@ (%4$@) 可供下載。是否要前往威注音網站下載新版來安裝?%5$@"; +"You're currently using vChewing %@ (%@), a new version %@ (%@) is now available. Do you want to visit vChewing's website to download the version?" = "目前使用的威注音官方版本是 %1$@ (%2$@),網路上有更新版本 %3$@ (%4$@) 可供下載。是否要前往威注音網站下載新版來安裝?"; "Force KangXi Writing" = "康熙正體字模式"; "NotificationSwitchON" = "✔ 已啟用"; "NotificationSwitchOFF" = "✘ 已停用"; diff --git a/vChewing.xcodeproj/project.pbxproj b/vChewing.xcodeproj/project.pbxproj index ecfcab80..430928b3 100644 --- a/vChewing.xcodeproj/project.pbxproj +++ b/vChewing.xcodeproj/project.pbxproj @@ -85,6 +85,7 @@ 5BBBB77527AED70B0023B93A /* MenuIcon-SCVIM.png in Resources */ = {isa = PBXBuildFile; fileRef = 5BBBB77127AED70B0023B93A /* MenuIcon-SCVIM.png */; }; 5BBBB77627AED70B0023B93A /* MenuIcon-TCVIM.png in Resources */ = {isa = PBXBuildFile; fileRef = 5BBBB77227AED70B0023B93A /* MenuIcon-TCVIM.png */; }; 5BBBB77A27AEDC690023B93A /* clsSFX.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BBBB77927AEDC690023B93A /* clsSFX.swift */; }; + 5BBC9EFC28CA042500041196 /* UpdateSputnik.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BBC9EFB28CA042500041196 /* UpdateSputnik.swift */; }; 5BC2652227E04B7E00700291 /* uninstall.sh in Resources */ = {isa = PBXBuildFile; fileRef = 5BC2652127E04B7B00700291 /* uninstall.sh */; }; 5BC4479D2865686500EDC323 /* data-chs.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5BEDB71C283B4AEA0078EB25 /* data-chs.plist */; }; 5BC4479E2865686500EDC323 /* data-cht.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5BEDB720283B4AEA0078EB25 /* data-cht.plist */; }; @@ -303,6 +304,7 @@ 5BBBB77227AED70B0023B93A /* MenuIcon-TCVIM.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "MenuIcon-TCVIM.png"; sourceTree = ""; }; 5BBBB77727AEDB290023B93A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/MainMenu.strings; sourceTree = ""; }; 5BBBB77927AEDC690023B93A /* clsSFX.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = clsSFX.swift; sourceTree = ""; tabWidth = 2; usesTabs = 0; }; + 5BBC9EFB28CA042500041196 /* UpdateSputnik.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateSputnik.swift; sourceTree = ""; }; 5BBD627827B6C4D900271480 /* Update-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Update-Info.plist"; sourceTree = ""; }; 5BC0AAC927F58472002D33E9 /* pkgPreInstall.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = pkgPreInstall.sh; sourceTree = ""; }; 5BC0AACA27F58472002D33E9 /* pkgPostInstall.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = pkgPostInstall.sh; sourceTree = ""; }; @@ -542,6 +544,7 @@ 5B5E535127EF261400C6AA1E /* IME.swift */, 5B175FFA28C5CDDC0078D1B4 /* IMKHelper.swift */, 5B62A33527AE795800A19448 /* mgrPrefs.swift */, + 5BBC9EFB28CA042500041196 /* UpdateSputnik.swift */, ); path = IMEModules; sourceTree = ""; @@ -1214,6 +1217,7 @@ 5B40730C281672610023DFFF /* lmAssociates.swift in Sources */, D427F76C278CA2B0004A2160 /* AppDelegate.swift in Sources */, 5BA9FD4527FEF3C9002DE248 /* ToolbarItemStyleViewController.swift in Sources */, + 5BBC9EFC28CA042500041196 /* UpdateSputnik.swift in Sources */, 5BA9FD4127FEF3C8002DE248 /* PreferencesStyle.swift in Sources */, 5B7F225D2808501000DDD3CB /* KeyHandler_HandleInput.swift in Sources */, 5BA9FD1227FEDB6B002DE248 /* suiPrefPaneExperience.swift in Sources */,