From e8f32ec07b870c8b6541f7e77509a812ded25732 Mon Sep 17 00:00:00 2001 From: Josh <36625023+JoshuaBrest@users.noreply.github.com> Date: Fri, 15 Nov 2024 14:24:56 -0800 Subject: [PATCH] feat: prototype of new "hub" style launcher. --- Mythic.xcodeproj/project.pbxproj | 88 ++++-- Sources/App/AppDelegate.swift | 10 +- Sources/Localizable.xcstrings | 30 ++ .../Models/BackgroundEventServiceModel.swift | 2 +- .../AppSettingsPersistentStateModel.swift | 9 - .../Models/SparkleUpdateControlerModel.swift | 40 ++- .../Legendary/LegendaryInterface.swift | 21 ++ Sources/Views/Components/GenericWebView.swift | 270 ++++++++++++++++++ .../WindowBlurView.swift | 0 Sources/Views/Hub/HubView.swift | 31 ++ .../Views/Hub/Library/HubLibraryView.swift | 18 ++ .../Views/Hub/Settings/HubSettingsView.swift | 16 ++ .../Hub/Store/HubStoreEpicGamesView.swift | 151 ++++++++++ Sources/Views/Hub/Store/HubStoreView.swift | 16 ++ .../OnboardingEpicGamesLoginStepView.swift | 6 +- Sources/Views/Onboarding/OnboardingView.swift | 1 + Sources/Windows/Hub/HubWindow.xib | 4 +- Sources/Windows/Hub/HubWindowController.swift | 2 +- Sources/Windows/Setup/SetupWindow.xib | 2 +- 19 files changed, 664 insertions(+), 53 deletions(-) create mode 100644 Sources/Views/Components/GenericWebView.swift rename Sources/Views/{Unified/Modules => Components}/WindowBlurView.swift (100%) create mode 100644 Sources/Views/Hub/HubView.swift create mode 100644 Sources/Views/Hub/Library/HubLibraryView.swift create mode 100644 Sources/Views/Hub/Settings/HubSettingsView.swift create mode 100644 Sources/Views/Hub/Store/HubStoreEpicGamesView.swift create mode 100644 Sources/Views/Hub/Store/HubStoreView.swift diff --git a/Mythic.xcodeproj/project.pbxproj b/Mythic.xcodeproj/project.pbxproj index fa7f3e29..82bc36b9 100644 --- a/Mythic.xcodeproj/project.pbxproj +++ b/Mythic.xcodeproj/project.pbxproj @@ -66,10 +66,6 @@ 6A1EF00C2CE3A62B00C1F652 /* _sha2.cpython-313-darwin.so in Resources */ = {isa = PBXBuildFile; fileRef = 6A1EEFBC2CE3A62B00C1F652 /* _sha2.cpython-313-darwin.so */; }; 6A1EF00D2CE3A62B00C1F652 /* mmap.cpython-313-darwin.so in Resources */ = {isa = PBXBuildFile; fileRef = 6A1EEFC82CE3A62B00C1F652 /* mmap.cpython-313-darwin.so */; }; 6A1EF00E2CE3A62B00C1F652 /* _heapq.cpython-313-darwin.so in Resources */ = {isa = PBXBuildFile; fileRef = 6A1EEFAE2CE3A62B00C1F652 /* _heapq.cpython-313-darwin.so */; }; - 6A1EF00F2CE3A62B00C1F652 /* Python.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6A1EEFD42CE3A62B00C1F652 /* Python.framework */; }; - 6A1EF0102CE3A62B00C1F652 /* libcrypto.3.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 6A1EEFD12CE3A62B00C1F652 /* libcrypto.3.dylib */; }; - 6A1EF0112CE3A62B00C1F652 /* Python in Frameworks */ = {isa = PBXBuildFile; fileRef = 6A1EEFD32CE3A62B00C1F652 /* Python */; }; - 6A1EF0122CE3A62B00C1F652 /* libssl.3.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 6A1EEFD22CE3A62B00C1F652 /* libssl.3.dylib */; }; 6A2935322BFCFAFD0035CE4B /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6A2934AE2BFCFAFD0035CE4B /* Preview Assets.xcassets */; }; 6A2935332BFCFAFD0035CE4B /* Engine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A2934B02BFCFAFD0035CE4B /* Engine.swift */; }; 6A2935342BFCFAFD0035CE4B /* EngineExt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A2934B12BFCFAFD0035CE4B /* EngineExt.swift */; }; @@ -185,21 +181,15 @@ EBB8DBC92CE543840005D181 /* SparkleUpdaterFinishView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBB8DBC82CE543840005D181 /* SparkleUpdaterFinishView.swift */; }; EBB8DBCB2CE5448C0005D181 /* SparkleUpdaterSheetViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBB8DBCA2CE5448C0005D181 /* SparkleUpdaterSheetViewModifier.swift */; }; EBB8DBD02CE573B70005D181 /* RichAlertView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBB8DBCF2CE573B70005D181 /* RichAlertView.swift */; }; + EBB8DBD22CE669210005D181 /* BackgroundEventServiceModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBB8DBD12CE669210005D181 /* BackgroundEventServiceModel.swift */; }; + EBB8DBD72CE6C1520005D181 /* HubView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBB8DBD62CE6C1520005D181 /* HubView.swift */; }; + EBB8DBDB2CE6C37F0005D181 /* HubLibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBB8DBDA2CE6C37F0005D181 /* HubLibraryView.swift */; }; + EBB8DBDE2CE6C6440005D181 /* HubStoreEpicGamesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBB8DBDD2CE6C6440005D181 /* HubStoreEpicGamesView.swift */; }; + EBB8DBE02CE6C67A0005D181 /* GenericWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBB8DBDF2CE6C67A0005D181 /* GenericWebView.swift */; }; + EBC3A5482CE7E8CB00492C89 /* HubStoreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBC3A5472CE7E8CB00492C89 /* HubStoreView.swift */; }; + EBC3A54C2CE8014400492C89 /* HubSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBC3A54B2CE8014400492C89 /* HubSettingsView.swift */; }; /* End PBXBuildFile section */ -/* Begin PBXCopyFilesBuildPhase section */ - 6A71D3D32BFD00B600A2C74D /* Embed Libraries */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - ); - name = "Embed Libraries"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - /* Begin PBXFileReference section */ 5A62AE972C27DB1200BA31D2 /* GameListEvoVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameListEvoVM.swift; sourceTree = ""; }; 5A9573A92C29BBEC009C8F85 /* SparkleController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SparkleController.swift; sourceTree = ""; }; @@ -367,6 +357,13 @@ EBB8DBC82CE543840005D181 /* SparkleUpdaterFinishView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SparkleUpdaterFinishView.swift; sourceTree = ""; }; EBB8DBCA2CE5448C0005D181 /* SparkleUpdaterSheetViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SparkleUpdaterSheetViewModifier.swift; sourceTree = ""; }; EBB8DBCF2CE573B70005D181 /* RichAlertView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RichAlertView.swift; sourceTree = ""; }; + EBB8DBD12CE669210005D181 /* BackgroundEventServiceModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundEventServiceModel.swift; sourceTree = ""; }; + EBB8DBD62CE6C1520005D181 /* HubView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HubView.swift; sourceTree = ""; }; + EBB8DBDA2CE6C37F0005D181 /* HubLibraryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HubLibraryView.swift; sourceTree = ""; }; + EBB8DBDD2CE6C6440005D181 /* HubStoreEpicGamesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HubStoreEpicGamesView.swift; sourceTree = ""; }; + EBB8DBDF2CE6C67A0005D181 /* GenericWebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericWebView.swift; sourceTree = ""; }; + EBC3A5472CE7E8CB00492C89 /* HubStoreView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HubStoreView.swift; sourceTree = ""; }; + EBC3A54B2CE8014400492C89 /* HubSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HubSettingsView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -385,10 +382,6 @@ 6A12FF8E2B73AC4E00AA948C /* Glur in Frameworks */, 6A34366E2B8D7F1200D35BCA /* Shimmer in Frameworks */, 6AAD31152B08693D0035FA69 /* SemanticVersion in Frameworks */, - 6A1EF00F2CE3A62B00C1F652 /* Python.framework in Frameworks */, - 6A1EF0102CE3A62B00C1F652 /* libcrypto.3.dylib in Frameworks */, - 6A1EF0112CE3A62B00C1F652 /* Python in Frameworks */, - 6A1EF0122CE3A62B00C1F652 /* libssl.3.dylib in Frameworks */, 6A2961072CE1DD6200917E90 /* FirebaseCrashlytics in Frameworks */, 6A2961032CE1DD6200917E90 /* FirebaseAnalytics in Frameworks */, 6A371B592AE7DFBF0054BF7A /* ZIPFoundation in Frameworks */, @@ -596,7 +589,6 @@ 6A2934DD2BFCFAFD0035CE4B /* GameInstallProgressView.swift */, 6A2934DE2BFCFAFD0035CE4B /* SubscriptedTextView.swift */, 6A2934EB2BFCFAFD0035CE4B /* WebView.swift */, - 6AEEFA462CA9173C0025C840 /* WindowBlurView.swift */, ); path = Modules; sourceTree = ""; @@ -636,6 +628,7 @@ EB7D07962CC981970072D64E /* Components */, EBB8DBB32CE3ED260005D181 /* SparkleUpdater */, EB90A3602CD940CA001F0871 /* Onboarding */, + EBB8DBD52CE6C11E0005D181 /* Hub */, 6A2934DA2BFCFAFD0035CE4B /* Navigation */, 6A2934EC2BFCFAFD0035CE4B /* Unified */, ); @@ -733,8 +726,10 @@ isa = PBXGroup; children = ( EB7D07972CC981C10072D64E /* ColorfulBackgroundView.swift */, + 6AEEFA462CA9173C0025C840 /* WindowBlurView.swift */, EBB8DBB02CE3C0470005D181 /* BundleIconView.swift */, EBB8DBCF2CE573B70005D181 /* RichAlertView.swift */, + EBB8DBDF2CE6C67A0005D181 /* GenericWebView.swift */, ); path = Components; sourceTree = ""; @@ -784,6 +779,7 @@ EB7D07C82CCC52A80072D64E /* AppLoggerModel.swift */, EB7D07C22CCAB5EF0072D64E /* EngineInstallerModel.swift */, EBB8DBAC2CE3405F0005D181 /* SparkleUpdateControlerModel.swift */, + EBB8DBD12CE669210005D181 /* BackgroundEventServiceModel.swift */, ); path = Models; sourceTree = ""; @@ -834,6 +830,42 @@ name = Frameworks; sourceTree = ""; }; + EBB8DBD52CE6C11E0005D181 /* Hub */ = { + isa = PBXGroup; + children = ( + EBC3A54A2CE8013600492C89 /* Settings */, + EBC3A5492CE8011600492C89 /* Library */, + EBB8DBDC2CE6C6300005D181 /* Store */, + EBB8DBD62CE6C1520005D181 /* HubView.swift */, + ); + path = Hub; + sourceTree = ""; + }; + EBB8DBDC2CE6C6300005D181 /* Store */ = { + isa = PBXGroup; + children = ( + EBB8DBDD2CE6C6440005D181 /* HubStoreEpicGamesView.swift */, + EBC3A5472CE7E8CB00492C89 /* HubStoreView.swift */, + ); + path = Store; + sourceTree = ""; + }; + EBC3A5492CE8011600492C89 /* Library */ = { + isa = PBXGroup; + children = ( + EBB8DBDA2CE6C37F0005D181 /* HubLibraryView.swift */, + ); + path = Library; + sourceTree = ""; + }; + EBC3A54A2CE8013600492C89 /* Settings */ = { + isa = PBXGroup; + children = ( + EBC3A54B2CE8014400492C89 /* HubSettingsView.swift */, + ); + path = Settings; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -845,7 +877,6 @@ 6AB474922AACBBE900AB9C63 /* Frameworks */, 6AB474932AACBBE900AB9C63 /* Resources */, 6E47429A2B0A85C4004FA8A8 /* SwiftLint */, - 6A71D3D32BFD00B600A2C74D /* Embed Libraries */, 6ACCEC192CD088A400611BEF /* Crashlytics */, ); buildRules = ( @@ -1047,7 +1078,9 @@ buildActionMask = 2147483647; files = ( EBB8DBB62CE3F15F0005D181 /* SparkleUpdaterPreviewView.swift in Sources */, + EBB8DBD72CE6C1520005D181 /* HubView.swift in Sources */, EB7D07A72CC9C6980072D64E /* AppDelegate.swift in Sources */, + EBB8DBDE2CE6C6440005D181 /* HubStoreEpicGamesView.swift in Sources */, 6A29353A2BFCFAFD0035CE4B /* Task.swift in Sources */, EB7D07B82CC9E25F0072D64E /* main.swift in Sources */, 6A2935592BFCFAFD0035CE4B /* ContainerCreationView.swift in Sources */, @@ -1065,6 +1098,7 @@ 6A496A732C1AF75B00FD637B /* Game.swift in Sources */, EB7D07C72CCC4DA00072D64E /* AppSettingsPersistentStateModel.swift in Sources */, 6A2935602BFCFAFD0035CE4B /* ContainerListView.swift in Sources */, + EBB8DBE02CE6C67A0005D181 /* GenericWebView.swift in Sources */, 6A29355E2BFCFAFD0035CE4B /* StopDownloadAlert.swift in Sources */, EBB8DBC32CE525770005D181 /* SparkleUpdaterDownloadingView.swift in Sources */, 6A2935662BFCFAFD0035CE4B /* AppDelegate1.swift in Sources */, @@ -1073,10 +1107,12 @@ EB7D07AC2CC9C7030072D64E /* HubWindowController.swift in Sources */, 5A62AE982C27DB1200BA31D2 /* GameListEvoVM.swift in Sources */, 6A29354C2BFCFAFD0035CE4B /* LibraryView.swift in Sources */, + EBB8DBDB2CE6C37F0005D181 /* HubLibraryView.swift in Sources */, EB90A36E2CD982BB001F0871 /* OnboardingEpicGamesWelcomeStepView.swift in Sources */, 6A2935472BFCFAFD0035CE4B /* VariableManager.swift in Sources */, EB90A3682CD9422B001F0871 /* OnboardingIntroStepView.swift in Sources */, 6A2935332BFCFAFD0035CE4B /* Engine.swift in Sources */, + EBC3A54C2CE8014400492C89 /* HubSettingsView.swift in Sources */, 6A0688442C2BCE8B004DF10F /* DownloadCard.swift in Sources */, 6A2935462BFCFAFD0035CE4B /* Rosetta.swift in Sources */, 6A2935532BFCFAFD0035CE4B /* StoreView.swift in Sources */, @@ -1085,6 +1121,7 @@ 6A29355F2BFCFAFD0035CE4B /* UninstallGameView.swift in Sources */, EBB8DBD02CE573B70005D181 /* RichAlertView.swift in Sources */, EB90A3662CD941EF001F0871 /* OnboardingEpicGamesLoginStepView.swift in Sources */, + EBC3A5482CE7E8CB00492C89 /* HubStoreView.swift in Sources */, 6A29355A2BFCFAFD0035CE4B /* ContainerSettingsView.swift in Sources */, 6A448E0E2CC4A53A001E9F47 /* GameListCard.swift in Sources */, 6A31421C2CE39911006838CC /* SemanticVersion.swift in Sources */, @@ -1110,6 +1147,7 @@ EB90A3732CD9AE5D001F0871 /* StorablePersistentStateModel.swift in Sources */, 6A2935582BFCFAFD0035CE4B /* SubscriptedTextView.swift in Sources */, 6A2935542BFCFAFD0035CE4B /* SupportView.swift in Sources */, + EBB8DBD22CE669210005D181 /* BackgroundEventServiceModel.swift in Sources */, 6A29354E2BFCFAFD0035CE4B /* ContainersView.swift in Sources */, 6A2935412BFCFAFD0035CE4B /* WineInterface.swift in Sources */, EBB8DBC52CE541D90005D181 /* SparkleUpdaterInstallingView.swift in Sources */, @@ -1283,7 +1321,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 3262; + CURRENT_PROJECT_VERSION = 3338; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"Sources/Preview Content\""; DEVELOPMENT_TEAM = ""; @@ -1334,7 +1372,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 3262; + CURRENT_PROJECT_VERSION = 3338; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"Sources/Preview Content\""; DEVELOPMENT_TEAM = ""; diff --git a/Sources/App/AppDelegate.swift b/Sources/App/AppDelegate.swift index 09d40b06..748cf7ae 100644 --- a/Sources/App/AppDelegate.swift +++ b/Sources/App/AppDelegate.swift @@ -35,8 +35,6 @@ public class AppDelegate: NSObject, NSApplicationDelegate { /// Listen for events @MainActor private func listenEvents() { - // Flush old cancellables - cancellables.forEach({ $0.cancel() }) cancellables.removeAll() // Listen for onboarding state changes @@ -51,6 +49,14 @@ public class AppDelegate: NSObject, NSApplicationDelegate { appMenuController.setRestartOnboardingVisibility(true) } }).store(in: &cancellables) + + SparkleUpdateControlerModel.shared.$state.sink(receiveValue: { [self] value in + switch value { + case .updateAvailable, .checkingForUpdates, .readyToRelaunch, .noUpdateAvailable: + restoreActiveRootWindow() + default: () + } + }).store(in: &cancellables) } /// Get the setup window or switch to it. diff --git a/Sources/Localizable.xcstrings b/Sources/Localizable.xcstrings index c117c571..046d93fb 100644 --- a/Sources/Localizable.xcstrings +++ b/Sources/Localizable.xcstrings @@ -11631,6 +11631,9 @@ } } } + }, + "common.back" : { + }, "common.cancel" : { "localizations" : { @@ -11663,6 +11666,12 @@ } } } + }, + "common.forward" : { + + }, + "common.homepage" : { + }, "common.next" : { "localizations" : { @@ -11706,6 +11715,9 @@ } } } + }, + "common.refresh" : { + }, "common.skip" : { "localizations" : { @@ -22802,6 +22814,12 @@ } } } + }, + "genericWebView.error" : { + + }, + "genericWebView.retry" : { + }, "Get support/Support Mythic" : { "localizations" : { @@ -24138,6 +24156,15 @@ } } } + }, + "hubLibraryView.title" : { + + }, + "hubStoreEpicGamesView.title" : { + + }, + "hubStoreView.title" : { + }, "I have read and agreed to the terms of the software license agreement." : { "extractionState" : "stale", @@ -29665,6 +29692,9 @@ } } } + }, + "Library View!" : { + }, "List" : { "localizations" : { diff --git a/Sources/Models/BackgroundEventServiceModel.swift b/Sources/Models/BackgroundEventServiceModel.swift index 2e8bf1f7..7ee905d7 100644 --- a/Sources/Models/BackgroundEventServiceModel.swift +++ b/Sources/Models/BackgroundEventServiceModel.swift @@ -11,5 +11,5 @@ public struct BackgroundEventServiceModel { /// The dispatch queue for the events public let queue: DispatchQueue = DispatchQueue(label: "app.getmythic.MythicMacOS.BackgroundEventService", - qos: .background) + qos: .background) } diff --git a/Sources/Models/PersistentState/AppSettingsPersistentStateModel.swift b/Sources/Models/PersistentState/AppSettingsPersistentStateModel.swift index e0d96e0a..ec42c7ef 100644 --- a/Sources/Models/PersistentState/AppSettingsPersistentStateModel.swift +++ b/Sources/Models/PersistentState/AppSettingsPersistentStateModel.swift @@ -16,12 +16,6 @@ public struct AppSettingsPersistentStateModel: StorablePersistentStateModel.Stat .init() } - /// Library Display Mode. - public enum LibraryDisplayMode: String, Codable, Hashable { - case list - case grid - } - /// Auto update settings. public enum AutoUpdateAction: String, Codable, Hashable { case off @@ -47,9 +41,6 @@ public struct AppSettingsPersistentStateModel: StorablePersistentStateModel.Stat /// Engine update action. public var engineUpdateAction: AutoUpdateAction = .check - /// Library display mode. - public var libraryDisplayMode: LibraryDisplayMode = .grid - /// Hide the main window on game launch. public var hideOnGameLaunch: Bool = false /// Close opened games on quit. diff --git a/Sources/Models/SparkleUpdateControlerModel.swift b/Sources/Models/SparkleUpdateControlerModel.swift index cb927db6..8b4111b5 100644 --- a/Sources/Models/SparkleUpdateControlerModel.swift +++ b/Sources/Models/SparkleUpdateControlerModel.swift @@ -90,7 +90,7 @@ public class SparkleUpdateControlerModel: NSObject, SPUUserDriver, ObservableObj do { try updaterController.start() } catch { logger.error("Sparkle failed to start: \(error.localizedDescription).") } - + DispatchQueue.main.async { self.manageBackgroundTask(AppSettingsPersistentStateModel.shared.store.sparkleUpdateAction != .off) AppSettingsPersistentStateModel.shared.$store @@ -157,17 +157,18 @@ public class SparkleUpdateControlerModel: NSObject, SPUUserDriver, ObservableObj /// Check for updates /// - Parameter userInitiated: If the check was initiated by the user. @MainActor public func checkForUpdates(userInitiated: Bool = false) { - if let updater = sparkleUpdater, !updater.sessionInProgress { - logger.info("\(userInitiated ? "User-initiated" : "Automatic") update check initiated...") - _ = clearState() - userInitiatedCheck = userInitiated + guard let updater = sparkleUpdater, !updater.sessionInProgress else { + logger.info("\(userInitiated ? "User-initiated" : "Automatic") update check ignored due to in-progress update session.") + if userInitiated { + userInitiatedCheck = true + } return } - logger.info("\(userInitiated ? "User-initiated" : "Automatic") update check ignored due to in-progress update session.") - if userInitiated { - userInitiatedCheck = true - } + logger.info("\(userInitiated ? "User-initiated" : "Automatic") update check initiated...") + _ = clearState() + userInitiatedCheck = userInitiated + updater.checkForUpdates() } /// Get the updater action @@ -179,6 +180,15 @@ public class SparkleUpdateControlerModel: NSObject, SPUUserDriver, ObservableObj return action } + /// Get the updater action + private func preferSilent() -> Bool { + var action: Bool = false + DispatchQueue.main.sync { + action = !AppSettingsPersistentStateModel.shared.store.inOnboarding + } + return action + } + /// Initialize the settings for the updater. /// Implementation of `SPUUserDriver` protocol. public func show(_ request: SPUUpdatePermissionRequest) async -> SUUpdatePermissionResponse { @@ -208,6 +218,8 @@ public class SparkleUpdateControlerModel: NSObject, SPUUserDriver, ObservableObj reply(.install) self.state = .initializingUpdate return + } else if getUpdaterAction() == .check && !preferSilent() { + userInitiatedCheck = true } self.state = .updateAvailable(choice: { choice in @@ -330,6 +342,11 @@ public class SparkleUpdateControlerModel: NSObject, SPUUserDriver, ObservableObj /// Implementation of `SPUUserDriver` protocol. public func showReady(toInstallAndRelaunch reply: @escaping (SPUUserUpdateChoice) -> Void) { logger.debug("Update ready to install.") + + if !userInitiatedCheck && !preferSilent() { + userInitiatedCheck = true + } + state = .readyToRelaunch { choice in switch choice { case .update: @@ -363,6 +380,11 @@ public class SparkleUpdateControlerModel: NSObject, SPUUserDriver, ObservableObj /// Dismiss the updater. /// Implementation of `SPUUserDriver` protocol. public func dismissUpdateInstallation() { + guard userInitiatedCheck else { + self.state = .idle + return + } + if case .checkingForUpdates(_) = state { // No updates were found. state = .noUpdateAvailable { diff --git a/Sources/Utilities/Legendary/LegendaryInterface.swift b/Sources/Utilities/Legendary/LegendaryInterface.swift index e345722a..3a3101ff 100644 --- a/Sources/Utilities/Legendary/LegendaryInterface.swift +++ b/Sources/Utilities/Legendary/LegendaryInterface.swift @@ -491,6 +491,27 @@ final class Legendary { return String(describing: json["displayName"]) } + + /// Queries for the user that is currently signed into epic games. + static var refreshToken: String? { + let json: URL = .init(filePath: "\(configLocation)/user.json") + guard let json = try? JSON(data: .init(contentsOf: json)) else { + return nil + } + + return String(describing: json["refresh_token"]) + } + + /// Queries for the user that is currently signed into epic games. + static var accessToken: String? { + let json: URL = .init(filePath: "\(configLocation)/user.json") + guard let json = try? JSON(data: .init(contentsOf: json)) else { + return nil + } + + return String(describing: json["access_token"]) + } + /// Checks account signin state. static var signedIn: Bool { return user != nil } diff --git a/Sources/Views/Components/GenericWebView.swift b/Sources/Views/Components/GenericWebView.swift new file mode 100644 index 00000000..c1beb0d3 --- /dev/null +++ b/Sources/Views/Components/GenericWebView.swift @@ -0,0 +1,270 @@ +// +// GenericWebView.swift +// Mythic +// + +import SwiftUI +import Combine +import WebKit + +public struct GenericWebView: View { + @Binding private var canGoBack: Bool + @Binding private var canGoForward: Bool + @Binding private var isLoading: Bool + @Binding private var url: URL? + @Binding private var view: WKWebView? + private var initWebViewConfig: (@MainActor (_ webView: WKWebViewConfiguration) -> Void)? + private var initWebView: (@MainActor (_ webView: WKWebView) -> Void)? + private var handleNavigationAction: (@MainActor (_ webView: WKWebView, _ policy: WKNavigationAction) + -> NavigationPolicy)? + + @State private var error: Error? + @State private var cancelables: Set = .init() + + public enum NavigationPolicy: Int, CaseIterable, Identifiable, Hashable, Sendable { + public var id: Int { + rawValue + } + + case allow + case cancel + case openExternal + } + + public init(canGoBack: Binding = .constant(false), + canGoForward: Binding = .constant(false), + isLoading: Binding = .constant(false), + url: Binding = .constant(nil), + view: (Binding)? = nil, + initWebView: (@MainActor (_ webView: WKWebView) -> Void)? = nil, + initWebViewConfig: (@MainActor (_ webView: WKWebViewConfiguration) -> Void)? = nil, + handleNavigationAction: (@MainActor (_ webView: WKWebView, _ policy: WKNavigationAction) + -> NavigationPolicy)? = nil) { + self._canGoBack = canGoBack + self._canGoForward = canGoForward + self._isLoading = isLoading + self._url = url + if let view = view { + self._view = view + } else { + self._view = State.init(initialValue: nil).projectedValue + } + self.initWebView = initWebView + self.initWebViewConfig = initWebViewConfig + self.handleNavigationAction = handleNavigationAction + } + + private class WebViewCordinator: NSObject, WKNavigationDelegate { + private let onCanGoBackChange: (Bool) -> Void + private let onCanGoForwardChange: (Bool) -> Void + private let onLoadingChange: (Bool) -> Void + private let onURLChange: (URL?) -> Void + private let handleNavigationAction: ((_ webView: WKWebView, _ policy: WKNavigationAction) + -> NavigationPolicy)? + private let onNavigationError: (Error?) -> Void + + init(onCanGoBackChange: @escaping (Bool) -> Void, + onCanGoForwardChange: @escaping (Bool) -> Void, + onLoadingChange: @escaping (Bool) -> Void, + onURLChange: @escaping (URL?) -> Void, + handleNavigationAction: ((_ webView: WKWebView, _ policy: WKNavigationAction) + -> NavigationPolicy)?, + onNavigationError: @escaping (Error?) -> Void) { + self.onCanGoBackChange = onCanGoBackChange + self.onCanGoForwardChange = onCanGoForwardChange + self.onLoadingChange = onLoadingChange + self.onURLChange = onURLChange + self.handleNavigationAction = handleNavigationAction + self.onNavigationError = onNavigationError + } + + func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation) { + onLoadingChange(true) + onCanGoBackChange(webView.canGoBack) + onCanGoForwardChange(webView.canGoForward) + onNavigationError(nil) + } + + func webView(_ webView: WKWebView, didFinish navigation: WKNavigation) { + onLoadingChange(false) + onCanGoBackChange(webView.canGoBack) + onCanGoForwardChange(webView.canGoForward) + onNavigationError(nil) + } + + func webView(_ webView: WKWebView, didFail navigation: WKNavigation, withError error: Error) { + onLoadingChange(false) + onCanGoBackChange(webView.canGoBack) + onCanGoForwardChange(webView.canGoForward) + onNavigationError(error) + } + + private func handlePageNavigation( + with webView: WKWebView, policy: WKNavigationAction, + action: @escaping (WKNavigationActionPolicy) -> Void + ) { + guard let handleNavigationAction = handleNavigationAction else { + action(.allow) + return + } + + let result = handleNavigationAction(webView, policy) + + switch result { + case .allow: + action(.allow) + case .cancel: + action(.cancel) + case .openExternal: + action(.cancel) + if let url = policy.request.url { + NSWorkspace.shared.open(url) + } + } + } + +#if compiler(>=6) + public func webView( + _ webView: WKWebView, + decidePolicyFor navigationAction: WKNavigationAction, + decisionHandler: @MainActor @escaping (WKNavigationActionPolicy) -> Void + ) { + handlePageNavigation( + with: webView, policy: navigationAction, + action: { result in + decisionHandler(result) + }) + } +#else + public func webView( + _ webView: WKWebView, + decidePolicyFor navigationAction: WKNavigationAction, + decisionHandler: @escaping (WKNavigationActionPolicy) -> Void + ) { + + handlePageNavigation( + with: webView, policy: navigationAction, + action: { result in + decisionHandler(result) + }) + } +#endif + } + + private struct WebView: NSViewRepresentable { + private let coordinator: WebViewCordinator + private let initWebViewConfig: ((_ webView: WKWebViewConfiguration) -> Void)? + private let initWebView: ((_ webView: WKWebView) -> Void)? + + init(coordinator: WebViewCordinator, + initWebViewConfig: ((_ webView: WKWebViewConfiguration) -> Void)?, + initWebView: ((_ webView: WKWebView) -> Void)?) { + self.coordinator = coordinator + self.initWebViewConfig = initWebViewConfig + self.initWebView = initWebView + } + + func makeNSView(context: Context) -> WKWebView { + let config = WKWebViewConfiguration() + initWebViewConfig?(config) + + + let webView = WKWebView(frame: .zero, configuration: config) + webView.navigationDelegate = coordinator + webView.setValue(false, forKey: "drawsBackground") + + initWebView?(webView) + + return webView + } + + func updateNSView(_ nsView: WKWebView, context: Context) { + nsView.navigationDelegate = coordinator + } + + func makeCoordinator() -> WebViewCordinator { + coordinator + } + + static func dismantleNSView(_ nsView: WKWebView, coordinator: Coordinator) { + nsView.stopLoading() + } + } + + public var body: some View { + WebView( + coordinator: WebViewCordinator( + onCanGoBackChange: { canGoBack = $0 }, + onCanGoForwardChange: { canGoForward = $0 }, + onLoadingChange: { isLoading = $0 }, + onURLChange: { url = $0 }, + handleNavigationAction: handleNavigationAction, + onNavigationError: { error in + DispatchQueue.main.async { + withAnimation { + self.error = error + } + } + } + ), initWebViewConfig: initWebViewConfig, initWebView: { view in + DispatchQueue.main.async { + self.view = view + initWebView?(view) + } + }) + .overlay { + if let error = error { + VStack(alignment: .center, spacing: 16) { + Image(systemName: "pc") + .resizable() + .scaledToFit() + .frame(width: 96, height: 96) + Text(String(format: String(localized: "genericWebView.error"), error.localizedDescription)) + .multilineTextAlignment(.center) + .font(.callout) + .opacity(0.6) + Button { + withAnimation { + self.error = nil + } + if let url = url, let view = view { + view.load(URLRequest(url: url)) + } + } label: { + Label("genericWebView.retry", systemImage: "arrow.clockwise") + .padding(6) + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity) + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) + .background(.ultraThinMaterial) + } + } + .onChange(of: url) { + guard let view = view else { return } + if let url = url, view.url != url { + view.load(URLRequest(url: url)) + } + } + .onChange(of: view) { + guard let view = view else { return } + + cancelables.removeAll() + + cancelables.insert(view.observe(\.url) { view, _ in + url = view.url + }) + cancelables.insert(view.observe(\.canGoBack) { view, _ in + canGoBack = view.canGoBack + }) + cancelables.insert(view.observe(\.canGoForward) { view, _ in + canGoForward = view.canGoForward + }) + + if let url = url, view.url != url { + view.load(URLRequest(url: url)) + } + } + } +} diff --git a/Sources/Views/Unified/Modules/WindowBlurView.swift b/Sources/Views/Components/WindowBlurView.swift similarity index 100% rename from Sources/Views/Unified/Modules/WindowBlurView.swift rename to Sources/Views/Components/WindowBlurView.swift diff --git a/Sources/Views/Hub/HubView.swift b/Sources/Views/Hub/HubView.swift new file mode 100644 index 00000000..efa65ee1 --- /dev/null +++ b/Sources/Views/Hub/HubView.swift @@ -0,0 +1,31 @@ +// +// HubView.swift +// Mythic +// + +import SwiftUI + +public struct HubView: View { + public var body: some View { + TabView { + HubLibraryView() + .tabItem { + Label("hubLibraryView.title", systemImage: "book") + } + HubStoreView() + .tabItem { + Label("hubStoreView.title", systemImage: "cart") + } + HubSettingsView() + .tabItem { + Label("hubSettingsView.title", systemImage: "gear") + } + } + .modifier(SparkleUpdaterSheetViewModifier()) + .frame(minWidth: 896, minHeight: 512) + } +} + +#Preview { + HubView() +} diff --git a/Sources/Views/Hub/Library/HubLibraryView.swift b/Sources/Views/Hub/Library/HubLibraryView.swift new file mode 100644 index 00000000..f4ab5ad7 --- /dev/null +++ b/Sources/Views/Hub/Library/HubLibraryView.swift @@ -0,0 +1,18 @@ +// +// HubLibraryView.swift +// Mythic +// + +import SwiftUI + +public struct HubLibraryView: View { + + + public var body: some View { + Text("Library View!") + } +} + +#Preview { + HubLibraryView() +} diff --git a/Sources/Views/Hub/Settings/HubSettingsView.swift b/Sources/Views/Hub/Settings/HubSettingsView.swift new file mode 100644 index 00000000..bebbc649 --- /dev/null +++ b/Sources/Views/Hub/Settings/HubSettingsView.swift @@ -0,0 +1,16 @@ +// +// HubSettingsView.swift +// Mythic +// + +import SwiftUI + +struct HubSettingsView: View { + var body: some View { + Text("Settings!") + } +} + +#Preview { + HubSettingsView() +} diff --git a/Sources/Views/Hub/Store/HubStoreEpicGamesView.swift b/Sources/Views/Hub/Store/HubStoreEpicGamesView.swift new file mode 100644 index 00000000..790caa17 --- /dev/null +++ b/Sources/Views/Hub/Store/HubStoreEpicGamesView.swift @@ -0,0 +1,151 @@ +// +// HubStoreEpicGamesView.swift +// Mythic +// + +import SwiftUI +import WebKit + +public struct HubStoreEpicGamesView: View { + private static let homepage: URL? = URL(string: "https://store.epicgames.com/") + + @State private var canGoBack: Bool = false + @State private var canGoForward: Bool = false + @State private var isLoading: Bool = false + @State private var url: URL? = homepage + @State private var view: WKWebView? + + public var body: some View { + GenericWebView(canGoBack: $canGoBack, + canGoForward: $canGoForward, + isLoading: $isLoading, + url: $url, + view: $view, + initWebView: { webView in + webView.allowsBackForwardNavigationGestures = true + }, + initWebViewConfig: { config in + config.websiteDataStore = .nonPersistent() + + if let accessToken = Legendary.accessToken, + let refreshToken = Legendary.refreshToken { + let accessCookie = HTTPCookie(properties: [ + .domain: ".store.epicgames.com", + .path: "/", + .name: "EPIC_EG1", + .value: accessToken, + .secure: "TRUE", + .expires: Date(timeIntervalSinceNow: 60 * 60 * 24 * 365) + ]) + let refreshCookie = HTTPCookie(properties: [ + .domain: ".store.epicgames.com", + .path: "/", + .name: "REFRESH_EPIC_EG1", + .value: refreshToken, + .secure: "TRUE", + .expires: Date(timeIntervalSinceNow: 60 * 60 * 24 * 365) + ]) + + if let accessCookie = accessCookie, let refreshCookie = refreshCookie { + config.websiteDataStore.httpCookieStore.setCookie(accessCookie) + config.websiteDataStore.httpCookieStore.setCookie(refreshCookie) + } + } + + let script = """ + window.addEventListener('DOMContentLoaded', function() { + const cssSelectorOf = function(element) { + const classes = element.getAttribute('class'); + if (!classes) return ''; + return classes.split(' ').map(c => `.${c}`).join(''); + }; + + const mainRoot = document.getElementsByTagName('main')[0]; + if (!mainRoot) return; + const previousSibling = mainRoot.previousElementSibling; + const nextSibling = mainRoot.nextElementSibling; + if (!previousSibling || !nextSibling) return; + const css = ` + body { + overflow: overlay; + } + ::-webkit-scrollbar { + background-color: transparent; + } + ${cssSelectorOf(previousSibling)}, ${cssSelectorOf(nextSibling)} { + display: none !important; + } + `; + const viewportMeta = document.createElement('meta'); + viewportMeta.name = 'viewport'; + viewportMeta.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0'; + document.head.appendChild(viewportMeta); + const style = document.createElement('style'); + style.innerHTML = css; + document.head.appendChild(style); + }); + """ + let userScript = WKUserScript(source: script, injectionTime: .atDocumentStart, forMainFrameOnly: true) + config.userContentController.addUserScript(userScript) + }, + handleNavigationAction: { _, policy in + guard let url = policy.request.url else { return .cancel } + + if url.host != nil && url.scheme == "https" { + return .allow + } + + return .cancel + }) + + .navigationTitle("hubStoreEpicGamesView.title") + .toolbar { + ToolbarItem(placement: .navigation) { + Button { + if let view = view { + view.goBack() + } + } label: { + Label("common.back", systemImage: "chevron.left") + } + .disabled(!canGoBack) + } + ToolbarItem(placement: .navigation) { + Button { + if let view = view { + view.goForward() + } + } label: { + Label("common.forward", systemImage: "chevron.right") + } + .disabled(!canGoForward) + } + ToolbarItem(placement: .navigation) { + Button { + if let view = view { + view.reload() + } + } label: { + Label("common.refresh", systemImage: "arrow.clockwise") + } + } + ToolbarItem(placement: .navigation) { + if isLoading { + ProgressView() + .controlSize(.small) + } + } + ToolbarItem(placement: .primaryAction) { + Button { + url = Self.homepage + } label: { + Label("common.homepage", systemImage: "house") + } + } + } + } +} + +#Preview { + HubStoreEpicGamesView() +} diff --git a/Sources/Views/Hub/Store/HubStoreView.swift b/Sources/Views/Hub/Store/HubStoreView.swift new file mode 100644 index 00000000..042a5ae8 --- /dev/null +++ b/Sources/Views/Hub/Store/HubStoreView.swift @@ -0,0 +1,16 @@ +// +// HubStoreView.swift +// Mythic +// + +import SwiftUI + +public struct HubStoreView: View { + public var body: some View { + HubStoreEpicGamesView() + } +} + +#Preview { + HubStoreView() +} diff --git a/Sources/Views/Onboarding/OnboardingEpicGamesLoginStepView.swift b/Sources/Views/Onboarding/OnboardingEpicGamesLoginStepView.swift index c51773ad..3f363435 100644 --- a/Sources/Views/Onboarding/OnboardingEpicGamesLoginStepView.swift +++ b/Sources/Views/Onboarding/OnboardingEpicGamesLoginStepView.swift @@ -74,14 +74,14 @@ public struct OnboardingEpicGamesLoginStepView: View { URLSession.shared.dataTask(with: url) { data, response, error in guard let data = data, let string = String(data: data, encoding: .utf8) else { - action(false) + action(true) return } let decoder = JSONDecoder() guard let jsonData = string.data(using: .utf8), let json = try? decoder.decode(JSONData.self, from: jsonData) else { - action(false) + action(true) return } @@ -309,7 +309,7 @@ public struct OnboardingEpicGamesLoginStepView: View { var signInSuccess: Bool = false var errorString: String? do { - try await Legendary.command(arguments: ["auth", "--delete"], identifier: "signout") { _ in } + try? await Legendary.command(arguments: ["auth", "--delete"], identifier: "signout") { _ in } try await Legendary.signIn(authKey: authorizationCode) signInSuccess = true } catch { diff --git a/Sources/Views/Onboarding/OnboardingView.swift b/Sources/Views/Onboarding/OnboardingView.swift index 2731793b..ed60c4b4 100644 --- a/Sources/Views/Onboarding/OnboardingView.swift +++ b/Sources/Views/Onboarding/OnboardingView.swift @@ -226,6 +226,7 @@ struct OnboardingView: View { } .background(WindowBlurView().ignoresSafeArea()) .frame(minWidth: 768, minHeight: 512) + .modifier(SparkleUpdaterSheetViewModifier()) } private func getNextStep() -> Step? { diff --git a/Sources/Windows/Hub/HubWindow.xib b/Sources/Windows/Hub/HubWindow.xib index cb9b3150..db60bb3d 100644 --- a/Sources/Windows/Hub/HubWindow.xib +++ b/Sources/Windows/Hub/HubWindow.xib @@ -13,11 +13,11 @@ - + - + diff --git a/Sources/Windows/Hub/HubWindowController.swift b/Sources/Windows/Hub/HubWindowController.swift index bcf4a9d3..d6e144f8 100644 --- a/Sources/Windows/Hub/HubWindowController.swift +++ b/Sources/Windows/Hub/HubWindowController.swift @@ -14,7 +14,7 @@ public class HubWindowController: NSWindowController, NSWindowDelegate { guard let window = window else { return } window.center() window.isMovableByWindowBackground = true - window.contentView = NSHostingView(rootView: MainView()) + window.contentView = NSHostingView(rootView: HubView()) } deinit { diff --git a/Sources/Windows/Setup/SetupWindow.xib b/Sources/Windows/Setup/SetupWindow.xib index 3aa2ae8d..41df3b3f 100644 --- a/Sources/Windows/Setup/SetupWindow.xib +++ b/Sources/Windows/Setup/SetupWindow.xib @@ -17,7 +17,7 @@ - +