From 5fa3cc3919fa15abc88f2e1551e858bec5ff97a6 Mon Sep 17 00:00:00 2001 From: JoshuaBrest <36625023+JoshuaBrest@users.noreply.github.com> Date: Wed, 29 Nov 2023 02:21:31 -0800 Subject: [PATCH 01/22] Cool app updates --- Whisky.xcodeproj/project.pbxproj | 41 +- .../xcshareddata/swiftpm/Package.resolved | 24 +- Whisky/Info.plist | 4 + Whisky/Localizable.xcstrings | 373 +++++++++++++++--- Whisky/Utils/SparkleUpdaterEvents.swift | 133 +++++++ Whisky/Views/ContentView.swift | 9 +- .../Views/Updater/UpdateControlerView.swift | 322 +++++++++++++++ Whisky/Views/Updater/UpdatePreviewView.swift | 169 ++++++++ Whisky/Views/WhiskyApp.swift | 19 +- 9 files changed, 1018 insertions(+), 76 deletions(-) create mode 100644 Whisky/Utils/SparkleUpdaterEvents.swift create mode 100644 Whisky/Views/Updater/UpdateControlerView.swift create mode 100644 Whisky/Views/Updater/UpdatePreviewView.swift diff --git a/Whisky.xcodeproj/project.pbxproj b/Whisky.xcodeproj/project.pbxproj index a297c5a74..110b036c9 100644 --- a/Whisky.xcodeproj/project.pbxproj +++ b/Whisky.xcodeproj/project.pbxproj @@ -56,6 +56,10 @@ 8CB681E52AED7C6F0018D319 /* WhiskyKit in Resources */ = {isa = PBXBuildFile; fileRef = 8CB681E42AED7C6F0018D319 /* WhiskyKit */; }; 8CB681E72AED7CD00018D319 /* WhiskyKit in Frameworks */ = {isa = PBXBuildFile; productRef = 8CB681E62AED7CD00018D319 /* WhiskyKit */; }; DB696FC82AFAE5DA0037EB2F /* PinCreationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB696FC72AFAE5DA0037EB2F /* PinCreationView.swift */; }; + EB051A092B150F7100F5F5B7 /* UpdatePreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB051A082B150F7100F5F5B7 /* UpdatePreviewView.swift */; }; + EB051A0E2B16EA7E00F5F5B7 /* MarkdownUI in Frameworks */ = {isa = PBXBuildFile; productRef = EB051A0D2B16EA7E00F5F5B7 /* MarkdownUI */; }; + EB051A102B1710A700F5F5B7 /* SparkleUpdaterEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB051A0F2B1710A700F5F5B7 /* SparkleUpdaterEvents.swift */; }; + EB051A132B17263300F5F5B7 /* UpdateControlerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB051A122B17263300F5F5B7 /* UpdateControlerView.swift */; }; EB58FB552A499896002DC184 /* SemanticVersion in Frameworks */ = {isa = PBXBuildFile; productRef = EB58FB542A499896002DC184 /* SemanticVersion */; }; EEA5A2462A31DD65008274AE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEA5A2452A31DD65008274AE /* AppDelegate.swift */; }; /* End PBXBuildFile section */ @@ -151,6 +155,9 @@ 8C73E1332AF472FC00B6FB45 /* ProgramMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgramMenuView.swift; sourceTree = ""; }; 8CB681E42AED7C6F0018D319 /* WhiskyKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = WhiskyKit; sourceTree = ""; }; DB696FC72AFAE5DA0037EB2F /* PinCreationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinCreationView.swift; sourceTree = ""; }; + EB051A082B150F7100F5F5B7 /* UpdatePreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdatePreviewView.swift; sourceTree = ""; }; + EB051A0F2B1710A700F5F5B7 /* SparkleUpdaterEvents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SparkleUpdaterEvents.swift; sourceTree = ""; }; + EB051A122B17263300F5F5B7 /* UpdateControlerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateControlerView.swift; sourceTree = ""; }; EEA5A2452A31DD65008274AE /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -162,6 +169,7 @@ 8C2AEFC82AED79B700CB568F /* WhiskyKit in Frameworks */, 6E064B1229DD32A200D9A2D2 /* Sparkle in Frameworks */, EB58FB552A499896002DC184 /* SemanticVersion in Frameworks */, + EB051A0E2B16EA7E00F5F5B7 /* MarkdownUI in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -226,8 +234,8 @@ 6E40498029CCA8B0006E3F1B /* BottleView.swift */, 6E50D98429CDF25B008C39F6 /* BottleCreationView.swift */, 6EA2D47D29DDAE5E00C84668 /* BottleRenameView.swift */, - 6E355E5729D78249002D83BE /* ConfigView.swift */, 6E182FC92B0BF64E00AADE81 /* WinetricksView.swift */, + 6E355E5729D78249002D83BE /* ConfigView.swift */, 6E17B6422AF3FD6E00831173 /* Pins */, ); path = Bottle; @@ -302,6 +310,7 @@ 6E40495729CCA19C006E3F1B /* ContentView.swift */, 6E064B1329DD331F00D9A2D2 /* SparkleView.swift */, 6E7C07BF2AAF570100F6E66B /* FileOpenView.swift */, + EB051A112B17261A00F5F5B7 /* Updater */, ); path = Views; sourceTree = ""; @@ -329,6 +338,7 @@ 6E621CEE2A5F631200C9AAB3 /* Winetricks.swift */, 6E70A4A02A9A280C007799E9 /* WhiskyCmd.swift */, 6E7C07BD2AAE7B0100F6E66B /* ProgramShortcut.swift */, + EB051A0F2B1710A700F5F5B7 /* SparkleUpdaterEvents.swift */, ); path = Utils; sourceTree = ""; @@ -373,6 +383,15 @@ path = WhiskyThumbnail; sourceTree = ""; }; + EB051A112B17261A00F5F5B7 /* Updater */ = { + isa = PBXGroup; + children = ( + EB051A082B150F7100F5F5B7 /* UpdatePreviewView.swift */, + EB051A122B17263300F5F5B7 /* UpdateControlerView.swift */, + ); + path = Updater; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -397,6 +416,7 @@ 6E064B1129DD32A200D9A2D2 /* Sparkle */, EB58FB542A499896002DC184 /* SemanticVersion */, 8C2AEFC72AED79B700CB568F /* WhiskyKit */, + EB051A0D2B16EA7E00F5F5B7 /* MarkdownUI */, ); productName = Whisky; productReference = 6E40495229CCA19C006E3F1B /* Whisky.app */; @@ -501,6 +521,7 @@ 6E95F66E2AB3F33C00D585D1 /* XCRemoteSwiftPackageReference "SwiftyTextTable" */, 6E95F6712AB3F67200D585D1 /* XCRemoteSwiftPackageReference "Progress" */, 8C2AEFC62AED79B700CB568F /* XCLocalSwiftPackageReference "WhiskyKit" */, + EB051A0C2B16EA2E00F5F5B7 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */, ); productRefGroup = 6E40495329CCA19C006E3F1B /* Products */; projectDirPath = ""; @@ -570,14 +591,17 @@ DB696FC82AFAE5DA0037EB2F /* PinCreationView.swift in Sources */, 6E7C07BE2AAE7B0100F6E66B /* ProgramShortcut.swift in Sources */, 6E355E5829D78249002D83BE /* ConfigView.swift in Sources */, + EB051A102B1710A700F5F5B7 /* SparkleUpdaterEvents.swift in Sources */, 6EA2D47E29DDAE5E00C84668 /* BottleRenameView.swift in Sources */, 63FFDE862ADF0C7700178665 /* BottomBar.swift in Sources */, + EB051A092B150F7100F5F5B7 /* UpdatePreviewView.swift in Sources */, 6E6C0CF62A419A8300356232 /* GPTKDownloadView.swift in Sources */, 6E50D98529CDF25B008C39F6 /* BottleCreationView.swift in Sources */, 6E182FCA2B0BF64E00AADE81 /* WinetricksView.swift in Sources */, 6E17B6492AF4118F00831173 /* EnvironmentArgView.swift in Sources */, 6E6C0CF42A419A7600356232 /* RosettaView.swift in Sources */, 6E6C0CF82A419A8C00356232 /* GPTKInstallView.swift in Sources */, + EB051A132B17263300F5F5B7 /* UpdateControlerView.swift in Sources */, 6E40498329CCA91B006E3F1B /* Bottle+Extensions.swift in Sources */, 6E621CEF2A5F631300C9AAB3 /* Winetricks.swift in Sources */, 6E17B6462AF3FDC100831173 /* PinView.swift in Sources */, @@ -813,7 +837,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.isaacmarovitz.Whisky; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = Whisky; +"PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = Whisky; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; }; @@ -1016,6 +1040,14 @@ minimumVersion = 0.4.0; }; }; + EB051A0C2B16EA2E00F5F5B7 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/gonzalezreal/swift-markdown-ui.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.2.0; + }; + }; EB58FB532A499896002DC184 /* XCRemoteSwiftPackageReference "SemanticVersion" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/SwiftPackageIndex/SemanticVersion"; @@ -1065,6 +1097,11 @@ isa = XCSwiftPackageProductDependency; productName = WhiskyKit; }; + EB051A0D2B16EA7E00F5F5B7 /* MarkdownUI */ = { + isa = XCSwiftPackageProductDependency; + package = EB051A0C2B16EA2E00F5F5B7 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */; + productName = MarkdownUI; + }; EB58FB542A499896002DC184 /* SemanticVersion */ = { isa = XCSwiftPackageProductDependency; package = EB58FB532A499896002DC184 /* XCRemoteSwiftPackageReference "SemanticVersion" */; diff --git a/Whisky.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Whisky.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 067e72711..c07eb60cb 100644 --- a/Whisky.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Whisky.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,14 @@ { "pins" : [ + { + "identity" : "networkimage", + "kind" : "remoteSourceControl", + "location" : "https://github.com/gonzalezreal/NetworkImage", + "state" : { + "revision" : "7aff8d1b31148d32c5933d75557d42f6323ee3d1", + "version" : "6.0.0" + } + }, { "identity" : "progress.swift", "kind" : "remoteSourceControl", @@ -14,8 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/SwiftPackageIndex/SemanticVersion", "state" : { - "revision" : "a70840d5fca686ae3bd2fcf8aecc5ded0bd4f125", - "version" : "0.3.6" + "revision" : "ea8eea9d89842a29af1b8e6c7677f1c86e72fa42", + "version" : "0.4.0" } }, { @@ -24,7 +33,7 @@ "location" : "https://github.com/sparkle-project/Sparkle", "state" : { "branch" : "2.x", - "revision" : "b7b858dbf385cdd1fe1ab8a3f3ee8586fa850d5d" + "revision" : "1a0b023e1c4d37302ae6401b8f9c38af0729e21d" } }, { @@ -36,6 +45,15 @@ "version" : "1.2.3" } }, + { + "identity" : "swift-markdown-ui", + "kind" : "remoteSourceControl", + "location" : "https://github.com/gonzalezreal/swift-markdown-ui.git", + "state" : { + "revision" : "5df8a4adedd6ae4eb2455ef60ff75183984daeb8", + "version" : "2.2.0" + } + }, { "identity" : "swiftytexttable", "kind" : "remoteSourceControl", diff --git a/Whisky/Info.plist b/Whisky/Info.plist index e2e4738d6..21e7707ce 100644 --- a/Whisky/Info.plist +++ b/Whisky/Info.plist @@ -57,6 +57,10 @@ https://data.getwhisky.app/appcast.xml SUPublicEDKey tnZFAvPUfCpM7Tr7Sx5gKRm6BUQQ6htJQeOMP44evms= + GithubRepoOwner + Whisky-App + GithubRepoName + Whisky UTExportedTypeDeclarations diff --git a/Whisky/Localizable.xcstrings b/Whisky/Localizable.xcstrings index 686390f55..f8fa16c4a 100644 --- a/Whisky/Localizable.xcstrings +++ b/Whisky/Localizable.xcstrings @@ -237,6 +237,16 @@ } } }, + "button.cancel" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cancel" + } + } + } + }, "button.cDrive" : { "localizations" : { "da" : { @@ -1051,8 +1061,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Pin" + "state" : "translated", + "value" : "固定" } }, "zh-Hant" : { @@ -2349,8 +2359,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Unpin" + "state" : "translated", + "value" : "取消固定" } }, "zh-Hant" : { @@ -4119,8 +4129,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Enhanced Sync" + "state" : "translated", + "value" : "增强 Sync" } }, "zh-Hant" : { @@ -4237,8 +4247,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "None" + "state" : "translated", + "value" : "无" } }, "zh-Hant" : { @@ -7069,8 +7079,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Add" + "state" : "translated", + "value" : "加入" } }, "zh-Hant" : { @@ -7187,8 +7197,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Remove" + "state" : "translated", + "value" : "删掉" } }, "zh-Hant" : { @@ -7541,8 +7551,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Website" + "state" : "translated", + "value" : "网站" } }, "zh-Hant" : { @@ -8604,8 +8614,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Pin" + "state" : "translated", + "value" : "固定" } }, "zh-Hant" : { @@ -8723,8 +8733,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "\"%@\" is already pinned!" + "state" : "translated", + "value" : "\"%@\" 已经固定了!" } }, "zh-Hant" : { @@ -8841,8 +8851,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Error Pinning Program" + "state" : "translated", + "value" : "无法固定程序" } }, "zh-Hant" : { @@ -8959,8 +8969,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Pin Program" + "state" : "translated", + "value" : "固定软解" } }, "zh-Hant" : { @@ -9077,8 +9087,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Pin name:" + "state" : "translated", + "value" : "固定名字:" } }, "zh-Hant" : { @@ -9195,8 +9205,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Program path:" + "state" : "translated", + "value" : "然姐路径:" } }, "zh-Hant" : { @@ -9313,8 +9323,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Pin Program" + "state" : "translated", + "value" : "固定软解" } }, "zh-Hant" : { @@ -9549,8 +9559,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Add Selected to Blocklist" + "state" : "translated", + "value" : "添加已选的到阻止列表" } }, "zh-Hant" : { @@ -10257,8 +10267,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Remove Selected from Blocklist" + "state" : "translated", + "value" : "从阻止列表中移除已选的" } }, "zh-Hant" : { @@ -10375,8 +10385,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Settings" + "state" : "translated", + "value" : "设置" } }, "zh-Hant" : { @@ -11319,8 +11329,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "General" + "state" : "translated", + "value" : "通用" } }, "zh-Hant" : { @@ -11437,8 +11447,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Automatically check for GPTK updates" + "state" : "translated", + "value" : "自动检查 GPTK 更新" } }, "zh-Hant" : { @@ -11555,8 +11565,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Terminate Wine processes when Whisky closes" + "state" : "translated", + "value" : "当 Whisky 关闭时把 Wine 进程终止" } }, "zh-Hant" : { @@ -11673,8 +11683,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Automatically check for Whisky updates" + "state" : "translated", + "value" : "自动检查 Whisky 更新" } }, "zh-Hant" : { @@ -11791,8 +11801,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Updates" + "state" : "translated", + "value" : "更新" } }, "zh-Hant" : { @@ -13325,8 +13335,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Retry" + "state" : "translated", + "value" : "重试" } }, "zh-Hant" : { @@ -13561,8 +13571,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Rosetta 2 installation failed. Please install it manually." + "state" : "translated", + "value" : "无法自动安装 Rosetta 2。请手动安装。" } }, "zh-Hant" : { @@ -14989,6 +14999,132 @@ } } }, + "update.cancel" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ask Me Next Tme" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "下一次问" + } + } + } + }, + "update.changeLog" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Change Log" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "更新日志" + } + } + } + }, + "update.changeLogFailed" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Failed to get change log!" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "获取更新日志失败!" + } + } + } + }, + "update.checkingForUpdates" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Whisky is checking for updates...." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Whisky 正在检查更新..." + } + } + } + }, + "update.description" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "You are running an outdated version of Whisky. Would you like to update?" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "您正在运行一个过时版本的Whisky。您想要下载更新吗?" + } + } + } + }, + "update.descriptionLoaded" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "You are running an outdated version of Whisky. The latest version available is %2$@, you are running %1$@. Would you like to update?" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "您正在运行一个过时版本的Whisky。最新可用的版本是 %2$@,你在用 %1$@。您想要下载更新吗?" + } + } + } + }, + "update.downloading" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Downloading Update..." + } + } + } + }, + "update.error" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Update Error!" + } + } + } + }, + "update.extracting" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Extracting Files..." + } + } + } + }, "update.gptk.description" : { "localizations" : { "da" : { @@ -15343,6 +15479,114 @@ } } }, + "update.initializating" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Initializing..." + } + } + } + }, + "update.installing" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Finishing Up..." + } + } + } + }, + "update.noUpdateFound" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "No Updates Available" + } + } + } + }, + "update.noUpdateFound.description" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "No updates are available. You are on the latest version of Whisky." + } + } + } + }, + "update.readyRelaunch" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Update Complete!" + } + } + } + }, + "update.readyRelaunch.description" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Update Complete! Press \"OK\" to relaunch." + } + } + } + }, + "update.retryChangeLog" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Retry" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "重试" + } + } + } + }, + "update.title" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Update Available!" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "有更新了!" + } + } + } + }, + "update.update" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Update" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "下载更新" + } + } + } + }, "wine.clearShaderCaches" : { "localizations" : { "da" : { @@ -15568,8 +15812,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Apps" + "state" : "translated", + "value" : "软解" } }, "zh-Hant" : { @@ -15687,8 +15931,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Benchmarks" + "state" : "translated", + "value" : "皆准测试" } }, "zh-Hant" : { @@ -15700,6 +15944,7 @@ } }, "winetricks.category.dlls" : { + "comment" : "It's best to write DLL", "extractionState" : "manual", "localizations" : { "da" : { @@ -15806,8 +16051,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "DLLs" + "state" : "translated", + "value" : "DLL" } }, "zh-Hant" : { @@ -15925,8 +16170,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Fonts" + "state" : "translated", + "value" : "字体" } }, "zh-Hant" : { @@ -16044,8 +16289,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Games" + "state" : "translated", + "value" : "游戏" } }, "zh-Hant" : { @@ -16163,8 +16408,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Settings" + "state" : "translated", + "value" : "设置" } }, "zh-Hant" : { @@ -16281,8 +16526,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Description" + "state" : "translated", + "value" : "描述" } }, "zh-Hant" : { @@ -16399,8 +16644,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Command" + "state" : "translated", + "value" : "命令" } }, "zh-Hant" : { diff --git a/Whisky/Utils/SparkleUpdaterEvents.swift b/Whisky/Utils/SparkleUpdaterEvents.swift new file mode 100644 index 000000000..816afee86 --- /dev/null +++ b/Whisky/Utils/SparkleUpdaterEvents.swift @@ -0,0 +1,133 @@ +// +// SparkleUpdaterEvents.swift +// Whisky +// +// This file is part of Whisky. +// +// Whisky is free software: you can redistribute it and/or modify it under the terms +// of the GNU General Public License as published by the Free Software Foundation, +// either version 3 of the License, or (at your option) any later version. +// +// Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with Whisky. +// If not, see https://www.gnu.org/licenses/. +// + +import Foundation +import Sparkle + +class SparkleUpdaterEvents: NSObject, SPUUserDriver { + static let shared = SparkleUpdaterEvents() + + var checkingForUpdates: ((@escaping () -> Void) -> Void)? + var updateFound: ((SUAppcastItem, SPUUserUpdateState, @escaping (SPUUserUpdateChoice) -> Void) -> Void)? + var update: ((SPUDownloadData) -> Void)? + var updateError: ((NSError) -> Void)? + var updateDownloadState: (() -> Void)? + var updateExtractState: (() -> Void)? + var updateInstallState: (() -> Void)? + var updateReadyRelaunch: ((@escaping (SPUUserUpdateChoice) -> Void) -> Void)? + var updateDismiss: (() -> Void)? + + var expectedContentLength: Double = 0 + var receivedContentLength: Double = 0 + + var extractProgress: Double = 0 + + enum UpdateOption { + case install, dismiss + } + + func show(_ request: SPUUpdatePermissionRequest) async -> SUUpdatePermissionResponse { + return .init( + automaticUpdateChecks: false, + sendSystemProfile: false + ) + } + + func showUserInitiatedUpdateCheck(cancellation: @escaping () -> Void) { + checkingForUpdates?(cancellation) + } + + func showUpdateFound( + with appcastItem: SUAppcastItem, + state: SPUUserUpdateState, + reply: @escaping (SPUUserUpdateChoice) -> Void + ) { + if let updateFound = updateFound { + updateFound(appcastItem, state, reply) + } else { + reply(.dismiss) + } + } + + func showUpdateReleaseNotes(with downloadData: SPUDownloadData) { + update?(downloadData) + } + + func showUpdateReleaseNotesFailedToDownloadWithError(_ error: Error) { + updateError?(error as NSError) + } + + func showUpdateNotFoundWithError(_ error: Error, acknowledgement: @escaping () -> Void) { + updateError?(error as NSError) + + acknowledgement() + } + + func showUpdaterError(_ error: Error, acknowledgement: @escaping () -> Void) { + updateError?(error as NSError) + + acknowledgement() + } + + func showDownloadInitiated(cancellation: @escaping () -> Void) { + updateDownloadState?() + } + + func showDownloadDidReceiveExpectedContentLength(_ expectedContentLength: UInt64) { + self.expectedContentLength = Double(expectedContentLength) + updateDownloadState?() + } + + func showDownloadDidReceiveData(ofLength length: UInt64) { + receivedContentLength += Double(length) + updateDownloadState?() + } + + func showDownloadDidStartExtractingUpdate() { + updateExtractState?() + } + + func showExtractionReceivedProgress(_ progress: Double) { + extractProgress = progress + updateExtractState?() + } + + func showReady(toInstallAndRelaunch acknowledgement: @escaping (SPUUserUpdateChoice) -> Void) { + updateReadyRelaunch?(acknowledgement) + } + + func showInstallingUpdate( + withApplicationTerminated applicationTerminated: Bool, + retryTerminatingApplication: @escaping () -> Void + ) { + updateInstallState?() + } + + func showUpdateInstalledAndRelaunched(_ relaunched: Bool, acknowledgement: @escaping () -> Void) { + acknowledgement() + } + + func showUpdateInFocus() { + return + } + + func dismissUpdateInstallation() { + updateDismiss?() + return + } +} diff --git a/Whisky/Views/ContentView.swift b/Whisky/Views/ContentView.swift index 6730f20df..12130eb05 100644 --- a/Whisky/Views/ContentView.swift +++ b/Whisky/Views/ContentView.swift @@ -20,10 +20,14 @@ import SwiftUI import UniformTypeIdentifiers import WhiskyKit import SemanticVersion +import Sparkle struct ContentView: View { @AppStorage("selectedBottleURL") private var selectedBottleURL: URL? @EnvironmentObject var bottleVM: BottleVM + + let updater: SPUUpdater? + @Binding var showSetup: Bool @State var selected: URL? @State var showBottleCreation: Bool = false @@ -35,6 +39,9 @@ struct ContentView: View { @State private var refreshAnimation: Angle = .degrees(0) var body: some View { + if let updater { + UpdateControlerView(updater: updater) + } NavigationSplitView { ScrollViewReader { proxy in List(selection: $selected) { @@ -284,6 +291,6 @@ struct BottleListEntry: View { } #Preview { - ContentView(showSetup: .constant(false)) + ContentView(updater: .none, showSetup: .constant(false)) .environmentObject(BottleVM.shared) } diff --git a/Whisky/Views/Updater/UpdateControlerView.swift b/Whisky/Views/Updater/UpdateControlerView.swift new file mode 100644 index 000000000..085654082 --- /dev/null +++ b/Whisky/Views/Updater/UpdateControlerView.swift @@ -0,0 +1,322 @@ +// +// UpdateControlerView.swift +// Whisky +// +// This file is part of Whisky. +// +// Whisky is free software: you can redistribute it and/or modify it under the terms +// of the GNU General Public License as published by the Free Software Foundation, +// either version 3 of the License, or (at your option) any later version. +// +// Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with Whisky. +// If not, see https://www.gnu.org/licenses/. +// + +import SwiftUI +import Sparkle + +enum UpdateState { + case initializating, downloading, extracting, installing +} + +struct UpdateControlerView: View { + let updater: SPUUpdater + + @State var showCheckingForUpdates = false + @State var cancelCheckingForUpdates: (() -> Void)? + @State var showUpdatePreview = false + @State var updateUltimatum: ((Bool) -> Void)? + @State var showUpdater = false + @State var updateState: UpdateState = .initializating + @State var updateStateDownloadStatedAt: Date = Date.init(timeIntervalSince1970: 0) + @State var updateStateDownloadableBytes: Double = 0 + @State var updateStateDownloadedBytes: Double = 0 + @State var updateStateExtractProgress: Double = 0 + + var body: some View { + VStack {} + .sheet(isPresented: $showCheckingForUpdates, content: { + UpdateCheckingView(cancel: { + showCheckingForUpdates = false + cancelCheckingForUpdates?() + }) + .frame(width: 300) + .interactiveDismissDisabled() + }) + .sheet(isPresented: $showUpdatePreview, content: { + UpdatePreviewView(dismiss: { + showUpdatePreview = false + updateUltimatum?(false) + updateUltimatum = .none + }, install: { + updateUltimatum?(true) + updateUltimatum = .none + }) + .interactiveDismissDisabled() + .frame(width: 600, height: 400) + }) + .sheet(isPresented: $showUpdater, content: { + UpdateInstallingView( + state: $updateState, + downloadStatedAt: $updateStateDownloadStatedAt, + downloadableBytes: $updateStateDownloadableBytes, + downloadedBytes: $updateStateDownloadedBytes, + extractProgress: $updateStateExtractProgress + ) + .interactiveDismissDisabled() + .frame(width: 300) + }) + .onAppear { + // On fn called show sheet + SparkleUpdaterEvents.shared.checkingForUpdates = { cancel in + // Prevent updater from checking for updates + showCheckingForUpdates = false + showUpdatePreview = false + showUpdater = false + cancelCheckingForUpdates = { + cancel() + } + showCheckingForUpdates = true + } + SparkleUpdaterEvents.shared.updateFound = { _, _, reply in + showCheckingForUpdates = false + showUpdater = false + showUpdatePreview = false + updateUltimatum = { option in + if option { + + reply(.install) + } else { + reply(.dismiss) + } + } + showUpdatePreview = true + } + SparkleUpdaterEvents.shared.updateDismiss = { + // Show Alert + if showCheckingForUpdates { + showCheckingForUpdates = false + showUpdatePreview = false + showUpdater = false + // no update found + displayPrompt( + title: String(localized: "update.noUpdateFound"), + description: String(localized: "update.noUpdateFound.description"), + action: String(localized: "button.ok"), + actionHandler: { + // Dismiss + } + ) + } + } + SparkleUpdaterEvents.shared.updateError = { error in + showCheckingForUpdates = false + showUpdatePreview = false + showUpdater = false + displayPrompt( + title: String(localized: "update.error"), + description: error.localizedDescription, + action: String(localized: "button.ok"), + actionHandler: { + // Dismiss + } + ) + } + // Update download state + SparkleUpdaterEvents.shared.updateDownloadState = { + if updateStateDownloadStatedAt == Date(timeIntervalSince1970: 0) { + updateStateDownloadStatedAt = Date() + } + showCheckingForUpdates = false + showUpdatePreview = false + showUpdater = true + withAnimation { updateState = .downloading } + updateStateDownloadableBytes = SparkleUpdaterEvents.shared.expectedContentLength + updateStateDownloadedBytes = SparkleUpdaterEvents.shared.receivedContentLength + } + // Update extract state + SparkleUpdaterEvents.shared.updateExtractState = { + showCheckingForUpdates = false + showUpdatePreview = false + showUpdater = true + withAnimation { updateState = .extracting } + updateStateExtractProgress = SparkleUpdaterEvents.shared.extractProgress + } + // Update install state + SparkleUpdaterEvents.shared.updateInstallState = { + showCheckingForUpdates = false + showUpdatePreview = false + showUpdater = true + withAnimation { updateState = .installing } + } + // Update ready relaunch + SparkleUpdaterEvents.shared.updateReadyRelaunch = { relaunch in + showCheckingForUpdates = false + showUpdatePreview = false + showUpdater = false + displayPrompt( + title: String(localized: "update.readyRelaunch"), + description: String(localized: "update.readyRelaunch.description"), + action: String(localized: "button.ok"), + actionHandler: { + // Kill all bottles + await WhiskyApp.killBottles() + // Relaunch + relaunch(.install) + // Actualy relaunch (I don't want to make a helper for this so.... you get...) + let task = Process() + task.launchPath = "/bin/sh" + task.arguments = [ + "-c", + """ + kill "\(ProcessInfo.processInfo.processName)"; + sleep 0.5; open "\(Bundle.main.bundlePath)" + """ + ] + task.launch() + NSApp.terminate(nil) + exit(0) + } + ) + } + } + } + + func displayPrompt(title: String, description: String, action: String, actionHandler: @escaping () -> Void) { + showCheckingForUpdates = false + showUpdatePreview = false + showUpdater = false + Task(priority: .userInitiated) { + await MainActor.run { + let alert = NSAlert() + alert.messageText = title + alert.informativeText = description + alert.addButton(withTitle: action) + alert.runModal() + } + } + } +} + +struct UpdateCheckingView: View { + let cancel: () -> Void + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + Text("update.checkingForUpdates") + .fontWeight(.bold) + ProgressView() + .progressViewStyle(.linear) + HStack { + Spacer() + Button("button.cancel") { + cancel() + } + .buttonStyle(.borderedProminent) + .keyboardShortcut(.defaultAction) + } + } + .padding(20) + .frame(alignment: .leading) + } +} + +struct UpdateInstallingView: View { + @Binding var state: UpdateState + @Binding var downloadStatedAt: Date + @Binding var downloadableBytes: Double + @Binding var downloadedBytes: Double + @Binding var extractProgress: Double + + @State var fractionProgress: Double = 0 + @State var downloadSpeed: Double = 0 + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + Text( + state == .downloading + ? "update.downloading" + : state == .extracting + ? "update.extracting" + : state == .installing + ? "update.installing" + : "update.initializating" + ) + .fontWeight(.bold) + if state == .installing || state == .initializating { + ProgressView() + .progressViewStyle(.linear) + } else { + ProgressView(value: fractionProgress, total: 1) + .progressViewStyle(.linear) + if state == .downloading { + HStack { + HStack { + Text(String(format: String(localized: "setup.gptk.progress"), + formatBytes(bytes: downloadedBytes), + formatBytes(bytes: downloadableBytes))) + + Text(String(" ")) + + (shouldShowEstimate() ? + Text(String(format: String(localized: "setup.gptk.eta"), + formatRemainingTime(remainingBytes: downloadableBytes - downloadedBytes))) + : Text(String())) + Spacer() + } + .font(.subheadline) + .monospacedDigit() + } + } + } + } + .padding(20) + .frame(alignment: .leading) + .onChange(of: downloadedBytes) { + let currentTime = Date() + let elapsedTime = currentTime.timeIntervalSince(downloadStatedAt ?? currentTime) + if downloadedBytes > 0 { + downloadSpeed = Double(downloadedBytes) / elapsedTime + } + withAnimation { + if downloadableBytes > 0 { + fractionProgress = downloadedBytes / downloadableBytes + } else { + fractionProgress = 0 + } + } + } + .onChange(of: extractProgress) { + withAnimation { + fractionProgress = extractProgress + } + } + } + + func formatBytes(bytes: Double) -> String { + let formatter = ByteCountFormatter() + formatter.countStyle = .file + formatter.zeroPadsFractionDigits = true + return formatter.string(fromByteCount: Int64(bytes)) + } + + func shouldShowEstimate() -> Bool { + let elapsedTime = Date().timeIntervalSince(downloadStatedAt ?? Date()) + return Int(elapsedTime.rounded()) > 5 && downloadedBytes != 0 + } + + func formatRemainingTime(remainingBytes: Double) -> String { + let remainingTimeInSeconds = remainingBytes / downloadSpeed + + let formatter = DateComponentsFormatter() + formatter.allowedUnits = [.hour, .minute, .second] + formatter.unitsStyle = .full + if shouldShowEstimate() { + return formatter.string(from: TimeInterval(remainingTimeInSeconds)) ?? "" + } else { + return "" + } + } +} diff --git a/Whisky/Views/Updater/UpdatePreviewView.swift b/Whisky/Views/Updater/UpdatePreviewView.swift new file mode 100644 index 000000000..1993dc43d --- /dev/null +++ b/Whisky/Views/Updater/UpdatePreviewView.swift @@ -0,0 +1,169 @@ +// +// UpdateUI.swift +// Whisky +// +// This file is part of Whisky. +// +// Whisky is free software: you can redistribute it and/or modify it under the terms +// of the GNU General Public License as published by the Free Software Foundation, +// either version 3 of the License, or (at your option) any later version. +// +// Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with Whisky. +// If not, see https://www.gnu.org/licenses/. +// + +import SwiftUI +import Sparkle +import MarkdownUI + +struct UpdatePreviewView: View { + enum MarkdownTextState { + case loaded, error, loading + } + + let dismiss: () -> Void + let install: () -> Void + + let updater = SparkleUpdaterEvents.shared + @State var markdownTextState: MarkdownTextState = .loading + @State var markdownText = "# Hello" + @State var currentVersion = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "(nil)" + @State var nextVersion = "" + + var body: some View { + HStack { + VStack(alignment: .leading, spacing: 12) { + Text("update.title") + .font(.title) + .fontWeight(.bold) + Text(markdownTextState != .loaded + ? String(localized: "update.description") + : String(format: String(localized: "update.descriptionLoaded"), + "v" + currentVersion, + nextVersion)) + Spacer() + HStack { + Button("update.cancel") { + dismiss() + } + Spacer() + Button("update.update") { + Task(priority: .userInitiated) { + install() + } + } + .buttonStyle(.borderedProminent) + .keyboardShortcut(.defaultAction) + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) + .padding(20) + VStack { + if markdownTextState == .loading { + ProgressView() + } else if markdownTextState == .error { + VStack(spacing: 12) { + Text("update.changeLogFailed") + Button("update.retryChangeLog") { + Task(priority: .userInitiated) { + await getChangelog() + } + } + } + } else { + ScrollView { + VStack(alignment: .leading) { + Text("update.changeLog") + .font(.title2) + .fontWeight(.bold) + .padding(.bottom, 12) + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) + Markdown { + markdownText + } + .markdownTheme(.basic) + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .padding(20) + .background(.ultraThickMaterial) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .onAppear { + Task(priority: .userInitiated) { + await getChangelog() + } + } + } + + func getChangelog() async { + withAnimation { markdownTextState = .loading } + let ghOwner = (Bundle.main.object(forInfoDictionaryKey: "GithubRepoOwner") as? String) ?? "Whisky-App" + let ghRepo = (Bundle.main.object(forInfoDictionaryKey: "GithubRepoName") as? String) ?? "Whisky" + + // Make a request to the Github API to get the latest release + // Append path not using string interpolation to non-urlencoded paths + guard let url = URL(string: "https://api.github.com/")? + .appending(path: "repos") + .appending(path: ghOwner) + .appending(path: ghRepo) + .appending(path: "releases") + .appending(path: "latest") + else { + withAnimation { markdownTextState = .error } + return + } + + var request = URLRequest(url: url) + request.setValue("application/vnd.github+json", forHTTPHeaderField: "Accept") + request.setValue("WhiskyApp", forHTTPHeaderField: "User-Agent") + + let data: Data + do { + let (dataInfo, _) = try (await URLSession.shared.data(for: request)) + data = dataInfo + } catch { + print("Changelog request failed: \(error)") + withAnimation { markdownTextState = .error } + return + } + + // Decode the JSON + struct Release: Codable { + let body: String + let tagName: String + + enum CodingKeys: String, CodingKey { // swiftlint:disable:this nesting + case body + case tagName = "tag_name" + } + } + + let release: Release + do { + release = try JSONDecoder().decode(Release.self, from: data) + } catch { + print("Failed to decode release: \(error)") + withAnimation { markdownTextState = .error } + return + } + + withAnimation { + markdownText = release.body + nextVersion = release.tagName + markdownTextState = .loaded + } + } +} + +#Preview { + UpdatePreviewView(dismiss: {}, install: {}) + .frame(width: 600, height: 400) +} diff --git a/Whisky/Views/WhiskyApp.swift b/Whisky/Views/WhiskyApp.swift index bb16bc6ba..ed2b3ce87 100644 --- a/Whisky/Views/WhiskyApp.swift +++ b/Whisky/Views/WhiskyApp.swift @@ -23,19 +23,26 @@ import WhiskyKit @main struct WhiskyApp: App { @State var showSetup: Bool = false + @State var showUpdater = false @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate @Environment(\.openURL) var openURL - private let updaterController: SPUStandardUpdaterController + private let updaterController: SPUUpdater init() { - updaterController = SPUStandardUpdaterController(startingUpdater: true, - updaterDelegate: nil, - userDriverDelegate: nil) + updaterController = SPUUpdater( + hostBundle: .main, + applicationBundle: .main, + userDriver: SparkleUpdaterEvents.shared, + delegate: nil) + + do { try updaterController.start() } catch { + print(error) + } } var body: some Scene { WindowGroup { - ContentView(showSetup: $showSetup) + ContentView(updater: updaterController, showSetup: $showSetup) .frame(minWidth: 550, minHeight: 250) .environmentObject(BottleVM.shared) .onAppear { @@ -46,7 +53,7 @@ struct WhiskyApp: App { .handlesExternalEvents(matching: ["{same path of URL?}"]) .commands { CommandGroup(after: .appInfo) { - SparkleView(updater: updaterController.updater) + SparkleView(updater: updaterController) } CommandGroup(before: .systemServices) { Divider() From ce5e4b5599c982b65ed58ea740fabf2c516c3221 Mon Sep 17 00:00:00 2001 From: JoshuaBrest <36625023+JoshuaBrest@users.noreply.github.com> Date: Wed, 29 Nov 2023 02:36:59 -0800 Subject: [PATCH 02/22] Fix localization issues and implement automatic relaunch --- Whisky.xcodeproj/project.pbxproj | 2 +- Whisky/Localizable.xcstrings | 60 ++++++++++++++----- .../Views/Updater/UpdateControlerView.swift | 40 +++++-------- 3 files changed, 61 insertions(+), 41 deletions(-) diff --git a/Whisky.xcodeproj/project.pbxproj b/Whisky.xcodeproj/project.pbxproj index 110b036c9..229a81d8e 100644 --- a/Whisky.xcodeproj/project.pbxproj +++ b/Whisky.xcodeproj/project.pbxproj @@ -837,7 +837,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.isaacmarovitz.Whisky; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; -"PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = Whisky; + "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = Whisky; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; }; diff --git a/Whisky/Localizable.xcstrings b/Whisky/Localizable.xcstrings index f8fa16c4a..214ee79bc 100644 --- a/Whisky/Localizable.xcstrings +++ b/Whisky/Localizable.xcstrings @@ -244,6 +244,12 @@ "state" : "translated", "value" : "Cancel" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "取消" + } } } }, @@ -15102,6 +15108,12 @@ "state" : "translated", "value" : "Downloading Update..." } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "正在下载更新..." + } } } }, @@ -15112,6 +15124,12 @@ "state" : "translated", "value" : "Update Error!" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "更新错误!" + } } } }, @@ -15122,6 +15140,12 @@ "state" : "translated", "value" : "Extracting Files..." } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "正在解压文件..." + } } } }, @@ -15486,6 +15510,12 @@ "state" : "translated", "value" : "Initializing..." } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "处理中..." + } } } }, @@ -15496,6 +15526,12 @@ "state" : "translated", "value" : "Finishing Up..." } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "马上就完成..." + } } } }, @@ -15506,35 +15542,27 @@ "state" : "translated", "value" : "No Updates Available" } - } - } - }, - "update.noUpdateFound.description" : { - "localizations" : { - "en" : { + }, + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "No updates are available. You are on the latest version of Whisky." + "value" : "没有更新" } } } }, - "update.readyRelaunch" : { + "update.noUpdateFound.description" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Update Complete!" + "value" : "No updates are available. You are on the latest version of Whisky." } - } - } - }, - "update.readyRelaunch.description" : { - "localizations" : { - "en" : { + }, + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "Update Complete! Press \"OK\" to relaunch." + "value" : "没有更新了。你在用 Whisky 最新的版本。" } } } diff --git a/Whisky/Views/Updater/UpdateControlerView.swift b/Whisky/Views/Updater/UpdateControlerView.swift index 085654082..48669c89d 100644 --- a/Whisky/Views/Updater/UpdateControlerView.swift +++ b/Whisky/Views/Updater/UpdateControlerView.swift @@ -158,30 +158,22 @@ struct UpdateControlerView: View { showCheckingForUpdates = false showUpdatePreview = false showUpdater = false - displayPrompt( - title: String(localized: "update.readyRelaunch"), - description: String(localized: "update.readyRelaunch.description"), - action: String(localized: "button.ok"), - actionHandler: { - // Kill all bottles - await WhiskyApp.killBottles() - // Relaunch - relaunch(.install) - // Actualy relaunch (I don't want to make a helper for this so.... you get...) - let task = Process() - task.launchPath = "/bin/sh" - task.arguments = [ - "-c", - """ - kill "\(ProcessInfo.processInfo.processName)"; - sleep 0.5; open "\(Bundle.main.bundlePath)" - """ - ] - task.launch() - NSApp.terminate(nil) - exit(0) - } - ) + WhiskyApp.killBottles() + // Actualy relaunch (I don't want to make a helper for this so.... you get...) + let task = Process() + task.launchPath = "/bin/sh" + task.arguments = [ + "-c", + """ + kill "\(ProcessInfo.processInfo.processIdentifier)"; + sleep 0.5; open "\(Bundle.main.bundlePath)" + """ + ] + task.launch() + // Relaunch + relaunch(.install) + NSApp.terminate(nil) + exit(0) } } } From 8f0dbaedc909de192b9473a0d2845cb8113dfd75 Mon Sep 17 00:00:00 2001 From: JoshuaBrest <36625023+JoshuaBrest@users.noreply.github.com> Date: Wed, 29 Nov 2023 12:34:29 -0800 Subject: [PATCH 03/22] Use appcast not github --- Whisky/Localizable.xcstrings | 86 ++++++------ .../Views/Updater/UpdateControlerView.swift | 26 ++-- Whisky/Views/Updater/UpdatePreviewView.swift | 127 ++++-------------- 3 files changed, 86 insertions(+), 153 deletions(-) diff --git a/Whisky/Localizable.xcstrings b/Whisky/Localizable.xcstrings index 214ee79bc..6c699c7f3 100644 --- a/Whisky/Localizable.xcstrings +++ b/Whisky/Localizable.xcstrings @@ -15037,22 +15037,6 @@ } } }, - "update.changeLogFailed" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Failed to get change log!" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "获取更新日志失败!" - } - } - } - }, "update.checkingForUpdates" : { "localizations" : { "en" : { @@ -15070,22 +15054,6 @@ } }, "update.description" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "You are running an outdated version of Whisky. Would you like to update?" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "您正在运行一个过时版本的Whisky。您想要下载更新吗?" - } - } - } - }, - "update.descriptionLoaded" : { "localizations" : { "en" : { "stringUnit" : { @@ -15511,6 +15479,12 @@ "value" : "Initializing..." } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inicjalizacja..." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -15527,6 +15501,12 @@ "value" : "Finishing Up..." } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kończenie..." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -15535,50 +15515,62 @@ } } }, - "update.noUpdateFound" : { + "update.noChangeLog" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "No Updates Available" + "value" : "No change log available." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "没有更新" + "value" : "无可用的更改日志。" } } } }, - "update.noUpdateFound.description" : { + "update.noUpdateFound" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "No updates are available. You are on the latest version of Whisky." + "value" : "No Updates Available" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Brak dostępnych aktualizacji" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "没有更新了。你在用 Whisky 最新的版本。" + "value" : "没有更新" } } } }, - "update.retryChangeLog" : { + "update.noUpdateFound.description" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Retry" + "value" : "No updates are available. You are on the latest version of Whisky." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Brak dostępnych aktualizacji. Posiadasz najnowszą wersję Whisky." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "重试" + "value" : "没有更新了。你在用 Whisky 最新的版本。" } } } @@ -15591,6 +15583,12 @@ "value" : "Update Available!" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dostępna aktualizacja!" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -15607,6 +15605,12 @@ "value" : "Update" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktualizuj" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", diff --git a/Whisky/Views/Updater/UpdateControlerView.swift b/Whisky/Views/Updater/UpdateControlerView.swift index 48669c89d..8e6dfbc67 100644 --- a/Whisky/Views/Updater/UpdateControlerView.swift +++ b/Whisky/Views/Updater/UpdateControlerView.swift @@ -29,6 +29,8 @@ struct UpdateControlerView: View { @State var showCheckingForUpdates = false @State var cancelCheckingForUpdates: (() -> Void)? @State var showUpdatePreview = false + @State var previewNextVersion: String = "" + @State var previewMarkdown: String = "" @State var updateUltimatum: ((Bool) -> Void)? @State var showUpdater = false @State var updateState: UpdateState = .initializating @@ -48,14 +50,14 @@ struct UpdateControlerView: View { .interactiveDismissDisabled() }) .sheet(isPresented: $showUpdatePreview, content: { - UpdatePreviewView(dismiss: { - showUpdatePreview = false - updateUltimatum?(false) - updateUltimatum = .none - }, install: { - updateUltimatum?(true) - updateUltimatum = .none - }) + UpdatePreviewView(dismiss: { + showUpdatePreview = false + updateUltimatum?(false) + updateUltimatum = .none + }, install: { + updateUltimatum?(true) + updateUltimatum = .none + }, markdownText: $previewMarkdown, nextVersion: $previewNextVersion) .interactiveDismissDisabled() .frame(width: 600, height: 400) }) @@ -82,18 +84,19 @@ struct UpdateControlerView: View { } showCheckingForUpdates = true } - SparkleUpdaterEvents.shared.updateFound = { _, _, reply in + SparkleUpdaterEvents.shared.updateFound = { appcastItem, _, reply in showCheckingForUpdates = false showUpdater = false showUpdatePreview = false updateUltimatum = { option in if option { - reply(.install) } else { reply(.dismiss) } } + previewNextVersion = appcastItem.displayVersionString + previewMarkdown = appcastItem.itemDescription ?? String(localized: "update.noChangeLog") showUpdatePreview = true } SparkleUpdaterEvents.shared.updateDismiss = { @@ -172,7 +175,6 @@ struct UpdateControlerView: View { task.launch() // Relaunch relaunch(.install) - NSApp.terminate(nil) exit(0) } } @@ -268,7 +270,7 @@ struct UpdateInstallingView: View { .frame(alignment: .leading) .onChange(of: downloadedBytes) { let currentTime = Date() - let elapsedTime = currentTime.timeIntervalSince(downloadStatedAt ?? currentTime) + let elapsedTime = currentTime.timeIntervalSince(downloadStatedAt) if downloadedBytes > 0 { downloadSpeed = Double(downloadedBytes) / elapsedTime } diff --git a/Whisky/Views/Updater/UpdatePreviewView.swift b/Whisky/Views/Updater/UpdatePreviewView.swift index 1993dc43d..9515e0b25 100644 --- a/Whisky/Views/Updater/UpdatePreviewView.swift +++ b/Whisky/Views/Updater/UpdatePreviewView.swift @@ -21,18 +21,20 @@ import Sparkle import MarkdownUI struct UpdatePreviewView: View { - enum MarkdownTextState { - case loaded, error, loading - } +// enum MarkdownTextState { +// case loaded, error, loading +// } let dismiss: () -> Void let install: () -> Void + @Binding var markdownText: String + @Binding var nextVersion: String - let updater = SparkleUpdaterEvents.shared - @State var markdownTextState: MarkdownTextState = .loading - @State var markdownText = "# Hello" - @State var currentVersion = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "(nil)" - @State var nextVersion = "" + // let updater = SparkleUpdaterEvents.shared + // @State var markdownTextState: MarkdownTextState = .loading + // @State var markdownText = "# Hello" + let currentVersion = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "(nil)" + // @State var nextVersion = "" var body: some View { HStack { @@ -40,11 +42,9 @@ struct UpdatePreviewView: View { Text("update.title") .font(.title) .fontWeight(.bold) - Text(markdownTextState != .loaded - ? String(localized: "update.description") - : String(format: String(localized: "update.descriptionLoaded"), - "v" + currentVersion, - nextVersion)) + Text(String(format: String(localized: "update.description"), + "v" + currentVersion, + nextVersion)) Spacer() HStack { Button("update.cancel") { @@ -63,33 +63,20 @@ struct UpdatePreviewView: View { .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) .padding(20) VStack { - if markdownTextState == .loading { - ProgressView() - } else if markdownTextState == .error { - VStack(spacing: 12) { - Text("update.changeLogFailed") - Button("update.retryChangeLog") { - Task(priority: .userInitiated) { - await getChangelog() - } - } - } - } else { - ScrollView { - VStack(alignment: .leading) { - Text("update.changeLog") - .font(.title2) - .fontWeight(.bold) - .padding(.bottom, 12) - } - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) - Markdown { - markdownText - } - .markdownTheme(.basic) + ScrollView { + VStack(alignment: .leading) { + Text("update.changeLog") + .font(.title2) + .fontWeight(.bold) + .padding(.bottom, 12) } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) + Markdown { + markdownText + } + .markdownTheme(.basic) } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) } .frame(maxWidth: .infinity, maxHeight: .infinity) .padding(20) @@ -97,73 +84,13 @@ struct UpdatePreviewView: View { } .frame(maxWidth: .infinity, maxHeight: .infinity) .onAppear { - Task(priority: .userInitiated) { - await getChangelog() - } - } - } - - func getChangelog() async { - withAnimation { markdownTextState = .loading } - let ghOwner = (Bundle.main.object(forInfoDictionaryKey: "GithubRepoOwner") as? String) ?? "Whisky-App" - let ghRepo = (Bundle.main.object(forInfoDictionaryKey: "GithubRepoName") as? String) ?? "Whisky" - - // Make a request to the Github API to get the latest release - // Append path not using string interpolation to non-urlencoded paths - guard let url = URL(string: "https://api.github.com/")? - .appending(path: "repos") - .appending(path: ghOwner) - .appending(path: ghRepo) - .appending(path: "releases") - .appending(path: "latest") - else { - withAnimation { markdownTextState = .error } - return - } - - var request = URLRequest(url: url) - request.setValue("application/vnd.github+json", forHTTPHeaderField: "Accept") - request.setValue("WhiskyApp", forHTTPHeaderField: "User-Agent") - - let data: Data - do { - let (dataInfo, _) = try (await URLSession.shared.data(for: request)) - data = dataInfo - } catch { - print("Changelog request failed: \(error)") - withAnimation { markdownTextState = .error } - return - } - - // Decode the JSON - struct Release: Codable { - let body: String - let tagName: String - - enum CodingKeys: String, CodingKey { // swiftlint:disable:this nesting - case body - case tagName = "tag_name" - } - } - - let release: Release - do { - release = try JSONDecoder().decode(Release.self, from: data) - } catch { - print("Failed to decode release: \(error)") - withAnimation { markdownTextState = .error } - return - } - - withAnimation { - markdownText = release.body - nextVersion = release.tagName - markdownTextState = .loaded + print("test:") + print(markdownText) } } } #Preview { - UpdatePreviewView(dismiss: {}, install: {}) + UpdatePreviewView(dismiss: {}, install: {}, markdownText: .constant("# Hello"), nextVersion: .constant("v1.0.0")) .frame(width: 600, height: 400) } From 8811f92c4593693b5b4f5d3bc71c5fc23fabaad2 Mon Sep 17 00:00:00 2001 From: JoshuaBrest <36625023+JoshuaBrest@users.noreply.github.com> Date: Wed, 29 Nov 2023 12:57:21 -0800 Subject: [PATCH 04/22] Remove unnecessary keys from Info.plist --- Whisky/Info.plist | 4 ---- Whisky/Views/Updater/UpdatePreviewView.swift | 8 -------- 2 files changed, 12 deletions(-) diff --git a/Whisky/Info.plist b/Whisky/Info.plist index 21e7707ce..e2e4738d6 100644 --- a/Whisky/Info.plist +++ b/Whisky/Info.plist @@ -57,10 +57,6 @@ https://data.getwhisky.app/appcast.xml SUPublicEDKey tnZFAvPUfCpM7Tr7Sx5gKRm6BUQQ6htJQeOMP44evms= - GithubRepoOwner - Whisky-App - GithubRepoName - Whisky UTExportedTypeDeclarations diff --git a/Whisky/Views/Updater/UpdatePreviewView.swift b/Whisky/Views/Updater/UpdatePreviewView.swift index 9515e0b25..091fa29dc 100644 --- a/Whisky/Views/Updater/UpdatePreviewView.swift +++ b/Whisky/Views/Updater/UpdatePreviewView.swift @@ -21,20 +21,12 @@ import Sparkle import MarkdownUI struct UpdatePreviewView: View { -// enum MarkdownTextState { -// case loaded, error, loading -// } - let dismiss: () -> Void let install: () -> Void @Binding var markdownText: String @Binding var nextVersion: String - // let updater = SparkleUpdaterEvents.shared - // @State var markdownTextState: MarkdownTextState = .loading - // @State var markdownText = "# Hello" let currentVersion = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "(nil)" - // @State var nextVersion = "" var body: some View { HStack { From 892ee91299d3f4f6e3fa5cbfc7cba1ba2319360f Mon Sep 17 00:00:00 2001 From: JoshuaBrest <36625023+JoshuaBrest@users.noreply.github.com> Date: Wed, 29 Nov 2023 12:57:55 -0800 Subject: [PATCH 05/22] Remove unnecessary code in UpdatePreviewView --- Whisky/Views/Updater/UpdatePreviewView.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Whisky/Views/Updater/UpdatePreviewView.swift b/Whisky/Views/Updater/UpdatePreviewView.swift index 091fa29dc..fbad75897 100644 --- a/Whisky/Views/Updater/UpdatePreviewView.swift +++ b/Whisky/Views/Updater/UpdatePreviewView.swift @@ -75,10 +75,6 @@ struct UpdatePreviewView: View { .background(.ultraThickMaterial) } .frame(maxWidth: .infinity, maxHeight: .infinity) - .onAppear { - print("test:") - print(markdownText) - } } } From 9dcce89ab766458a6de608382d0adfcd5c82b9ad Mon Sep 17 00:00:00 2001 From: JoshuaBrest <36625023+JoshuaBrest@users.noreply.github.com> Date: Thu, 30 Nov 2023 19:59:47 -0800 Subject: [PATCH 06/22] Refactor --- Whisky.xcodeproj/project.pbxproj | 12 +- Whisky/Localizable.xcstrings | 92 +++-- Whisky/Utils/SparkleUpdaterEvents.swift | 226 ++++++++++--- Whisky/Views/ContentView.swift | 7 +- .../Views/Updater/UpdateControlerView.swift | 316 ------------------ .../UpdateControllerViewModifier.swift | 167 +++++++++ .../Views/Updater/UpdateInstallingView.swift | 201 +++++++++++ Whisky/Views/Updater/UpdatePreviewView.swift | 39 ++- Whisky/Views/WhiskyApp.swift | 3 +- 9 files changed, 641 insertions(+), 422 deletions(-) delete mode 100644 Whisky/Views/Updater/UpdateControlerView.swift create mode 100644 Whisky/Views/Updater/UpdateControllerViewModifier.swift create mode 100644 Whisky/Views/Updater/UpdateInstallingView.swift diff --git a/Whisky.xcodeproj/project.pbxproj b/Whisky.xcodeproj/project.pbxproj index 229a81d8e..74998b926 100644 --- a/Whisky.xcodeproj/project.pbxproj +++ b/Whisky.xcodeproj/project.pbxproj @@ -59,8 +59,9 @@ EB051A092B150F7100F5F5B7 /* UpdatePreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB051A082B150F7100F5F5B7 /* UpdatePreviewView.swift */; }; EB051A0E2B16EA7E00F5F5B7 /* MarkdownUI in Frameworks */ = {isa = PBXBuildFile; productRef = EB051A0D2B16EA7E00F5F5B7 /* MarkdownUI */; }; EB051A102B1710A700F5F5B7 /* SparkleUpdaterEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB051A0F2B1710A700F5F5B7 /* SparkleUpdaterEvents.swift */; }; - EB051A132B17263300F5F5B7 /* UpdateControlerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB051A122B17263300F5F5B7 /* UpdateControlerView.swift */; }; + EB051A132B17263300F5F5B7 /* UpdateControllerViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB051A122B17263300F5F5B7 /* UpdateControllerViewModifier.swift */; }; EB58FB552A499896002DC184 /* SemanticVersion in Frameworks */ = {isa = PBXBuildFile; productRef = EB58FB542A499896002DC184 /* SemanticVersion */; }; + EBB21B662B198370000C3FA0 /* UpdateInstallingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBB21B652B198370000C3FA0 /* UpdateInstallingView.swift */; }; EEA5A2462A31DD65008274AE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEA5A2452A31DD65008274AE /* AppDelegate.swift */; }; /* End PBXBuildFile section */ @@ -157,7 +158,8 @@ DB696FC72AFAE5DA0037EB2F /* PinCreationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinCreationView.swift; sourceTree = ""; }; EB051A082B150F7100F5F5B7 /* UpdatePreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdatePreviewView.swift; sourceTree = ""; }; EB051A0F2B1710A700F5F5B7 /* SparkleUpdaterEvents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SparkleUpdaterEvents.swift; sourceTree = ""; }; - EB051A122B17263300F5F5B7 /* UpdateControlerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateControlerView.swift; sourceTree = ""; }; + EB051A122B17263300F5F5B7 /* UpdateControllerViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateControllerViewModifier.swift; sourceTree = ""; }; + EBB21B652B198370000C3FA0 /* UpdateInstallingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateInstallingView.swift; sourceTree = ""; }; EEA5A2452A31DD65008274AE /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -387,7 +389,8 @@ isa = PBXGroup; children = ( EB051A082B150F7100F5F5B7 /* UpdatePreviewView.swift */, - EB051A122B17263300F5F5B7 /* UpdateControlerView.swift */, + EB051A122B17263300F5F5B7 /* UpdateControllerViewModifier.swift */, + EBB21B652B198370000C3FA0 /* UpdateInstallingView.swift */, ); path = Updater; sourceTree = ""; @@ -584,6 +587,7 @@ buildActionMask = 2147483647; files = ( EEA5A2462A31DD65008274AE /* AppDelegate.swift in Sources */, + EBB21B662B198370000C3FA0 /* UpdateInstallingView.swift in Sources */, 6E70A4A12A9A280C007799E9 /* WhiskyCmd.swift in Sources */, 6E40495829CCA19C006E3F1B /* ContentView.swift in Sources */, 6EF557982A410599001A4F09 /* SetupView.swift in Sources */, @@ -601,7 +605,7 @@ 6E17B6492AF4118F00831173 /* EnvironmentArgView.swift in Sources */, 6E6C0CF42A419A7600356232 /* RosettaView.swift in Sources */, 6E6C0CF82A419A8C00356232 /* GPTKInstallView.swift in Sources */, - EB051A132B17263300F5F5B7 /* UpdateControlerView.swift in Sources */, + EB051A132B17263300F5F5B7 /* UpdateControllerViewModifier.swift in Sources */, 6E40498329CCA91B006E3F1B /* Bottle+Extensions.swift in Sources */, 6E621CEF2A5F631300C9AAB3 /* Winetricks.swift in Sources */, 6E17B6462AF3FDC100831173 /* PinView.swift in Sources */, diff --git a/Whisky/Localizable.xcstrings b/Whisky/Localizable.xcstrings index 6c699c7f3..317957d8b 100644 --- a/Whisky/Localizable.xcstrings +++ b/Whisky/Localizable.xcstrings @@ -15042,61 +15042,61 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Whisky is checking for updates...." + "value" : "Checking for updates..." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "Whisky 正在检查更新..." + "value" : "正在检查更新..." } } } }, - "update.description" : { + "update.checkingForUpdates.description" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "You are running an outdated version of Whisky. The latest version available is %2$@, you are running %1$@. Would you like to update?" + "value" : "Whisky is checking for updates..." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "您正在运行一个过时版本的Whisky。最新可用的版本是 %2$@,你在用 %1$@。您想要下载更新吗?" + "value" : "Whisky 正在检查更新..." } } } }, - "update.downloading" : { + "update.description" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Downloading Update..." + "value" : "You are running an outdated version of Whisky. The latest version available is %2$@, you are running %1$@. Would you like to update?" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "正在下载更新..." + "value" : "您正在运行一个过时版本的Whisky。最新可用的版本是 %2$@,你在用 %1$@。您想要下载更新吗?" } } } }, - "update.error" : { + "update.downloading" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Update Error!" + "value" : "Downloading Update..." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "更新错误!" + "value" : "正在下载更新..." } } } @@ -15531,7 +15531,7 @@ } } }, - "update.noUpdateFound" : { + "update.noUpdatesFound" : { "localizations" : { "en" : { "stringUnit" : { @@ -15539,38 +15539,74 @@ "value" : "No Updates Available" } }, - "pl" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "Brak dostępnych aktualizacji" + "value" : "没有更新" + } + } + } + }, + "update.noUpdatesFound.description" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "No updates are available. You are on the latest version of Whisky." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "没有更新" + "value" : "没有更新了。你在用 Whisky 最新的版本。" } } } }, - "update.noUpdateFound.description" : { + "update.readyToRelaunch" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "No updates are available. You are on the latest version of Whisky." + "value" : "Update Ready" } }, - "pl" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "更新已准备好" + } + } + } + }, + "update.readyToRelaunch.description" : { + "localizations" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Brak dostępnych aktualizacji. Posiadasz najnowszą wersję Whisky." + "value" : "The update is ready to be installed. Press \"Relaunch\" to install the update and relaunch Whisky." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "没有更新了。你在用 Whisky 最新的版本。" + "value" : "更新已准备好安装。点击“重启”来安装更新并重启 Whisky。" + } + } + } + }, + "update.relaunch" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Relaunch" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "重启" } } } @@ -15619,6 +15655,22 @@ } } }, + "update.updaterError" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Update Error!" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "更新失败!" + } + } + } + }, "wine.clearShaderCaches" : { "localizations" : { "da" : { diff --git a/Whisky/Utils/SparkleUpdaterEvents.swift b/Whisky/Utils/SparkleUpdaterEvents.swift index 816afee86..dec961b7d 100644 --- a/Whisky/Utils/SparkleUpdaterEvents.swift +++ b/Whisky/Utils/SparkleUpdaterEvents.swift @@ -19,115 +19,231 @@ import Foundation import Sparkle -class SparkleUpdaterEvents: NSObject, SPUUserDriver { +class SparkleUpdaterEvents: NSObject, SPUUserDriver, ObservableObject { static let shared = SparkleUpdaterEvents() - var checkingForUpdates: ((@escaping () -> Void) -> Void)? - var updateFound: ((SUAppcastItem, SPUUserUpdateState, @escaping (SPUUserUpdateChoice) -> Void) -> Void)? - var update: ((SPUDownloadData) -> Void)? - var updateError: ((NSError) -> Void)? - var updateDownloadState: (() -> Void)? - var updateExtractState: (() -> Void)? - var updateInstallState: (() -> Void)? - var updateReadyRelaunch: ((@escaping (SPUUserUpdateChoice) -> Void) -> Void)? - var updateDismiss: (() -> Void)? - - var expectedContentLength: Double = 0 - var receivedContentLength: Double = 0 - - var extractProgress: Double = 0 + enum UpdaterState { + case idle, error, checking, updateFound, updateNotFound, initializing, + downloading, extracting, installing, readyToRelaunch + } enum UpdateOption { case install, dismiss } - func show(_ request: SPUUpdatePermissionRequest) async -> SUUpdatePermissionResponse { + @Published var state: UpdaterState = .idle + @Published var downloadBytesTotal: Double = 0 + @Published var downloadBytesReceived: Double = 0 + @Published var extractProgress: Double = 0 + + // Errors + var errorData: NSError? + private var errorAcknowledgementCallback: (() -> Void)? + + // Checking for updates + private var checkingForUpdatesCancellationCallback: (() -> Void)? + + // Update found + var appcastItem: SUAppcastItem? + private var updateFoundActionCallback: ((SPUUserUpdateChoice) -> Void)? + + // Downloading + var downloadStartedAt: Date? + private var downloadingCancellationCallback: (() -> Void)? + + // Ready to relaunch + private var updateReadyRelaunchCallback: ((SPUUserUpdateChoice) -> Void)? + + /// Clear all callbacks + private func clearCallbacks() { + self.checkingForUpdatesCancellationCallback = .none + self.updateFoundActionCallback = .none + self.downloadingCancellationCallback = .none + self.updateReadyRelaunchCallback = .none + } + + /// Implementation of `SPUUserDriver` protocol + internal func show(_ request: SPUUpdatePermissionRequest) async -> SUUpdatePermissionResponse { return .init( automaticUpdateChecks: false, sendSystemProfile: false ) } - func showUserInitiatedUpdateCheck(cancellation: @escaping () -> Void) { - checkingForUpdates?(cancellation) + /// Implementation of `SPUUserDriver` protocol + internal func showUserInitiatedUpdateCheck(cancellation: @escaping () -> Void) { + state = .checking + self.checkingForUpdatesCancellationCallback = cancellation + } + + /// Cancel the update check + func cancelUpdateCheck() { + self.checkingForUpdatesCancellationCallback?() + clearCallbacks() + self.state = .idle } - func showUpdateFound( + /// Implementation of `SPUUserDriver` protocol + internal func showUpdateFound( with appcastItem: SUAppcastItem, state: SPUUserUpdateState, reply: @escaping (SPUUserUpdateChoice) -> Void ) { - if let updateFound = updateFound { - updateFound(appcastItem, state, reply) - } else { - reply(.dismiss) + clearCallbacks() + self.appcastItem = appcastItem + self.updateFoundActionCallback = reply + self.state = .updateFound + } + + /// Call to tell sparkle to download / dissmiss the update + func shouldUpdate(_ action: UpdateOption) { + guard let callback = self.updateFoundActionCallback else { return } + switch action { + case .install: + callback(.install) + self.state = .initializing + case .dismiss: + callback(.dismiss) + self.state = .idle } + // Reset callback + clearCallbacks() } - func showUpdateReleaseNotes(with downloadData: SPUDownloadData) { - update?(downloadData) + /// Implementation of `SPUUserDriver` protocol + internal func showUpdateReleaseNotes(with downloadData: SPUDownloadData) { + // Never needed + return } - func showUpdateReleaseNotesFailedToDownloadWithError(_ error: Error) { - updateError?(error as NSError) + /// Implementation of `SPUUserDriver` protocol + internal func showUpdateReleaseNotesFailedToDownloadWithError(_ error: Error) { + clearCallbacks() + self.errorData = error as NSError + self.state = .error } - func showUpdateNotFoundWithError(_ error: Error, acknowledgement: @escaping () -> Void) { - updateError?(error as NSError) + /// Implementation of `SPUUserDriver` protocol + internal func showUpdateNotFoundWithError(_ error: Error, acknowledgement: @escaping () -> Void) { + clearCallbacks() + self.errorData = error as NSError + self.errorAcknowledgementCallback = acknowledgement + self.state = .error + } - acknowledgement() + /// Implementation of `SPUUserDriver` protocol + internal func showUpdaterError(_ error: Error, acknowledgement: @escaping () -> Void) { + clearCallbacks() + self.errorData = error as NSError + self.errorAcknowledgementCallback = acknowledgement + self.state = .error } - func showUpdaterError(_ error: Error, acknowledgement: @escaping () -> Void) { - updateError?(error as NSError) + /// Acknowledgement of the error + func errorAcknowledgement() { + self.errorAcknowledgementCallback?() + clearCallbacks() + self.errorData = .none + self.state = .idle + } - acknowledgement() + /// Implementation of `SPUUserDriver` protocol + internal func showDownloadInitiated(cancellation: @escaping () -> Void) { + clearCallbacks() + self.downloadStartedAt = Date() + self.downloadingCancellationCallback = cancellation + self.state = .downloading } - func showDownloadInitiated(cancellation: @escaping () -> Void) { - updateDownloadState?() + /// Cancel the download + func cancelDownload() { + self.downloadingCancellationCallback?() + // Reset download + clearCallbacks() + self.downloadBytesTotal = 0 + self.downloadBytesReceived = 0 + self.downloadStartedAt = .none + self.state = .idle } - func showDownloadDidReceiveExpectedContentLength(_ expectedContentLength: UInt64) { - self.expectedContentLength = Double(expectedContentLength) - updateDownloadState?() + /// Implementation of `SPUUserDriver` protocol + internal func showDownloadDidReceiveExpectedContentLength(_ expectedContentLength: UInt64) { + self.downloadBytesTotal = Double(expectedContentLength) } - func showDownloadDidReceiveData(ofLength length: UInt64) { - receivedContentLength += Double(length) - updateDownloadState?() + /// Implementation of `SPUUserDriver` protocol + internal func showDownloadDidReceiveData(ofLength length: UInt64) { + self.downloadBytesReceived += Double(length) } - func showDownloadDidStartExtractingUpdate() { - updateExtractState?() + /// Implementation of `SPUUserDriver` protocol + internal func showDownloadDidStartExtractingUpdate() { + clearCallbacks() + // Reset download + self.downloadBytesTotal = 0 + self.downloadBytesReceived = 0 + self.downloadStartedAt = .none + self.state = .extracting } - func showExtractionReceivedProgress(_ progress: Double) { - extractProgress = progress - updateExtractState?() + /// Implementation of `SPUUserDriver` protocol + internal func showExtractionReceivedProgress(_ progress: Double) { + self.extractProgress = progress } - func showReady(toInstallAndRelaunch acknowledgement: @escaping (SPUUserUpdateChoice) -> Void) { - updateReadyRelaunch?(acknowledgement) + /// Implementation of `SPUUserDriver` protocol + internal func showReady(toInstallAndRelaunch reply: @escaping (SPUUserUpdateChoice) -> Void) { + clearCallbacks() + self.updateReadyRelaunchCallback = reply + self.state = .readyToRelaunch } - func showInstallingUpdate( + /// Call to tell sparkle to install the update + func relaunch(_ action: UpdateOption) { + guard let callback = self.updateReadyRelaunchCallback else { return } + switch action { + case .install: + callback(.install) + self.state = .installing + case .dismiss: + callback(.dismiss) + self.state = .idle + } + // Reset callback + clearCallbacks() + } + + /// Implementation of `SPUUserDriver` protocol + internal func showInstallingUpdate( withApplicationTerminated applicationTerminated: Bool, retryTerminatingApplication: @escaping () -> Void ) { - updateInstallState?() + clearCallbacks() + // Reset extract + self.extractProgress = 0 + self.state = .installing } - func showUpdateInstalledAndRelaunched(_ relaunched: Bool, acknowledgement: @escaping () -> Void) { + /// Implementation of `SPUUserDriver` protocol + internal func showUpdateInstalledAndRelaunched(_ relaunched: Bool, acknowledgement: @escaping () -> Void) { + clearCallbacks() + // Never used acknowledgement() + return } - func showUpdateInFocus() { + /// Implementation of `SPUUserDriver` protocol + internal func showUpdateInFocus() { + // Never needed return } - func dismissUpdateInstallation() { - updateDismiss?() - return + /// Implementation of `SPUUserDriver` protocol + internal func dismissUpdateInstallation() { + // If it is in the checking state, it means that there is no update available + if self.state == .checking { + clearCallbacks() + self.state = .updateNotFound + } } } diff --git a/Whisky/Views/ContentView.swift b/Whisky/Views/ContentView.swift index 12130eb05..af86e191b 100644 --- a/Whisky/Views/ContentView.swift +++ b/Whisky/Views/ContentView.swift @@ -26,8 +26,6 @@ struct ContentView: View { @AppStorage("selectedBottleURL") private var selectedBottleURL: URL? @EnvironmentObject var bottleVM: BottleVM - let updater: SPUUpdater? - @Binding var showSetup: Bool @State var selected: URL? @State var showBottleCreation: Bool = false @@ -39,9 +37,6 @@ struct ContentView: View { @State private var refreshAnimation: Angle = .degrees(0) var body: some View { - if let updater { - UpdateControlerView(updater: updater) - } NavigationSplitView { ScrollViewReader { proxy in List(selection: $selected) { @@ -291,6 +286,6 @@ struct BottleListEntry: View { } #Preview { - ContentView(updater: .none, showSetup: .constant(false)) + ContentView(showSetup: .constant(false)) .environmentObject(BottleVM.shared) } diff --git a/Whisky/Views/Updater/UpdateControlerView.swift b/Whisky/Views/Updater/UpdateControlerView.swift deleted file mode 100644 index 8e6dfbc67..000000000 --- a/Whisky/Views/Updater/UpdateControlerView.swift +++ /dev/null @@ -1,316 +0,0 @@ -// -// UpdateControlerView.swift -// Whisky -// -// This file is part of Whisky. -// -// Whisky is free software: you can redistribute it and/or modify it under the terms -// of the GNU General Public License as published by the Free Software Foundation, -// either version 3 of the License, or (at your option) any later version. -// -// Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; -// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. -// See the GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License along with Whisky. -// If not, see https://www.gnu.org/licenses/. -// - -import SwiftUI -import Sparkle - -enum UpdateState { - case initializating, downloading, extracting, installing -} - -struct UpdateControlerView: View { - let updater: SPUUpdater - - @State var showCheckingForUpdates = false - @State var cancelCheckingForUpdates: (() -> Void)? - @State var showUpdatePreview = false - @State var previewNextVersion: String = "" - @State var previewMarkdown: String = "" - @State var updateUltimatum: ((Bool) -> Void)? - @State var showUpdater = false - @State var updateState: UpdateState = .initializating - @State var updateStateDownloadStatedAt: Date = Date.init(timeIntervalSince1970: 0) - @State var updateStateDownloadableBytes: Double = 0 - @State var updateStateDownloadedBytes: Double = 0 - @State var updateStateExtractProgress: Double = 0 - - var body: some View { - VStack {} - .sheet(isPresented: $showCheckingForUpdates, content: { - UpdateCheckingView(cancel: { - showCheckingForUpdates = false - cancelCheckingForUpdates?() - }) - .frame(width: 300) - .interactiveDismissDisabled() - }) - .sheet(isPresented: $showUpdatePreview, content: { - UpdatePreviewView(dismiss: { - showUpdatePreview = false - updateUltimatum?(false) - updateUltimatum = .none - }, install: { - updateUltimatum?(true) - updateUltimatum = .none - }, markdownText: $previewMarkdown, nextVersion: $previewNextVersion) - .interactiveDismissDisabled() - .frame(width: 600, height: 400) - }) - .sheet(isPresented: $showUpdater, content: { - UpdateInstallingView( - state: $updateState, - downloadStatedAt: $updateStateDownloadStatedAt, - downloadableBytes: $updateStateDownloadableBytes, - downloadedBytes: $updateStateDownloadedBytes, - extractProgress: $updateStateExtractProgress - ) - .interactiveDismissDisabled() - .frame(width: 300) - }) - .onAppear { - // On fn called show sheet - SparkleUpdaterEvents.shared.checkingForUpdates = { cancel in - // Prevent updater from checking for updates - showCheckingForUpdates = false - showUpdatePreview = false - showUpdater = false - cancelCheckingForUpdates = { - cancel() - } - showCheckingForUpdates = true - } - SparkleUpdaterEvents.shared.updateFound = { appcastItem, _, reply in - showCheckingForUpdates = false - showUpdater = false - showUpdatePreview = false - updateUltimatum = { option in - if option { - reply(.install) - } else { - reply(.dismiss) - } - } - previewNextVersion = appcastItem.displayVersionString - previewMarkdown = appcastItem.itemDescription ?? String(localized: "update.noChangeLog") - showUpdatePreview = true - } - SparkleUpdaterEvents.shared.updateDismiss = { - // Show Alert - if showCheckingForUpdates { - showCheckingForUpdates = false - showUpdatePreview = false - showUpdater = false - // no update found - displayPrompt( - title: String(localized: "update.noUpdateFound"), - description: String(localized: "update.noUpdateFound.description"), - action: String(localized: "button.ok"), - actionHandler: { - // Dismiss - } - ) - } - } - SparkleUpdaterEvents.shared.updateError = { error in - showCheckingForUpdates = false - showUpdatePreview = false - showUpdater = false - displayPrompt( - title: String(localized: "update.error"), - description: error.localizedDescription, - action: String(localized: "button.ok"), - actionHandler: { - // Dismiss - } - ) - } - // Update download state - SparkleUpdaterEvents.shared.updateDownloadState = { - if updateStateDownloadStatedAt == Date(timeIntervalSince1970: 0) { - updateStateDownloadStatedAt = Date() - } - showCheckingForUpdates = false - showUpdatePreview = false - showUpdater = true - withAnimation { updateState = .downloading } - updateStateDownloadableBytes = SparkleUpdaterEvents.shared.expectedContentLength - updateStateDownloadedBytes = SparkleUpdaterEvents.shared.receivedContentLength - } - // Update extract state - SparkleUpdaterEvents.shared.updateExtractState = { - showCheckingForUpdates = false - showUpdatePreview = false - showUpdater = true - withAnimation { updateState = .extracting } - updateStateExtractProgress = SparkleUpdaterEvents.shared.extractProgress - } - // Update install state - SparkleUpdaterEvents.shared.updateInstallState = { - showCheckingForUpdates = false - showUpdatePreview = false - showUpdater = true - withAnimation { updateState = .installing } - } - // Update ready relaunch - SparkleUpdaterEvents.shared.updateReadyRelaunch = { relaunch in - showCheckingForUpdates = false - showUpdatePreview = false - showUpdater = false - WhiskyApp.killBottles() - // Actualy relaunch (I don't want to make a helper for this so.... you get...) - let task = Process() - task.launchPath = "/bin/sh" - task.arguments = [ - "-c", - """ - kill "\(ProcessInfo.processInfo.processIdentifier)"; - sleep 0.5; open "\(Bundle.main.bundlePath)" - """ - ] - task.launch() - // Relaunch - relaunch(.install) - exit(0) - } - } - } - - func displayPrompt(title: String, description: String, action: String, actionHandler: @escaping () -> Void) { - showCheckingForUpdates = false - showUpdatePreview = false - showUpdater = false - Task(priority: .userInitiated) { - await MainActor.run { - let alert = NSAlert() - alert.messageText = title - alert.informativeText = description - alert.addButton(withTitle: action) - alert.runModal() - } - } - } -} - -struct UpdateCheckingView: View { - let cancel: () -> Void - - var body: some View { - VStack(alignment: .leading, spacing: 12) { - Text("update.checkingForUpdates") - .fontWeight(.bold) - ProgressView() - .progressViewStyle(.linear) - HStack { - Spacer() - Button("button.cancel") { - cancel() - } - .buttonStyle(.borderedProminent) - .keyboardShortcut(.defaultAction) - } - } - .padding(20) - .frame(alignment: .leading) - } -} - -struct UpdateInstallingView: View { - @Binding var state: UpdateState - @Binding var downloadStatedAt: Date - @Binding var downloadableBytes: Double - @Binding var downloadedBytes: Double - @Binding var extractProgress: Double - - @State var fractionProgress: Double = 0 - @State var downloadSpeed: Double = 0 - - var body: some View { - VStack(alignment: .leading, spacing: 12) { - Text( - state == .downloading - ? "update.downloading" - : state == .extracting - ? "update.extracting" - : state == .installing - ? "update.installing" - : "update.initializating" - ) - .fontWeight(.bold) - if state == .installing || state == .initializating { - ProgressView() - .progressViewStyle(.linear) - } else { - ProgressView(value: fractionProgress, total: 1) - .progressViewStyle(.linear) - if state == .downloading { - HStack { - HStack { - Text(String(format: String(localized: "setup.gptk.progress"), - formatBytes(bytes: downloadedBytes), - formatBytes(bytes: downloadableBytes))) - + Text(String(" ")) - + (shouldShowEstimate() ? - Text(String(format: String(localized: "setup.gptk.eta"), - formatRemainingTime(remainingBytes: downloadableBytes - downloadedBytes))) - : Text(String())) - Spacer() - } - .font(.subheadline) - .monospacedDigit() - } - } - } - } - .padding(20) - .frame(alignment: .leading) - .onChange(of: downloadedBytes) { - let currentTime = Date() - let elapsedTime = currentTime.timeIntervalSince(downloadStatedAt) - if downloadedBytes > 0 { - downloadSpeed = Double(downloadedBytes) / elapsedTime - } - withAnimation { - if downloadableBytes > 0 { - fractionProgress = downloadedBytes / downloadableBytes - } else { - fractionProgress = 0 - } - } - } - .onChange(of: extractProgress) { - withAnimation { - fractionProgress = extractProgress - } - } - } - - func formatBytes(bytes: Double) -> String { - let formatter = ByteCountFormatter() - formatter.countStyle = .file - formatter.zeroPadsFractionDigits = true - return formatter.string(fromByteCount: Int64(bytes)) - } - - func shouldShowEstimate() -> Bool { - let elapsedTime = Date().timeIntervalSince(downloadStatedAt ?? Date()) - return Int(elapsedTime.rounded()) > 5 && downloadedBytes != 0 - } - - func formatRemainingTime(remainingBytes: Double) -> String { - let remainingTimeInSeconds = remainingBytes / downloadSpeed - - let formatter = DateComponentsFormatter() - formatter.allowedUnits = [.hour, .minute, .second] - formatter.unitsStyle = .full - if shouldShowEstimate() { - return formatter.string(from: TimeInterval(remainingTimeInSeconds)) ?? "" - } else { - return "" - } - } -} diff --git a/Whisky/Views/Updater/UpdateControllerViewModifier.swift b/Whisky/Views/Updater/UpdateControllerViewModifier.swift new file mode 100644 index 000000000..d210dc3ad --- /dev/null +++ b/Whisky/Views/Updater/UpdateControllerViewModifier.swift @@ -0,0 +1,167 @@ +// +// UpdateControllerViewModifier.swift +// Whisky +// +// This file is part of Whisky. +// +// Whisky is free software: you can redistribute it and/or modify it under the terms +// of the GNU General Public License as published by the Free Software Foundation, +// either version 3 of the License, or (at your option) any later version. +// +// Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with Whisky. +// If not, see https://www.gnu.org/licenses/. +// + +import SwiftUI +import Sparkle + +enum UpdateState { + case initializating, downloading, extracting, installing +} + +extension View { + func updateController() -> some View { + return modifier(UpdateControllerViewModifier()) + } +} + +struct UpdateControllerViewModifier: ViewModifier { + @State private var sheetCheckingUpdateViewPresented = false + @State private var sheetUpdateNotFoundViewPresented = false + @State private var sheetChangeLogViewPresented = false + @State private var sheetUpdateInstallingViewPresented = false + @State private var sheetUpdateReadyRelaunchViewPresented = false + @State private var sheetUpdateErrorViewPresented = false + @ObservedObject private var updater = SparkleUpdaterEvents.shared + + func body(content: Content) -> some View { // swiftlint:disable:this function_body_length + content + .sheet(isPresented: $sheetCheckingUpdateViewPresented, content: { + VStack(alignment: .leading, spacing: 12) { + Text("update.checkingForUpdates") + .fontWeight(.bold) + Text("update.checkingForUpdates.description") + ProgressView() + .progressViewStyle(.linear) + HStack { + Spacer() + Button("button.cancel") { + updater.cancelUpdateCheck() + } + .buttonStyle(.borderedProminent) + .keyboardShortcut(.defaultAction) + } + } + .padding(20) + .frame(width: 500, alignment: .leading) + .interactiveDismissDisabled() + }) + .sheet(isPresented: $sheetUpdateNotFoundViewPresented, content: { + VStack(alignment: .leading, spacing: 12) { + Text("update.noUpdatesFound") + .fontWeight(.bold) + Text("update.noUpdatesFound.description") + HStack { + Spacer() + Button("button.ok") { + sheetUpdateNotFoundViewPresented = false + } + .buttonStyle(.borderedProminent) + .keyboardShortcut(.defaultAction) + } + } + .padding(20) + .frame(width: 500, alignment: .leading) + }) + .sheet(isPresented: $sheetChangeLogViewPresented, content: { + UpdatePreviewView( + dismiss: { + updater.shouldUpdate(.dismiss) + }, + install: { + updater.shouldUpdate(.install) + }, + markdownText: updater.appcastItem?.itemDescription, + nextVersion: updater.appcastItem?.displayVersionString ?? "v1.0.0" + ) + .interactiveDismissDisabled() + .frame(width: 600, height: 400) + }) + .sheet(isPresented: $sheetUpdateInstallingViewPresented, content: { + UpdateInstallingView( + downloadStatedAt: updater.downloadStartedAt, + cancelDownload: { + updater.cancelDownload() + }, + state: $updater.state, + downloadableBytes: $updater.downloadBytesTotal, + downloadedBytes: $updater.downloadBytesReceived, + extractProgress: $updater.extractProgress + ) + .interactiveDismissDisabled() + .frame(width: 500) + }) + .sheet(isPresented: $sheetUpdateReadyRelaunchViewPresented, content: { + VStack(alignment: .leading, spacing: 12) { + Text("update.readyToRelaunch") + .fontWeight(.bold) + Text("update.readyToRelaunch.description") + HStack { + Spacer() + Button("update.relaunch") { + updater.relaunch(.install) + } + .buttonStyle(.borderedProminent) + .keyboardShortcut(.defaultAction) + } + } + .padding(20) + .frame(width: 500, alignment: .leading) + }) + .sheet(isPresented: $sheetUpdateErrorViewPresented, content: { + VStack(alignment: .leading, spacing: 12) { + Text("update.updaterError") + .fontWeight(.bold) + Text(updater.errorData?.localizedDescription ?? "") + HStack { + Spacer() + Button("button.ok") { + updater.errorAcknowledgement() + } + .buttonStyle(.borderedProminent) + .keyboardShortcut(.defaultAction) + } + } + .padding(20) + .frame(width: 500, alignment: .leading) + }) + .onChange(of: updater.state, { _, newValue in + sheetCheckingUpdateViewPresented = false + sheetChangeLogViewPresented = false + sheetUpdateInstallingViewPresented = false + sheetUpdateReadyRelaunchViewPresented = false + sheetUpdateErrorViewPresented = false + sheetUpdateNotFoundViewPresented = false + switch newValue { + case .checking: + sheetCheckingUpdateViewPresented = true + case .updateFound: + sheetChangeLogViewPresented = true + case .initializing, .downloading, .extracting, .installing: + sheetUpdateInstallingViewPresented = true + case .readyToRelaunch: + sheetUpdateReadyRelaunchViewPresented = true + case .error: + sheetUpdateErrorViewPresented = true + case .updateNotFound: + sheetUpdateNotFoundViewPresented = true + case .idle: + break + } + }) + } +} diff --git a/Whisky/Views/Updater/UpdateInstallingView.swift b/Whisky/Views/Updater/UpdateInstallingView.swift new file mode 100644 index 000000000..69b704a3e --- /dev/null +++ b/Whisky/Views/Updater/UpdateInstallingView.swift @@ -0,0 +1,201 @@ +// +// UpdateInstallingView.swift +// Whisky +// +// This file is part of Whisky. +// +// Whisky is free software: you can redistribute it and/or modify it under the terms +// of the GNU General Public License as published by the Free Software Foundation, +// either version 3 of the License, or (at your option) any later version. +// +// Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with Whisky. +// If not, see https://www.gnu.org/licenses/. +// + +import SwiftUI + +struct UpdateInstallingView: View { + let downloadStatedAt: Date? + let cancelDownload: () -> Void + + @Binding var state: SparkleUpdaterEvents.UpdaterState + @Binding var downloadableBytes: Double + @Binding var downloadedBytes: Double + @Binding var extractProgress: Double + + @State private var fractionProgress: Double = 0 + @State private var downloadSpeed: Double = 0 + @State private var shouldShowEstimate: Bool = false + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + if state == .downloading { + Text("update.downloading") + .fontWeight(.bold) + } else if state == .extracting { + Text("update.extracting") + .fontWeight(.bold) + } else if state == .installing { + Text("update.installing") + .fontWeight(.bold) + } else if state == .initializing { + Text("update.initializating") + .fontWeight(.bold) + } + if state == .installing || state == .initializing { + ProgressView() + .progressViewStyle(.linear) + } else if state == .downloading || state == .extracting { + VStack(spacing: 2) { + ProgressView(value: fractionProgress, total: 1) + .progressViewStyle(.linear) + if state == .downloading { + HStack { + Text(String(format: String(localized: "setup.gptk.progress"), + formatBytes(bytes: downloadedBytes), + formatBytes(bytes: downloadableBytes))) + + Text(String(" ")) + + (shouldShowEstimate ? + Text(String(format: String(localized: "setup.gptk.eta"), + formatRemainingTime(remainingBytes: downloadableBytes - downloadedBytes))) + : Text(String())) + Spacer() + } + .font(.subheadline) + .monospacedDigit() + } + } + if state == .downloading { + HStack { + Spacer() + Button("button.cancel") { + cancelDownload() + } + .buttonStyle(.borderedProminent) + .keyboardShortcut(.defaultAction) + + } + .font(.subheadline) + .monospacedDigit() + } + } + } + .padding(20) + .frame(alignment: .leading) + .onChange(of: downloadedBytes) { + if state != .downloading { + return + } + if let downloadStatedAt = downloadStatedAt { + checkShouldShowEstimate() + let currentTime = Date() + let elapsedTime = currentTime.timeIntervalSince(downloadStatedAt) + if downloadedBytes > 0 { + downloadSpeed = Double(downloadedBytes) / elapsedTime + } else { + downloadSpeed = 0 + } + withAnimation { + if downloadableBytes > 0 { + fractionProgress = downloadedBytes / downloadableBytes + } else { + fractionProgress = 0 + } + } + } else { + downloadSpeed = 0 + fractionProgress = 0 + } + + } + .onChange(of: extractProgress) { + if state != .extracting { + return + } + withAnimation { + fractionProgress = extractProgress / 100 + } + } + .onChange(of: state) { + if state == .installing { + update() + } + } + .onAppear { + if state == .installing { + update() + } + } + } + + func formatBytes(bytes: Double) -> String { + let formatter = ByteCountFormatter() + formatter.countStyle = .file + formatter.zeroPadsFractionDigits = true + return formatter.string(fromByteCount: Int64(bytes)) + } + + func update() { + Task(priority: .low) { + // Stuff + try await Task.sleep(nanoseconds: UInt64(2 * Double(NSEC_PER_SEC))) + // Relaunch using sketchy ways + await WhiskyApp.killBottles() + // Actualy relaunch (I don't want to make a helper for this so.... you get...) + let task = Process() + task.launchPath = "/bin/sh" + task.arguments = [ + "-c", + """ + kill "\(ProcessInfo.processInfo.processIdentifier)"; + sleep 0.5; open "\(Bundle.main.bundlePath)" + """ + ] + task.launch() + // Relaunch + exit(0) + } + } + + func checkShouldShowEstimate() { + if let downloadStatedAt = downloadStatedAt { + let elapsedTime = Date().timeIntervalSince(downloadStatedAt) + withAnimation { + shouldShowEstimate = Int(elapsedTime.rounded()) > 5 && downloadedBytes != 0 + } + return + } + + withAnimation { + shouldShowEstimate = false + } + } + + func formatRemainingTime(remainingBytes: Double) -> String { + if downloadSpeed == 0 { + return "" + } + let remainingTimeInSeconds = remainingBytes / downloadSpeed + + let formatter = DateComponentsFormatter() + formatter.allowedUnits = [.hour, .minute, .second] + formatter.unitsStyle = .full + return formatter.string(from: TimeInterval(remainingTimeInSeconds)) ?? "" + } +} + +#Preview { + UpdateInstallingView( + downloadStatedAt: .none, + cancelDownload: {}, + state: .constant(.downloading), + downloadableBytes: .constant(1000000), + downloadedBytes: .constant(1000), + extractProgress: .constant(0)) + .frame(width: 500) + +} diff --git a/Whisky/Views/Updater/UpdatePreviewView.swift b/Whisky/Views/Updater/UpdatePreviewView.swift index fbad75897..6f6346c48 100644 --- a/Whisky/Views/Updater/UpdatePreviewView.swift +++ b/Whisky/Views/Updater/UpdatePreviewView.swift @@ -23,10 +23,10 @@ import MarkdownUI struct UpdatePreviewView: View { let dismiss: () -> Void let install: () -> Void - @Binding var markdownText: String - @Binding var nextVersion: String + let markdownText: String? + let nextVersion: String - let currentVersion = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "(nil)" + private let currentVersion = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "0.0.0" var body: some View { HStack { @@ -36,7 +36,7 @@ struct UpdatePreviewView: View { .fontWeight(.bold) Text(String(format: String(localized: "update.description"), "v" + currentVersion, - nextVersion)) + "v" + nextVersion)) Spacer() HStack { Button("update.cancel") { @@ -54,23 +54,22 @@ struct UpdatePreviewView: View { } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) .padding(20) - VStack { - ScrollView { - VStack(alignment: .leading) { - Text("update.changeLog") - .font(.title2) - .fontWeight(.bold) - .padding(.bottom, 12) - } - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) - Markdown { - markdownText - } - .markdownTheme(.basic) + ScrollView { + VStack(alignment: .leading) { + Text("update.changeLog") + .font(.title2) + .fontWeight(.bold) + .padding(.bottom, 12) } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) + if let markdownText = markdownText { + Markdown(markdownText) + .padding(.bottom, 20) + } else { + Text("update.noChangeLog") + } } - .frame(maxWidth: .infinity, maxHeight: .infinity) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) .padding(20) .background(.ultraThickMaterial) } @@ -79,6 +78,6 @@ struct UpdatePreviewView: View { } #Preview { - UpdatePreviewView(dismiss: {}, install: {}, markdownText: .constant("# Hello"), nextVersion: .constant("v1.0.0")) - .frame(width: 600, height: 400) + UpdatePreviewView(dismiss: {}, install: {}, markdownText: "# Hello", nextVersion: "v1.0.0") + .frame(width: 700, height: 400) } diff --git a/Whisky/Views/WhiskyApp.swift b/Whisky/Views/WhiskyApp.swift index ed2b3ce87..8c98138ae 100644 --- a/Whisky/Views/WhiskyApp.swift +++ b/Whisky/Views/WhiskyApp.swift @@ -42,7 +42,8 @@ struct WhiskyApp: App { var body: some Scene { WindowGroup { - ContentView(updater: updaterController, showSetup: $showSetup) + ContentView(showSetup: $showSetup) + .updateController() .frame(minWidth: 550, minHeight: 250) .environmentObject(BottleVM.shared) .onAppear { From 69afaa23b655882e0b90776fe04c770d0799f813 Mon Sep 17 00:00:00 2001 From: Josh <36625023+JoshuaBrest@users.noreply.github.com> Date: Mon, 8 Jan 2024 21:22:16 -0800 Subject: [PATCH 07/22] Rollback changes for rebuild --- Whisky/Localizable.xcstrings | 457 +++++------------------------------ 1 file changed, 64 insertions(+), 393 deletions(-) diff --git a/Whisky/Localizable.xcstrings b/Whisky/Localizable.xcstrings index 317957d8b..686390f55 100644 --- a/Whisky/Localizable.xcstrings +++ b/Whisky/Localizable.xcstrings @@ -237,22 +237,6 @@ } } }, - "button.cancel" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cancel" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "取消" - } - } - } - }, "button.cDrive" : { "localizations" : { "da" : { @@ -1067,8 +1051,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "translated", - "value" : "固定" + "state" : "needs_review", + "value" : "Pin" } }, "zh-Hant" : { @@ -2365,8 +2349,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "translated", - "value" : "取消固定" + "state" : "needs_review", + "value" : "Unpin" } }, "zh-Hant" : { @@ -4135,8 +4119,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "translated", - "value" : "增强 Sync" + "state" : "needs_review", + "value" : "Enhanced Sync" } }, "zh-Hant" : { @@ -4253,8 +4237,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "translated", - "value" : "无" + "state" : "needs_review", + "value" : "None" } }, "zh-Hant" : { @@ -7085,8 +7069,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "translated", - "value" : "加入" + "state" : "needs_review", + "value" : "Add" } }, "zh-Hant" : { @@ -7203,8 +7187,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "translated", - "value" : "删掉" + "state" : "needs_review", + "value" : "Remove" } }, "zh-Hant" : { @@ -7557,8 +7541,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "translated", - "value" : "网站" + "state" : "needs_review", + "value" : "Website" } }, "zh-Hant" : { @@ -8620,8 +8604,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "translated", - "value" : "固定" + "state" : "needs_review", + "value" : "Pin" } }, "zh-Hant" : { @@ -8739,8 +8723,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "translated", - "value" : "\"%@\" 已经固定了!" + "state" : "needs_review", + "value" : "\"%@\" is already pinned!" } }, "zh-Hant" : { @@ -8857,8 +8841,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "translated", - "value" : "无法固定程序" + "state" : "needs_review", + "value" : "Error Pinning Program" } }, "zh-Hant" : { @@ -8975,8 +8959,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "translated", - "value" : "固定软解" + "state" : "needs_review", + "value" : "Pin Program" } }, "zh-Hant" : { @@ -9093,8 +9077,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "translated", - "value" : "固定名字:" + "state" : "needs_review", + "value" : "Pin name:" } }, "zh-Hant" : { @@ -9211,8 +9195,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "translated", - "value" : "然姐路径:" + "state" : "needs_review", + "value" : "Program path:" } }, "zh-Hant" : { @@ -9329,8 +9313,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "translated", - "value" : "固定软解" + "state" : "needs_review", + "value" : "Pin Program" } }, "zh-Hant" : { @@ -9565,8 +9549,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "translated", - "value" : "添加已选的到阻止列表" + "state" : "needs_review", + "value" : "Add Selected to Blocklist" } }, "zh-Hant" : { @@ -10273,8 +10257,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "translated", - "value" : "从阻止列表中移除已选的" + "state" : "needs_review", + "value" : "Remove Selected from Blocklist" } }, "zh-Hant" : { @@ -10391,8 +10375,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "translated", - "value" : "设置" + "state" : "needs_review", + "value" : "Settings" } }, "zh-Hant" : { @@ -11335,8 +11319,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "translated", - "value" : "通用" + "state" : "needs_review", + "value" : "General" } }, "zh-Hant" : { @@ -11453,8 +11437,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "translated", - "value" : "自动检查 GPTK 更新" + "state" : "needs_review", + "value" : "Automatically check for GPTK updates" } }, "zh-Hant" : { @@ -11571,8 +11555,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "translated", - "value" : "当 Whisky 关闭时把 Wine 进程终止" + "state" : "needs_review", + "value" : "Terminate Wine processes when Whisky closes" } }, "zh-Hant" : { @@ -11689,8 +11673,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "translated", - "value" : "自动检查 Whisky 更新" + "state" : "needs_review", + "value" : "Automatically check for Whisky updates" } }, "zh-Hant" : { @@ -11807,8 +11791,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "translated", - "value" : "更新" + "state" : "needs_review", + "value" : "Updates" } }, "zh-Hant" : { @@ -13341,8 +13325,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "translated", - "value" : "重试" + "state" : "needs_review", + "value" : "Retry" } }, "zh-Hant" : { @@ -13577,8 +13561,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "translated", - "value" : "无法自动安装 Rosetta 2。请手动安装。" + "state" : "needs_review", + "value" : "Rosetta 2 installation failed. Please install it manually." } }, "zh-Hant" : { @@ -15005,118 +14989,6 @@ } } }, - "update.cancel" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ask Me Next Tme" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "下一次问" - } - } - } - }, - "update.changeLog" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Change Log" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "更新日志" - } - } - } - }, - "update.checkingForUpdates" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Checking for updates..." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "正在检查更新..." - } - } - } - }, - "update.checkingForUpdates.description" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Whisky is checking for updates..." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "Whisky 正在检查更新..." - } - } - } - }, - "update.description" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "You are running an outdated version of Whisky. The latest version available is %2$@, you are running %1$@. Would you like to update?" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "您正在运行一个过时版本的Whisky。最新可用的版本是 %2$@,你在用 %1$@。您想要下载更新吗?" - } - } - } - }, - "update.downloading" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Downloading Update..." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "正在下载更新..." - } - } - } - }, - "update.extracting" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Extracting Files..." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "正在解压文件..." - } - } - } - }, "update.gptk.description" : { "localizations" : { "da" : { @@ -15471,206 +15343,6 @@ } } }, - "update.initializating" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Initializing..." - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Inicjalizacja..." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "处理中..." - } - } - } - }, - "update.installing" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Finishing Up..." - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kończenie..." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "马上就完成..." - } - } - } - }, - "update.noChangeLog" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "No change log available." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "无可用的更改日志。" - } - } - } - }, - "update.noUpdatesFound" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "No Updates Available" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "没有更新" - } - } - } - }, - "update.noUpdatesFound.description" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "No updates are available. You are on the latest version of Whisky." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "没有更新了。你在用 Whisky 最新的版本。" - } - } - } - }, - "update.readyToRelaunch" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Update Ready" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "更新已准备好" - } - } - } - }, - "update.readyToRelaunch.description" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "The update is ready to be installed. Press \"Relaunch\" to install the update and relaunch Whisky." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "更新已准备好安装。点击“重启”来安装更新并重启 Whisky。" - } - } - } - }, - "update.relaunch" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Relaunch" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "重启" - } - } - } - }, - "update.title" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Update Available!" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dostępna aktualizacja!" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "有更新了!" - } - } - } - }, - "update.update" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Update" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aktualizuj" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "下载更新" - } - } - } - }, - "update.updaterError" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Update Error!" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "更新失败!" - } - } - } - }, "wine.clearShaderCaches" : { "localizations" : { "da" : { @@ -15896,8 +15568,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "translated", - "value" : "软解" + "state" : "needs_review", + "value" : "Apps" } }, "zh-Hant" : { @@ -16015,8 +15687,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "translated", - "value" : "皆准测试" + "state" : "needs_review", + "value" : "Benchmarks" } }, "zh-Hant" : { @@ -16028,7 +15700,6 @@ } }, "winetricks.category.dlls" : { - "comment" : "It's best to write DLL", "extractionState" : "manual", "localizations" : { "da" : { @@ -16135,8 +15806,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "translated", - "value" : "DLL" + "state" : "needs_review", + "value" : "DLLs" } }, "zh-Hant" : { @@ -16254,8 +15925,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "translated", - "value" : "字体" + "state" : "needs_review", + "value" : "Fonts" } }, "zh-Hant" : { @@ -16373,8 +16044,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "translated", - "value" : "游戏" + "state" : "needs_review", + "value" : "Games" } }, "zh-Hant" : { @@ -16492,8 +16163,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "translated", - "value" : "设置" + "state" : "needs_review", + "value" : "Settings" } }, "zh-Hant" : { @@ -16610,8 +16281,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "translated", - "value" : "描述" + "state" : "needs_review", + "value" : "Description" } }, "zh-Hant" : { @@ -16728,8 +16399,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "translated", - "value" : "命令" + "state" : "needs_review", + "value" : "Command" } }, "zh-Hant" : { From 4824966ccf7998d52938dc41936d5cd5f3f46f7c Mon Sep 17 00:00:00 2001 From: JoshuaBrest <36625023+JoshuaBrest@users.noreply.github.com> Date: Wed, 29 Nov 2023 02:21:31 -0800 Subject: [PATCH 08/22] Fix merge conflicts --- Whisky.xcodeproj/project.pbxproj | 41 ++- .../xcshareddata/swiftpm/Package.resolved | 24 +- Whisky/Info.plist | 4 + Whisky/Localizable.xcstrings | 227 +++++++++++- Whisky/Utils/SparkleUpdaterEvents.swift | 133 ++++++++ Whisky/Views/ContentView.swift | 9 +- .../Views/Updater/UpdateControlerView.swift | 322 ++++++++++++++++++ Whisky/Views/Updater/UpdatePreviewView.swift | 169 +++++++++ Whisky/Views/WhiskyApp.swift | 19 +- 9 files changed, 932 insertions(+), 16 deletions(-) create mode 100644 Whisky/Utils/SparkleUpdaterEvents.swift create mode 100644 Whisky/Views/Updater/UpdateControlerView.swift create mode 100644 Whisky/Views/Updater/UpdatePreviewView.swift diff --git a/Whisky.xcodeproj/project.pbxproj b/Whisky.xcodeproj/project.pbxproj index b2bb8e826..9651bfc67 100644 --- a/Whisky.xcodeproj/project.pbxproj +++ b/Whisky.xcodeproj/project.pbxproj @@ -57,6 +57,10 @@ 8CB681E52AED7C6F0018D319 /* WhiskyKit in Resources */ = {isa = PBXBuildFile; fileRef = 8CB681E42AED7C6F0018D319 /* WhiskyKit */; }; 8CB681E72AED7CD00018D319 /* WhiskyKit in Frameworks */ = {isa = PBXBuildFile; productRef = 8CB681E62AED7CD00018D319 /* WhiskyKit */; }; DB696FC82AFAE5DA0037EB2F /* PinCreationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB696FC72AFAE5DA0037EB2F /* PinCreationView.swift */; }; + EB051A092B150F7100F5F5B7 /* UpdatePreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB051A082B150F7100F5F5B7 /* UpdatePreviewView.swift */; }; + EB051A0E2B16EA7E00F5F5B7 /* MarkdownUI in Frameworks */ = {isa = PBXBuildFile; productRef = EB051A0D2B16EA7E00F5F5B7 /* MarkdownUI */; }; + EB051A102B1710A700F5F5B7 /* SparkleUpdaterEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB051A0F2B1710A700F5F5B7 /* SparkleUpdaterEvents.swift */; }; + EB051A132B17263300F5F5B7 /* UpdateControlerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB051A122B17263300F5F5B7 /* UpdateControlerView.swift */; }; EB58FB552A499896002DC184 /* SemanticVersion in Frameworks */ = {isa = PBXBuildFile; productRef = EB58FB542A499896002DC184 /* SemanticVersion */; }; EEA5A2462A31DD65008274AE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEA5A2452A31DD65008274AE /* AppDelegate.swift */; }; /* End PBXBuildFile section */ @@ -153,6 +157,9 @@ 8C73E1332AF472FC00B6FB45 /* ProgramMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgramMenuView.swift; sourceTree = ""; }; 8CB681E42AED7C6F0018D319 /* WhiskyKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = WhiskyKit; sourceTree = ""; }; DB696FC72AFAE5DA0037EB2F /* PinCreationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinCreationView.swift; sourceTree = ""; }; + EB051A082B150F7100F5F5B7 /* UpdatePreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdatePreviewView.swift; sourceTree = ""; }; + EB051A0F2B1710A700F5F5B7 /* SparkleUpdaterEvents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SparkleUpdaterEvents.swift; sourceTree = ""; }; + EB051A122B17263300F5F5B7 /* UpdateControlerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateControlerView.swift; sourceTree = ""; }; EEA5A2452A31DD65008274AE /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -164,6 +171,7 @@ 8C2AEFC82AED79B700CB568F /* WhiskyKit in Frameworks */, 6E064B1229DD32A200D9A2D2 /* Sparkle in Frameworks */, EB58FB552A499896002DC184 /* SemanticVersion in Frameworks */, + EB051A0E2B16EA7E00F5F5B7 /* MarkdownUI in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -236,6 +244,7 @@ 6E40498029CCA8B0006E3F1B /* BottleView.swift */, 6E50D98429CDF25B008C39F6 /* BottleCreationView.swift */, 6E355E5729D78249002D83BE /* ConfigView.swift */, + 6EA2D47D29DDAE5E00C84668 /* BottleRenameView.swift */, 6E182FC92B0BF64E00AADE81 /* WinetricksView.swift */, 6E17B6422AF3FD6E00831173 /* Pins */, 6365C4C22B1AA8CD00AAE1FD /* BottleListEntry.swift */, @@ -313,6 +322,7 @@ 6E40495729CCA19C006E3F1B /* ContentView.swift */, 6E064B1329DD331F00D9A2D2 /* SparkleView.swift */, 6E7C07BF2AAF570100F6E66B /* FileOpenView.swift */, + EB051A112B17261A00F5F5B7 /* Updater */, ); path = Views; sourceTree = ""; @@ -340,6 +350,7 @@ 6E621CEE2A5F631200C9AAB3 /* Winetricks.swift */, 6E70A4A02A9A280C007799E9 /* WhiskyCmd.swift */, 6E7C07BD2AAE7B0100F6E66B /* ProgramShortcut.swift */, + EB051A0F2B1710A700F5F5B7 /* SparkleUpdaterEvents.swift */, ); path = Utils; sourceTree = ""; @@ -384,6 +395,15 @@ path = WhiskyThumbnail; sourceTree = ""; }; + EB051A112B17261A00F5F5B7 /* Updater */ = { + isa = PBXGroup; + children = ( + EB051A082B150F7100F5F5B7 /* UpdatePreviewView.swift */, + EB051A122B17263300F5F5B7 /* UpdateControlerView.swift */, + ); + path = Updater; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -408,6 +428,7 @@ 6E064B1129DD32A200D9A2D2 /* Sparkle */, EB58FB542A499896002DC184 /* SemanticVersion */, 8C2AEFC72AED79B700CB568F /* WhiskyKit */, + EB051A0D2B16EA7E00F5F5B7 /* MarkdownUI */, ); productName = Whisky; productReference = 6E40495229CCA19C006E3F1B /* Whisky.app */; @@ -513,6 +534,7 @@ 6E95F66E2AB3F33C00D585D1 /* XCRemoteSwiftPackageReference "SwiftyTextTable" */, 6E95F6712AB3F67200D585D1 /* XCRemoteSwiftPackageReference "Progress" */, 8C2AEFC62AED79B700CB568F /* XCLocalSwiftPackageReference "WhiskyKit" */, + EB051A0C2B16EA2E00F5F5B7 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */, ); productRefGroup = 6E40495329CCA19C006E3F1B /* Products */; projectDirPath = ""; @@ -582,7 +604,10 @@ DB696FC82AFAE5DA0037EB2F /* PinCreationView.swift in Sources */, 6E7C07BE2AAE7B0100F6E66B /* ProgramShortcut.swift in Sources */, 6E355E5829D78249002D83BE /* ConfigView.swift in Sources */, + EB051A102B1710A700F5F5B7 /* SparkleUpdaterEvents.swift in Sources */, + 6EA2D47E29DDAE5E00C84668 /* BottleRenameView.swift in Sources */, 63FFDE862ADF0C7700178665 /* BottomBar.swift in Sources */, + EB051A092B150F7100F5F5B7 /* UpdatePreviewView.swift in Sources */, 6E6C0CF62A419A8300356232 /* GPTKDownloadView.swift in Sources */, 6365C4C32B1AA8CD00AAE1FD /* BottleListEntry.swift in Sources */, 6E50D98529CDF25B008C39F6 /* BottleCreationView.swift in Sources */, @@ -592,6 +617,7 @@ 6E6C0CF42A419A7600356232 /* RosettaView.swift in Sources */, 6E6C0CF82A419A8C00356232 /* GPTKInstallView.swift in Sources */, 6365C4C12B1AA69D00AAE1FD /* Animation+Extensions.swift in Sources */, + EB051A132B17263300F5F5B7 /* UpdateControlerView.swift in Sources */, 6E40498329CCA91B006E3F1B /* Bottle+Extensions.swift in Sources */, 6E621CEF2A5F631300C9AAB3 /* Winetricks.swift in Sources */, 6E17B6462AF3FDC100831173 /* PinView.swift in Sources */, @@ -826,7 +852,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.isaacmarovitz.Whisky; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = Whisky; +"PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = Whisky; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; }; @@ -1029,6 +1055,14 @@ minimumVersion = 0.4.0; }; }; + EB051A0C2B16EA2E00F5F5B7 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/gonzalezreal/swift-markdown-ui.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.2.0; + }; + }; EB58FB532A499896002DC184 /* XCRemoteSwiftPackageReference "SemanticVersion" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/SwiftPackageIndex/SemanticVersion"; @@ -1078,6 +1112,11 @@ isa = XCSwiftPackageProductDependency; productName = WhiskyKit; }; + EB051A0D2B16EA7E00F5F5B7 /* MarkdownUI */ = { + isa = XCSwiftPackageProductDependency; + package = EB051A0C2B16EA2E00F5F5B7 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */; + productName = MarkdownUI; + }; EB58FB542A499896002DC184 /* SemanticVersion */ = { isa = XCSwiftPackageProductDependency; package = EB58FB532A499896002DC184 /* XCRemoteSwiftPackageReference "SemanticVersion" */; diff --git a/Whisky.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Whisky.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 067e72711..c07eb60cb 100644 --- a/Whisky.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Whisky.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,14 @@ { "pins" : [ + { + "identity" : "networkimage", + "kind" : "remoteSourceControl", + "location" : "https://github.com/gonzalezreal/NetworkImage", + "state" : { + "revision" : "7aff8d1b31148d32c5933d75557d42f6323ee3d1", + "version" : "6.0.0" + } + }, { "identity" : "progress.swift", "kind" : "remoteSourceControl", @@ -14,8 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/SwiftPackageIndex/SemanticVersion", "state" : { - "revision" : "a70840d5fca686ae3bd2fcf8aecc5ded0bd4f125", - "version" : "0.3.6" + "revision" : "ea8eea9d89842a29af1b8e6c7677f1c86e72fa42", + "version" : "0.4.0" } }, { @@ -24,7 +33,7 @@ "location" : "https://github.com/sparkle-project/Sparkle", "state" : { "branch" : "2.x", - "revision" : "b7b858dbf385cdd1fe1ab8a3f3ee8586fa850d5d" + "revision" : "1a0b023e1c4d37302ae6401b8f9c38af0729e21d" } }, { @@ -36,6 +45,15 @@ "version" : "1.2.3" } }, + { + "identity" : "swift-markdown-ui", + "kind" : "remoteSourceControl", + "location" : "https://github.com/gonzalezreal/swift-markdown-ui.git", + "state" : { + "revision" : "5df8a4adedd6ae4eb2455ef60ff75183984daeb8", + "version" : "2.2.0" + } + }, { "identity" : "swiftytexttable", "kind" : "remoteSourceControl", diff --git a/Whisky/Info.plist b/Whisky/Info.plist index e2e4738d6..21e7707ce 100644 --- a/Whisky/Info.plist +++ b/Whisky/Info.plist @@ -57,6 +57,10 @@ https://data.getwhisky.app/appcast.xml SUPublicEDKey tnZFAvPUfCpM7Tr7Sx5gKRm6BUQQ6htJQeOMP44evms= + GithubRepoOwner + Whisky-App + GithubRepoName + Whisky UTExportedTypeDeclarations diff --git a/Whisky/Localizable.xcstrings b/Whisky/Localizable.xcstrings index a3cd00e14..38d590416 100644 --- a/Whisky/Localizable.xcstrings +++ b/Whisky/Localizable.xcstrings @@ -249,6 +249,16 @@ } } }, + "button.cancel" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cancel" + } + } + } + }, "button.cDrive" : { "localizations" : { "da" : { @@ -14636,13 +14646,117 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "已安装的程序" + "value" : "下一次问" + } + } + } + }, + "update.changeLog" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Change Log" } }, - "zh-Hant" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "更新日志" + } + } + } + }, + "update.changeLogFailed" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Failed to get change log!" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "获取更新日志失败!" + } + } + } + }, + "update.checkingForUpdates" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Whisky is checking for updates...." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Whisky 正在检查更新..." + } + } + } + }, + "update.description" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "You are running an outdated version of Whisky. Would you like to update?" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "您正在运行一个过时版本的Whisky。您想要下载更新吗?" + } + } + } + }, + "update.descriptionLoaded" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "You are running an outdated version of Whisky. The latest version available is %2$@, you are running %1$@. Would you like to update?" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "您正在运行一个过时版本的Whisky。最新可用的版本是 %2$@,你在用 %1$@。您想要下载更新吗?" + } + } + } + }, + "update.downloading" : { + "localizations" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "已安裝的程式" + "value" : "Downloading Update..." + } + } + } + }, + "update.error" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Update Error!" + } + } + } + }, + "update.extracting" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Extracting Files..." } } } @@ -14974,11 +15088,113 @@ "state" : "translated", "value" : "更新" } + } + } + }, + "update.initializating" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Initializing..." + } + } + } + }, + "update.installing" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Finishing Up..." + } + } + } + }, + "update.noUpdateFound" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "No Updates Available" + } + } + } + }, + "update.noUpdateFound.description" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "No updates are available. You are on the latest version of Whisky." + } + } + } + }, + "update.readyRelaunch" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Update Complete!" + } + } + } + }, + "update.readyRelaunch.description" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Update Complete! Press \"OK\" to relaunch." + } + } + } + }, + "update.retryChangeLog" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Retry" + } }, - "zh-Hant" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "更新" + "value" : "重试" + } + } + } + }, + "update.title" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Update Available!" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "有更新了!" + } + } + } + }, + "update.update" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Update" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "下载更新" } } } @@ -15304,6 +15520,7 @@ } }, "winetricks.category.dlls" : { + "comment" : "It's best to write DLL", "extractionState" : "manual", "localizations" : { "da" : { diff --git a/Whisky/Utils/SparkleUpdaterEvents.swift b/Whisky/Utils/SparkleUpdaterEvents.swift new file mode 100644 index 000000000..816afee86 --- /dev/null +++ b/Whisky/Utils/SparkleUpdaterEvents.swift @@ -0,0 +1,133 @@ +// +// SparkleUpdaterEvents.swift +// Whisky +// +// This file is part of Whisky. +// +// Whisky is free software: you can redistribute it and/or modify it under the terms +// of the GNU General Public License as published by the Free Software Foundation, +// either version 3 of the License, or (at your option) any later version. +// +// Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with Whisky. +// If not, see https://www.gnu.org/licenses/. +// + +import Foundation +import Sparkle + +class SparkleUpdaterEvents: NSObject, SPUUserDriver { + static let shared = SparkleUpdaterEvents() + + var checkingForUpdates: ((@escaping () -> Void) -> Void)? + var updateFound: ((SUAppcastItem, SPUUserUpdateState, @escaping (SPUUserUpdateChoice) -> Void) -> Void)? + var update: ((SPUDownloadData) -> Void)? + var updateError: ((NSError) -> Void)? + var updateDownloadState: (() -> Void)? + var updateExtractState: (() -> Void)? + var updateInstallState: (() -> Void)? + var updateReadyRelaunch: ((@escaping (SPUUserUpdateChoice) -> Void) -> Void)? + var updateDismiss: (() -> Void)? + + var expectedContentLength: Double = 0 + var receivedContentLength: Double = 0 + + var extractProgress: Double = 0 + + enum UpdateOption { + case install, dismiss + } + + func show(_ request: SPUUpdatePermissionRequest) async -> SUUpdatePermissionResponse { + return .init( + automaticUpdateChecks: false, + sendSystemProfile: false + ) + } + + func showUserInitiatedUpdateCheck(cancellation: @escaping () -> Void) { + checkingForUpdates?(cancellation) + } + + func showUpdateFound( + with appcastItem: SUAppcastItem, + state: SPUUserUpdateState, + reply: @escaping (SPUUserUpdateChoice) -> Void + ) { + if let updateFound = updateFound { + updateFound(appcastItem, state, reply) + } else { + reply(.dismiss) + } + } + + func showUpdateReleaseNotes(with downloadData: SPUDownloadData) { + update?(downloadData) + } + + func showUpdateReleaseNotesFailedToDownloadWithError(_ error: Error) { + updateError?(error as NSError) + } + + func showUpdateNotFoundWithError(_ error: Error, acknowledgement: @escaping () -> Void) { + updateError?(error as NSError) + + acknowledgement() + } + + func showUpdaterError(_ error: Error, acknowledgement: @escaping () -> Void) { + updateError?(error as NSError) + + acknowledgement() + } + + func showDownloadInitiated(cancellation: @escaping () -> Void) { + updateDownloadState?() + } + + func showDownloadDidReceiveExpectedContentLength(_ expectedContentLength: UInt64) { + self.expectedContentLength = Double(expectedContentLength) + updateDownloadState?() + } + + func showDownloadDidReceiveData(ofLength length: UInt64) { + receivedContentLength += Double(length) + updateDownloadState?() + } + + func showDownloadDidStartExtractingUpdate() { + updateExtractState?() + } + + func showExtractionReceivedProgress(_ progress: Double) { + extractProgress = progress + updateExtractState?() + } + + func showReady(toInstallAndRelaunch acknowledgement: @escaping (SPUUserUpdateChoice) -> Void) { + updateReadyRelaunch?(acknowledgement) + } + + func showInstallingUpdate( + withApplicationTerminated applicationTerminated: Bool, + retryTerminatingApplication: @escaping () -> Void + ) { + updateInstallState?() + } + + func showUpdateInstalledAndRelaunched(_ relaunched: Bool, acknowledgement: @escaping () -> Void) { + acknowledgement() + } + + func showUpdateInFocus() { + return + } + + func dismissUpdateInstallation() { + updateDismiss?() + return + } +} diff --git a/Whisky/Views/ContentView.swift b/Whisky/Views/ContentView.swift index 616fb790c..ffdfd1d76 100644 --- a/Whisky/Views/ContentView.swift +++ b/Whisky/Views/ContentView.swift @@ -20,10 +20,14 @@ import SwiftUI import UniformTypeIdentifiers import WhiskyKit import SemanticVersion +import Sparkle struct ContentView: View { @AppStorage("selectedBottleURL") private var selectedBottleURL: URL? @EnvironmentObject var bottleVM: BottleVM + + let updater: SPUUpdater? + @Binding var showSetup: Bool @State private var selected: URL? @@ -38,6 +42,9 @@ struct ContentView: View { @State private var bottleFilter = "" var body: some View { + if let updater { + UpdateControlerView(updater: updater) + } NavigationSplitView { sidebar } detail: { @@ -206,6 +213,6 @@ struct ContentView: View { } #Preview { - ContentView(showSetup: .constant(false)) + ContentView(updater: .none, showSetup: .constant(false)) .environmentObject(BottleVM.shared) } diff --git a/Whisky/Views/Updater/UpdateControlerView.swift b/Whisky/Views/Updater/UpdateControlerView.swift new file mode 100644 index 000000000..085654082 --- /dev/null +++ b/Whisky/Views/Updater/UpdateControlerView.swift @@ -0,0 +1,322 @@ +// +// UpdateControlerView.swift +// Whisky +// +// This file is part of Whisky. +// +// Whisky is free software: you can redistribute it and/or modify it under the terms +// of the GNU General Public License as published by the Free Software Foundation, +// either version 3 of the License, or (at your option) any later version. +// +// Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with Whisky. +// If not, see https://www.gnu.org/licenses/. +// + +import SwiftUI +import Sparkle + +enum UpdateState { + case initializating, downloading, extracting, installing +} + +struct UpdateControlerView: View { + let updater: SPUUpdater + + @State var showCheckingForUpdates = false + @State var cancelCheckingForUpdates: (() -> Void)? + @State var showUpdatePreview = false + @State var updateUltimatum: ((Bool) -> Void)? + @State var showUpdater = false + @State var updateState: UpdateState = .initializating + @State var updateStateDownloadStatedAt: Date = Date.init(timeIntervalSince1970: 0) + @State var updateStateDownloadableBytes: Double = 0 + @State var updateStateDownloadedBytes: Double = 0 + @State var updateStateExtractProgress: Double = 0 + + var body: some View { + VStack {} + .sheet(isPresented: $showCheckingForUpdates, content: { + UpdateCheckingView(cancel: { + showCheckingForUpdates = false + cancelCheckingForUpdates?() + }) + .frame(width: 300) + .interactiveDismissDisabled() + }) + .sheet(isPresented: $showUpdatePreview, content: { + UpdatePreviewView(dismiss: { + showUpdatePreview = false + updateUltimatum?(false) + updateUltimatum = .none + }, install: { + updateUltimatum?(true) + updateUltimatum = .none + }) + .interactiveDismissDisabled() + .frame(width: 600, height: 400) + }) + .sheet(isPresented: $showUpdater, content: { + UpdateInstallingView( + state: $updateState, + downloadStatedAt: $updateStateDownloadStatedAt, + downloadableBytes: $updateStateDownloadableBytes, + downloadedBytes: $updateStateDownloadedBytes, + extractProgress: $updateStateExtractProgress + ) + .interactiveDismissDisabled() + .frame(width: 300) + }) + .onAppear { + // On fn called show sheet + SparkleUpdaterEvents.shared.checkingForUpdates = { cancel in + // Prevent updater from checking for updates + showCheckingForUpdates = false + showUpdatePreview = false + showUpdater = false + cancelCheckingForUpdates = { + cancel() + } + showCheckingForUpdates = true + } + SparkleUpdaterEvents.shared.updateFound = { _, _, reply in + showCheckingForUpdates = false + showUpdater = false + showUpdatePreview = false + updateUltimatum = { option in + if option { + + reply(.install) + } else { + reply(.dismiss) + } + } + showUpdatePreview = true + } + SparkleUpdaterEvents.shared.updateDismiss = { + // Show Alert + if showCheckingForUpdates { + showCheckingForUpdates = false + showUpdatePreview = false + showUpdater = false + // no update found + displayPrompt( + title: String(localized: "update.noUpdateFound"), + description: String(localized: "update.noUpdateFound.description"), + action: String(localized: "button.ok"), + actionHandler: { + // Dismiss + } + ) + } + } + SparkleUpdaterEvents.shared.updateError = { error in + showCheckingForUpdates = false + showUpdatePreview = false + showUpdater = false + displayPrompt( + title: String(localized: "update.error"), + description: error.localizedDescription, + action: String(localized: "button.ok"), + actionHandler: { + // Dismiss + } + ) + } + // Update download state + SparkleUpdaterEvents.shared.updateDownloadState = { + if updateStateDownloadStatedAt == Date(timeIntervalSince1970: 0) { + updateStateDownloadStatedAt = Date() + } + showCheckingForUpdates = false + showUpdatePreview = false + showUpdater = true + withAnimation { updateState = .downloading } + updateStateDownloadableBytes = SparkleUpdaterEvents.shared.expectedContentLength + updateStateDownloadedBytes = SparkleUpdaterEvents.shared.receivedContentLength + } + // Update extract state + SparkleUpdaterEvents.shared.updateExtractState = { + showCheckingForUpdates = false + showUpdatePreview = false + showUpdater = true + withAnimation { updateState = .extracting } + updateStateExtractProgress = SparkleUpdaterEvents.shared.extractProgress + } + // Update install state + SparkleUpdaterEvents.shared.updateInstallState = { + showCheckingForUpdates = false + showUpdatePreview = false + showUpdater = true + withAnimation { updateState = .installing } + } + // Update ready relaunch + SparkleUpdaterEvents.shared.updateReadyRelaunch = { relaunch in + showCheckingForUpdates = false + showUpdatePreview = false + showUpdater = false + displayPrompt( + title: String(localized: "update.readyRelaunch"), + description: String(localized: "update.readyRelaunch.description"), + action: String(localized: "button.ok"), + actionHandler: { + // Kill all bottles + await WhiskyApp.killBottles() + // Relaunch + relaunch(.install) + // Actualy relaunch (I don't want to make a helper for this so.... you get...) + let task = Process() + task.launchPath = "/bin/sh" + task.arguments = [ + "-c", + """ + kill "\(ProcessInfo.processInfo.processName)"; + sleep 0.5; open "\(Bundle.main.bundlePath)" + """ + ] + task.launch() + NSApp.terminate(nil) + exit(0) + } + ) + } + } + } + + func displayPrompt(title: String, description: String, action: String, actionHandler: @escaping () -> Void) { + showCheckingForUpdates = false + showUpdatePreview = false + showUpdater = false + Task(priority: .userInitiated) { + await MainActor.run { + let alert = NSAlert() + alert.messageText = title + alert.informativeText = description + alert.addButton(withTitle: action) + alert.runModal() + } + } + } +} + +struct UpdateCheckingView: View { + let cancel: () -> Void + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + Text("update.checkingForUpdates") + .fontWeight(.bold) + ProgressView() + .progressViewStyle(.linear) + HStack { + Spacer() + Button("button.cancel") { + cancel() + } + .buttonStyle(.borderedProminent) + .keyboardShortcut(.defaultAction) + } + } + .padding(20) + .frame(alignment: .leading) + } +} + +struct UpdateInstallingView: View { + @Binding var state: UpdateState + @Binding var downloadStatedAt: Date + @Binding var downloadableBytes: Double + @Binding var downloadedBytes: Double + @Binding var extractProgress: Double + + @State var fractionProgress: Double = 0 + @State var downloadSpeed: Double = 0 + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + Text( + state == .downloading + ? "update.downloading" + : state == .extracting + ? "update.extracting" + : state == .installing + ? "update.installing" + : "update.initializating" + ) + .fontWeight(.bold) + if state == .installing || state == .initializating { + ProgressView() + .progressViewStyle(.linear) + } else { + ProgressView(value: fractionProgress, total: 1) + .progressViewStyle(.linear) + if state == .downloading { + HStack { + HStack { + Text(String(format: String(localized: "setup.gptk.progress"), + formatBytes(bytes: downloadedBytes), + formatBytes(bytes: downloadableBytes))) + + Text(String(" ")) + + (shouldShowEstimate() ? + Text(String(format: String(localized: "setup.gptk.eta"), + formatRemainingTime(remainingBytes: downloadableBytes - downloadedBytes))) + : Text(String())) + Spacer() + } + .font(.subheadline) + .monospacedDigit() + } + } + } + } + .padding(20) + .frame(alignment: .leading) + .onChange(of: downloadedBytes) { + let currentTime = Date() + let elapsedTime = currentTime.timeIntervalSince(downloadStatedAt ?? currentTime) + if downloadedBytes > 0 { + downloadSpeed = Double(downloadedBytes) / elapsedTime + } + withAnimation { + if downloadableBytes > 0 { + fractionProgress = downloadedBytes / downloadableBytes + } else { + fractionProgress = 0 + } + } + } + .onChange(of: extractProgress) { + withAnimation { + fractionProgress = extractProgress + } + } + } + + func formatBytes(bytes: Double) -> String { + let formatter = ByteCountFormatter() + formatter.countStyle = .file + formatter.zeroPadsFractionDigits = true + return formatter.string(fromByteCount: Int64(bytes)) + } + + func shouldShowEstimate() -> Bool { + let elapsedTime = Date().timeIntervalSince(downloadStatedAt ?? Date()) + return Int(elapsedTime.rounded()) > 5 && downloadedBytes != 0 + } + + func formatRemainingTime(remainingBytes: Double) -> String { + let remainingTimeInSeconds = remainingBytes / downloadSpeed + + let formatter = DateComponentsFormatter() + formatter.allowedUnits = [.hour, .minute, .second] + formatter.unitsStyle = .full + if shouldShowEstimate() { + return formatter.string(from: TimeInterval(remainingTimeInSeconds)) ?? "" + } else { + return "" + } + } +} diff --git a/Whisky/Views/Updater/UpdatePreviewView.swift b/Whisky/Views/Updater/UpdatePreviewView.swift new file mode 100644 index 000000000..1993dc43d --- /dev/null +++ b/Whisky/Views/Updater/UpdatePreviewView.swift @@ -0,0 +1,169 @@ +// +// UpdateUI.swift +// Whisky +// +// This file is part of Whisky. +// +// Whisky is free software: you can redistribute it and/or modify it under the terms +// of the GNU General Public License as published by the Free Software Foundation, +// either version 3 of the License, or (at your option) any later version. +// +// Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with Whisky. +// If not, see https://www.gnu.org/licenses/. +// + +import SwiftUI +import Sparkle +import MarkdownUI + +struct UpdatePreviewView: View { + enum MarkdownTextState { + case loaded, error, loading + } + + let dismiss: () -> Void + let install: () -> Void + + let updater = SparkleUpdaterEvents.shared + @State var markdownTextState: MarkdownTextState = .loading + @State var markdownText = "# Hello" + @State var currentVersion = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "(nil)" + @State var nextVersion = "" + + var body: some View { + HStack { + VStack(alignment: .leading, spacing: 12) { + Text("update.title") + .font(.title) + .fontWeight(.bold) + Text(markdownTextState != .loaded + ? String(localized: "update.description") + : String(format: String(localized: "update.descriptionLoaded"), + "v" + currentVersion, + nextVersion)) + Spacer() + HStack { + Button("update.cancel") { + dismiss() + } + Spacer() + Button("update.update") { + Task(priority: .userInitiated) { + install() + } + } + .buttonStyle(.borderedProminent) + .keyboardShortcut(.defaultAction) + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) + .padding(20) + VStack { + if markdownTextState == .loading { + ProgressView() + } else if markdownTextState == .error { + VStack(spacing: 12) { + Text("update.changeLogFailed") + Button("update.retryChangeLog") { + Task(priority: .userInitiated) { + await getChangelog() + } + } + } + } else { + ScrollView { + VStack(alignment: .leading) { + Text("update.changeLog") + .font(.title2) + .fontWeight(.bold) + .padding(.bottom, 12) + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) + Markdown { + markdownText + } + .markdownTheme(.basic) + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .padding(20) + .background(.ultraThickMaterial) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .onAppear { + Task(priority: .userInitiated) { + await getChangelog() + } + } + } + + func getChangelog() async { + withAnimation { markdownTextState = .loading } + let ghOwner = (Bundle.main.object(forInfoDictionaryKey: "GithubRepoOwner") as? String) ?? "Whisky-App" + let ghRepo = (Bundle.main.object(forInfoDictionaryKey: "GithubRepoName") as? String) ?? "Whisky" + + // Make a request to the Github API to get the latest release + // Append path not using string interpolation to non-urlencoded paths + guard let url = URL(string: "https://api.github.com/")? + .appending(path: "repos") + .appending(path: ghOwner) + .appending(path: ghRepo) + .appending(path: "releases") + .appending(path: "latest") + else { + withAnimation { markdownTextState = .error } + return + } + + var request = URLRequest(url: url) + request.setValue("application/vnd.github+json", forHTTPHeaderField: "Accept") + request.setValue("WhiskyApp", forHTTPHeaderField: "User-Agent") + + let data: Data + do { + let (dataInfo, _) = try (await URLSession.shared.data(for: request)) + data = dataInfo + } catch { + print("Changelog request failed: \(error)") + withAnimation { markdownTextState = .error } + return + } + + // Decode the JSON + struct Release: Codable { + let body: String + let tagName: String + + enum CodingKeys: String, CodingKey { // swiftlint:disable:this nesting + case body + case tagName = "tag_name" + } + } + + let release: Release + do { + release = try JSONDecoder().decode(Release.self, from: data) + } catch { + print("Failed to decode release: \(error)") + withAnimation { markdownTextState = .error } + return + } + + withAnimation { + markdownText = release.body + nextVersion = release.tagName + markdownTextState = .loaded + } + } +} + +#Preview { + UpdatePreviewView(dismiss: {}, install: {}) + .frame(width: 600, height: 400) +} diff --git a/Whisky/Views/WhiskyApp.swift b/Whisky/Views/WhiskyApp.swift index bb16bc6ba..ed2b3ce87 100644 --- a/Whisky/Views/WhiskyApp.swift +++ b/Whisky/Views/WhiskyApp.swift @@ -23,19 +23,26 @@ import WhiskyKit @main struct WhiskyApp: App { @State var showSetup: Bool = false + @State var showUpdater = false @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate @Environment(\.openURL) var openURL - private let updaterController: SPUStandardUpdaterController + private let updaterController: SPUUpdater init() { - updaterController = SPUStandardUpdaterController(startingUpdater: true, - updaterDelegate: nil, - userDriverDelegate: nil) + updaterController = SPUUpdater( + hostBundle: .main, + applicationBundle: .main, + userDriver: SparkleUpdaterEvents.shared, + delegate: nil) + + do { try updaterController.start() } catch { + print(error) + } } var body: some Scene { WindowGroup { - ContentView(showSetup: $showSetup) + ContentView(updater: updaterController, showSetup: $showSetup) .frame(minWidth: 550, minHeight: 250) .environmentObject(BottleVM.shared) .onAppear { @@ -46,7 +53,7 @@ struct WhiskyApp: App { .handlesExternalEvents(matching: ["{same path of URL?}"]) .commands { CommandGroup(after: .appInfo) { - SparkleView(updater: updaterController.updater) + SparkleView(updater: updaterController) } CommandGroup(before: .systemServices) { Divider() From a3cfe4606ccc7392aa40e47027249262c0bd73c9 Mon Sep 17 00:00:00 2001 From: JoshuaBrest <36625023+JoshuaBrest@users.noreply.github.com> Date: Wed, 29 Nov 2023 02:36:59 -0800 Subject: [PATCH 09/22] Fix localization issues and implement automatic relaunch --- Whisky.xcodeproj/project.pbxproj | 2 +- Whisky/Localizable.xcstrings | 60 ++++++++++++++----- .../Views/Updater/UpdateControlerView.swift | 40 +++++-------- 3 files changed, 61 insertions(+), 41 deletions(-) diff --git a/Whisky.xcodeproj/project.pbxproj b/Whisky.xcodeproj/project.pbxproj index 9651bfc67..b8e43a158 100644 --- a/Whisky.xcodeproj/project.pbxproj +++ b/Whisky.xcodeproj/project.pbxproj @@ -852,7 +852,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.isaacmarovitz.Whisky; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; -"PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = Whisky; + "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = Whisky; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; }; diff --git a/Whisky/Localizable.xcstrings b/Whisky/Localizable.xcstrings index 38d590416..b42ffb5e9 100644 --- a/Whisky/Localizable.xcstrings +++ b/Whisky/Localizable.xcstrings @@ -256,6 +256,12 @@ "state" : "translated", "value" : "Cancel" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "取消" + } } } }, @@ -14738,6 +14744,12 @@ "state" : "translated", "value" : "Downloading Update..." } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "正在下载更新..." + } } } }, @@ -14748,6 +14760,12 @@ "state" : "translated", "value" : "Update Error!" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "更新错误!" + } } } }, @@ -14758,6 +14776,12 @@ "state" : "translated", "value" : "Extracting Files..." } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "正在解压文件..." + } } } }, @@ -15098,6 +15122,12 @@ "state" : "translated", "value" : "Initializing..." } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "处理中..." + } } } }, @@ -15108,6 +15138,12 @@ "state" : "translated", "value" : "Finishing Up..." } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "马上就完成..." + } } } }, @@ -15118,35 +15154,27 @@ "state" : "translated", "value" : "No Updates Available" } - } - } - }, - "update.noUpdateFound.description" : { - "localizations" : { - "en" : { + }, + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "No updates are available. You are on the latest version of Whisky." + "value" : "没有更新" } } } }, - "update.readyRelaunch" : { + "update.noUpdateFound.description" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Update Complete!" + "value" : "No updates are available. You are on the latest version of Whisky." } - } - } - }, - "update.readyRelaunch.description" : { - "localizations" : { - "en" : { + }, + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "Update Complete! Press \"OK\" to relaunch." + "value" : "没有更新了。你在用 Whisky 最新的版本。" } } } diff --git a/Whisky/Views/Updater/UpdateControlerView.swift b/Whisky/Views/Updater/UpdateControlerView.swift index 085654082..48669c89d 100644 --- a/Whisky/Views/Updater/UpdateControlerView.swift +++ b/Whisky/Views/Updater/UpdateControlerView.swift @@ -158,30 +158,22 @@ struct UpdateControlerView: View { showCheckingForUpdates = false showUpdatePreview = false showUpdater = false - displayPrompt( - title: String(localized: "update.readyRelaunch"), - description: String(localized: "update.readyRelaunch.description"), - action: String(localized: "button.ok"), - actionHandler: { - // Kill all bottles - await WhiskyApp.killBottles() - // Relaunch - relaunch(.install) - // Actualy relaunch (I don't want to make a helper for this so.... you get...) - let task = Process() - task.launchPath = "/bin/sh" - task.arguments = [ - "-c", - """ - kill "\(ProcessInfo.processInfo.processName)"; - sleep 0.5; open "\(Bundle.main.bundlePath)" - """ - ] - task.launch() - NSApp.terminate(nil) - exit(0) - } - ) + WhiskyApp.killBottles() + // Actualy relaunch (I don't want to make a helper for this so.... you get...) + let task = Process() + task.launchPath = "/bin/sh" + task.arguments = [ + "-c", + """ + kill "\(ProcessInfo.processInfo.processIdentifier)"; + sleep 0.5; open "\(Bundle.main.bundlePath)" + """ + ] + task.launch() + // Relaunch + relaunch(.install) + NSApp.terminate(nil) + exit(0) } } } From adc91d0a76ebe9594658cf9eac8a8d070985b880 Mon Sep 17 00:00:00 2001 From: JoshuaBrest <36625023+JoshuaBrest@users.noreply.github.com> Date: Wed, 29 Nov 2023 12:34:29 -0800 Subject: [PATCH 10/22] Use appcast not github --- Whisky/Localizable.xcstrings | 86 ++++++------ .../Views/Updater/UpdateControlerView.swift | 26 ++-- Whisky/Views/Updater/UpdatePreviewView.swift | 127 ++++-------------- 3 files changed, 86 insertions(+), 153 deletions(-) diff --git a/Whisky/Localizable.xcstrings b/Whisky/Localizable.xcstrings index b42ffb5e9..a35df00c4 100644 --- a/Whisky/Localizable.xcstrings +++ b/Whisky/Localizable.xcstrings @@ -14673,22 +14673,6 @@ } } }, - "update.changeLogFailed" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Failed to get change log!" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "获取更新日志失败!" - } - } - } - }, "update.checkingForUpdates" : { "localizations" : { "en" : { @@ -14706,22 +14690,6 @@ } }, "update.description" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "You are running an outdated version of Whisky. Would you like to update?" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "您正在运行一个过时版本的Whisky。您想要下载更新吗?" - } - } - } - }, - "update.descriptionLoaded" : { "localizations" : { "en" : { "stringUnit" : { @@ -15123,6 +15091,12 @@ "value" : "Initializing..." } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inicjalizacja..." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -15139,6 +15113,12 @@ "value" : "Finishing Up..." } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kończenie..." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -15147,50 +15127,62 @@ } } }, - "update.noUpdateFound" : { + "update.noChangeLog" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "No Updates Available" + "value" : "No change log available." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "没有更新" + "value" : "无可用的更改日志。" } } } }, - "update.noUpdateFound.description" : { + "update.noUpdateFound" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "No updates are available. You are on the latest version of Whisky." + "value" : "No Updates Available" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Brak dostępnych aktualizacji" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "没有更新了。你在用 Whisky 最新的版本。" + "value" : "没有更新" } } } }, - "update.retryChangeLog" : { + "update.noUpdateFound.description" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Retry" + "value" : "No updates are available. You are on the latest version of Whisky." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Brak dostępnych aktualizacji. Posiadasz najnowszą wersję Whisky." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "重试" + "value" : "没有更新了。你在用 Whisky 最新的版本。" } } } @@ -15203,6 +15195,12 @@ "value" : "Update Available!" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dostępna aktualizacja!" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -15219,6 +15217,12 @@ "value" : "Update" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktualizuj" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", diff --git a/Whisky/Views/Updater/UpdateControlerView.swift b/Whisky/Views/Updater/UpdateControlerView.swift index 48669c89d..8e6dfbc67 100644 --- a/Whisky/Views/Updater/UpdateControlerView.swift +++ b/Whisky/Views/Updater/UpdateControlerView.swift @@ -29,6 +29,8 @@ struct UpdateControlerView: View { @State var showCheckingForUpdates = false @State var cancelCheckingForUpdates: (() -> Void)? @State var showUpdatePreview = false + @State var previewNextVersion: String = "" + @State var previewMarkdown: String = "" @State var updateUltimatum: ((Bool) -> Void)? @State var showUpdater = false @State var updateState: UpdateState = .initializating @@ -48,14 +50,14 @@ struct UpdateControlerView: View { .interactiveDismissDisabled() }) .sheet(isPresented: $showUpdatePreview, content: { - UpdatePreviewView(dismiss: { - showUpdatePreview = false - updateUltimatum?(false) - updateUltimatum = .none - }, install: { - updateUltimatum?(true) - updateUltimatum = .none - }) + UpdatePreviewView(dismiss: { + showUpdatePreview = false + updateUltimatum?(false) + updateUltimatum = .none + }, install: { + updateUltimatum?(true) + updateUltimatum = .none + }, markdownText: $previewMarkdown, nextVersion: $previewNextVersion) .interactiveDismissDisabled() .frame(width: 600, height: 400) }) @@ -82,18 +84,19 @@ struct UpdateControlerView: View { } showCheckingForUpdates = true } - SparkleUpdaterEvents.shared.updateFound = { _, _, reply in + SparkleUpdaterEvents.shared.updateFound = { appcastItem, _, reply in showCheckingForUpdates = false showUpdater = false showUpdatePreview = false updateUltimatum = { option in if option { - reply(.install) } else { reply(.dismiss) } } + previewNextVersion = appcastItem.displayVersionString + previewMarkdown = appcastItem.itemDescription ?? String(localized: "update.noChangeLog") showUpdatePreview = true } SparkleUpdaterEvents.shared.updateDismiss = { @@ -172,7 +175,6 @@ struct UpdateControlerView: View { task.launch() // Relaunch relaunch(.install) - NSApp.terminate(nil) exit(0) } } @@ -268,7 +270,7 @@ struct UpdateInstallingView: View { .frame(alignment: .leading) .onChange(of: downloadedBytes) { let currentTime = Date() - let elapsedTime = currentTime.timeIntervalSince(downloadStatedAt ?? currentTime) + let elapsedTime = currentTime.timeIntervalSince(downloadStatedAt) if downloadedBytes > 0 { downloadSpeed = Double(downloadedBytes) / elapsedTime } diff --git a/Whisky/Views/Updater/UpdatePreviewView.swift b/Whisky/Views/Updater/UpdatePreviewView.swift index 1993dc43d..9515e0b25 100644 --- a/Whisky/Views/Updater/UpdatePreviewView.swift +++ b/Whisky/Views/Updater/UpdatePreviewView.swift @@ -21,18 +21,20 @@ import Sparkle import MarkdownUI struct UpdatePreviewView: View { - enum MarkdownTextState { - case loaded, error, loading - } +// enum MarkdownTextState { +// case loaded, error, loading +// } let dismiss: () -> Void let install: () -> Void + @Binding var markdownText: String + @Binding var nextVersion: String - let updater = SparkleUpdaterEvents.shared - @State var markdownTextState: MarkdownTextState = .loading - @State var markdownText = "# Hello" - @State var currentVersion = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "(nil)" - @State var nextVersion = "" + // let updater = SparkleUpdaterEvents.shared + // @State var markdownTextState: MarkdownTextState = .loading + // @State var markdownText = "# Hello" + let currentVersion = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "(nil)" + // @State var nextVersion = "" var body: some View { HStack { @@ -40,11 +42,9 @@ struct UpdatePreviewView: View { Text("update.title") .font(.title) .fontWeight(.bold) - Text(markdownTextState != .loaded - ? String(localized: "update.description") - : String(format: String(localized: "update.descriptionLoaded"), - "v" + currentVersion, - nextVersion)) + Text(String(format: String(localized: "update.description"), + "v" + currentVersion, + nextVersion)) Spacer() HStack { Button("update.cancel") { @@ -63,33 +63,20 @@ struct UpdatePreviewView: View { .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) .padding(20) VStack { - if markdownTextState == .loading { - ProgressView() - } else if markdownTextState == .error { - VStack(spacing: 12) { - Text("update.changeLogFailed") - Button("update.retryChangeLog") { - Task(priority: .userInitiated) { - await getChangelog() - } - } - } - } else { - ScrollView { - VStack(alignment: .leading) { - Text("update.changeLog") - .font(.title2) - .fontWeight(.bold) - .padding(.bottom, 12) - } - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) - Markdown { - markdownText - } - .markdownTheme(.basic) + ScrollView { + VStack(alignment: .leading) { + Text("update.changeLog") + .font(.title2) + .fontWeight(.bold) + .padding(.bottom, 12) } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) + Markdown { + markdownText + } + .markdownTheme(.basic) } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) } .frame(maxWidth: .infinity, maxHeight: .infinity) .padding(20) @@ -97,73 +84,13 @@ struct UpdatePreviewView: View { } .frame(maxWidth: .infinity, maxHeight: .infinity) .onAppear { - Task(priority: .userInitiated) { - await getChangelog() - } - } - } - - func getChangelog() async { - withAnimation { markdownTextState = .loading } - let ghOwner = (Bundle.main.object(forInfoDictionaryKey: "GithubRepoOwner") as? String) ?? "Whisky-App" - let ghRepo = (Bundle.main.object(forInfoDictionaryKey: "GithubRepoName") as? String) ?? "Whisky" - - // Make a request to the Github API to get the latest release - // Append path not using string interpolation to non-urlencoded paths - guard let url = URL(string: "https://api.github.com/")? - .appending(path: "repos") - .appending(path: ghOwner) - .appending(path: ghRepo) - .appending(path: "releases") - .appending(path: "latest") - else { - withAnimation { markdownTextState = .error } - return - } - - var request = URLRequest(url: url) - request.setValue("application/vnd.github+json", forHTTPHeaderField: "Accept") - request.setValue("WhiskyApp", forHTTPHeaderField: "User-Agent") - - let data: Data - do { - let (dataInfo, _) = try (await URLSession.shared.data(for: request)) - data = dataInfo - } catch { - print("Changelog request failed: \(error)") - withAnimation { markdownTextState = .error } - return - } - - // Decode the JSON - struct Release: Codable { - let body: String - let tagName: String - - enum CodingKeys: String, CodingKey { // swiftlint:disable:this nesting - case body - case tagName = "tag_name" - } - } - - let release: Release - do { - release = try JSONDecoder().decode(Release.self, from: data) - } catch { - print("Failed to decode release: \(error)") - withAnimation { markdownTextState = .error } - return - } - - withAnimation { - markdownText = release.body - nextVersion = release.tagName - markdownTextState = .loaded + print("test:") + print(markdownText) } } } #Preview { - UpdatePreviewView(dismiss: {}, install: {}) + UpdatePreviewView(dismiss: {}, install: {}, markdownText: .constant("# Hello"), nextVersion: .constant("v1.0.0")) .frame(width: 600, height: 400) } From 6c64dd6b84dfe3a59557818b99a4f30c93b8a347 Mon Sep 17 00:00:00 2001 From: JoshuaBrest <36625023+JoshuaBrest@users.noreply.github.com> Date: Wed, 29 Nov 2023 12:57:21 -0800 Subject: [PATCH 11/22] Remove unnecessary keys from Info.plist --- Whisky/Info.plist | 4 ---- Whisky/Views/Updater/UpdatePreviewView.swift | 8 -------- 2 files changed, 12 deletions(-) diff --git a/Whisky/Info.plist b/Whisky/Info.plist index 21e7707ce..e2e4738d6 100644 --- a/Whisky/Info.plist +++ b/Whisky/Info.plist @@ -57,10 +57,6 @@ https://data.getwhisky.app/appcast.xml SUPublicEDKey tnZFAvPUfCpM7Tr7Sx5gKRm6BUQQ6htJQeOMP44evms= - GithubRepoOwner - Whisky-App - GithubRepoName - Whisky UTExportedTypeDeclarations diff --git a/Whisky/Views/Updater/UpdatePreviewView.swift b/Whisky/Views/Updater/UpdatePreviewView.swift index 9515e0b25..091fa29dc 100644 --- a/Whisky/Views/Updater/UpdatePreviewView.swift +++ b/Whisky/Views/Updater/UpdatePreviewView.swift @@ -21,20 +21,12 @@ import Sparkle import MarkdownUI struct UpdatePreviewView: View { -// enum MarkdownTextState { -// case loaded, error, loading -// } - let dismiss: () -> Void let install: () -> Void @Binding var markdownText: String @Binding var nextVersion: String - // let updater = SparkleUpdaterEvents.shared - // @State var markdownTextState: MarkdownTextState = .loading - // @State var markdownText = "# Hello" let currentVersion = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "(nil)" - // @State var nextVersion = "" var body: some View { HStack { From c22f4af04eca3e14278e334512279485a16663f6 Mon Sep 17 00:00:00 2001 From: JoshuaBrest <36625023+JoshuaBrest@users.noreply.github.com> Date: Wed, 29 Nov 2023 12:57:55 -0800 Subject: [PATCH 12/22] Remove unnecessary code in UpdatePreviewView --- Whisky/Views/Updater/UpdatePreviewView.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Whisky/Views/Updater/UpdatePreviewView.swift b/Whisky/Views/Updater/UpdatePreviewView.swift index 091fa29dc..fbad75897 100644 --- a/Whisky/Views/Updater/UpdatePreviewView.swift +++ b/Whisky/Views/Updater/UpdatePreviewView.swift @@ -75,10 +75,6 @@ struct UpdatePreviewView: View { .background(.ultraThickMaterial) } .frame(maxWidth: .infinity, maxHeight: .infinity) - .onAppear { - print("test:") - print(markdownText) - } } } From 3b2321621fd53b90253076117fb695d01d3afec7 Mon Sep 17 00:00:00 2001 From: JoshuaBrest <36625023+JoshuaBrest@users.noreply.github.com> Date: Thu, 30 Nov 2023 19:59:47 -0800 Subject: [PATCH 13/22] Resolve Merge Conflicts --- Whisky.xcodeproj/project.pbxproj | 12 +- .../xcshareddata/swiftpm/Package.resolved | 68 ---- Whisky/Localizable.xcstrings | 92 +++-- Whisky/Utils/SparkleUpdaterEvents.swift | 226 ++++++++++--- Whisky/Views/ContentView.swift | 7 +- .../Views/Updater/UpdateControlerView.swift | 316 ------------------ .../UpdateControllerViewModifier.swift | 167 +++++++++ .../Views/Updater/UpdateInstallingView.swift | 201 +++++++++++ Whisky/Views/Updater/UpdatePreviewView.swift | 39 ++- Whisky/Views/WhiskyApp.swift | 3 +- 10 files changed, 641 insertions(+), 490 deletions(-) delete mode 100644 Whisky.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved delete mode 100644 Whisky/Views/Updater/UpdateControlerView.swift create mode 100644 Whisky/Views/Updater/UpdateControllerViewModifier.swift create mode 100644 Whisky/Views/Updater/UpdateInstallingView.swift diff --git a/Whisky.xcodeproj/project.pbxproj b/Whisky.xcodeproj/project.pbxproj index b8e43a158..7c719b715 100644 --- a/Whisky.xcodeproj/project.pbxproj +++ b/Whisky.xcodeproj/project.pbxproj @@ -60,8 +60,9 @@ EB051A092B150F7100F5F5B7 /* UpdatePreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB051A082B150F7100F5F5B7 /* UpdatePreviewView.swift */; }; EB051A0E2B16EA7E00F5F5B7 /* MarkdownUI in Frameworks */ = {isa = PBXBuildFile; productRef = EB051A0D2B16EA7E00F5F5B7 /* MarkdownUI */; }; EB051A102B1710A700F5F5B7 /* SparkleUpdaterEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB051A0F2B1710A700F5F5B7 /* SparkleUpdaterEvents.swift */; }; - EB051A132B17263300F5F5B7 /* UpdateControlerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB051A122B17263300F5F5B7 /* UpdateControlerView.swift */; }; + EB051A132B17263300F5F5B7 /* UpdateControllerViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB051A122B17263300F5F5B7 /* UpdateControllerViewModifier.swift */; }; EB58FB552A499896002DC184 /* SemanticVersion in Frameworks */ = {isa = PBXBuildFile; productRef = EB58FB542A499896002DC184 /* SemanticVersion */; }; + EBB21B662B198370000C3FA0 /* UpdateInstallingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBB21B652B198370000C3FA0 /* UpdateInstallingView.swift */; }; EEA5A2462A31DD65008274AE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEA5A2452A31DD65008274AE /* AppDelegate.swift */; }; /* End PBXBuildFile section */ @@ -159,7 +160,8 @@ DB696FC72AFAE5DA0037EB2F /* PinCreationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinCreationView.swift; sourceTree = ""; }; EB051A082B150F7100F5F5B7 /* UpdatePreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdatePreviewView.swift; sourceTree = ""; }; EB051A0F2B1710A700F5F5B7 /* SparkleUpdaterEvents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SparkleUpdaterEvents.swift; sourceTree = ""; }; - EB051A122B17263300F5F5B7 /* UpdateControlerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateControlerView.swift; sourceTree = ""; }; + EB051A122B17263300F5F5B7 /* UpdateControllerViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateControllerViewModifier.swift; sourceTree = ""; }; + EBB21B652B198370000C3FA0 /* UpdateInstallingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateInstallingView.swift; sourceTree = ""; }; EEA5A2452A31DD65008274AE /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -399,7 +401,8 @@ isa = PBXGroup; children = ( EB051A082B150F7100F5F5B7 /* UpdatePreviewView.swift */, - EB051A122B17263300F5F5B7 /* UpdateControlerView.swift */, + EB051A122B17263300F5F5B7 /* UpdateControllerViewModifier.swift */, + EBB21B652B198370000C3FA0 /* UpdateInstallingView.swift */, ); path = Updater; sourceTree = ""; @@ -597,6 +600,7 @@ buildActionMask = 2147483647; files = ( EEA5A2462A31DD65008274AE /* AppDelegate.swift in Sources */, + EBB21B662B198370000C3FA0 /* UpdateInstallingView.swift in Sources */, 6E70A4A12A9A280C007799E9 /* WhiskyCmd.swift in Sources */, 6E40495829CCA19C006E3F1B /* ContentView.swift in Sources */, 6EF557982A410599001A4F09 /* SetupView.swift in Sources */, @@ -617,7 +621,7 @@ 6E6C0CF42A419A7600356232 /* RosettaView.swift in Sources */, 6E6C0CF82A419A8C00356232 /* GPTKInstallView.swift in Sources */, 6365C4C12B1AA69D00AAE1FD /* Animation+Extensions.swift in Sources */, - EB051A132B17263300F5F5B7 /* UpdateControlerView.swift in Sources */, + EB051A132B17263300F5F5B7 /* UpdateControllerViewModifier.swift in Sources */, 6E40498329CCA91B006E3F1B /* Bottle+Extensions.swift in Sources */, 6E621CEF2A5F631300C9AAB3 /* Winetricks.swift in Sources */, 6E17B6462AF3FDC100831173 /* PinView.swift in Sources */, diff --git a/Whisky.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Whisky.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index c07eb60cb..000000000 --- a/Whisky.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,68 +0,0 @@ -{ - "pins" : [ - { - "identity" : "networkimage", - "kind" : "remoteSourceControl", - "location" : "https://github.com/gonzalezreal/NetworkImage", - "state" : { - "revision" : "7aff8d1b31148d32c5933d75557d42f6323ee3d1", - "version" : "6.0.0" - } - }, - { - "identity" : "progress.swift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/jkandzi/Progress.swift", - "state" : { - "revision" : "fed6598735d7982058690acf8f52a0a5fdaeb3e0", - "version" : "0.4.0" - } - }, - { - "identity" : "semanticversion", - "kind" : "remoteSourceControl", - "location" : "https://github.com/SwiftPackageIndex/SemanticVersion", - "state" : { - "revision" : "ea8eea9d89842a29af1b8e6c7677f1c86e72fa42", - "version" : "0.4.0" - } - }, - { - "identity" : "sparkle", - "kind" : "remoteSourceControl", - "location" : "https://github.com/sparkle-project/Sparkle", - "state" : { - "branch" : "2.x", - "revision" : "1a0b023e1c4d37302ae6401b8f9c38af0729e21d" - } - }, - { - "identity" : "swift-argument-parser", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-argument-parser.git", - "state" : { - "revision" : "8f4d2753f0e4778c76d5f05ad16c74f707390531", - "version" : "1.2.3" - } - }, - { - "identity" : "swift-markdown-ui", - "kind" : "remoteSourceControl", - "location" : "https://github.com/gonzalezreal/swift-markdown-ui.git", - "state" : { - "revision" : "5df8a4adedd6ae4eb2455ef60ff75183984daeb8", - "version" : "2.2.0" - } - }, - { - "identity" : "swiftytexttable", - "kind" : "remoteSourceControl", - "location" : "https://github.com/scottrhoyt/SwiftyTextTable", - "state" : { - "revision" : "c6df6cf533d120716bff38f8ff9885e1ce2a4ac3", - "version" : "0.9.0" - } - } - ], - "version" : 2 -} diff --git a/Whisky/Localizable.xcstrings b/Whisky/Localizable.xcstrings index a35df00c4..9f57f0be8 100644 --- a/Whisky/Localizable.xcstrings +++ b/Whisky/Localizable.xcstrings @@ -14678,61 +14678,61 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Whisky is checking for updates...." + "value" : "Checking for updates..." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "Whisky 正在检查更新..." + "value" : "正在检查更新..." } } } }, - "update.description" : { + "update.checkingForUpdates.description" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "You are running an outdated version of Whisky. The latest version available is %2$@, you are running %1$@. Would you like to update?" + "value" : "Whisky is checking for updates..." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "您正在运行一个过时版本的Whisky。最新可用的版本是 %2$@,你在用 %1$@。您想要下载更新吗?" + "value" : "Whisky 正在检查更新..." } } } }, - "update.downloading" : { + "update.description" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Downloading Update..." + "value" : "You are running an outdated version of Whisky. The latest version available is %2$@, you are running %1$@. Would you like to update?" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "正在下载更新..." + "value" : "您正在运行一个过时版本的Whisky。最新可用的版本是 %2$@,你在用 %1$@。您想要下载更新吗?" } } } }, - "update.error" : { + "update.downloading" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Update Error!" + "value" : "Downloading Update..." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "更新错误!" + "value" : "正在下载更新..." } } } @@ -15143,7 +15143,7 @@ } } }, - "update.noUpdateFound" : { + "update.noUpdatesFound" : { "localizations" : { "en" : { "stringUnit" : { @@ -15151,38 +15151,74 @@ "value" : "No Updates Available" } }, - "pl" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "Brak dostępnych aktualizacji" + "value" : "没有更新" + } + } + } + }, + "update.noUpdatesFound.description" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "No updates are available. You are on the latest version of Whisky." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "没有更新" + "value" : "没有更新了。你在用 Whisky 最新的版本。" } } } }, - "update.noUpdateFound.description" : { + "update.readyToRelaunch" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "No updates are available. You are on the latest version of Whisky." + "value" : "Update Ready" } }, - "pl" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "更新已准备好" + } + } + } + }, + "update.readyToRelaunch.description" : { + "localizations" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "Brak dostępnych aktualizacji. Posiadasz najnowszą wersję Whisky." + "value" : "The update is ready to be installed. Press \"Relaunch\" to install the update and relaunch Whisky." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "没有更新了。你在用 Whisky 最新的版本。" + "value" : "更新已准备好安装。点击“重启”来安装更新并重启 Whisky。" + } + } + } + }, + "update.relaunch" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Relaunch" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "重启" } } } @@ -15231,6 +15267,22 @@ } } }, + "update.updaterError" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Update Error!" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "更新失败!" + } + } + } + }, "wine.clearShaderCaches" : { "localizations" : { "da" : { diff --git a/Whisky/Utils/SparkleUpdaterEvents.swift b/Whisky/Utils/SparkleUpdaterEvents.swift index 816afee86..dec961b7d 100644 --- a/Whisky/Utils/SparkleUpdaterEvents.swift +++ b/Whisky/Utils/SparkleUpdaterEvents.swift @@ -19,115 +19,231 @@ import Foundation import Sparkle -class SparkleUpdaterEvents: NSObject, SPUUserDriver { +class SparkleUpdaterEvents: NSObject, SPUUserDriver, ObservableObject { static let shared = SparkleUpdaterEvents() - var checkingForUpdates: ((@escaping () -> Void) -> Void)? - var updateFound: ((SUAppcastItem, SPUUserUpdateState, @escaping (SPUUserUpdateChoice) -> Void) -> Void)? - var update: ((SPUDownloadData) -> Void)? - var updateError: ((NSError) -> Void)? - var updateDownloadState: (() -> Void)? - var updateExtractState: (() -> Void)? - var updateInstallState: (() -> Void)? - var updateReadyRelaunch: ((@escaping (SPUUserUpdateChoice) -> Void) -> Void)? - var updateDismiss: (() -> Void)? - - var expectedContentLength: Double = 0 - var receivedContentLength: Double = 0 - - var extractProgress: Double = 0 + enum UpdaterState { + case idle, error, checking, updateFound, updateNotFound, initializing, + downloading, extracting, installing, readyToRelaunch + } enum UpdateOption { case install, dismiss } - func show(_ request: SPUUpdatePermissionRequest) async -> SUUpdatePermissionResponse { + @Published var state: UpdaterState = .idle + @Published var downloadBytesTotal: Double = 0 + @Published var downloadBytesReceived: Double = 0 + @Published var extractProgress: Double = 0 + + // Errors + var errorData: NSError? + private var errorAcknowledgementCallback: (() -> Void)? + + // Checking for updates + private var checkingForUpdatesCancellationCallback: (() -> Void)? + + // Update found + var appcastItem: SUAppcastItem? + private var updateFoundActionCallback: ((SPUUserUpdateChoice) -> Void)? + + // Downloading + var downloadStartedAt: Date? + private var downloadingCancellationCallback: (() -> Void)? + + // Ready to relaunch + private var updateReadyRelaunchCallback: ((SPUUserUpdateChoice) -> Void)? + + /// Clear all callbacks + private func clearCallbacks() { + self.checkingForUpdatesCancellationCallback = .none + self.updateFoundActionCallback = .none + self.downloadingCancellationCallback = .none + self.updateReadyRelaunchCallback = .none + } + + /// Implementation of `SPUUserDriver` protocol + internal func show(_ request: SPUUpdatePermissionRequest) async -> SUUpdatePermissionResponse { return .init( automaticUpdateChecks: false, sendSystemProfile: false ) } - func showUserInitiatedUpdateCheck(cancellation: @escaping () -> Void) { - checkingForUpdates?(cancellation) + /// Implementation of `SPUUserDriver` protocol + internal func showUserInitiatedUpdateCheck(cancellation: @escaping () -> Void) { + state = .checking + self.checkingForUpdatesCancellationCallback = cancellation + } + + /// Cancel the update check + func cancelUpdateCheck() { + self.checkingForUpdatesCancellationCallback?() + clearCallbacks() + self.state = .idle } - func showUpdateFound( + /// Implementation of `SPUUserDriver` protocol + internal func showUpdateFound( with appcastItem: SUAppcastItem, state: SPUUserUpdateState, reply: @escaping (SPUUserUpdateChoice) -> Void ) { - if let updateFound = updateFound { - updateFound(appcastItem, state, reply) - } else { - reply(.dismiss) + clearCallbacks() + self.appcastItem = appcastItem + self.updateFoundActionCallback = reply + self.state = .updateFound + } + + /// Call to tell sparkle to download / dissmiss the update + func shouldUpdate(_ action: UpdateOption) { + guard let callback = self.updateFoundActionCallback else { return } + switch action { + case .install: + callback(.install) + self.state = .initializing + case .dismiss: + callback(.dismiss) + self.state = .idle } + // Reset callback + clearCallbacks() } - func showUpdateReleaseNotes(with downloadData: SPUDownloadData) { - update?(downloadData) + /// Implementation of `SPUUserDriver` protocol + internal func showUpdateReleaseNotes(with downloadData: SPUDownloadData) { + // Never needed + return } - func showUpdateReleaseNotesFailedToDownloadWithError(_ error: Error) { - updateError?(error as NSError) + /// Implementation of `SPUUserDriver` protocol + internal func showUpdateReleaseNotesFailedToDownloadWithError(_ error: Error) { + clearCallbacks() + self.errorData = error as NSError + self.state = .error } - func showUpdateNotFoundWithError(_ error: Error, acknowledgement: @escaping () -> Void) { - updateError?(error as NSError) + /// Implementation of `SPUUserDriver` protocol + internal func showUpdateNotFoundWithError(_ error: Error, acknowledgement: @escaping () -> Void) { + clearCallbacks() + self.errorData = error as NSError + self.errorAcknowledgementCallback = acknowledgement + self.state = .error + } - acknowledgement() + /// Implementation of `SPUUserDriver` protocol + internal func showUpdaterError(_ error: Error, acknowledgement: @escaping () -> Void) { + clearCallbacks() + self.errorData = error as NSError + self.errorAcknowledgementCallback = acknowledgement + self.state = .error } - func showUpdaterError(_ error: Error, acknowledgement: @escaping () -> Void) { - updateError?(error as NSError) + /// Acknowledgement of the error + func errorAcknowledgement() { + self.errorAcknowledgementCallback?() + clearCallbacks() + self.errorData = .none + self.state = .idle + } - acknowledgement() + /// Implementation of `SPUUserDriver` protocol + internal func showDownloadInitiated(cancellation: @escaping () -> Void) { + clearCallbacks() + self.downloadStartedAt = Date() + self.downloadingCancellationCallback = cancellation + self.state = .downloading } - func showDownloadInitiated(cancellation: @escaping () -> Void) { - updateDownloadState?() + /// Cancel the download + func cancelDownload() { + self.downloadingCancellationCallback?() + // Reset download + clearCallbacks() + self.downloadBytesTotal = 0 + self.downloadBytesReceived = 0 + self.downloadStartedAt = .none + self.state = .idle } - func showDownloadDidReceiveExpectedContentLength(_ expectedContentLength: UInt64) { - self.expectedContentLength = Double(expectedContentLength) - updateDownloadState?() + /// Implementation of `SPUUserDriver` protocol + internal func showDownloadDidReceiveExpectedContentLength(_ expectedContentLength: UInt64) { + self.downloadBytesTotal = Double(expectedContentLength) } - func showDownloadDidReceiveData(ofLength length: UInt64) { - receivedContentLength += Double(length) - updateDownloadState?() + /// Implementation of `SPUUserDriver` protocol + internal func showDownloadDidReceiveData(ofLength length: UInt64) { + self.downloadBytesReceived += Double(length) } - func showDownloadDidStartExtractingUpdate() { - updateExtractState?() + /// Implementation of `SPUUserDriver` protocol + internal func showDownloadDidStartExtractingUpdate() { + clearCallbacks() + // Reset download + self.downloadBytesTotal = 0 + self.downloadBytesReceived = 0 + self.downloadStartedAt = .none + self.state = .extracting } - func showExtractionReceivedProgress(_ progress: Double) { - extractProgress = progress - updateExtractState?() + /// Implementation of `SPUUserDriver` protocol + internal func showExtractionReceivedProgress(_ progress: Double) { + self.extractProgress = progress } - func showReady(toInstallAndRelaunch acknowledgement: @escaping (SPUUserUpdateChoice) -> Void) { - updateReadyRelaunch?(acknowledgement) + /// Implementation of `SPUUserDriver` protocol + internal func showReady(toInstallAndRelaunch reply: @escaping (SPUUserUpdateChoice) -> Void) { + clearCallbacks() + self.updateReadyRelaunchCallback = reply + self.state = .readyToRelaunch } - func showInstallingUpdate( + /// Call to tell sparkle to install the update + func relaunch(_ action: UpdateOption) { + guard let callback = self.updateReadyRelaunchCallback else { return } + switch action { + case .install: + callback(.install) + self.state = .installing + case .dismiss: + callback(.dismiss) + self.state = .idle + } + // Reset callback + clearCallbacks() + } + + /// Implementation of `SPUUserDriver` protocol + internal func showInstallingUpdate( withApplicationTerminated applicationTerminated: Bool, retryTerminatingApplication: @escaping () -> Void ) { - updateInstallState?() + clearCallbacks() + // Reset extract + self.extractProgress = 0 + self.state = .installing } - func showUpdateInstalledAndRelaunched(_ relaunched: Bool, acknowledgement: @escaping () -> Void) { + /// Implementation of `SPUUserDriver` protocol + internal func showUpdateInstalledAndRelaunched(_ relaunched: Bool, acknowledgement: @escaping () -> Void) { + clearCallbacks() + // Never used acknowledgement() + return } - func showUpdateInFocus() { + /// Implementation of `SPUUserDriver` protocol + internal func showUpdateInFocus() { + // Never needed return } - func dismissUpdateInstallation() { - updateDismiss?() - return + /// Implementation of `SPUUserDriver` protocol + internal func dismissUpdateInstallation() { + // If it is in the checking state, it means that there is no update available + if self.state == .checking { + clearCallbacks() + self.state = .updateNotFound + } } } diff --git a/Whisky/Views/ContentView.swift b/Whisky/Views/ContentView.swift index ffdfd1d76..d7b5cad7c 100644 --- a/Whisky/Views/ContentView.swift +++ b/Whisky/Views/ContentView.swift @@ -26,8 +26,6 @@ struct ContentView: View { @AppStorage("selectedBottleURL") private var selectedBottleURL: URL? @EnvironmentObject var bottleVM: BottleVM - let updater: SPUUpdater? - @Binding var showSetup: Bool @State private var selected: URL? @@ -42,9 +40,6 @@ struct ContentView: View { @State private var bottleFilter = "" var body: some View { - if let updater { - UpdateControlerView(updater: updater) - } NavigationSplitView { sidebar } detail: { @@ -213,6 +208,6 @@ struct ContentView: View { } #Preview { - ContentView(updater: .none, showSetup: .constant(false)) + ContentView(showSetup: .constant(false)) .environmentObject(BottleVM.shared) } diff --git a/Whisky/Views/Updater/UpdateControlerView.swift b/Whisky/Views/Updater/UpdateControlerView.swift deleted file mode 100644 index 8e6dfbc67..000000000 --- a/Whisky/Views/Updater/UpdateControlerView.swift +++ /dev/null @@ -1,316 +0,0 @@ -// -// UpdateControlerView.swift -// Whisky -// -// This file is part of Whisky. -// -// Whisky is free software: you can redistribute it and/or modify it under the terms -// of the GNU General Public License as published by the Free Software Foundation, -// either version 3 of the License, or (at your option) any later version. -// -// Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; -// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. -// See the GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License along with Whisky. -// If not, see https://www.gnu.org/licenses/. -// - -import SwiftUI -import Sparkle - -enum UpdateState { - case initializating, downloading, extracting, installing -} - -struct UpdateControlerView: View { - let updater: SPUUpdater - - @State var showCheckingForUpdates = false - @State var cancelCheckingForUpdates: (() -> Void)? - @State var showUpdatePreview = false - @State var previewNextVersion: String = "" - @State var previewMarkdown: String = "" - @State var updateUltimatum: ((Bool) -> Void)? - @State var showUpdater = false - @State var updateState: UpdateState = .initializating - @State var updateStateDownloadStatedAt: Date = Date.init(timeIntervalSince1970: 0) - @State var updateStateDownloadableBytes: Double = 0 - @State var updateStateDownloadedBytes: Double = 0 - @State var updateStateExtractProgress: Double = 0 - - var body: some View { - VStack {} - .sheet(isPresented: $showCheckingForUpdates, content: { - UpdateCheckingView(cancel: { - showCheckingForUpdates = false - cancelCheckingForUpdates?() - }) - .frame(width: 300) - .interactiveDismissDisabled() - }) - .sheet(isPresented: $showUpdatePreview, content: { - UpdatePreviewView(dismiss: { - showUpdatePreview = false - updateUltimatum?(false) - updateUltimatum = .none - }, install: { - updateUltimatum?(true) - updateUltimatum = .none - }, markdownText: $previewMarkdown, nextVersion: $previewNextVersion) - .interactiveDismissDisabled() - .frame(width: 600, height: 400) - }) - .sheet(isPresented: $showUpdater, content: { - UpdateInstallingView( - state: $updateState, - downloadStatedAt: $updateStateDownloadStatedAt, - downloadableBytes: $updateStateDownloadableBytes, - downloadedBytes: $updateStateDownloadedBytes, - extractProgress: $updateStateExtractProgress - ) - .interactiveDismissDisabled() - .frame(width: 300) - }) - .onAppear { - // On fn called show sheet - SparkleUpdaterEvents.shared.checkingForUpdates = { cancel in - // Prevent updater from checking for updates - showCheckingForUpdates = false - showUpdatePreview = false - showUpdater = false - cancelCheckingForUpdates = { - cancel() - } - showCheckingForUpdates = true - } - SparkleUpdaterEvents.shared.updateFound = { appcastItem, _, reply in - showCheckingForUpdates = false - showUpdater = false - showUpdatePreview = false - updateUltimatum = { option in - if option { - reply(.install) - } else { - reply(.dismiss) - } - } - previewNextVersion = appcastItem.displayVersionString - previewMarkdown = appcastItem.itemDescription ?? String(localized: "update.noChangeLog") - showUpdatePreview = true - } - SparkleUpdaterEvents.shared.updateDismiss = { - // Show Alert - if showCheckingForUpdates { - showCheckingForUpdates = false - showUpdatePreview = false - showUpdater = false - // no update found - displayPrompt( - title: String(localized: "update.noUpdateFound"), - description: String(localized: "update.noUpdateFound.description"), - action: String(localized: "button.ok"), - actionHandler: { - // Dismiss - } - ) - } - } - SparkleUpdaterEvents.shared.updateError = { error in - showCheckingForUpdates = false - showUpdatePreview = false - showUpdater = false - displayPrompt( - title: String(localized: "update.error"), - description: error.localizedDescription, - action: String(localized: "button.ok"), - actionHandler: { - // Dismiss - } - ) - } - // Update download state - SparkleUpdaterEvents.shared.updateDownloadState = { - if updateStateDownloadStatedAt == Date(timeIntervalSince1970: 0) { - updateStateDownloadStatedAt = Date() - } - showCheckingForUpdates = false - showUpdatePreview = false - showUpdater = true - withAnimation { updateState = .downloading } - updateStateDownloadableBytes = SparkleUpdaterEvents.shared.expectedContentLength - updateStateDownloadedBytes = SparkleUpdaterEvents.shared.receivedContentLength - } - // Update extract state - SparkleUpdaterEvents.shared.updateExtractState = { - showCheckingForUpdates = false - showUpdatePreview = false - showUpdater = true - withAnimation { updateState = .extracting } - updateStateExtractProgress = SparkleUpdaterEvents.shared.extractProgress - } - // Update install state - SparkleUpdaterEvents.shared.updateInstallState = { - showCheckingForUpdates = false - showUpdatePreview = false - showUpdater = true - withAnimation { updateState = .installing } - } - // Update ready relaunch - SparkleUpdaterEvents.shared.updateReadyRelaunch = { relaunch in - showCheckingForUpdates = false - showUpdatePreview = false - showUpdater = false - WhiskyApp.killBottles() - // Actualy relaunch (I don't want to make a helper for this so.... you get...) - let task = Process() - task.launchPath = "/bin/sh" - task.arguments = [ - "-c", - """ - kill "\(ProcessInfo.processInfo.processIdentifier)"; - sleep 0.5; open "\(Bundle.main.bundlePath)" - """ - ] - task.launch() - // Relaunch - relaunch(.install) - exit(0) - } - } - } - - func displayPrompt(title: String, description: String, action: String, actionHandler: @escaping () -> Void) { - showCheckingForUpdates = false - showUpdatePreview = false - showUpdater = false - Task(priority: .userInitiated) { - await MainActor.run { - let alert = NSAlert() - alert.messageText = title - alert.informativeText = description - alert.addButton(withTitle: action) - alert.runModal() - } - } - } -} - -struct UpdateCheckingView: View { - let cancel: () -> Void - - var body: some View { - VStack(alignment: .leading, spacing: 12) { - Text("update.checkingForUpdates") - .fontWeight(.bold) - ProgressView() - .progressViewStyle(.linear) - HStack { - Spacer() - Button("button.cancel") { - cancel() - } - .buttonStyle(.borderedProminent) - .keyboardShortcut(.defaultAction) - } - } - .padding(20) - .frame(alignment: .leading) - } -} - -struct UpdateInstallingView: View { - @Binding var state: UpdateState - @Binding var downloadStatedAt: Date - @Binding var downloadableBytes: Double - @Binding var downloadedBytes: Double - @Binding var extractProgress: Double - - @State var fractionProgress: Double = 0 - @State var downloadSpeed: Double = 0 - - var body: some View { - VStack(alignment: .leading, spacing: 12) { - Text( - state == .downloading - ? "update.downloading" - : state == .extracting - ? "update.extracting" - : state == .installing - ? "update.installing" - : "update.initializating" - ) - .fontWeight(.bold) - if state == .installing || state == .initializating { - ProgressView() - .progressViewStyle(.linear) - } else { - ProgressView(value: fractionProgress, total: 1) - .progressViewStyle(.linear) - if state == .downloading { - HStack { - HStack { - Text(String(format: String(localized: "setup.gptk.progress"), - formatBytes(bytes: downloadedBytes), - formatBytes(bytes: downloadableBytes))) - + Text(String(" ")) - + (shouldShowEstimate() ? - Text(String(format: String(localized: "setup.gptk.eta"), - formatRemainingTime(remainingBytes: downloadableBytes - downloadedBytes))) - : Text(String())) - Spacer() - } - .font(.subheadline) - .monospacedDigit() - } - } - } - } - .padding(20) - .frame(alignment: .leading) - .onChange(of: downloadedBytes) { - let currentTime = Date() - let elapsedTime = currentTime.timeIntervalSince(downloadStatedAt) - if downloadedBytes > 0 { - downloadSpeed = Double(downloadedBytes) / elapsedTime - } - withAnimation { - if downloadableBytes > 0 { - fractionProgress = downloadedBytes / downloadableBytes - } else { - fractionProgress = 0 - } - } - } - .onChange(of: extractProgress) { - withAnimation { - fractionProgress = extractProgress - } - } - } - - func formatBytes(bytes: Double) -> String { - let formatter = ByteCountFormatter() - formatter.countStyle = .file - formatter.zeroPadsFractionDigits = true - return formatter.string(fromByteCount: Int64(bytes)) - } - - func shouldShowEstimate() -> Bool { - let elapsedTime = Date().timeIntervalSince(downloadStatedAt ?? Date()) - return Int(elapsedTime.rounded()) > 5 && downloadedBytes != 0 - } - - func formatRemainingTime(remainingBytes: Double) -> String { - let remainingTimeInSeconds = remainingBytes / downloadSpeed - - let formatter = DateComponentsFormatter() - formatter.allowedUnits = [.hour, .minute, .second] - formatter.unitsStyle = .full - if shouldShowEstimate() { - return formatter.string(from: TimeInterval(remainingTimeInSeconds)) ?? "" - } else { - return "" - } - } -} diff --git a/Whisky/Views/Updater/UpdateControllerViewModifier.swift b/Whisky/Views/Updater/UpdateControllerViewModifier.swift new file mode 100644 index 000000000..d210dc3ad --- /dev/null +++ b/Whisky/Views/Updater/UpdateControllerViewModifier.swift @@ -0,0 +1,167 @@ +// +// UpdateControllerViewModifier.swift +// Whisky +// +// This file is part of Whisky. +// +// Whisky is free software: you can redistribute it and/or modify it under the terms +// of the GNU General Public License as published by the Free Software Foundation, +// either version 3 of the License, or (at your option) any later version. +// +// Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with Whisky. +// If not, see https://www.gnu.org/licenses/. +// + +import SwiftUI +import Sparkle + +enum UpdateState { + case initializating, downloading, extracting, installing +} + +extension View { + func updateController() -> some View { + return modifier(UpdateControllerViewModifier()) + } +} + +struct UpdateControllerViewModifier: ViewModifier { + @State private var sheetCheckingUpdateViewPresented = false + @State private var sheetUpdateNotFoundViewPresented = false + @State private var sheetChangeLogViewPresented = false + @State private var sheetUpdateInstallingViewPresented = false + @State private var sheetUpdateReadyRelaunchViewPresented = false + @State private var sheetUpdateErrorViewPresented = false + @ObservedObject private var updater = SparkleUpdaterEvents.shared + + func body(content: Content) -> some View { // swiftlint:disable:this function_body_length + content + .sheet(isPresented: $sheetCheckingUpdateViewPresented, content: { + VStack(alignment: .leading, spacing: 12) { + Text("update.checkingForUpdates") + .fontWeight(.bold) + Text("update.checkingForUpdates.description") + ProgressView() + .progressViewStyle(.linear) + HStack { + Spacer() + Button("button.cancel") { + updater.cancelUpdateCheck() + } + .buttonStyle(.borderedProminent) + .keyboardShortcut(.defaultAction) + } + } + .padding(20) + .frame(width: 500, alignment: .leading) + .interactiveDismissDisabled() + }) + .sheet(isPresented: $sheetUpdateNotFoundViewPresented, content: { + VStack(alignment: .leading, spacing: 12) { + Text("update.noUpdatesFound") + .fontWeight(.bold) + Text("update.noUpdatesFound.description") + HStack { + Spacer() + Button("button.ok") { + sheetUpdateNotFoundViewPresented = false + } + .buttonStyle(.borderedProminent) + .keyboardShortcut(.defaultAction) + } + } + .padding(20) + .frame(width: 500, alignment: .leading) + }) + .sheet(isPresented: $sheetChangeLogViewPresented, content: { + UpdatePreviewView( + dismiss: { + updater.shouldUpdate(.dismiss) + }, + install: { + updater.shouldUpdate(.install) + }, + markdownText: updater.appcastItem?.itemDescription, + nextVersion: updater.appcastItem?.displayVersionString ?? "v1.0.0" + ) + .interactiveDismissDisabled() + .frame(width: 600, height: 400) + }) + .sheet(isPresented: $sheetUpdateInstallingViewPresented, content: { + UpdateInstallingView( + downloadStatedAt: updater.downloadStartedAt, + cancelDownload: { + updater.cancelDownload() + }, + state: $updater.state, + downloadableBytes: $updater.downloadBytesTotal, + downloadedBytes: $updater.downloadBytesReceived, + extractProgress: $updater.extractProgress + ) + .interactiveDismissDisabled() + .frame(width: 500) + }) + .sheet(isPresented: $sheetUpdateReadyRelaunchViewPresented, content: { + VStack(alignment: .leading, spacing: 12) { + Text("update.readyToRelaunch") + .fontWeight(.bold) + Text("update.readyToRelaunch.description") + HStack { + Spacer() + Button("update.relaunch") { + updater.relaunch(.install) + } + .buttonStyle(.borderedProminent) + .keyboardShortcut(.defaultAction) + } + } + .padding(20) + .frame(width: 500, alignment: .leading) + }) + .sheet(isPresented: $sheetUpdateErrorViewPresented, content: { + VStack(alignment: .leading, spacing: 12) { + Text("update.updaterError") + .fontWeight(.bold) + Text(updater.errorData?.localizedDescription ?? "") + HStack { + Spacer() + Button("button.ok") { + updater.errorAcknowledgement() + } + .buttonStyle(.borderedProminent) + .keyboardShortcut(.defaultAction) + } + } + .padding(20) + .frame(width: 500, alignment: .leading) + }) + .onChange(of: updater.state, { _, newValue in + sheetCheckingUpdateViewPresented = false + sheetChangeLogViewPresented = false + sheetUpdateInstallingViewPresented = false + sheetUpdateReadyRelaunchViewPresented = false + sheetUpdateErrorViewPresented = false + sheetUpdateNotFoundViewPresented = false + switch newValue { + case .checking: + sheetCheckingUpdateViewPresented = true + case .updateFound: + sheetChangeLogViewPresented = true + case .initializing, .downloading, .extracting, .installing: + sheetUpdateInstallingViewPresented = true + case .readyToRelaunch: + sheetUpdateReadyRelaunchViewPresented = true + case .error: + sheetUpdateErrorViewPresented = true + case .updateNotFound: + sheetUpdateNotFoundViewPresented = true + case .idle: + break + } + }) + } +} diff --git a/Whisky/Views/Updater/UpdateInstallingView.swift b/Whisky/Views/Updater/UpdateInstallingView.swift new file mode 100644 index 000000000..69b704a3e --- /dev/null +++ b/Whisky/Views/Updater/UpdateInstallingView.swift @@ -0,0 +1,201 @@ +// +// UpdateInstallingView.swift +// Whisky +// +// This file is part of Whisky. +// +// Whisky is free software: you can redistribute it and/or modify it under the terms +// of the GNU General Public License as published by the Free Software Foundation, +// either version 3 of the License, or (at your option) any later version. +// +// Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with Whisky. +// If not, see https://www.gnu.org/licenses/. +// + +import SwiftUI + +struct UpdateInstallingView: View { + let downloadStatedAt: Date? + let cancelDownload: () -> Void + + @Binding var state: SparkleUpdaterEvents.UpdaterState + @Binding var downloadableBytes: Double + @Binding var downloadedBytes: Double + @Binding var extractProgress: Double + + @State private var fractionProgress: Double = 0 + @State private var downloadSpeed: Double = 0 + @State private var shouldShowEstimate: Bool = false + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + if state == .downloading { + Text("update.downloading") + .fontWeight(.bold) + } else if state == .extracting { + Text("update.extracting") + .fontWeight(.bold) + } else if state == .installing { + Text("update.installing") + .fontWeight(.bold) + } else if state == .initializing { + Text("update.initializating") + .fontWeight(.bold) + } + if state == .installing || state == .initializing { + ProgressView() + .progressViewStyle(.linear) + } else if state == .downloading || state == .extracting { + VStack(spacing: 2) { + ProgressView(value: fractionProgress, total: 1) + .progressViewStyle(.linear) + if state == .downloading { + HStack { + Text(String(format: String(localized: "setup.gptk.progress"), + formatBytes(bytes: downloadedBytes), + formatBytes(bytes: downloadableBytes))) + + Text(String(" ")) + + (shouldShowEstimate ? + Text(String(format: String(localized: "setup.gptk.eta"), + formatRemainingTime(remainingBytes: downloadableBytes - downloadedBytes))) + : Text(String())) + Spacer() + } + .font(.subheadline) + .monospacedDigit() + } + } + if state == .downloading { + HStack { + Spacer() + Button("button.cancel") { + cancelDownload() + } + .buttonStyle(.borderedProminent) + .keyboardShortcut(.defaultAction) + + } + .font(.subheadline) + .monospacedDigit() + } + } + } + .padding(20) + .frame(alignment: .leading) + .onChange(of: downloadedBytes) { + if state != .downloading { + return + } + if let downloadStatedAt = downloadStatedAt { + checkShouldShowEstimate() + let currentTime = Date() + let elapsedTime = currentTime.timeIntervalSince(downloadStatedAt) + if downloadedBytes > 0 { + downloadSpeed = Double(downloadedBytes) / elapsedTime + } else { + downloadSpeed = 0 + } + withAnimation { + if downloadableBytes > 0 { + fractionProgress = downloadedBytes / downloadableBytes + } else { + fractionProgress = 0 + } + } + } else { + downloadSpeed = 0 + fractionProgress = 0 + } + + } + .onChange(of: extractProgress) { + if state != .extracting { + return + } + withAnimation { + fractionProgress = extractProgress / 100 + } + } + .onChange(of: state) { + if state == .installing { + update() + } + } + .onAppear { + if state == .installing { + update() + } + } + } + + func formatBytes(bytes: Double) -> String { + let formatter = ByteCountFormatter() + formatter.countStyle = .file + formatter.zeroPadsFractionDigits = true + return formatter.string(fromByteCount: Int64(bytes)) + } + + func update() { + Task(priority: .low) { + // Stuff + try await Task.sleep(nanoseconds: UInt64(2 * Double(NSEC_PER_SEC))) + // Relaunch using sketchy ways + await WhiskyApp.killBottles() + // Actualy relaunch (I don't want to make a helper for this so.... you get...) + let task = Process() + task.launchPath = "/bin/sh" + task.arguments = [ + "-c", + """ + kill "\(ProcessInfo.processInfo.processIdentifier)"; + sleep 0.5; open "\(Bundle.main.bundlePath)" + """ + ] + task.launch() + // Relaunch + exit(0) + } + } + + func checkShouldShowEstimate() { + if let downloadStatedAt = downloadStatedAt { + let elapsedTime = Date().timeIntervalSince(downloadStatedAt) + withAnimation { + shouldShowEstimate = Int(elapsedTime.rounded()) > 5 && downloadedBytes != 0 + } + return + } + + withAnimation { + shouldShowEstimate = false + } + } + + func formatRemainingTime(remainingBytes: Double) -> String { + if downloadSpeed == 0 { + return "" + } + let remainingTimeInSeconds = remainingBytes / downloadSpeed + + let formatter = DateComponentsFormatter() + formatter.allowedUnits = [.hour, .minute, .second] + formatter.unitsStyle = .full + return formatter.string(from: TimeInterval(remainingTimeInSeconds)) ?? "" + } +} + +#Preview { + UpdateInstallingView( + downloadStatedAt: .none, + cancelDownload: {}, + state: .constant(.downloading), + downloadableBytes: .constant(1000000), + downloadedBytes: .constant(1000), + extractProgress: .constant(0)) + .frame(width: 500) + +} diff --git a/Whisky/Views/Updater/UpdatePreviewView.swift b/Whisky/Views/Updater/UpdatePreviewView.swift index fbad75897..6f6346c48 100644 --- a/Whisky/Views/Updater/UpdatePreviewView.swift +++ b/Whisky/Views/Updater/UpdatePreviewView.swift @@ -23,10 +23,10 @@ import MarkdownUI struct UpdatePreviewView: View { let dismiss: () -> Void let install: () -> Void - @Binding var markdownText: String - @Binding var nextVersion: String + let markdownText: String? + let nextVersion: String - let currentVersion = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "(nil)" + private let currentVersion = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "0.0.0" var body: some View { HStack { @@ -36,7 +36,7 @@ struct UpdatePreviewView: View { .fontWeight(.bold) Text(String(format: String(localized: "update.description"), "v" + currentVersion, - nextVersion)) + "v" + nextVersion)) Spacer() HStack { Button("update.cancel") { @@ -54,23 +54,22 @@ struct UpdatePreviewView: View { } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) .padding(20) - VStack { - ScrollView { - VStack(alignment: .leading) { - Text("update.changeLog") - .font(.title2) - .fontWeight(.bold) - .padding(.bottom, 12) - } - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) - Markdown { - markdownText - } - .markdownTheme(.basic) + ScrollView { + VStack(alignment: .leading) { + Text("update.changeLog") + .font(.title2) + .fontWeight(.bold) + .padding(.bottom, 12) } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) + if let markdownText = markdownText { + Markdown(markdownText) + .padding(.bottom, 20) + } else { + Text("update.noChangeLog") + } } - .frame(maxWidth: .infinity, maxHeight: .infinity) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) .padding(20) .background(.ultraThickMaterial) } @@ -79,6 +78,6 @@ struct UpdatePreviewView: View { } #Preview { - UpdatePreviewView(dismiss: {}, install: {}, markdownText: .constant("# Hello"), nextVersion: .constant("v1.0.0")) - .frame(width: 600, height: 400) + UpdatePreviewView(dismiss: {}, install: {}, markdownText: "# Hello", nextVersion: "v1.0.0") + .frame(width: 700, height: 400) } diff --git a/Whisky/Views/WhiskyApp.swift b/Whisky/Views/WhiskyApp.swift index ed2b3ce87..8c98138ae 100644 --- a/Whisky/Views/WhiskyApp.swift +++ b/Whisky/Views/WhiskyApp.swift @@ -42,7 +42,8 @@ struct WhiskyApp: App { var body: some Scene { WindowGroup { - ContentView(updater: updaterController, showSetup: $showSetup) + ContentView(showSetup: $showSetup) + .updateController() .frame(minWidth: 550, minHeight: 250) .environmentObject(BottleVM.shared) .onAppear { From 7c8ce9f37bfb272b28968ae3c9fa9ef195a315c8 Mon Sep 17 00:00:00 2001 From: Josh <36625023+JoshuaBrest@users.noreply.github.com> Date: Mon, 8 Jan 2024 21:22:16 -0800 Subject: [PATCH 14/22] Rollback changes for rebuild --- Whisky/Localizable.xcstrings | 509 +++++++++++++++++++---------------- 1 file changed, 274 insertions(+), 235 deletions(-) diff --git a/Whisky/Localizable.xcstrings b/Whisky/Localizable.xcstrings index 9f57f0be8..e9bbd542f 100644 --- a/Whisky/Localizable.xcstrings +++ b/Whisky/Localizable.xcstrings @@ -249,22 +249,6 @@ } } }, - "button.cancel" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cancel" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "取消" - } - } - } - }, "button.cDrive" : { "localizations" : { "da" : { @@ -1085,8 +1069,19 @@ }, "zh-Hans" : { "stringUnit" : { +<<<<<<< HEAD "state" : "translated", "value" : "置顶" +======= + "state" : "needs_review", + "value" : "Pin" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Pin" +>>>>>>> 69afaa23 (Rollback changes for rebuild) } } } @@ -2413,8 +2408,19 @@ }, "zh-Hans" : { "stringUnit" : { +<<<<<<< HEAD "state" : "translated", "value" : "取消置顶" +======= + "state" : "needs_review", + "value" : "Unpin" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Unpin" +>>>>>>> 69afaa23 (Rollback changes for rebuild) } } } @@ -4111,8 +4117,19 @@ }, "zh-Hans" : { "stringUnit" : { +<<<<<<< HEAD "state" : "translated", "value" : "增强同步" +======= + "state" : "needs_review", + "value" : "Enhanced Sync" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Enhanced Sync" +>>>>>>> 69afaa23 (Rollback changes for rebuild) } } } @@ -4211,8 +4228,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "translated", - "value" : "无" + "state" : "needs_review", + "value" : "None" } } } @@ -7043,8 +7060,19 @@ }, "zh-Hans" : { "stringUnit" : { +<<<<<<< HEAD "state" : "translated", "value" : "新建" +======= + "state" : "needs_review", + "value" : "Add" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Add" +>>>>>>> 69afaa23 (Rollback changes for rebuild) } } } @@ -7143,8 +7171,19 @@ }, "zh-Hans" : { "stringUnit" : { +<<<<<<< HEAD "state" : "translated", "value" : "移除" +======= + "state" : "needs_review", + "value" : "Remove" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Remove" +>>>>>>> 69afaa23 (Rollback changes for rebuild) } } } @@ -7455,8 +7494,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "translated", - "value" : "网站" + "state" : "needs_review", + "value" : "Website" } } } @@ -8524,8 +8563,19 @@ }, "zh-Hans" : { "stringUnit" : { +<<<<<<< HEAD "state" : "translated", "value" : "置顶" +======= + "state" : "needs_review", + "value" : "Pin" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Pin" +>>>>>>> 69afaa23 (Rollback changes for rebuild) } } } @@ -8625,8 +8675,19 @@ }, "zh-Hans" : { "stringUnit" : { +<<<<<<< HEAD "state" : "translated", "value" : "\"%@\" 已被置顶!" +======= + "state" : "needs_review", + "value" : "\"%@\" is already pinned!" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "\"%@\" is already pinned!" +>>>>>>> 69afaa23 (Rollback changes for rebuild) } } } @@ -8725,8 +8786,19 @@ }, "zh-Hans" : { "stringUnit" : { +<<<<<<< HEAD "state" : "translated", "value" : "置顶程序出错" +======= + "state" : "needs_review", + "value" : "Error Pinning Program" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Error Pinning Program" +>>>>>>> 69afaa23 (Rollback changes for rebuild) } } } @@ -8825,8 +8897,19 @@ }, "zh-Hans" : { "stringUnit" : { +<<<<<<< HEAD "state" : "translated", "value" : "置顶程序" +======= + "state" : "needs_review", + "value" : "Pin Program" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Pin Program" +>>>>>>> 69afaa23 (Rollback changes for rebuild) } } } @@ -8925,8 +9008,19 @@ }, "zh-Hans" : { "stringUnit" : { +<<<<<<< HEAD "state" : "translated", "value" : "置顶名称" +======= + "state" : "needs_review", + "value" : "Pin name:" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Pin name:" +>>>>>>> 69afaa23 (Rollback changes for rebuild) } } } @@ -9025,8 +9119,19 @@ }, "zh-Hans" : { "stringUnit" : { +<<<<<<< HEAD "state" : "translated", "value" : "程序路径" +======= + "state" : "needs_review", + "value" : "Program path:" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Program path:" +>>>>>>> 69afaa23 (Rollback changes for rebuild) } } } @@ -9125,8 +9230,19 @@ }, "zh-Hans" : { "stringUnit" : { +<<<<<<< HEAD "state" : "translated", "value" : "置顶程序" +======= + "state" : "needs_review", + "value" : "Pin Program" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Pin Program" +>>>>>>> 69afaa23 (Rollback changes for rebuild) } } } @@ -9343,8 +9459,19 @@ }, "zh-Hans" : { "stringUnit" : { +<<<<<<< HEAD "state" : "translated", "value" : "将所选内容加入黑名单" +======= + "state" : "needs_review", + "value" : "Add Selected to Blocklist" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Add Selected to Blocklist" +>>>>>>> 69afaa23 (Rollback changes for rebuild) } } } @@ -10051,8 +10178,19 @@ }, "zh-Hans" : { "stringUnit" : { +<<<<<<< HEAD "state" : "translated", "value" : "将所选内容从黑名单中移除" +======= + "state" : "needs_review", + "value" : "Remove Selected from Blocklist" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Remove Selected from Blocklist" +>>>>>>> 69afaa23 (Rollback changes for rebuild) } } } @@ -10157,8 +10295,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "translated", - "value" : "设置" + "state" : "needs_review", + "value" : "Settings" } } } @@ -11071,8 +11209,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "translated", - "value" : "通用" + "state" : "needs_review", + "value" : "General" } } } @@ -11165,8 +11303,19 @@ }, "zh-Hans" : { "stringUnit" : { +<<<<<<< HEAD "state" : "translated", "value" : "自动检查GPTK更新" +======= + "state" : "needs_review", + "value" : "Automatically check for GPTK updates" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Automatically check for GPTK updates" +>>>>>>> 69afaa23 (Rollback changes for rebuild) } } } @@ -11259,8 +11408,19 @@ }, "zh-Hans" : { "stringUnit" : { +<<<<<<< HEAD "state" : "translated", "value" : "关闭Whisky时中止Wine进程" +======= + "state" : "needs_review", + "value" : "Terminate Wine processes when Whisky closes" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Terminate Wine processes when Whisky closes" +>>>>>>> 69afaa23 (Rollback changes for rebuild) } } } @@ -11353,8 +11513,19 @@ }, "zh-Hans" : { "stringUnit" : { +<<<<<<< HEAD "state" : "translated", "value" : "自动检查Whisky更新" +======= + "state" : "needs_review", + "value" : "Automatically check for Whisky updates" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Automatically check for Whisky updates" +>>>>>>> 69afaa23 (Rollback changes for rebuild) } } } @@ -11453,8 +11624,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "translated", - "value" : "更新" + "state" : "needs_review", + "value" : "Updates" } } } @@ -13005,8 +13176,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "translated", - "value" : "重试" + "state" : "needs_review", + "value" : "Retry" } } } @@ -13223,8 +13394,19 @@ }, "zh-Hans" : { "stringUnit" : { +<<<<<<< HEAD "state" : "translated", "value" : "Rosetta 2安装失败。请手动安装。" +======= + "state" : "needs_review", + "value" : "Rosetta 2 installation failed. Please install it manually." + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Rosetta 2 installation failed. Please install it manually." +>>>>>>> 69afaa23 (Rollback changes for rebuild) } } } @@ -14649,6 +14831,7 @@ "value" : "Встановлені програми" } }, +<<<<<<< HEAD "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -14663,11 +14846,18 @@ "stringUnit" : { "state" : "translated", "value" : "Change Log" +======= + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Installed Programs" +>>>>>>> 69afaa23 (Rollback changes for rebuild) } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", +<<<<<<< HEAD "value" : "更新日志" } } @@ -14749,6 +14939,15 @@ "stringUnit" : { "state" : "translated", "value" : "正在解压文件..." +======= + "value" : "已安装的程序" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "已安裝的程式" +>>>>>>> 69afaa23 (Rollback changes for rebuild) } } } @@ -15083,206 +15282,6 @@ } } }, - "update.initializating" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Initializing..." - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Inicjalizacja..." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "处理中..." - } - } - } - }, - "update.installing" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Finishing Up..." - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kończenie..." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "马上就完成..." - } - } - } - }, - "update.noChangeLog" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "No change log available." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "无可用的更改日志。" - } - } - } - }, - "update.noUpdatesFound" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "No Updates Available" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "没有更新" - } - } - } - }, - "update.noUpdatesFound.description" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "No updates are available. You are on the latest version of Whisky." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "没有更新了。你在用 Whisky 最新的版本。" - } - } - } - }, - "update.readyToRelaunch" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Update Ready" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "更新已准备好" - } - } - } - }, - "update.readyToRelaunch.description" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "The update is ready to be installed. Press \"Relaunch\" to install the update and relaunch Whisky." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "更新已准备好安装。点击“重启”来安装更新并重启 Whisky。" - } - } - } - }, - "update.relaunch" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Relaunch" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "重启" - } - } - } - }, - "update.title" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Update Available!" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dostępna aktualizacja!" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "有更新了!" - } - } - } - }, - "update.update" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Update" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aktualizuj" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "下载更新" - } - } - } - }, - "update.updaterError" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Update Error!" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "更新失败!" - } - } - } - }, "wine.clearShaderCaches" : { "localizations" : { "da" : { @@ -15502,8 +15501,19 @@ }, "zh-Hans" : { "stringUnit" : { +<<<<<<< HEAD "state" : "translated", "value" : "应用程序" +======= + "state" : "needs_review", + "value" : "Apps" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Apps" +>>>>>>> 69afaa23 (Rollback changes for rebuild) } } } @@ -15597,14 +15607,23 @@ }, "zh-Hans" : { "stringUnit" : { +<<<<<<< HEAD "state" : "translated", +======= + "state" : "needs_review", + "value" : "Benchmarks" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", +>>>>>>> 69afaa23 (Rollback changes for rebuild) "value" : "Benchmarks" } } } }, "winetricks.category.dlls" : { - "comment" : "It's best to write DLL", "extractionState" : "manual", "localizations" : { "da" : { @@ -15693,7 +15712,17 @@ }, "zh-Hans" : { "stringUnit" : { +<<<<<<< HEAD "state" : "translated", +======= + "state" : "needs_review", + "value" : "DLLs" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", +>>>>>>> 69afaa23 (Rollback changes for rebuild) "value" : "DLLs" } } @@ -15794,8 +15823,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "translated", - "value" : "字体" + "state" : "needs_review", + "value" : "Fonts" } } } @@ -15889,8 +15918,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "translated", - "value" : "游戏" + "state" : "needs_review", + "value" : "Games" } }, "zh-Hant" : { @@ -15996,8 +16025,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "translated", - "value" : "设置" + "state" : "needs_review", + "value" : "Settings" } } } @@ -16090,8 +16119,13 @@ }, "zh-Hans" : { "stringUnit" : { +<<<<<<< HEAD "state" : "translated", "value" : "说明" +======= + "state" : "needs_review", + "value" : "Description" +>>>>>>> 69afaa23 (Rollback changes for rebuild) } }, "zh-Hant" : { @@ -16196,8 +16230,13 @@ }, "zh-Hans" : { "stringUnit" : { +<<<<<<< HEAD "state" : "translated", "value" : "指令" +======= + "state" : "needs_review", + "value" : "Command" +>>>>>>> 69afaa23 (Rollback changes for rebuild) } }, "zh-Hant" : { From 606fe9cee146bd756167672ba9c60b160e4c769c Mon Sep 17 00:00:00 2001 From: Josh <36625023+JoshuaBrest@users.noreply.github.com> Date: Mon, 8 Jan 2024 22:01:08 -0800 Subject: [PATCH 15/22] Remove merge conflict markers in project.pbxproj file --- Whisky.xcodeproj/project.pbxproj | 3 --- 1 file changed, 3 deletions(-) diff --git a/Whisky.xcodeproj/project.pbxproj b/Whisky.xcodeproj/project.pbxproj index dacee1f20..3e0c064be 100644 --- a/Whisky.xcodeproj/project.pbxproj +++ b/Whisky.xcodeproj/project.pbxproj @@ -624,10 +624,7 @@ 6E17B6492AF4118F00831173 /* EnvironmentArgView.swift in Sources */, 6E6C0CF42A419A7600356232 /* RosettaView.swift in Sources */, 6E6C0CF82A419A8C00356232 /* GPTKInstallView.swift in Sources */, -<<<<<<< HEAD 6365C4C12B1AA69D00AAE1FD /* Animation+Extensions.swift in Sources */, -======= ->>>>>>> 69afaa23b655882e0b90776fe04c770d0799f813 EB051A132B17263300F5F5B7 /* UpdateControllerViewModifier.swift in Sources */, 6E40498329CCA91B006E3F1B /* Bottle+Extensions.swift in Sources */, 6E621CEF2A5F631300C9AAB3 /* Winetricks.swift in Sources */, From c22f9948e41abf395b4c63049359607edd16321a Mon Sep 17 00:00:00 2001 From: Josh <36625023+JoshuaBrest@users.noreply.github.com> Date: Mon, 8 Jan 2024 22:01:46 -0800 Subject: [PATCH 16/22] Remove duplicate ConfigView.swift and resolve merge conflict --- Whisky.xcodeproj/project.pbxproj | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Whisky.xcodeproj/project.pbxproj b/Whisky.xcodeproj/project.pbxproj index 3e0c064be..7c719b715 100644 --- a/Whisky.xcodeproj/project.pbxproj +++ b/Whisky.xcodeproj/project.pbxproj @@ -245,13 +245,9 @@ children = ( 6E40498029CCA8B0006E3F1B /* BottleView.swift */, 6E50D98429CDF25B008C39F6 /* BottleCreationView.swift */, -<<<<<<< HEAD 6E355E5729D78249002D83BE /* ConfigView.swift */, -======= ->>>>>>> 69afaa23b655882e0b90776fe04c770d0799f813 6EA2D47D29DDAE5E00C84668 /* BottleRenameView.swift */, 6E182FC92B0BF64E00AADE81 /* WinetricksView.swift */, - 6E355E5729D78249002D83BE /* ConfigView.swift */, 6E17B6422AF3FD6E00831173 /* Pins */, 6365C4C22B1AA8CD00AAE1FD /* BottleListEntry.swift */, ); From 9a229a180fe6d7c16aad4b0a0bd2e999bbd21029 Mon Sep 17 00:00:00 2001 From: Josh <36625023+JoshuaBrest@users.noreply.github.com> Date: Mon, 8 Jan 2024 22:03:31 -0800 Subject: [PATCH 17/22] Accept all incomming for localizable. Idk why the markers are still there... --- Whisky/Localizable.xcstrings | 214 ----------------------------------- 1 file changed, 214 deletions(-) diff --git a/Whisky/Localizable.xcstrings b/Whisky/Localizable.xcstrings index e9bbd542f..6d6918c28 100644 --- a/Whisky/Localizable.xcstrings +++ b/Whisky/Localizable.xcstrings @@ -1069,10 +1069,6 @@ }, "zh-Hans" : { "stringUnit" : { -<<<<<<< HEAD - "state" : "translated", - "value" : "置顶" -======= "state" : "needs_review", "value" : "Pin" } @@ -1081,7 +1077,6 @@ "stringUnit" : { "state" : "needs_review", "value" : "Pin" ->>>>>>> 69afaa23 (Rollback changes for rebuild) } } } @@ -2408,10 +2403,6 @@ }, "zh-Hans" : { "stringUnit" : { -<<<<<<< HEAD - "state" : "translated", - "value" : "取消置顶" -======= "state" : "needs_review", "value" : "Unpin" } @@ -2420,7 +2411,6 @@ "stringUnit" : { "state" : "needs_review", "value" : "Unpin" ->>>>>>> 69afaa23 (Rollback changes for rebuild) } } } @@ -4117,10 +4107,6 @@ }, "zh-Hans" : { "stringUnit" : { -<<<<<<< HEAD - "state" : "translated", - "value" : "增强同步" -======= "state" : "needs_review", "value" : "Enhanced Sync" } @@ -4129,7 +4115,6 @@ "stringUnit" : { "state" : "needs_review", "value" : "Enhanced Sync" ->>>>>>> 69afaa23 (Rollback changes for rebuild) } } } @@ -7060,10 +7045,6 @@ }, "zh-Hans" : { "stringUnit" : { -<<<<<<< HEAD - "state" : "translated", - "value" : "新建" -======= "state" : "needs_review", "value" : "Add" } @@ -7072,7 +7053,6 @@ "stringUnit" : { "state" : "needs_review", "value" : "Add" ->>>>>>> 69afaa23 (Rollback changes for rebuild) } } } @@ -7171,10 +7151,6 @@ }, "zh-Hans" : { "stringUnit" : { -<<<<<<< HEAD - "state" : "translated", - "value" : "移除" -======= "state" : "needs_review", "value" : "Remove" } @@ -7183,7 +7159,6 @@ "stringUnit" : { "state" : "needs_review", "value" : "Remove" ->>>>>>> 69afaa23 (Rollback changes for rebuild) } } } @@ -8563,10 +8538,6 @@ }, "zh-Hans" : { "stringUnit" : { -<<<<<<< HEAD - "state" : "translated", - "value" : "置顶" -======= "state" : "needs_review", "value" : "Pin" } @@ -8575,7 +8546,6 @@ "stringUnit" : { "state" : "needs_review", "value" : "Pin" ->>>>>>> 69afaa23 (Rollback changes for rebuild) } } } @@ -8675,10 +8645,6 @@ }, "zh-Hans" : { "stringUnit" : { -<<<<<<< HEAD - "state" : "translated", - "value" : "\"%@\" 已被置顶!" -======= "state" : "needs_review", "value" : "\"%@\" is already pinned!" } @@ -8687,7 +8653,6 @@ "stringUnit" : { "state" : "needs_review", "value" : "\"%@\" is already pinned!" ->>>>>>> 69afaa23 (Rollback changes for rebuild) } } } @@ -8786,10 +8751,6 @@ }, "zh-Hans" : { "stringUnit" : { -<<<<<<< HEAD - "state" : "translated", - "value" : "置顶程序出错" -======= "state" : "needs_review", "value" : "Error Pinning Program" } @@ -8798,7 +8759,6 @@ "stringUnit" : { "state" : "needs_review", "value" : "Error Pinning Program" ->>>>>>> 69afaa23 (Rollback changes for rebuild) } } } @@ -8897,10 +8857,6 @@ }, "zh-Hans" : { "stringUnit" : { -<<<<<<< HEAD - "state" : "translated", - "value" : "置顶程序" -======= "state" : "needs_review", "value" : "Pin Program" } @@ -8909,7 +8865,6 @@ "stringUnit" : { "state" : "needs_review", "value" : "Pin Program" ->>>>>>> 69afaa23 (Rollback changes for rebuild) } } } @@ -9008,10 +8963,6 @@ }, "zh-Hans" : { "stringUnit" : { -<<<<<<< HEAD - "state" : "translated", - "value" : "置顶名称" -======= "state" : "needs_review", "value" : "Pin name:" } @@ -9020,7 +8971,6 @@ "stringUnit" : { "state" : "needs_review", "value" : "Pin name:" ->>>>>>> 69afaa23 (Rollback changes for rebuild) } } } @@ -9119,10 +9069,6 @@ }, "zh-Hans" : { "stringUnit" : { -<<<<<<< HEAD - "state" : "translated", - "value" : "程序路径" -======= "state" : "needs_review", "value" : "Program path:" } @@ -9131,7 +9077,6 @@ "stringUnit" : { "state" : "needs_review", "value" : "Program path:" ->>>>>>> 69afaa23 (Rollback changes for rebuild) } } } @@ -9230,10 +9175,6 @@ }, "zh-Hans" : { "stringUnit" : { -<<<<<<< HEAD - "state" : "translated", - "value" : "置顶程序" -======= "state" : "needs_review", "value" : "Pin Program" } @@ -9242,7 +9183,6 @@ "stringUnit" : { "state" : "needs_review", "value" : "Pin Program" ->>>>>>> 69afaa23 (Rollback changes for rebuild) } } } @@ -9459,10 +9399,6 @@ }, "zh-Hans" : { "stringUnit" : { -<<<<<<< HEAD - "state" : "translated", - "value" : "将所选内容加入黑名单" -======= "state" : "needs_review", "value" : "Add Selected to Blocklist" } @@ -9471,7 +9407,6 @@ "stringUnit" : { "state" : "needs_review", "value" : "Add Selected to Blocklist" ->>>>>>> 69afaa23 (Rollback changes for rebuild) } } } @@ -10178,10 +10113,6 @@ }, "zh-Hans" : { "stringUnit" : { -<<<<<<< HEAD - "state" : "translated", - "value" : "将所选内容从黑名单中移除" -======= "state" : "needs_review", "value" : "Remove Selected from Blocklist" } @@ -10190,7 +10121,6 @@ "stringUnit" : { "state" : "needs_review", "value" : "Remove Selected from Blocklist" ->>>>>>> 69afaa23 (Rollback changes for rebuild) } } } @@ -11303,10 +11233,6 @@ }, "zh-Hans" : { "stringUnit" : { -<<<<<<< HEAD - "state" : "translated", - "value" : "自动检查GPTK更新" -======= "state" : "needs_review", "value" : "Automatically check for GPTK updates" } @@ -11315,7 +11241,6 @@ "stringUnit" : { "state" : "needs_review", "value" : "Automatically check for GPTK updates" ->>>>>>> 69afaa23 (Rollback changes for rebuild) } } } @@ -11408,10 +11333,6 @@ }, "zh-Hans" : { "stringUnit" : { -<<<<<<< HEAD - "state" : "translated", - "value" : "关闭Whisky时中止Wine进程" -======= "state" : "needs_review", "value" : "Terminate Wine processes when Whisky closes" } @@ -11420,7 +11341,6 @@ "stringUnit" : { "state" : "needs_review", "value" : "Terminate Wine processes when Whisky closes" ->>>>>>> 69afaa23 (Rollback changes for rebuild) } } } @@ -11513,10 +11433,6 @@ }, "zh-Hans" : { "stringUnit" : { -<<<<<<< HEAD - "state" : "translated", - "value" : "自动检查Whisky更新" -======= "state" : "needs_review", "value" : "Automatically check for Whisky updates" } @@ -11525,7 +11441,6 @@ "stringUnit" : { "state" : "needs_review", "value" : "Automatically check for Whisky updates" ->>>>>>> 69afaa23 (Rollback changes for rebuild) } } } @@ -13394,10 +13309,6 @@ }, "zh-Hans" : { "stringUnit" : { -<<<<<<< HEAD - "state" : "translated", - "value" : "Rosetta 2安装失败。请手动安装。" -======= "state" : "needs_review", "value" : "Rosetta 2 installation failed. Please install it manually." } @@ -13406,7 +13317,6 @@ "stringUnit" : { "state" : "needs_review", "value" : "Rosetta 2 installation failed. Please install it manually." ->>>>>>> 69afaa23 (Rollback changes for rebuild) } } } @@ -14831,115 +14741,15 @@ "value" : "Встановлені програми" } }, -<<<<<<< HEAD - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "下一次问" - } - } - } - }, - "update.changeLog" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Change Log" -======= "vi" : { "stringUnit" : { "state" : "translated", "value" : "Installed Programs" ->>>>>>> 69afaa23 (Rollback changes for rebuild) } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", -<<<<<<< HEAD - "value" : "更新日志" - } - } - } - }, - "update.checkingForUpdates" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Checking for updates..." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "正在检查更新..." - } - } - } - }, - "update.checkingForUpdates.description" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Whisky is checking for updates..." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "Whisky 正在检查更新..." - } - } - } - }, - "update.description" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "You are running an outdated version of Whisky. The latest version available is %2$@, you are running %1$@. Would you like to update?" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "您正在运行一个过时版本的Whisky。最新可用的版本是 %2$@,你在用 %1$@。您想要下载更新吗?" - } - } - } - }, - "update.downloading" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Downloading Update..." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "正在下载更新..." - } - } - } - }, - "update.extracting" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Extracting Files..." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "正在解压文件..." -======= "value" : "已安装的程序" } }, @@ -14947,7 +14757,6 @@ "stringUnit" : { "state" : "translated", "value" : "已安裝的程式" ->>>>>>> 69afaa23 (Rollback changes for rebuild) } } } @@ -15501,10 +15310,6 @@ }, "zh-Hans" : { "stringUnit" : { -<<<<<<< HEAD - "state" : "translated", - "value" : "应用程序" -======= "state" : "needs_review", "value" : "Apps" } @@ -15513,7 +15318,6 @@ "stringUnit" : { "state" : "needs_review", "value" : "Apps" ->>>>>>> 69afaa23 (Rollback changes for rebuild) } } } @@ -15607,9 +15411,6 @@ }, "zh-Hans" : { "stringUnit" : { -<<<<<<< HEAD - "state" : "translated", -======= "state" : "needs_review", "value" : "Benchmarks" } @@ -15617,7 +15418,6 @@ "zh-Hant" : { "stringUnit" : { "state" : "needs_review", ->>>>>>> 69afaa23 (Rollback changes for rebuild) "value" : "Benchmarks" } } @@ -15712,9 +15512,6 @@ }, "zh-Hans" : { "stringUnit" : { -<<<<<<< HEAD - "state" : "translated", -======= "state" : "needs_review", "value" : "DLLs" } @@ -15722,7 +15519,6 @@ "zh-Hant" : { "stringUnit" : { "state" : "needs_review", ->>>>>>> 69afaa23 (Rollback changes for rebuild) "value" : "DLLs" } } @@ -16119,13 +15915,8 @@ }, "zh-Hans" : { "stringUnit" : { -<<<<<<< HEAD - "state" : "translated", - "value" : "说明" -======= "state" : "needs_review", "value" : "Description" ->>>>>>> 69afaa23 (Rollback changes for rebuild) } }, "zh-Hant" : { @@ -16230,13 +16021,8 @@ }, "zh-Hans" : { "stringUnit" : { -<<<<<<< HEAD - "state" : "translated", - "value" : "指令" -======= "state" : "needs_review", "value" : "Command" ->>>>>>> 69afaa23 (Rollback changes for rebuild) } }, "zh-Hant" : { From 9897588502415713bb2570635771ee229e189e0d Mon Sep 17 00:00:00 2001 From: Josh <36625023+JoshuaBrest@users.noreply.github.com> Date: Mon, 8 Jan 2024 23:57:59 -0800 Subject: [PATCH 18/22] Refactor + Update UI --- Whisky.xcodeproj/project.pbxproj | 49 +++-- .../xcshareddata/swiftpm/Package.resolved | 68 +++++++ Whisky/Localizable.xcstrings | 190 ++++++++++++++++++ Whisky/Utils/SparkleUpdaterEvents.swift | 16 +- Whisky/Views/Common/BundleIcon.swift | 39 ++++ .../UpdateControllerViewModifier.swift | 187 ++++++++++------- .../Views/Updater/UpdateInstallingView.swift | 85 ++++---- Whisky/Views/Updater/UpdatePreviewView.swift | 65 ++++-- 8 files changed, 540 insertions(+), 159 deletions(-) create mode 100644 Whisky.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 Whisky/Views/Common/BundleIcon.swift diff --git a/Whisky.xcodeproj/project.pbxproj b/Whisky.xcodeproj/project.pbxproj index 7c719b715..eefad852d 100644 --- a/Whisky.xcodeproj/project.pbxproj +++ b/Whisky.xcodeproj/project.pbxproj @@ -61,6 +61,7 @@ EB051A0E2B16EA7E00F5F5B7 /* MarkdownUI in Frameworks */ = {isa = PBXBuildFile; productRef = EB051A0D2B16EA7E00F5F5B7 /* MarkdownUI */; }; EB051A102B1710A700F5F5B7 /* SparkleUpdaterEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB051A0F2B1710A700F5F5B7 /* SparkleUpdaterEvents.swift */; }; EB051A132B17263300F5F5B7 /* UpdateControllerViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB051A122B17263300F5F5B7 /* UpdateControllerViewModifier.swift */; }; + EB0CD3672B4D217B006C9CA9 /* BundleIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB0CD3662B4D217B006C9CA9 /* BundleIcon.swift */; }; EB58FB552A499896002DC184 /* SemanticVersion in Frameworks */ = {isa = PBXBuildFile; productRef = EB58FB542A499896002DC184 /* SemanticVersion */; }; EBB21B662B198370000C3FA0 /* UpdateInstallingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBB21B652B198370000C3FA0 /* UpdateInstallingView.swift */; }; EEA5A2462A31DD65008274AE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEA5A2452A31DD65008274AE /* AppDelegate.swift */; }; @@ -161,6 +162,7 @@ EB051A082B150F7100F5F5B7 /* UpdatePreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdatePreviewView.swift; sourceTree = ""; }; EB051A0F2B1710A700F5F5B7 /* SparkleUpdaterEvents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SparkleUpdaterEvents.swift; sourceTree = ""; }; EB051A122B17263300F5F5B7 /* UpdateControllerViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateControllerViewModifier.swift; sourceTree = ""; }; + EB0CD3662B4D217B006C9CA9 /* BundleIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleIcon.swift; sourceTree = ""; }; EBB21B652B198370000C3FA0 /* UpdateInstallingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateInstallingView.swift; sourceTree = ""; }; EEA5A2452A31DD65008274AE /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -215,6 +217,7 @@ children = ( 63FFDE852ADF0C7700178665 /* BottomBar.swift */, 6330DD952B1B0EA4007A625A /* RenameView.swift */, + EB0CD3662B4D217B006C9CA9 /* BundleIcon.swift */, ); path = Common; sourceTree = ""; @@ -246,7 +249,6 @@ 6E40498029CCA8B0006E3F1B /* BottleView.swift */, 6E50D98429CDF25B008C39F6 /* BottleCreationView.swift */, 6E355E5729D78249002D83BE /* ConfigView.swift */, - 6EA2D47D29DDAE5E00C84668 /* BottleRenameView.swift */, 6E182FC92B0BF64E00AADE81 /* WinetricksView.swift */, 6E17B6422AF3FD6E00831173 /* Pins */, 6365C4C22B1AA8CD00AAE1FD /* BottleListEntry.swift */, @@ -609,7 +611,6 @@ 6E7C07BE2AAE7B0100F6E66B /* ProgramShortcut.swift in Sources */, 6E355E5829D78249002D83BE /* ConfigView.swift in Sources */, EB051A102B1710A700F5F5B7 /* SparkleUpdaterEvents.swift in Sources */, - 6EA2D47E29DDAE5E00C84668 /* BottleRenameView.swift in Sources */, 63FFDE862ADF0C7700178665 /* BottomBar.swift in Sources */, EB051A092B150F7100F5F5B7 /* UpdatePreviewView.swift in Sources */, 6E6C0CF62A419A8300356232 /* GPTKDownloadView.swift in Sources */, @@ -634,6 +635,7 @@ 6E7C07C02AAF570100F6E66B /* FileOpenView.swift in Sources */, 6E355E6029D7D8BD002D83BE /* Program+Extensions.swift in Sources */, 6E6C0CF22A419A6800356232 /* WelcomeView.swift in Sources */, + EB0CD3672B4D217B006C9CA9 /* BundleIcon.swift in Sources */, 6E40498129CCA8B0006E3F1B /* BottleView.swift in Sources */, 6E355E5E29D7D85D002D83BE /* ProgramView.swift in Sources */, ); @@ -793,13 +795,12 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = Whisky/Whisky.entitlements; - "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 35; + CURRENT_PROJECT_VERSION = 34; DEVELOPMENT_ASSET_PATHS = "\"Whisky/Preview Content\""; - DEVELOPMENT_TEAM = ""; - "DEVELOPMENT_TEAM[sdk=macosx*]" = 92S3SG4PTH; + DEVELOPMENT_TEAM = VAH5CYW2VU; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -814,7 +815,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.0; - MARKETING_VERSION = 2.2.2; + MARKETING_VERSION = 2.2.1; PRODUCT_BUNDLE_IDENTIFIER = com.isaacmarovitz.Whisky; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -834,7 +835,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application"; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 35; + CURRENT_PROJECT_VERSION = 34; DEVELOPMENT_ASSET_PATHS = "\"Whisky/Preview Content\""; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=macosx*]" = 92S3SG4PTH; @@ -852,7 +853,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.0; - MARKETING_VERSION = 2.2.2; + MARKETING_VERSION = 2.2.1; PRODUCT_BUNDLE_IDENTIFIER = com.isaacmarovitz.Whisky; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -867,10 +868,9 @@ buildSettings = { ARCHS = arm64; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application"; - CODE_SIGN_STYLE = Manual; - DEVELOPMENT_TEAM = ""; - "DEVELOPMENT_TEAM[sdk=macosx*]" = 92S3SG4PTH; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = VAH5CYW2VU; ENABLE_HARDENED_RUNTIME = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -892,10 +892,9 @@ buildSettings = { ARCHS = arm64; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application"; - CODE_SIGN_STYLE = Manual; - DEVELOPMENT_TEAM = ""; - "DEVELOPMENT_TEAM[sdk=macosx*]" = 92S3SG4PTH; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = VAH5CYW2VU; ENABLE_HARDENED_RUNTIME = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -916,11 +915,10 @@ buildSettings = { ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CODE_SIGN_ENTITLEMENTS = WhiskyThumbnail/WhiskyThumbnail.entitlements; - "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 35; - DEVELOPMENT_TEAM = ""; - "DEVELOPMENT_TEAM[sdk=macosx*]" = 92S3SG4PTH; + DEVELOPMENT_TEAM = VAH5CYW2VU; ENABLE_HARDENED_RUNTIME = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -950,11 +948,10 @@ buildSettings = { ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CODE_SIGN_ENTITLEMENTS = WhiskyThumbnail/WhiskyThumbnail.entitlements; - "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 35; - DEVELOPMENT_TEAM = ""; - "DEVELOPMENT_TEAM[sdk=macosx*]" = 92S3SG4PTH; + DEVELOPMENT_TEAM = VAH5CYW2VU; ENABLE_HARDENED_RUNTIME = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; diff --git a/Whisky.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Whisky.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 000000000..c07eb60cb --- /dev/null +++ b/Whisky.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,68 @@ +{ + "pins" : [ + { + "identity" : "networkimage", + "kind" : "remoteSourceControl", + "location" : "https://github.com/gonzalezreal/NetworkImage", + "state" : { + "revision" : "7aff8d1b31148d32c5933d75557d42f6323ee3d1", + "version" : "6.0.0" + } + }, + { + "identity" : "progress.swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/jkandzi/Progress.swift", + "state" : { + "revision" : "fed6598735d7982058690acf8f52a0a5fdaeb3e0", + "version" : "0.4.0" + } + }, + { + "identity" : "semanticversion", + "kind" : "remoteSourceControl", + "location" : "https://github.com/SwiftPackageIndex/SemanticVersion", + "state" : { + "revision" : "ea8eea9d89842a29af1b8e6c7677f1c86e72fa42", + "version" : "0.4.0" + } + }, + { + "identity" : "sparkle", + "kind" : "remoteSourceControl", + "location" : "https://github.com/sparkle-project/Sparkle", + "state" : { + "branch" : "2.x", + "revision" : "1a0b023e1c4d37302ae6401b8f9c38af0729e21d" + } + }, + { + "identity" : "swift-argument-parser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-argument-parser.git", + "state" : { + "revision" : "8f4d2753f0e4778c76d5f05ad16c74f707390531", + "version" : "1.2.3" + } + }, + { + "identity" : "swift-markdown-ui", + "kind" : "remoteSourceControl", + "location" : "https://github.com/gonzalezreal/swift-markdown-ui.git", + "state" : { + "revision" : "5df8a4adedd6ae4eb2455ef60ff75183984daeb8", + "version" : "2.2.0" + } + }, + { + "identity" : "swiftytexttable", + "kind" : "remoteSourceControl", + "location" : "https://github.com/scottrhoyt/SwiftyTextTable", + "state" : { + "revision" : "c6df6cf533d120716bff38f8ff9885e1ce2a4ac3", + "version" : "0.9.0" + } + } + ], + "version" : 2 +} diff --git a/Whisky/Localizable.xcstrings b/Whisky/Localizable.xcstrings index 6d6918c28..2bcb0745d 100644 --- a/Whisky/Localizable.xcstrings +++ b/Whisky/Localizable.xcstrings @@ -249,6 +249,36 @@ } } }, + "app.name" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Whisky" + } + } + } + }, + "app.version" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ (%@)" + } + } + } + }, + "button.cancel" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cancel" + } + } + } + }, "button.cDrive" : { "localizations" : { "da" : { @@ -14761,6 +14791,56 @@ } } }, + "update.checkingForUpdates" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Checking for updates..." + } + } + } + }, + "update.checkingForUpdates.description" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Whisky is checking for updates..." + } + } + } + }, + "update.description" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "You're running %@ (%@). The latest available update is %@ (%@). Would you like to update?" + } + } + } + }, + "update.downloading" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Downloading Update..." + } + } + } + }, + "update.extracting" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Extracting Files..." + } + } + } + }, "update.gptk.description" : { "localizations" : { "da" : { @@ -15091,6 +15171,116 @@ } } }, + "update.initializating" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Initializing..." + } + } + } + }, + "update.installing" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Installing update..." + } + } + } + }, + "update.newUpdate" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Update Available" + } + } + } + }, + "update.noChangeLog" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "No change log available." + } + } + } + }, + "update.noUpdatesFound" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "No Updates Available" + } + } + } + }, + "update.noUpdatesFound.description" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "No updates are available. You are on the latest version of Whisky." + } + } + } + }, + "update.readyToRelaunch" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Update Ready" + } + } + } + }, + "update.readyToRelaunch.description" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "The update is ready to be installed. Press \"Relaunch\" to install the update and relaunch Whisky." + } + } + } + }, + "update.relaunch" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Relaunch" + } + } + } + }, + "update.update" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Update" + } + } + } + }, + "update.updaterError" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Update Error!" + } + } + } + }, "wine.clearShaderCaches" : { "localizations" : { "da" : { diff --git a/Whisky/Utils/SparkleUpdaterEvents.swift b/Whisky/Utils/SparkleUpdaterEvents.swift index dec961b7d..c890b3de0 100644 --- a/Whisky/Utils/SparkleUpdaterEvents.swift +++ b/Whisky/Utils/SparkleUpdaterEvents.swift @@ -47,6 +47,9 @@ class SparkleUpdaterEvents: NSObject, SPUUserDriver, ObservableObject { var appcastItem: SUAppcastItem? private var updateFoundActionCallback: ((SPUUserUpdateChoice) -> Void)? + // Update not found + private var updateNotFoundAcknowledgementCallback: (() -> Void)? + // Downloading var downloadStartedAt: Date? private var downloadingCancellationCallback: (() -> Void)? @@ -57,6 +60,7 @@ class SparkleUpdaterEvents: NSObject, SPUUserDriver, ObservableObject { /// Clear all callbacks private func clearCallbacks() { self.checkingForUpdatesCancellationCallback = .none + self.updateNotFoundAcknowledgementCallback = .none self.updateFoundActionCallback = .none self.downloadingCancellationCallback = .none self.updateReadyRelaunchCallback = .none @@ -126,9 +130,15 @@ class SparkleUpdaterEvents: NSObject, SPUUserDriver, ObservableObject { /// Implementation of `SPUUserDriver` protocol internal func showUpdateNotFoundWithError(_ error: Error, acknowledgement: @escaping () -> Void) { clearCallbacks() - self.errorData = error as NSError - self.errorAcknowledgementCallback = acknowledgement - self.state = .error + self.updateNotFoundAcknowledgementCallback = acknowledgement + self.state = .updateNotFound + } + + /// Acknowledgement of the update not being found + func updateNotFoundAcknowledgement() { + self.updateNotFoundAcknowledgementCallback?() + clearCallbacks() + self.state = .idle } /// Implementation of `SPUUserDriver` protocol diff --git a/Whisky/Views/Common/BundleIcon.swift b/Whisky/Views/Common/BundleIcon.swift new file mode 100644 index 000000000..33ed35d3f --- /dev/null +++ b/Whisky/Views/Common/BundleIcon.swift @@ -0,0 +1,39 @@ +// +// BundleIcon.swift +// Whisky +// +// This file is part of Whisky. +// +// Whisky is free software: you can redistribute it and/or modify it under the terms +// of the GNU General Public License as published by the Free Software Foundation, +// either version 3 of the License, or (at your option) any later version. +// +// Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with Whisky. +// If not, see https://www.gnu.org/licenses/. +// + +import SwiftUI + +struct BundleIcon: View { + var body: some View { + if let image = NSImage(named: "AppIcon") { + Image(nsImage: image) + .resizable() + .scaledToFit() + .clipShape(RoundedRectangle(cornerRadius: 8)) + } else { + Image(systemName: "nosign.app") + .resizable() + .scaledToFit() + } + } +} + +#Preview { + BundleIcon() + .frame(width: 50, height: 50) +} diff --git a/Whisky/Views/Updater/UpdateControllerViewModifier.swift b/Whisky/Views/Updater/UpdateControllerViewModifier.swift index d210dc3ad..662e78a1c 100644 --- a/Whisky/Views/Updater/UpdateControllerViewModifier.swift +++ b/Whisky/Views/Updater/UpdateControllerViewModifier.swift @@ -34,48 +34,17 @@ struct UpdateControllerViewModifier: ViewModifier { @State private var sheetUpdateNotFoundViewPresented = false @State private var sheetChangeLogViewPresented = false @State private var sheetUpdateInstallingViewPresented = false - @State private var sheetUpdateReadyRelaunchViewPresented = false + @State private var sheetUpdateReadyToRelaunchViewPresented = false @State private var sheetUpdateErrorViewPresented = false @ObservedObject private var updater = SparkleUpdaterEvents.shared - func body(content: Content) -> some View { // swiftlint:disable:this function_body_length + func body(content: Content) -> some View { content .sheet(isPresented: $sheetCheckingUpdateViewPresented, content: { - VStack(alignment: .leading, spacing: 12) { - Text("update.checkingForUpdates") - .fontWeight(.bold) - Text("update.checkingForUpdates.description") - ProgressView() - .progressViewStyle(.linear) - HStack { - Spacer() - Button("button.cancel") { - updater.cancelUpdateCheck() - } - .buttonStyle(.borderedProminent) - .keyboardShortcut(.defaultAction) - } - } - .padding(20) - .frame(width: 500, alignment: .leading) - .interactiveDismissDisabled() + UpdateControllerCheckingForUpdatesView(updater: updater) }) .sheet(isPresented: $sheetUpdateNotFoundViewPresented, content: { - VStack(alignment: .leading, spacing: 12) { - Text("update.noUpdatesFound") - .fontWeight(.bold) - Text("update.noUpdatesFound.description") - HStack { - Spacer() - Button("button.ok") { - sheetUpdateNotFoundViewPresented = false - } - .buttonStyle(.borderedProminent) - .keyboardShortcut(.defaultAction) - } - } - .padding(20) - .frame(width: 500, alignment: .leading) + UpdateControllerUpdateNotFoundView(updater: updater) }) .sheet(isPresented: $sheetChangeLogViewPresented, content: { UpdatePreviewView( @@ -86,10 +55,10 @@ struct UpdateControllerViewModifier: ViewModifier { updater.shouldUpdate(.install) }, markdownText: updater.appcastItem?.itemDescription, - nextVersion: updater.appcastItem?.displayVersionString ?? "v1.0.0" + nextVersion: updater.appcastItem?.displayVersionString ?? "1.0.0", + nextVersionNumber: updater.appcastItem?.versionString ?? "0" ) .interactiveDismissDisabled() - .frame(width: 600, height: 400) }) .sheet(isPresented: $sheetUpdateInstallingViewPresented, content: { UpdateInstallingView( @@ -105,47 +74,22 @@ struct UpdateControllerViewModifier: ViewModifier { .interactiveDismissDisabled() .frame(width: 500) }) - .sheet(isPresented: $sheetUpdateReadyRelaunchViewPresented, content: { - VStack(alignment: .leading, spacing: 12) { - Text("update.readyToRelaunch") - .fontWeight(.bold) - Text("update.readyToRelaunch.description") - HStack { - Spacer() - Button("update.relaunch") { - updater.relaunch(.install) - } - .buttonStyle(.borderedProminent) - .keyboardShortcut(.defaultAction) - } - } - .padding(20) - .frame(width: 500, alignment: .leading) + .sheet(isPresented: $sheetUpdateReadyToRelaunchViewPresented, content: { + UpdateControllerReadyToRelaunchView(updater: updater) }) .sheet(isPresented: $sheetUpdateErrorViewPresented, content: { - VStack(alignment: .leading, spacing: 12) { - Text("update.updaterError") - .fontWeight(.bold) - Text(updater.errorData?.localizedDescription ?? "") - HStack { - Spacer() - Button("button.ok") { - updater.errorAcknowledgement() - } - .buttonStyle(.borderedProminent) - .keyboardShortcut(.defaultAction) - } - } - .padding(20) - .frame(width: 500, alignment: .leading) + UpdateControllerErrorView(updater: updater) }) .onChange(of: updater.state, { _, newValue in + // Dissmiss all old views sheetCheckingUpdateViewPresented = false sheetChangeLogViewPresented = false sheetUpdateInstallingViewPresented = false - sheetUpdateReadyRelaunchViewPresented = false + sheetUpdateReadyToRelaunchViewPresented = false sheetUpdateErrorViewPresented = false sheetUpdateNotFoundViewPresented = false + + // Enable new view switch newValue { case .checking: sheetCheckingUpdateViewPresented = true @@ -154,7 +98,7 @@ struct UpdateControllerViewModifier: ViewModifier { case .initializing, .downloading, .extracting, .installing: sheetUpdateInstallingViewPresented = true case .readyToRelaunch: - sheetUpdateReadyRelaunchViewPresented = true + sheetUpdateReadyToRelaunchViewPresented = true case .error: sheetUpdateErrorViewPresented = true case .updateNotFound: @@ -165,3 +109,106 @@ struct UpdateControllerViewModifier: ViewModifier { }) } } + +struct UpdateControllerCheckingForUpdatesView: View { + let updater: SparkleUpdaterEvents + + var body: some View { + HStack(alignment: .top, spacing: 20) { + BundleIcon().frame(width: 60, height: 60) + VStack(alignment: .leading, spacing: 12) { + Text("update.checkingForUpdates") + .fontWeight(.bold) + Text("update.checkingForUpdates.description") + ProgressView() + .progressViewStyle(.linear) + HStack { + Spacer() + Button("button.cancel") { + updater.cancelUpdateCheck() + } + .buttonStyle(.borderedProminent) + .keyboardShortcut(.defaultAction) + } + } + } + .padding(20) + .frame(width: 500, alignment: .leading) + .interactiveDismissDisabled() + } +} + +struct UpdateControllerUpdateNotFoundView: View { + let updater: SparkleUpdaterEvents + + var body: some View { + HStack(alignment: .top, spacing: 20) { + BundleIcon().frame(width: 60, height: 60) + VStack(alignment: .leading, spacing: 12) { + Text("update.noUpdatesFound") + .fontWeight(.bold) + Text("update.noUpdatesFound.description") + HStack { + Spacer() + Button("button.ok") { + updater.updateNotFoundAcknowledgement() + } + .buttonStyle(.borderedProminent) + .keyboardShortcut(.defaultAction) + } + } + } + .padding(20) + .frame(width: 500, alignment: .leading) + } +} + +struct UpdateControllerReadyToRelaunchView: View { + let updater: SparkleUpdaterEvents + + var body: some View { + HStack(alignment: .top, spacing: 20) { + BundleIcon().frame(width: 60, height: 60) + VStack(alignment: .leading, spacing: 12) { + Text("update.readyToRelaunch") + .fontWeight(.bold) + Text("update.readyToRelaunch.description") + HStack { + Spacer() + Button("update.relaunch") { + updater.relaunch(.install) + } + .buttonStyle(.borderedProminent) + .keyboardShortcut(.defaultAction) + } + } + } + .padding(20) + .frame(width: 500, alignment: .leading) + } +} + +struct UpdateControllerErrorView: View { + let updater: SparkleUpdaterEvents + + var body: some View { + HStack(alignment: .top, spacing: 20) { + BundleIcon().frame(width: 60, height: 60) + VStack(alignment: .leading, spacing: 12) { + Text("update.updaterError") + .fontWeight(.bold) + Text(updater.errorData?.localizedDescription ?? "") + HStack { + Spacer() + Button("button.ok") { + updater.errorAcknowledgement() + } + .buttonStyle(.borderedProminent) + .keyboardShortcut(.defaultAction) + } + } + } + .padding(20) + .frame(width: 500, alignment: .leading) + } +} diff --git a/Whisky/Views/Updater/UpdateInstallingView.swift b/Whisky/Views/Updater/UpdateInstallingView.swift index 69b704a3e..921e2d316 100644 --- a/Whisky/Views/Updater/UpdateInstallingView.swift +++ b/Whisky/Views/Updater/UpdateInstallingView.swift @@ -32,56 +32,59 @@ struct UpdateInstallingView: View { @State private var shouldShowEstimate: Bool = false var body: some View { - VStack(alignment: .leading, spacing: 12) { - if state == .downloading { - Text("update.downloading") - .fontWeight(.bold) - } else if state == .extracting { - Text("update.extracting") - .fontWeight(.bold) - } else if state == .installing { - Text("update.installing") - .fontWeight(.bold) - } else if state == .initializing { - Text("update.initializating") - .fontWeight(.bold) - } - if state == .installing || state == .initializing { - ProgressView() - .progressViewStyle(.linear) - } else if state == .downloading || state == .extracting { - VStack(spacing: 2) { - ProgressView(value: fractionProgress, total: 1) + HStack(alignment: .top, spacing: 20) { + BundleIcon().frame(width: 60, height: 60) + VStack(alignment: .leading, spacing: 12) { + if state == .downloading { + Text("update.downloading") + .fontWeight(.bold) + } else if state == .extracting { + Text("update.extracting") + .fontWeight(.bold) + } else if state == .installing { + Text("update.installing") + .fontWeight(.bold) + } else if state == .initializing { + Text("update.initializating") + .fontWeight(.bold) + } + if state == .installing || state == .initializing { + ProgressView() .progressViewStyle(.linear) + } else if state == .downloading || state == .extracting { + VStack(spacing: 2) { + ProgressView(value: fractionProgress, total: 1) + .progressViewStyle(.linear) + if state == .downloading { + HStack { + Text(String(format: String(localized: "setup.gptk.progress"), + formatBytes(bytes: downloadedBytes), + formatBytes(bytes: downloadableBytes))) + + Text(String(" ")) + + (shouldShowEstimate ? + Text(String(format: String(localized: "setup.gptk.eta"), + formatRemainingTime(remainingBytes: downloadableBytes - downloadedBytes))) + : Text(String())) + Spacer() + } + .font(.subheadline) + .monospacedDigit() + } + } if state == .downloading { HStack { - Text(String(format: String(localized: "setup.gptk.progress"), - formatBytes(bytes: downloadedBytes), - formatBytes(bytes: downloadableBytes))) - + Text(String(" ")) - + (shouldShowEstimate ? - Text(String(format: String(localized: "setup.gptk.eta"), - formatRemainingTime(remainingBytes: downloadableBytes - downloadedBytes))) - : Text(String())) Spacer() + Button("button.cancel") { + cancelDownload() + } + .buttonStyle(.borderedProminent) + .keyboardShortcut(.defaultAction) + } .font(.subheadline) .monospacedDigit() } } - if state == .downloading { - HStack { - Spacer() - Button("button.cancel") { - cancelDownload() - } - .buttonStyle(.borderedProminent) - .keyboardShortcut(.defaultAction) - - } - .font(.subheadline) - .monospacedDigit() - } } } .padding(20) diff --git a/Whisky/Views/Updater/UpdatePreviewView.swift b/Whisky/Views/Updater/UpdatePreviewView.swift index 6f6346c48..f38177c56 100644 --- a/Whisky/Views/Updater/UpdatePreviewView.swift +++ b/Whisky/Views/Updater/UpdatePreviewView.swift @@ -25,46 +25,74 @@ struct UpdatePreviewView: View { let install: () -> Void let markdownText: String? let nextVersion: String + let nextVersionNumber: String private let currentVersion = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "0.0.0" + private let currentVersionNumber = (Bundle.main.infoDictionary?["CFBundleVersion"] as? String) ?? "0" var body: some View { HStack { - VStack(alignment: .leading, spacing: 12) { - Text("update.title") - .font(.title) - .fontWeight(.bold) + VStack(alignment: .center, spacing: 12) { + BundleIcon().frame(width: 80, height: 80) + VStack(alignment: .center, spacing: 2) { + Text("app.name") + .font(.title) + .fontWeight(.bold) + Text(String(format: String(localized: "app.version"), + "v" + currentVersion, + currentVersionNumber)) + .opacity(0.8) + } Text(String(format: String(localized: "update.description"), "v" + currentVersion, - "v" + nextVersion)) + currentVersionNumber, + "v" + nextVersion, + nextVersionNumber)) + .opacity(0.8) + .multilineTextAlignment(.center) Spacer() - HStack { - Button("update.cancel") { - dismiss() - } - Spacer() - Button("update.update") { + .frame(height: 8) + VStack(spacing: 12) { + Button { Task(priority: .userInitiated) { install() } + } label: { + Text("update.update") + .padding(8) + .frame(maxWidth: .infinity) } + .frame(maxWidth: .infinity) .buttonStyle(.borderedProminent) .keyboardShortcut(.defaultAction) + Button { + dismiss() + } label: { + Text("button.cancel") + .padding(8) + .frame(maxWidth: .infinity) + } + .buttonStyle(.borderless) } } - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) - .padding(20) + .frame(maxHeight: .infinity, alignment: .center) + .frame(width: 200) + .padding(.horizontal, 40) + .padding(.vertical, 20) ScrollView { VStack(alignment: .leading) { - Text("update.changeLog") + Text("update.newUpdate") .font(.title2) .fontWeight(.bold) .padding(.bottom, 12) } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) if let markdownText = markdownText { - Markdown(markdownText) - .padding(.bottom, 20) + VStack(alignment: .leading) { + Markdown(markdownText) + .padding(.bottom, 20) + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) } else { Text("update.noChangeLog") } @@ -73,11 +101,10 @@ struct UpdatePreviewView: View { .padding(20) .background(.ultraThickMaterial) } - .frame(maxWidth: .infinity, maxHeight: .infinity) + .frame(width: 700, height: 400) } } #Preview { - UpdatePreviewView(dismiss: {}, install: {}, markdownText: "# Hello", nextVersion: "v1.0.0") - .frame(width: 700, height: 400) + UpdatePreviewView(dismiss: {}, install: {}, markdownText: "# Hello", nextVersion: "1.0.0", nextVersionNumber: "10") } From d3bc763a7749b24a46c7047ad152a4dfa67b31e9 Mon Sep 17 00:00:00 2001 From: Josh <36625023+JoshuaBrest@users.noreply.github.com> Date: Mon, 8 Jan 2024 23:59:54 -0800 Subject: [PATCH 19/22] Update project version and marketing version to original --- Whisky.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Whisky.xcodeproj/project.pbxproj b/Whisky.xcodeproj/project.pbxproj index eefad852d..16932405b 100644 --- a/Whisky.xcodeproj/project.pbxproj +++ b/Whisky.xcodeproj/project.pbxproj @@ -798,7 +798,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 34; + CURRENT_PROJECT_VERSION = 35; DEVELOPMENT_ASSET_PATHS = "\"Whisky/Preview Content\""; DEVELOPMENT_TEAM = VAH5CYW2VU; ENABLE_HARDENED_RUNTIME = YES; @@ -815,7 +815,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.0; - MARKETING_VERSION = 2.2.1; + MARKETING_VERSION = 2.2.2; PRODUCT_BUNDLE_IDENTIFIER = com.isaacmarovitz.Whisky; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -835,7 +835,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application"; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 34; + CURRENT_PROJECT_VERSION = 35; DEVELOPMENT_ASSET_PATHS = "\"Whisky/Preview Content\""; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=macosx*]" = 92S3SG4PTH; @@ -853,7 +853,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.0; - MARKETING_VERSION = 2.2.1; + MARKETING_VERSION = 2.2.2; PRODUCT_BUNDLE_IDENTIFIER = com.isaacmarovitz.Whisky; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; From f7e86ba91d97b5a785338e161c54c243e891d131 Mon Sep 17 00:00:00 2001 From: Josh <36625023+JoshuaBrest@users.noreply.github.com> Date: Tue, 9 Jan 2024 00:09:09 -0800 Subject: [PATCH 20/22] Update code signing and development team settings --- Whisky.xcodeproj/project.pbxproj | 34 ++- Whisky/Localizable.xcstrings | 450 +++++-------------------------- 2 files changed, 87 insertions(+), 397 deletions(-) diff --git a/Whisky.xcodeproj/project.pbxproj b/Whisky.xcodeproj/project.pbxproj index 16932405b..44af5f66e 100644 --- a/Whisky.xcodeproj/project.pbxproj +++ b/Whisky.xcodeproj/project.pbxproj @@ -795,12 +795,13 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = Whisky/Whisky.entitlements; - CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application"; + CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 35; DEVELOPMENT_ASSET_PATHS = "\"Whisky/Preview Content\""; - DEVELOPMENT_TEAM = VAH5CYW2VU; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=macosx*]" = 92S3SG4PTH; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -868,9 +869,10 @@ buildSettings = { ARCHS = arm64; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = VAH5CYW2VU; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application"; + CODE_SIGN_STYLE = Manual; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=macosx*]" = 92S3SG4PTH; ENABLE_HARDENED_RUNTIME = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -893,8 +895,10 @@ ARCHS = arm64; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = VAH5CYW2VU; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application"; + CODE_SIGN_STYLE = Manual; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=macosx*]" = 92S3SG4PTH; ENABLE_HARDENED_RUNTIME = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -915,10 +919,11 @@ buildSettings = { ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CODE_SIGN_ENTITLEMENTS = WhiskyThumbnail/WhiskyThumbnail.entitlements; - CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application"; + CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 35; - DEVELOPMENT_TEAM = VAH5CYW2VU; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=macosx*]" = 92S3SG4PTH; ENABLE_HARDENED_RUNTIME = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -948,10 +953,11 @@ buildSettings = { ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CODE_SIGN_ENTITLEMENTS = WhiskyThumbnail/WhiskyThumbnail.entitlements; - CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application"; + CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 35; - DEVELOPMENT_TEAM = VAH5CYW2VU; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=macosx*]" = 92S3SG4PTH; ENABLE_HARDENED_RUNTIME = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; diff --git a/Whisky/Localizable.xcstrings b/Whisky/Localizable.xcstrings index 2bcb0745d..a0b9d56f8 100644 --- a/Whisky/Localizable.xcstrings +++ b/Whisky/Localizable.xcstrings @@ -249,36 +249,6 @@ } } }, - "app.name" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Whisky" - } - } - } - }, - "app.version" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "%@ (%@)" - } - } - } - }, - "button.cancel" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cancel" - } - } - } - }, "button.cDrive" : { "localizations" : { "da" : { @@ -1099,14 +1069,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Pin" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Pin" + "state" : "translated", + "value" : "置顶" } } } @@ -2433,14 +2397,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Unpin" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Unpin" + "state" : "translated", + "value" : "取消置顶" } } } @@ -4137,14 +4095,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Enhanced Sync" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Enhanced Sync" + "state" : "translated", + "value" : "增强同步" } } } @@ -4243,8 +4195,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "None" + "state" : "translated", + "value" : "无" } } } @@ -6520,7 +6472,7 @@ "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Nom de la bouteille :" + "value" : "Nom de la bouteille :" } }, "it" : { @@ -7075,14 +7027,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Add" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Add" + "state" : "translated", + "value" : "新建" } } } @@ -7181,14 +7127,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Remove" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Remove" + "state" : "translated", + "value" : "移除" } } } @@ -7499,8 +7439,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Website" + "state" : "translated", + "value" : "网站" } } } @@ -8568,14 +8508,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Pin" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Pin" + "state" : "translated", + "value" : "置顶" } } } @@ -8675,14 +8609,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "\"%@\" is already pinned!" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "\"%@\" is already pinned!" + "state" : "translated", + "value" : "\"%@\" 已被置顶!" } } } @@ -8781,14 +8709,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Error Pinning Program" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Error Pinning Program" + "state" : "translated", + "value" : "置顶程序出错" } } } @@ -8887,14 +8809,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Pin Program" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Pin Program" + "state" : "translated", + "value" : "置顶程序" } } } @@ -8993,14 +8909,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Pin name:" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Pin name:" + "state" : "translated", + "value" : "置顶名称" } } } @@ -9099,14 +9009,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Program path:" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Program path:" + "state" : "translated", + "value" : "程序路径" } } } @@ -9205,14 +9109,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Pin Program" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Pin Program" + "state" : "translated", + "value" : "置顶程序" } } } @@ -9429,14 +9327,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Add Selected to Blocklist" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Add Selected to Blocklist" + "state" : "translated", + "value" : "将所选内容加入黑名单" } } } @@ -10143,14 +10035,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Remove Selected from Blocklist" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Remove Selected from Blocklist" + "state" : "translated", + "value" : "将所选内容从黑名单中移除" } } } @@ -10255,8 +10141,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Settings" + "state" : "translated", + "value" : "设置" } } } @@ -10532,7 +10418,7 @@ "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Nouveau nom :" + "value" : "Nouveau nom :" } }, "it" : { @@ -11169,8 +11055,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "General" + "state" : "translated", + "value" : "通用" } } } @@ -11263,14 +11149,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Automatically check for GPTK updates" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Automatically check for GPTK updates" + "state" : "translated", + "value" : "自动检查GPTK更新" } } } @@ -11363,14 +11243,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Terminate Wine processes when Whisky closes" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Terminate Wine processes when Whisky closes" + "state" : "translated", + "value" : "关闭Whisky时中止Wine进程" } } } @@ -11463,14 +11337,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Automatically check for Whisky updates" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Automatically check for Whisky updates" + "state" : "translated", + "value" : "自动检查Whisky更新" } } } @@ -11569,8 +11437,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Updates" + "state" : "translated", + "value" : "更新" } } } @@ -13121,8 +12989,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Retry" + "state" : "translated", + "value" : "重试" } } } @@ -13339,14 +13207,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Rosetta 2 installation failed. Please install it manually." - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Rosetta 2 installation failed. Please install it manually." + "state" : "translated", + "value" : "Rosetta 2安装失败。请手动安装。" } } } @@ -14771,12 +14633,6 @@ "value" : "Встановлені програми" } }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Installed Programs" - } - }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -14791,56 +14647,6 @@ } } }, - "update.checkingForUpdates" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Checking for updates..." - } - } - } - }, - "update.checkingForUpdates.description" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Whisky is checking for updates..." - } - } - } - }, - "update.description" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "You're running %@ (%@). The latest available update is %@ (%@). Would you like to update?" - } - } - } - }, - "update.downloading" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Downloading Update..." - } - } - } - }, - "update.extracting" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Extracting Files..." - } - } - } - }, "update.gptk.description" : { "localizations" : { "da" : { @@ -15168,115 +14974,11 @@ "state" : "translated", "value" : "更新" } - } - } - }, - "update.initializating" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Initializing..." - } - } - } - }, - "update.installing" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Installing update..." - } - } - } - }, - "update.newUpdate" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Update Available" - } - } - } - }, - "update.noChangeLog" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "No change log available." - } - } - } - }, - "update.noUpdatesFound" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "No Updates Available" - } - } - } - }, - "update.noUpdatesFound.description" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "No updates are available. You are on the latest version of Whisky." - } - } - } - }, - "update.readyToRelaunch" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Update Ready" - } - } - } - }, - "update.readyToRelaunch.description" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "The update is ready to be installed. Press \"Relaunch\" to install the update and relaunch Whisky." - } - } - } - }, - "update.relaunch" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Relaunch" - } - } - } - }, - "update.update" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Update" - } - } - } - }, - "update.updaterError" : { - "localizations" : { - "en" : { + }, + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "Update Error!" + "value" : "更新" } } } @@ -15500,14 +15202,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Apps" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "needs_review", - "value" : "Apps" + "state" : "translated", + "value" : "应用程序" } } } @@ -15601,13 +15297,7 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Benchmarks" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Benchmarks" } } @@ -15702,13 +15392,7 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "DLLs" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "DLLs" } } @@ -15809,8 +15493,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Fonts" + "state" : "translated", + "value" : "字体" } } } @@ -15904,8 +15588,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Games" + "state" : "translated", + "value" : "游戏" } }, "zh-Hant" : { @@ -16011,8 +15695,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Settings" + "state" : "translated", + "value" : "设置" } } } @@ -16105,8 +15789,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Description" + "state" : "translated", + "value" : "说明" } }, "zh-Hant" : { @@ -16211,8 +15895,8 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", - "value" : "Command" + "state" : "translated", + "value" : "指令" } }, "zh-Hant" : { From 37bcdec732e2662410aec20c33faaa9d4a9244b8 Mon Sep 17 00:00:00 2001 From: Josh <36625023+JoshuaBrest@users.noreply.github.com> Date: Tue, 9 Jan 2024 00:15:42 -0800 Subject: [PATCH 21/22] Add localized strings for update functionality --- Whisky/Localizable.xcstrings | 190 +++++++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) diff --git a/Whisky/Localizable.xcstrings b/Whisky/Localizable.xcstrings index a0b9d56f8..1e9746efd 100644 --- a/Whisky/Localizable.xcstrings +++ b/Whisky/Localizable.xcstrings @@ -249,6 +249,36 @@ } } }, + "app.name" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Whisky" + } + } + } + }, + "app.version" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ (%@)" + } + } + } + }, + "button.cancel" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cancel" + } + } + } + }, "button.cDrive" : { "localizations" : { "da" : { @@ -14647,6 +14677,56 @@ } } }, + "update.checkingForUpdates" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Checking for updates..." + } + } + } + }, + "update.checkingForUpdates.description" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Whisky is checking for updates..." + } + } + } + }, + "update.description" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "You're running %@ (%@). The latest available update is %@ (%@). Would you like to update?" + } + } + } + }, + "update.downloading" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Downloading Update..." + } + } + } + }, + "update.extracting" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Extracting Files..." + } + } + } + }, "update.gptk.description" : { "localizations" : { "da" : { @@ -14983,6 +15063,116 @@ } } }, + "update.initializating" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Initializing..." + } + } + } + }, + "update.installing" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Installing update..." + } + } + } + }, + "update.newUpdate" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Updates Available" + } + } + } + }, + "update.noChangeLog" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "No change log available." + } + } + } + }, + "update.noUpdatesFound" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "No Updates Available" + } + } + } + }, + "update.noUpdatesFound.description" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Congrats! You are on the latest version of Whisky," + } + } + } + }, + "update.readyToRelaunch" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Update Ready" + } + } + } + }, + "update.readyToRelaunch.description" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "The update is ready to be installed. Press \\\"Relaunch\\\" to install the update and relaunch Whisky." + } + } + } + }, + "update.relaunch" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Relaunch" + } + } + } + }, + "update.update" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Update" + } + } + } + }, + "update.updaterError" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Failed to Update" + } + } + } + }, "wine.clearShaderCaches" : { "localizations" : { "da" : { From 4cb352582d1c984922787f3f2786ca7c3b8eaad2 Mon Sep 17 00:00:00 2001 From: Josh <36625023+JoshuaBrest@users.noreply.github.com> Date: Tue, 9 Jan 2024 00:19:44 -0800 Subject: [PATCH 22/22] Refactor updateViews function to improve code readability --- .../UpdateControllerViewModifier.swift | 54 ++++++++++--------- .../Views/Updater/UpdateInstallingView.swift | 3 +- 2 files changed, 31 insertions(+), 26 deletions(-) diff --git a/Whisky/Views/Updater/UpdateControllerViewModifier.swift b/Whisky/Views/Updater/UpdateControllerViewModifier.swift index 662e78a1c..98519dd3b 100644 --- a/Whisky/Views/Updater/UpdateControllerViewModifier.swift +++ b/Whisky/Views/Updater/UpdateControllerViewModifier.swift @@ -81,33 +81,37 @@ struct UpdateControllerViewModifier: ViewModifier { UpdateControllerErrorView(updater: updater) }) .onChange(of: updater.state, { _, newValue in - // Dissmiss all old views - sheetCheckingUpdateViewPresented = false - sheetChangeLogViewPresented = false - sheetUpdateInstallingViewPresented = false - sheetUpdateReadyToRelaunchViewPresented = false - sheetUpdateErrorViewPresented = false - sheetUpdateNotFoundViewPresented = false - - // Enable new view - switch newValue { - case .checking: - sheetCheckingUpdateViewPresented = true - case .updateFound: - sheetChangeLogViewPresented = true - case .initializing, .downloading, .extracting, .installing: - sheetUpdateInstallingViewPresented = true - case .readyToRelaunch: - sheetUpdateReadyToRelaunchViewPresented = true - case .error: - sheetUpdateErrorViewPresented = true - case .updateNotFound: - sheetUpdateNotFoundViewPresented = true - case .idle: - break - } + updateViews(newState: newValue) }) } + + func updateViews(newState: SparkleUpdaterEvents.UpdaterState) { + // Dissmiss all old views + sheetCheckingUpdateViewPresented = false + sheetChangeLogViewPresented = false + sheetUpdateInstallingViewPresented = false + sheetUpdateReadyToRelaunchViewPresented = false + sheetUpdateErrorViewPresented = false + sheetUpdateNotFoundViewPresented = false + + // Enable new view + switch newState { + case .checking: + sheetCheckingUpdateViewPresented = true + case .updateFound: + sheetChangeLogViewPresented = true + case .initializing, .downloading, .extracting, .installing: + sheetUpdateInstallingViewPresented = true + case .readyToRelaunch: + sheetUpdateReadyToRelaunchViewPresented = true + case .error: + sheetUpdateErrorViewPresented = true + case .updateNotFound: + sheetUpdateNotFoundViewPresented = true + case .idle: + break + } + } } struct UpdateControllerCheckingForUpdatesView: View { diff --git a/Whisky/Views/Updater/UpdateInstallingView.swift b/Whisky/Views/Updater/UpdateInstallingView.swift index 921e2d316..73db1256f 100644 --- a/Whisky/Views/Updater/UpdateInstallingView.swift +++ b/Whisky/Views/Updater/UpdateInstallingView.swift @@ -63,7 +63,8 @@ struct UpdateInstallingView: View { + Text(String(" ")) + (shouldShowEstimate ? Text(String(format: String(localized: "setup.gptk.eta"), - formatRemainingTime(remainingBytes: downloadableBytes - downloadedBytes))) + formatRemainingTime( + remainingBytes: downloadableBytes - downloadedBytes))) : Text(String())) Spacer() }