diff --git a/.gitea/workflows/updateApp.yml b/.gitea/workflows/updateApp.yml new file mode 100644 index 000000000..0c4e7429b --- /dev/null +++ b/.gitea/workflows/updateApp.yml @@ -0,0 +1,49 @@ +name: Update apps.json on new release + +on: + release: + types: [published] + +jobs: + update: + runs-on: debian-trixie + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt-get install -y jq + + - name: Extract release data + id: release + run: | + echo "VERSION=${GITEA_REF_NAME}" >> $GITHUB_OUTPUT + echo "DESCRIPTION=$(echo '${GITEA_EVENT_RELEASE_BODY}' | jq -Rs .)" >> $GITHUB_OUTPUT + echo "DATE=$(date '+%Y-%m-%d')" >> $GITHUB_OUTPUT + IPA_URL=$(echo '${GITEA_EVENT_RELEASE_ASSETS}' | jq -r '.[0].browser_download_url') + echo "DOWNLOAD_URL=$IPA_URL" >> $GITHUB_OUTPUT + + - name: Update apps.json + run: | + jq --arg version "${{ steps.release.outputs.VERSION }}" \ + --arg buildVersion "1" \ + --arg date "${{ steps.release.outputs.DATE }}" \ + --arg localizedDescription "${{ steps.release.outputs.DESCRIPTION }}" \ + --arg downloadURL "${{ steps.release.outputs.DOWNLOAD_URL }}" \ + '.apps[0].versions |= [{"version": $version, "buildVersion": $buildVersion, "date": $date, "localizedDescription": $localizedDescription, "downloadURL": $downloadURL, "minOSVersion": "15.0"}]' \ + apps.json > tmp.json && mv tmp.json apps.json + + - name: Commit and push + run: | + git config user.name "gitea-actions" + git config user.email "gitea-actions@localhost" + git add apps.json + git commit -m "Update apps.json for release ${{ steps.release.outputs.VERSION }}" + git push + env: + GIT_AUTHOR_NAME: gitea-actions + GIT_AUTHOR_EMAIL: gitea-actions@localhost + GIT_COMMITTER_NAME: gitea-actions + GIT_COMMITTER_EMAIL: gitea-actions@localhost diff --git a/source.json b/source.json new file mode 100644 index 000000000..c62614502 --- /dev/null +++ b/source.json @@ -0,0 +1,48 @@ +{ + "name": "MeloNX", + "subtitle": "A source for the MeloNX Application", + "description": "Welcome to the MeloNX source! The latest download for MeloNX.", + "iconURL": "https://git.743378673.xyz/CycloKid/assets/media/branch/main/Melo/AppIcons/MeloNX.png", + "headerURL": "https://cdn.discordapp.com/attachments/1320760161836466257/1331670540447912090/melon-x-not-melo-nx-amiright-guys.png?ex=67f556d6&is=67f40556&hm=71be8f109a14f1c47d8f4965aa017bccb5617962b7a9f5cdfb936a5a8135dad7&", + "website": "https://MeloNX.org", + "tintColor": "#AE34EB", + "featuredApps": [ + "com.stossy11.MeloNX" + ], + "apps": [ + { + "name": "MeloNX", + "bundleIdentifier": "com.stossy11.MeloNX", + "developerName": "Stossy11", + "subtitle": "An NX Emulator.", + "localizedDescription": "MeloNX is an iOS Nintendo Switch emulator based on Ryujinx, written primarily in C#. Designed to bring accurate performance and a user-friendly interface to iOS, MeloNX makes Switch games accessible on Apple devices. Developed from the ground up, MeloNX is open-source and available on Github under the MeloNX license (Based on MIT) (requires increased memory limit)", + "iconURL": "https://example.com/myapp_icon.png", + "tintColor": "#AE34EB", + "category": "games", + "screenshots": [ + "https://git.743378673.xyz/stossy11/screenshots/raw/branch/main/IMG_0380.PNG", + "https://git.743378673.xyz/stossy11/screenshots/raw/branch/main/IMG_0381.PNG" + ], + "versions": [ + { + "version": "1.7.0", + "buildVersion": "1", + "date": "2025-04-8", + "localizedDescription": "First AltStore release!", + "downloadURL": "https://git.743378673.xyz/MeloNX/MeloNX/releases/download/1.7.0/MeloNX.ipa", + "minOSVersion": "15.0" + } + ], + "appPermissions": { + "entitlements": [ + "com.apple.developer.kernel.increased-memory-limit" + ], + "privacy": { + "NSPhotoLibraryAddUsageDescription": "MeloNX needs access to your Photo Library in order to save images." + } + }, + "patreon": {} + } + ], + "news": [] +} diff --git a/src/MeloNX/MeloNX.xcodeproj/project.pbxproj b/src/MeloNX/MeloNX.xcodeproj/project.pbxproj index 184998d30..17c42c37d 100644 --- a/src/MeloNX/MeloNX.xcodeproj/project.pbxproj +++ b/src/MeloNX/MeloNX.xcodeproj/project.pbxproj @@ -128,10 +128,6 @@ "Dependencies/Dynamic Libraries/libavutil.dylib" = ( CodeSignOnCopy, ); - Dependencies/XCFrameworks/MoltenVK.xcframework = ( - CodeSignOnCopy, - RemoveHeadersOnCopy, - ); Dependencies/XCFrameworks/SDL2.xcframework = ( CodeSignOnCopy, RemoveHeadersOnCopy, @@ -185,7 +181,6 @@ Dependencies/XCFrameworks/libswresample.xcframework, Dependencies/XCFrameworks/libswscale.xcframework, Dependencies/XCFrameworks/libteakra.xcframework, - Dependencies/XCFrameworks/MoltenVK.xcframework, Dependencies/XCFrameworks/SDL2.xcframework, ); }; @@ -648,7 +643,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = 95J8WZ4TN8; + DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; ENABLE_TESTABILITY = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -727,6 +722,8 @@ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", ); GCC_OPTIMIZATION_LEVEL = z; GENERATE_INFOPLIST_FILE = YES; @@ -742,7 +739,7 @@ INFOPLIST_KEY_UIRequiresFullScreen = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; INFOPLIST_KEY_UISupportsDocumentBrowser = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -889,6 +886,16 @@ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", ); MARKETING_VERSION = "$(VERSION)"; PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX; @@ -991,6 +998,8 @@ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", ); GCC_OPTIMIZATION_LEVEL = z; GENERATE_INFOPLIST_FILE = YES; @@ -1006,7 +1015,7 @@ INFOPLIST_KEY_UIRequiresFullScreen = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; INFOPLIST_KEY_UISupportsDocumentBrowser = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1153,6 +1162,16 @@ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", + "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries", ); MARKETING_VERSION = "$(VERSION)"; PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX; diff --git a/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/stossy11.xcuserdatad/UserInterfaceState.xcuserstate b/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/stossy11.xcuserdatad/UserInterfaceState.xcuserstate index dd00f3279..f637a4a0a 100644 Binary files a/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/stossy11.xcuserdatad/UserInterfaceState.xcuserstate and b/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/stossy11.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/src/MeloNX/MeloNX/App/Core/Entitlements/EntitlementChecker.swift b/src/MeloNX/MeloNX/App/Core/Entitlements/EntitlementChecker.swift index 9c9d41ad8..6883f7dfc 100644 --- a/src/MeloNX/MeloNX/App/Core/Entitlements/EntitlementChecker.swift +++ b/src/MeloNX/MeloNX/App/Core/Entitlements/EntitlementChecker.swift @@ -31,12 +31,12 @@ func SecTaskCopyValuesForEntitlements( func checkAppEntitlements(_ ents: [String]) -> [String: Any] { guard let task = SecTaskCreateFromSelf(nil) else { - print("Failed to create SecTask") + // print("Failed to create SecTask") return [:] } guard let entitlements = SecTaskCopyValuesForEntitlements(task, ents as CFArray, nil) else { - print("Failed to get entitlements") + // print("Failed to get entitlements") return [:] } @@ -45,12 +45,12 @@ func checkAppEntitlements(_ ents: [String]) -> [String: Any] { func checkAppEntitlement(_ ent: String) -> Bool { guard let task = SecTaskCreateFromSelf(nil) else { - print("Failed to create SecTask") + // print("Failed to create SecTask") return false } guard let entitlements = SecTaskCopyValueForEntitlement(task, ent as NSString, nil) else { - print("Failed to get entitlements") + // print("Failed to get entitlements") return false } diff --git a/src/MeloNX/MeloNX/App/Core/JIT/IsJITEnabled.swift b/src/MeloNX/MeloNX/App/Core/JIT/IsJITEnabled.swift index 44fbf1a72..ec328c68f 100644 --- a/src/MeloNX/MeloNX/App/Core/JIT/IsJITEnabled.swift +++ b/src/MeloNX/MeloNX/App/Core/JIT/IsJITEnabled.swift @@ -34,7 +34,7 @@ func checkMemoryPermissions(at address: UnsafeRawPointer) -> Bool { } if result != KERN_SUCCESS { - print("Failed to reach \(address)") + // print("Failed to reach \(address)") return false } diff --git a/src/MeloNX/MeloNX/App/Core/JIT/JitStreamerEB/EnableJIT.swift b/src/MeloNX/MeloNX/App/Core/JIT/JitStreamerEB/EnableJIT.swift index 93d8b0b0f..23f747397 100644 --- a/src/MeloNX/MeloNX/App/Core/JIT/JitStreamerEB/EnableJIT.swift +++ b/src/MeloNX/MeloNX/App/Core/JIT/JitStreamerEB/EnableJIT.swift @@ -23,7 +23,7 @@ func enableJITEB() { func enableJITEBRequest() { let pid = Int(getpid()) - print(pid) + // print(pid) let address = URL(string: "http://[fd00::]:9172/attach/\(pid)")! var request = URLRequest(url: address) @@ -90,7 +90,7 @@ func pingSite(host: String = "http://[fd00::]:9172/hello", completion: @escaping let task = session.dataTask(with: request) { _, response, error in if let error = error { - print("Ping failed: \(error.localizedDescription)") + // print("Ping failed: \(error.localizedDescription)") completion(false) } else if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 { completion(true) @@ -140,12 +140,12 @@ func showLaunchAppAlert(jsonData: Data, in viewController: UIViewController) { viewController.present(alert, animated: true) } } else { - print("Hopefully JIT is enabled now...") + // print("Hopefully JIT is enabled now...") Ryujinx.shared.ryuIsJITEnabled() } } catch { - print(String(data: jsonData, encoding: .utf8)) + // print(String(data: jsonData, encoding: .utf8)) let alert = UIAlertController(title: "Decoding Error", message: error.localizedDescription, preferredStyle: .alert) alert.addAction(UIAlertAction(title: "OK", style: .default)) diff --git a/src/MeloNX/MeloNX/App/Core/JIT/StikJIT/StikEnableJIT.swift b/src/MeloNX/MeloNX/App/Core/JIT/StikJIT/StikEnableJIT.swift index 25a6806fb..509ab4c1f 100644 --- a/src/MeloNX/MeloNX/App/Core/JIT/StikJIT/StikEnableJIT.swift +++ b/src/MeloNX/MeloNX/App/Core/JIT/StikJIT/StikEnableJIT.swift @@ -13,24 +13,7 @@ func enableJITStik() { let bundleid = Bundle.main.bundleIdentifier ?? "Unknown" let address = URL(string: "stikjit://enable-jit?bundle-id=\(bundleid)")! - var request = URLRequest(url: address) - request.httpMethod = "POST" - request.setValue("application/json", forHTTPHeaderField: "Content-Type") - - let task = URLSession.shared.dataTask(with: request) { data, response, error in - if let error = error { - presentAlert(title: "Request Error", message: error.localizedDescription) - return - } - - DispatchQueue.main.async { - if let data = data, let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene { - // JIT, wow - } else { - fatalError("Unable to get Window") - } - } + if UIApplication.shared.canOpenURL(address) { + UIApplication.shared.open(address) } - - task.resume() } diff --git a/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/NativeController.swift b/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/NativeController.swift index c26d23fdb..54bc1533c 100644 --- a/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/NativeController.swift +++ b/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/NativeController.swift @@ -49,50 +49,50 @@ class NativeController: Hashable { // Update joystick state here }, SetPlayerIndex: { userdata, playerIndex in - print("Player index set to \(playerIndex)") + // print("Player index set to \(playerIndex)") }, Rumble: { userdata, lowFreq, highFreq in - print("Rumble with \(lowFreq), \(highFreq)") + // print("Rumble with \(lowFreq), \(highFreq)") guard let userdata else { return 0 } let _self = Unmanaged.fromOpaque(userdata).takeUnretainedValue() VirtualController.rumble(lowFreq: Float(lowFreq), highFreq: Float(highFreq), engine: _self.controllerHaptics) return 0 }, RumbleTriggers: { userdata, leftRumble, rightRumble in - print("Trigger rumble with \(leftRumble), \(rightRumble)") + // print("Trigger rumble with \(leftRumble), \(rightRumble)") return 0 }, SetLED: { userdata, red, green, blue in - print("Set LED to RGB(\(red), \(green), \(blue))") + // print("Set LED to RGB(\(red), \(green), \(blue))") return 0 }, SendEffect: { userdata, data, size in - print("Effect sent with size \(size)") + // print("Effect sent with size \(size)") return 0 } ) instanceID = SDL_JoystickAttachVirtualEx(&joystickDesc)// SDL_JoystickAttachVirtual(SDL_JoystickType(SDL_JOYSTICK_TYPE_GAMECONTROLLER.rawValue), 6, 15, 1) if instanceID < 0 { - print("Failed to create virtual joystick: \(String(cString: SDL_GetError()))") + // print("Failed to create virtual joystick: \(String(cString: SDL_GetError()))") return } controller = SDL_GameControllerOpen(Int32(instanceID)) if controller == nil { - print("Failed to create virtual controller: \(String(cString: SDL_GetError()))") + // print("Failed to create virtual controller: \(String(cString: SDL_GetError()))") return } if #available(iOS 16, *) { guard let gamepad = nativeController.extendedGamepad else { return } - - setupButtonChangeListener(gamepad.buttonA, for: .A) - setupButtonChangeListener(gamepad.buttonB, for: .B) - setupButtonChangeListener(gamepad.buttonX, for: .X) - setupButtonChangeListener(gamepad.buttonY, for: .Y) + + setupButtonChangeListener(gamepad.buttonA, for: UserDefaults.standard.bool(forKey: "swapBandA") ? .B : .A) + setupButtonChangeListener(gamepad.buttonB, for: UserDefaults.standard.bool(forKey: "swapBandA") ? .A : .B) + setupButtonChangeListener(gamepad.buttonX, for: UserDefaults.standard.bool(forKey: "swapBandA") ? .Y : .X) + setupButtonChangeListener(gamepad.buttonY, for: UserDefaults.standard.bool(forKey: "swapBandA") ? .X : .Y) setupButtonChangeListener(gamepad.dpad.up, for: .dPadUp) setupButtonChangeListener(gamepad.dpad.down, for: .dPadDown) @@ -139,7 +139,7 @@ class NativeController: Hashable { func setupTriggerChangeListener(_ button: GCControllerButtonInput, for key: ThumbstickType) { button.valueChangedHandler = { [unowned self] _, value, pressed in -// print("Value: \(value), Is pressed: \(pressed)") +// // print("Value: \(value), Is pressed: \(pressed)") let axis: SDL_GameControllerAxis = (key == .left) ? SDL_CONTROLLER_AXIS_TRIGGERLEFT : SDL_CONTROLLER_AXIS_TRIGGERRIGHT let scaledValue = Sint16(value * 32767.0) updateAxisValue(value: scaledValue, forAxis: axis) @@ -177,7 +177,7 @@ class NativeController: Hashable { try highFreqPlayer.start(atTime: 0.2) } catch { - print("Error creating haptic patterns: \(error)") + // print("Error creating haptic patterns: \(error)") } } @@ -206,7 +206,7 @@ class NativeController: Hashable { func setButtonState(_ state: Uint8, for button: VirtualControllerButton) { guard controller != nil else { return } -// print("Button: \(button.rawValue) {state: \(state)}") +// // print("Button: \(button.rawValue) {state: \(state)}") if (button == .leftTrigger || button == .rightTrigger) && (state == 1 || state == 0) { let axis: SDL_GameControllerAxis = (button == .leftTrigger) ? SDL_CONTROLLER_AXIS_TRIGGERLEFT : SDL_CONTROLLER_AXIS_TRIGGERRIGHT let value: Int = (state == 1) ? 32767 : 0 diff --git a/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/VirtualController.swift b/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/VirtualController.swift index 810ed604f..da6a73a29 100644 --- a/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/VirtualController.swift +++ b/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/VirtualController.swift @@ -41,39 +41,39 @@ class VirtualController { // Update joystick state here }, SetPlayerIndex: { userdata, playerIndex in - print("Player index set to \(playerIndex)") + // print("Player index set to \(playerIndex)") }, Rumble: { userdata, lowFreq, highFreq in - print("Rumble with \(lowFreq), \(highFreq)") + // print("Rumble with \(lowFreq), \(highFreq)") if UIDevice.current.userInterfaceIdiom == .phone { VirtualController.rumble(lowFreq: Float(lowFreq), highFreq: Float(highFreq)) } return 0 }, RumbleTriggers: { userdata, leftRumble, rightRumble in - print("Trigger rumble with \(leftRumble), \(rightRumble)") + // print("Trigger rumble with \(leftRumble), \(rightRumble)") return 0 }, SetLED: { userdata, red, green, blue in - print("Set LED to RGB(\(red), \(green), \(blue))") + // print("Set LED to RGB(\(red), \(green), \(blue))") return 0 }, SendEffect: { userdata, data, size in - print("Effect sent with size \(size)") + // print("Effect sent with size \(size)") return 0 } ) instanceID = SDL_JoystickAttachVirtualEx(&joystickDesc)// SDL_JoystickAttachVirtual(SDL_JoystickType(SDL_JOYSTICK_TYPE_GAMECONTROLLER.rawValue), 6, 15, 1) if instanceID < 0 { - print("Failed to create virtual joystick: \(String(cString: SDL_GetError()))") + // print("Failed to create virtual joystick: \(String(cString: SDL_GetError()))") return } controller = SDL_GameControllerOpen(Int32(instanceID)) if controller == nil { - print("Failed to create virtual controller: \(String(cString: SDL_GetError()))") + // print("Failed to create virtual controller: \(String(cString: SDL_GetError()))") return } } @@ -107,7 +107,7 @@ class VirtualController { } guard let engine else { - return print("Error creating haptic patterns: hapticEngine is nil") + return // print("Error creating haptic patterns: hapticEngine is nil") } let lowFreqPlayer = try engine.makePlayer(with: lowFreqPattern) @@ -117,7 +117,7 @@ class VirtualController { try highFreqPlayer.start(atTime: 0) } catch { - print("Error creating haptic patterns: \(error)") + // print("Error creating haptic patterns: \(error)") } } @@ -146,7 +146,7 @@ class VirtualController { func setButtonState(_ state: Uint8, for button: VirtualControllerButton) { guard controller != nil else { return } - print("Button: \(button.rawValue) {state: \(state)}") + // // print("Button: \(button.rawValue) {state: \(state)}") if (button == .leftTrigger || button == .rightTrigger) && (state == 1 || state == 0) { let axis: SDL_GameControllerAxis = (button == .leftTrigger) ? SDL_CONTROLLER_AXIS_TRIGGERLEFT : SDL_CONTROLLER_AXIS_TRIGGERRIGHT let value: Int = (state == 1) ? 32767 : 0 diff --git a/src/MeloNX/MeloNX/App/Core/Ryujinx/Display/MemoryDisplay/MemoryUsageMonitor.swift b/src/MeloNX/MeloNX/App/Core/Ryujinx/Display/MemoryDisplay/MemoryUsageMonitor.swift index 06070a38b..c67422752 100644 --- a/src/MeloNX/MeloNX/App/Core/Ryujinx/Display/MemoryDisplay/MemoryUsageMonitor.swift +++ b/src/MeloNX/MeloNX/App/Core/Ryujinx/Display/MemoryDisplay/MemoryUsageMonitor.swift @@ -35,8 +35,8 @@ class MemoryUsageMonitor: ObservableObject { memoryUsage = taskInfo.phys_footprint } else { - print("Error with task_info(): " + - (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error")) + // print("Error with task_info(): " + + // (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error")) } } diff --git a/src/MeloNX/MeloNX/App/Core/Ryujinx/MetalHUD/MTLHUD.swift b/src/MeloNX/MeloNX/App/Core/Ryujinx/MetalHUD/MTLHUD.swift index 27280051b..7b7ec9bed 100644 --- a/src/MeloNX/MeloNX/App/Core/Ryujinx/MetalHUD/MTLHUD.swift +++ b/src/MeloNX/MeloNX/App/Core/Ryujinx/MetalHUD/MTLHUD.swift @@ -32,7 +32,7 @@ class MTLHud { } func toggle() { - print(UserDefaults.standard.bool(forKey: "MTL_HUD_ENABLED")) + // print(UserDefaults.standard.bool(forKey: "MTL_HUD_ENABLED")) if UserDefaults.standard.bool(forKey: "MTL_HUD_ENABLED") { enable() } else { @@ -44,12 +44,12 @@ class MTLHud { let path = "/usr/lib/libMTLHud.dylib" if dlopen(path, RTLD_NOW) != nil { - print("Library loaded from \(path)") + // print("Library loaded from \(path)") canMetalHud = true return true } else { if let error = String(validatingUTF8: dlerror()) { - print("Error loading library: \(error)") + // print("Error loading library: \(error)") } canMetalHud = false return false diff --git a/src/MeloNX/MeloNX/App/Core/Ryujinx/Ryujinx.swift b/src/MeloNX/MeloNX/App/Core/Ryujinx/Ryujinx.swift index 6e5555a69..eea8c81a9 100644 --- a/src/MeloNX/MeloNX/App/Core/Ryujinx/Ryujinx.swift +++ b/src/MeloNX/MeloNX/App/Core/Ryujinx/Ryujinx.swift @@ -11,6 +11,93 @@ import GameController import MetalKit import Metal +class LogCapture { + static let shared = LogCapture() + + private var stdoutPipe: Pipe? + private var stderrPipe: Pipe? + private let originalStdout: Int32 + private let originalStderr: Int32 + + var capturedLogs: [String] = [] { + didSet { + DispatchQueue.main.async { + NotificationCenter.default.post(name: .newLogCaptured, object: nil) + } + } + } + + private init() { + originalStdout = dup(STDOUT_FILENO) + originalStderr = dup(STDERR_FILENO) + startCapturing() + } + + func startCapturing() { + stdoutPipe = Pipe() + stderrPipe = Pipe() + + redirectOutput(to: stdoutPipe!, fileDescriptor: STDOUT_FILENO) + redirectOutput(to: stderrPipe!, fileDescriptor: STDERR_FILENO) + + setupReadabilityHandler(for: stdoutPipe!, isStdout: true) + setupReadabilityHandler(for: stderrPipe!, isStdout: false) + } + + func stopCapturing() { + dup2(originalStdout, STDOUT_FILENO) + dup2(originalStderr, STDERR_FILENO) + + stdoutPipe?.fileHandleForReading.readabilityHandler = nil + stderrPipe?.fileHandleForReading.readabilityHandler = nil + } + + private func redirectOutput(to pipe: Pipe, fileDescriptor: Int32) { + dup2(pipe.fileHandleForWriting.fileDescriptor, fileDescriptor) + } + + private func setupReadabilityHandler(for pipe: Pipe, isStdout: Bool) { + pipe.fileHandleForReading.readabilityHandler = { [weak self] fileHandle in + let data = fileHandle.availableData + let originalFD = isStdout ? self?.originalStdout : self?.originalStderr + write(originalFD ?? STDOUT_FILENO, (data as NSData).bytes, data.count) + + if let logString = String(data: data, encoding: .utf8), + let cleanedLog = self?.cleanLog(logString), !cleanedLog.isEmpty { + self?.capturedLogs.append(cleanedLog) + } + } + } + + private func cleanLog(_ raw: String) -> String? { + let lines = raw.split(separator: "\n") + let filteredLines = lines.filter { line in + !line.contains("SwiftUI") && + !line.contains("ForEach") && + !line.contains("VStack") && + !line.contains("Invalid frame dimension (negative or non-finite).") + } + + let cleaned = filteredLines.map { line -> String in + if let tabRange = line.range(of: "\t") { + return line[tabRange.upperBound...].trimmingCharacters(in: .whitespacesAndNewlines) + } + return line.trimmingCharacters(in: .whitespacesAndNewlines) + }.joined(separator: "\n") + + return cleaned.isEmpty ? nil : cleaned.replacingOccurrences(of: "\n\n", with: "\n") + } + + deinit { + stopCapturing() + } +} + + +extension Notification.Name { + static let newLogCaptured = Notification.Name("newLogCaptured") +} + struct Controller: Identifiable, Hashable { var id: String var name: String @@ -46,7 +133,7 @@ class Ryujinx : ObservableObject { @Published var defMLContentSize: CGFloat? - var thread: Thread! + var thread: Thread = Thread { } @Published var jitenabled = false @@ -150,6 +237,7 @@ class Ryujinx : ObservableObject { self.config = config + thread = Thread { [self] in isRunning = true @@ -180,7 +268,35 @@ class Ryujinx : ObservableObject { } } catch { self.isRunning = false - Self.log("Emulation failed to start: \(error)") + Thread.sleep(forTimeInterval: 0.3) + let logs = LogCapture.shared.capturedLogs + let parsedLogs = extractExceptionInfo(logs) + if let parsedLogs { + DispatchQueue.main.async { + let result = Array(logs.suffix(from: parsedLogs.lineIndex)) + + LogCapture.shared.capturedLogs = Array(LogCapture.shared.capturedLogs.prefix(upTo: parsedLogs.lineIndex)) + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd_HH-mm-ss" + let currentDate = Date() + let dateString = dateFormatter.string(from: currentDate) + let path = URL.documentsDirectory.appendingPathComponent("StackTrace").appendingPathComponent("StackTrace-\(dateString).txt").path + + self.saveArrayAsTextFile(strings: result, filePath: path) + + + presentAlert(title: "MeloNX Crashed!", message: parsedLogs.exceptionType + ": " + parsedLogs.message) { + + assert(true, parsedLogs.exceptionType) + } + } + } else { + DispatchQueue.main.async { + presentAlert(title: "MeloNX Crashed!", message: "Unknown Error") { + assert(true, "Exception was not detected") + } + } + } } } @@ -188,8 +304,63 @@ class Ryujinx : ObservableObject { thread.name = "MeloNX" thread.start() } + + func saveArrayAsTextFile(strings: [String], filePath: String) { + let text = strings.joined(separator: "\n") + + let path = URL.documentsDirectory.appendingPathComponent("StackTrace").path + + do { + try FileManager.default.createDirectory(atPath: path, withIntermediateDirectories: false) + } catch { + + } + + do { + try text.write(to: URL(fileURLWithPath: filePath), atomically: true, encoding: .utf8) + print("File saved successfully.") + } catch { + print("Error saving file: \(error)") + } + } + + struct ExceptionInfo { + let exceptionType: String + let message: String + let lineIndex: Int + } + func extractExceptionInfo(_ logs: [String]) -> ExceptionInfo? { + for i in (0.. Bool { diff --git a/src/MeloNX/MeloNX/App/Intents/LaunchGameIntent.swift b/src/MeloNX/MeloNX/App/Intents/LaunchGameIntent.swift index 481dd7724..33b8407e2 100644 --- a/src/MeloNX/MeloNX/App/Intents/LaunchGameIntent.swift +++ b/src/MeloNX/MeloNX/App/Intents/LaunchGameIntent.swift @@ -35,7 +35,7 @@ struct LaunchGameIntentDef: AppIntent { let name = findClosestGameName(input: gameName, games: ryujinx.compactMap(\.titleName)) let urlString = "melonx://game?name=\(name ?? gameName)" - print(urlString) + // print(urlString) if let url = URL(string: urlString) { UIApplication.shared.open(url, options: [:], completionHandler: nil) } diff --git a/src/MeloNX/MeloNX/App/Models/Game.swift b/src/MeloNX/MeloNX/App/Models/Game.swift index 1a154e869..af5751e44 100644 --- a/src/MeloNX/MeloNX/App/Models/Game.swift +++ b/src/MeloNX/MeloNX/App/Models/Game.swift @@ -57,7 +57,7 @@ public struct Game: Identifiable, Equatable, Hashable { gameTemp.icon = UIImage(data: imageData) } else { - print("Invalid image size.") + // print("Invalid image size.") } return gameTemp } @@ -67,7 +67,7 @@ public struct Game: Identifiable, Equatable, Hashable { let imageSize = Int(gameInfoValue.ImageSize) guard imageSize > 0, imageSize <= 1024 * 1024 else { - print("Invalid image size.") + // print("Invalid image size.") return nil } diff --git a/src/MeloNX/MeloNX/App/Views/Main/ContentView.swift b/src/MeloNX/MeloNX/App/Views/Main/ContentView.swift index af7cfc31e..407f98eb3 100644 --- a/src/MeloNX/MeloNX/App/Views/Main/ContentView.swift +++ b/src/MeloNX/MeloNX/App/Views/Main/ContentView.swift @@ -80,7 +80,7 @@ struct ContentView: View { _settings = State(initialValue: defaultSettings) - print(SDL_CONTROLLER_BUTTON_LEFTSTICK.rawValue) + // print(SDL_CONTROLLER_BUTTON_LEFTSTICK.rawValue) initializeSDL() } @@ -120,7 +120,7 @@ struct ContentView: View { private var jitErrorView: some View { Text("") - .sheet(isPresented:Binding( + .fullScreenCover(isPresented:Binding( get: { !ryujinx.jitenabled }, set: { newValue in ryujinx.jitenabled = newValue @@ -131,7 +131,7 @@ struct ContentView: View { JITPopover() { ryujinx.jitenabled = false } - .interactiveDismissDisabled() + // .interactiveDismissDisabled() } } @@ -154,7 +154,7 @@ struct ContentView: View { } - print(MTLHud.shared.isEnabled) + // print(MTLHud.shared.isEnabled) initControllerObservers() @@ -289,7 +289,7 @@ struct ContentView: View { queue: .main ) { notification in if let controller = notification.object as? GCController { - print("Controller connected: \(controller.productCategory)") + // print("Controller connected: \(controller.productCategory)") nativeControllers[controller] = .init(controller) refreshControllersList() } @@ -301,7 +301,7 @@ struct ContentView: View { queue: .main ) { notification in if let controller = notification.object as? GCController { - print("Controller disconnected: \(controller.productCategory)") + // print("Controller disconnected: \(controller.productCategory)") nativeControllers[controller]?.cleanup() nativeControllers[controller] = nil refreshControllersList() @@ -355,7 +355,7 @@ struct ContentView: View { do { try ryujinx.start(with: config) } catch { - print("Error: \(error.localizedDescription)") + // print("Error: \(error.localizedDescription)") } } @@ -366,7 +366,7 @@ struct ContentView: View { } if syncqsubmits { - setenv("MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS", "2", 1) + setenv("MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS", "1", 1) } } @@ -389,7 +389,7 @@ struct ContentView: View { } else if jitStreamerEB { enableJITEB() } else { - print("no JIT") + // print("no JIT") } } } diff --git a/src/MeloNX/MeloNX/App/Views/Main/ControllerView/ControllerView.swift b/src/MeloNX/MeloNX/App/Views/Main/ControllerView/ControllerView.swift index 7aee87c93..3a40027d6 100644 --- a/src/MeloNX/MeloNX/App/Views/Main/ControllerView/ControllerView.swift +++ b/src/MeloNX/MeloNX/App/Views/Main/ControllerView/ControllerView.swift @@ -305,7 +305,6 @@ struct ButtonView: View { } ) .onAppear { - print(String(buttonText.dropFirst(2))) configureSizeForButton() } } diff --git a/src/MeloNX/MeloNX/App/Views/Main/ControllerView/Haptics/Haptics.swift b/src/MeloNX/MeloNX/App/Views/Main/ControllerView/Haptics/Haptics.swift index 5dd555815..4409a4da2 100644 --- a/src/MeloNX/MeloNX/App/Views/Main/ControllerView/Haptics/Haptics.swift +++ b/src/MeloNX/MeloNX/App/Views/Main/ControllerView/Haptics/Haptics.swift @@ -15,7 +15,6 @@ class Haptics { private init() { } func play(_ feedbackStyle: UIImpactFeedbackGenerator.FeedbackStyle) { - print("haptics") UIImpactFeedbackGenerator(style: feedbackStyle).impactOccurred() } diff --git a/src/MeloNX/MeloNX/App/Views/Main/ControllerView/Joystick/Joystick.swift b/src/MeloNX/MeloNX/App/Views/Main/ControllerView/Joystick/Joystick.swift index be8b267fa..3b455d0ac 100644 --- a/src/MeloNX/MeloNX/App/Views/Main/ControllerView/Joystick/Joystick.swift +++ b/src/MeloNX/MeloNX/App/Views/Main/ControllerView/Joystick/Joystick.swift @@ -61,8 +61,7 @@ struct Joystick: View { Circle() .fill(Color.gray.opacity(0.4)) .frame(width: boundarySize, height: boundarySize) - .animation(.easeInOut(duration: 0.05), value: showBackground) - .transition(.scale) + .animation(.easeInOut(duration: 0.1), value: showBackground) } Circle() diff --git a/src/MeloNX/MeloNX/App/Views/Main/ControllerView/Joystick/JoystickView.swift b/src/MeloNX/MeloNX/App/Views/Main/ControllerView/Joystick/JoystickView.swift index fb258687e..06a978cfc 100644 --- a/src/MeloNX/MeloNX/App/Views/Main/ControllerView/Joystick/JoystickView.swift +++ b/src/MeloNX/MeloNX/App/Views/Main/ControllerView/Joystick/JoystickView.swift @@ -30,7 +30,7 @@ struct JoystickController: View { .onChange(of: position) { newValue in let scaledX = Float(newValue.x) let scaledY = Float(newValue.y) // my dumbass broke this by having -y instead of y :/ - print("Joystick Position: (\(scaledX), \(scaledY))") + // print("Joystick Position: (\(scaledX), \(scaledY))") if iscool != nil { Ryujinx.shared.virtualController.thumbstickMoved(.right, x: newValue.x, y: newValue.y) diff --git a/src/MeloNX/MeloNX/App/Views/Main/Emulation/AirPlay/Air.swift b/src/MeloNX/MeloNX/App/Views/Main/Emulation/AirPlay/Air.swift index 8842c50ea..4230ffae1 100644 --- a/src/MeloNX/MeloNX/App/Views/Main/Emulation/AirPlay/Air.swift +++ b/src/MeloNX/MeloNX/App/Views/Main/Emulation/AirPlay/Air.swift @@ -58,7 +58,7 @@ public class Air { } @objc func didConnect(sender: NSNotification) { - print("AirKit - Connect") + // print("AirKit - Connect") self.connected = true guard let screen: UIScreen = sender.object as? UIScreen else { return } add(screen: screen) { success in @@ -69,35 +69,35 @@ public class Air { func add(screen: UIScreen, completion: @escaping (Bool) -> ()) { - print("AirKit - Add Screen") + // print("AirKit - Add Screen") airScreen = screen airWindow = UIWindow(frame: airScreen!.bounds) guard let viewController: UIViewController = hostingController else { - print("AirKit - Add - Failed: Hosting Controller Not Found") + // print("AirKit - Add - Failed: Hosting Controller Not Found") completion(false) return } findWindowScene(for: airScreen!) { windowScene in guard let airWindowScene: UIWindowScene = windowScene else { - print("AirKit - Add - Failed: Window Scene Not Found") + // print("AirKit - Add - Failed: Window Scene Not Found") completion(false) return } self.airWindow?.rootViewController = viewController self.airWindow?.windowScene = airWindowScene self.airWindow?.isHidden = false - print("AirKit - Add Screen - Done") + // print("AirKit - Add Screen - Done") completion(true) } } func findWindowScene(for screen: UIScreen, shouldRecurse: Bool = true, completion: @escaping (UIWindowScene?) -> ()) { - print("AirKit - Find Window Scene") + // print("AirKit - Find Window Scene") var matchingWindowScene: UIWindowScene? = nil let scenes = UIApplication.shared.connectedScenes for scene in scenes { @@ -120,23 +120,23 @@ public class Air { } @objc func didDisconnect() { - print("AirKit - Disconnect") + // print("AirKit - Disconnect") remove() connected = false } func remove() { - print("AirKit - Remove") + // print("AirKit - Remove") airWindow = nil airScreen = nil } @objc func didBecomeActive() { - print("AirKit - App Active") + // print("AirKit - App Active") } @objc func willResignActive() { - print("AirKit - App Inactive") + // print("AirKit - App Inactive") } diff --git a/src/MeloNX/MeloNX/App/Views/Main/Emulation/AirPlay/AirPlay.swift b/src/MeloNX/MeloNX/App/Views/Main/Emulation/AirPlay/AirPlay.swift index a3c90b241..0eeb7c835 100644 --- a/src/MeloNX/MeloNX/App/Views/Main/Emulation/AirPlay/AirPlay.swift +++ b/src/MeloNX/MeloNX/App/Views/Main/Emulation/AirPlay/AirPlay.swift @@ -4,7 +4,7 @@ import SwiftUI public extension View { func airPlay() -> some View { - print("AirKit - airPlay") + // print("AirKit - airPlay") Air.play(AnyView(self)) return self } diff --git a/src/MeloNX/MeloNX/App/Views/Main/Emulation/EmulationView/EmulationView.swift b/src/MeloNX/MeloNX/App/Views/Main/Emulation/EmulationView/EmulationView.swift index 5e1a3279e..e3166b147 100644 --- a/src/MeloNX/MeloNX/App/Views/Main/Emulation/EmulationView/EmulationView.swift +++ b/src/MeloNX/MeloNX/App/Views/Main/Emulation/EmulationView/EmulationView.swift @@ -91,7 +91,7 @@ struct EmulationView: View { Air.shared.connectionCallbacks.append { cool in DispatchQueue.main.async { isAirplaying = cool - print(cool) + // print(cool) } } } diff --git a/src/MeloNX/MeloNX/App/Views/Main/Emulation/MetalView/MeloMTKView.swift b/src/MeloNX/MeloNX/App/Views/Main/Emulation/MetalView/MeloMTKView.swift index 6c4625e19..95d5f66a3 100644 --- a/src/MeloNX/MeloNX/App/Views/Main/Emulation/MetalView/MeloMTKView.swift +++ b/src/MeloNX/MeloNX/App/Views/Main/Emulation/MetalView/MeloMTKView.swift @@ -100,7 +100,7 @@ class MeloMTKView: MTKView { let index = activeTouches.firstIndex(of: touch)! let scaledLocation = scaleToTargetResolution(location)! - print("Touch began at: \(scaledLocation) and \(self.aspectRatio)") + // // print("Touch began at: \(scaledLocation) and \(self.aspectRatio)") touch_began(Float(scaledLocation.x), Float(scaledLocation.y), Int32(index)) } } @@ -119,7 +119,7 @@ class MeloMTKView: MTKView { if let index = activeTouches.firstIndex(of: touch) { activeTouches.remove(at: index) - print("Touch ended for index \(index)") + // // print("Touch ended for index \(index)") touch_ended(Int32(index)) } } @@ -139,14 +139,14 @@ class MeloMTKView: MTKView { guard let scaledLocation = scaleToTargetResolution(location) else { if let index = activeTouches.firstIndex(of: touch) { activeTouches.remove(at: index) - print("Touch left active area, removed index \(index)") + // // print("Touch left active area, removed index \(index)") touch_ended(Int32(index)) } continue } if let index = activeTouches.firstIndex(of: touch) { - print("Touch moved to: \(scaledLocation)") + // // print("Touch moved to: \(scaledLocation)") touch_moved(Float(scaledLocation.x), Float(scaledLocation.y), Int32(index)) } } diff --git a/src/MeloNX/MeloNX/App/Views/Main/GamesList/GameInfoSheet.swift b/src/MeloNX/MeloNX/App/Views/Main/GamesList/GameInfoSheet.swift index 5c4f9c3c8..f864020ca 100644 --- a/src/MeloNX/MeloNX/App/Views/Main/GamesList/GameInfoSheet.swift +++ b/src/MeloNX/MeloNX/App/Views/Main/GamesList/GameInfoSheet.swift @@ -10,7 +10,7 @@ import SwiftUI struct GameInfoSheet: View { let game: Game - @Environment(\.dismiss) var dismiss + @Environment(\.presentationMode) var presentationMode var body: some View { iOSNav { @@ -44,7 +44,7 @@ struct GameInfoSheet: View { .multilineTextAlignment(.center) Text(game.developer) .font(.caption) - .foregroundStyle(.secondary) + .foregroundColor(.secondary) } .padding(.vertical, 3) } @@ -56,7 +56,7 @@ struct GameInfoSheet: View { Text("**Version**") Spacer() Text(game.version) - .foregroundStyle(Color.secondary) + .foregroundColor(Color.secondary) } HStack { Text("**Title ID**") @@ -69,36 +69,36 @@ struct GameInfoSheet: View { } Spacer() Text(game.titleId) - .foregroundStyle(Color.secondary) + .foregroundColor(Color.secondary) } HStack { Text("**Game Size**") Spacer() Text("\(fetchFileSize(for: game.fileURL) ?? 0) bytes") - .foregroundStyle(Color.secondary) + .foregroundColor(Color.secondary) } HStack { Text("**File Type**") Spacer() Text(getFileType(game.fileURL)) - .foregroundStyle(Color.secondary) + .foregroundColor(Color.secondary) } VStack(alignment: .leading, spacing: 4) { Text("**Game URL**") Text(trimGameURL(game.fileURL)) - .foregroundStyle(Color.secondary) + .foregroundColor(Color.secondary) } } header: { Text("Information") } - .headerProminence(.increased) + // .headerProminence(.increased) } .navigationTitle(game.titleName) .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Button("Done") { - dismiss() + presentationMode.wrappedValue.dismiss() } } } @@ -113,7 +113,7 @@ struct GameInfoSheet: View { return size } } catch { - print("Error getting file size: \(error)") + // print("Error getting file size: \(error)") } return nil } diff --git a/src/MeloNX/MeloNX/App/Views/Main/GamesList/GameListView.swift b/src/MeloNX/MeloNX/App/Views/Main/GamesList/GameListView.swift index 23ce27fd5..e130166ea 100644 --- a/src/MeloNX/MeloNX/App/Views/Main/GamesList/GameListView.swift +++ b/src/MeloNX/MeloNX/App/Views/Main/GamesList/GameListView.swift @@ -15,7 +15,6 @@ extension UTType { struct GameLibraryView: View { @Binding var startemu: Game? - // @State var importDLCs = false @State private var searchText = "" @State private var isSearching = false @AppStorage("recentGames") private var recentGamesData: Data = Data() @@ -32,6 +31,8 @@ struct GameLibraryView: View { @StateObject var ryujinx = Ryujinx.shared @State var gameInfo: Game? @State var gameRequirements: [GameRequirements] = [] + @State private var showingOptions = false + var games: Binding<[Game]> { Binding( get: { Ryujinx.shared.games }, @@ -60,139 +61,74 @@ struct GameLibraryView: View { var body: some View { iOSNav { - List { - if Ryujinx.shared.games.isEmpty { - VStack(spacing: 16) { - Image(systemName: "gamecontroller.fill") - .font(.system(size: 64)) - .foregroundColor(.secondary.opacity(0.7)) - .padding(.top, 60) - Text("No Games Found") - .font(.title2.bold()) - .foregroundColor(.primary) - Text("Add ROM, Keys and Firmware to get started") - .font(.subheadline) - .foregroundColor(.secondary) + ZStack { + // Background color + Color(UIColor.systemBackground) + .ignoresSafeArea() + + VStack(spacing: 0) { + // Header with stats + if !Ryujinx.shared.games.isEmpty { + GameLibraryHeader( + totalGames: Ryujinx.shared.games.count, + recentGames: realRecentGames.count, + firmwareVersion: firmwareversion + ) } - .frame(maxWidth: .infinity) - .padding(.top, 40) - } else { - if !isSearching && !realRecentGames.isEmpty { - Section { - ForEach(realRecentGames) { game in - GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, isSelectingGameDLC: $isSelectingGameDLC, gameRequirements: $gameRequirements, gameInfo: $gameInfo) - .swipeActions(edge: .trailing, allowsFullSwipe: true) { - Button(role: .destructive) { - removeFromRecentGames(game) - } label: { - Label("Delete", systemImage: "trash") - } - } - } - } header: { - Text("Recent") - } - - Section { - ForEach(filteredGames) { game in - GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, isSelectingGameDLC: $isSelectingGameDLC, gameRequirements: $gameRequirements, gameInfo: $gameInfo) - } - } header: { - Text("Others") - } + + // Game list + if Ryujinx.shared.games.isEmpty { + EmptyGameLibraryView( + isSelectingGameFile: $isSelectingGameFile, + isImporting: $isImporting + ) } else { - ForEach(filteredGames) { game in - GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, isSelectingGameDLC: $isSelectingGameDLC, gameRequirements: $gameRequirements, gameInfo: $gameInfo) - } + gameListView + .animation(.easeInOut(duration: 0.3), value: searchText) } } } - .navigationTitle("Games") + .navigationTitle("Game Library") .navigationBarTitleDisplayMode(.large) .onAppear { loadRecentGames() - - let firmware = Ryujinx.shared.fetchFirmwareVersion() - firmwareversion = (firmware == "" ? "0" : firmware) + firmwareversion = (Ryujinx.shared.fetchFirmwareVersion() == "" ? "0" : Ryujinx.shared.fetchFirmwareVersion()) - pullGameCompatibility() { game in - switch game { - case .success(let sucees): - gameRequirements = sucees + pullGameCompatibility() { result in + switch result { + case .success(let success): + gameRequirements = success case .failure(_): - print("uhohh stinki") + print("Failed to load game compatibility data") } } } - .fileImporter(isPresented: $firmwareInstaller, allowedContentTypes: [.item]) { result in - switch result { - case .success(let url): - do { - let fun = url.startAccessingSecurityScopedResource() - let path = url.path - - Ryujinx.shared.installFirmware(firmwarePath: path) - - firmwareversion = (Ryujinx.shared.fetchFirmwareVersion() == "" ? "0" : Ryujinx.shared.fetchFirmwareVersion()) - if fun { - url.stopAccessingSecurityScopedResource() - } - } - case .failure(let error): - print(error) - } - } .toolbar { ToolbarItem(placement: .topBarTrailing) { + Button { isSelectingGameFile = true - isImporting = true } label: { - Image(systemName: "plus") + Label("Add Game", systemImage: "plus") + .labelStyle(.iconOnly) + .font(.system(size: 16, weight: .semibold)) } + // .buttonStyle(.bordered) + .accentColor(.blue) } - + ToolbarItem(placement: .topBarLeading) { Menu { - Text("Firmware Version: \(firmwareversion)") - .tint(.white) + firmwareSection - if firmwareversion == "0" { - Button { - DispatchQueue.main.async { - firmwareInstaller.toggle() - } - } label: { - Text("Install Firmware") - } - - } else { - Menu("Firmware") { - Button { - Ryujinx.shared.removeFirmware() - let firmware = Ryujinx.shared.fetchFirmwareVersion() - firmwareversion = (firmware == "" ? "0" : firmware) - } label: { - Text("Remove Firmware") - } - - Button { - let game = Game(containerFolder: URL(string: "none")!, fileType: .item, fileURL: URL(string: "MiiMaker")!, titleName: "Mii Maker", titleId: "0", developer: "Nintendo", version: firmwareversion) - - self.startemu = game - } label: { - Text("Mii Maker") - } - } - } + Divider() Button { isSelectingGameFile = false - isImporting = true } label: { - Text("Open Game") + Label("Open Game", systemImage: "square.and.arrow.down") } Button { @@ -201,121 +137,181 @@ struct GameLibraryView: View { if ProcessInfo.processInfo.isiOSAppOnMac { sharedurl = documentsUrl.absoluteString } - print(sharedurl) - let furl = URL(string: sharedurl)! - if UIApplication.shared.canOpenURL(furl) { - UIApplication.shared.open(furl, options: [:]) + if UIApplication.shared.canOpenURL(URL(string: sharedurl)!) { + UIApplication.shared.open(URL(string: sharedurl)!, options: [:]) } } label: { - Text("Show MeloNX Folder") + Label("Show MeloNX Folder", systemImage: "folder") } } label: { - Image(systemName: "ellipsis.circle") + Label("Options", systemImage: "ellipsis.circle") + .labelStyle(.iconOnly) .foregroundColor(.blue) } } - - ToolbarItem(placement: .topBarLeading) { - if ryujinx.jitenabled { - Image(systemName: "checkmark") - .foregroundStyle(.green) + } + .overlay(Group { + if ryujinx.jitenabled { + VStack { + HStack { + Spacer() + Circle() + .frame(width: 12, height: 12) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .foregroundColor(Color.green) + .padding() + } + Spacer() } } - } + }) .onChange(of: startemu) { game in guard let game else { return } addToRecentGames(game) } - } - .searchable(text: $searchText) - .animation(.easeInOut, value: searchText) - .onChange(of: searchText) { _ in - isSearching = !searchText.isEmpty - } - .fileImporter(isPresented: $isImporting, allowedContentTypes: [.folder, .nsp, .xci, .zip, .item]) { result in - if isSelectingGameFile { - switch result { - case .success(let url): - guard url.startAccessingSecurityScopedResource() else { - print("Failed to access security-scoped resource") - return - } - defer { url.stopAccessingSecurityScopedResource() } - - do { - let fileManager = FileManager.default - let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first! - let romsDirectory = documentsDirectory.appendingPathComponent("roms") - - if !fileManager.fileExists(atPath: romsDirectory.path) { - try fileManager.createDirectory(at: romsDirectory, withIntermediateDirectories: true, attributes: nil) - } - - let destinationURL = romsDirectory.appendingPathComponent(url.lastPathComponent) - try fileManager.copyItem(at: url, to: destinationURL) - - Ryujinx.shared.games = Ryujinx.shared.loadGames() - } catch { - print("Error copying game file: \(error)") - } - case .failure(let err): - print("File import failed: \(err.localizedDescription)") - } - - } else { - - switch result { - case .success(let url): - guard url.startAccessingSecurityScopedResource() else { - print("Failed to access security-scoped resource") - return - } - - do { - let handle = try FileHandle(forReadingFrom: url) - let fileExtension = (url.pathExtension as NSString).utf8String - let extensionPtr = UnsafeMutablePointer(mutating: fileExtension) - - let gameInfo = get_game_info(handle.fileDescriptor, extensionPtr) - - let game = Game.convertGameInfoToGame(gameInfo: gameInfo, url: url) - - DispatchQueue.main.async { - startemu = game - } - } catch { - print(error) - } - - case .failure(let err): - print("File import failed: \(err.localizedDescription)") - } + // .searchable(text: $searchText, placement: .toolbar, prompt: "Search games or developers") + .onChange(of: searchText) { _ in + isSearching = !searchText.isEmpty } - } - .sheet(isPresented: $isSelectingGameUpdate) { - UpdateManagerSheet(game: $gameInfo) - } - .sheet(isPresented: $isSelectingGameDLC) { - DLCManagerSheet(game: $gameInfo) - } - .sheet(isPresented: Binding( - get: { isViewingGameInfo && gameInfo != nil }, - set: { newValue in - if !newValue { - isViewingGameInfo = false - gameInfo = nil - } + .fileImporter(isPresented: $isImporting, allowedContentTypes: [.folder, .nsp, .xci, .zip, .item]) { result in + handleFileImport(result: result) } - )) { - if let game = gameInfo { - GameInfoSheet(game: game) + .fileImporter(isPresented: $firmwareInstaller, allowedContentTypes: [.item]) { result in + handleFirmwareImport(result: result) + } + .sheet(isPresented: $isSelectingGameUpdate) { + UpdateManagerSheet(game: $gameInfo) + } + .sheet(isPresented: $isSelectingGameDLC) { + DLCManagerSheet(game: $gameInfo) + } + .sheet(isPresented: Binding( + get: { isViewingGameInfo && gameInfo != nil }, + set: { newValue in + if !newValue { + isViewingGameInfo = false + gameInfo = nil + } + } + )) { + if let game = gameInfo { + GameInfoSheet(game: game) + } } } } + // MARK: - Subviews + + private var gameListView: some View { + ScrollView { + LazyVStack(spacing: 0) { + if !isSearching && !realRecentGames.isEmpty { + // Recent Games Section + VStack(alignment: .leading, spacing: 0) { + Text("Recent Games") + .font(.headline) + .foregroundColor(.primary) + .padding(.horizontal) + .padding(.top) + + ScrollView(.horizontal, showsIndicators: false) { + LazyHStack(spacing: 16) { + ForEach(realRecentGames) { game in + GameCardView( + game: game, + startemu: $startemu, + games: games, + isViewingGameInfo: $isViewingGameInfo, + isSelectingGameUpdate: $isSelectingGameUpdate, + isSelectingGameDLC: $isSelectingGameDLC, + gameRequirements: $gameRequirements, + gameInfo: $gameInfo + ) + .contextMenu { + gameContextMenu(for: game) + } + } + } + .padding() + } + } + + // Library Section + VStack(alignment: .leading) { + Text("Library") + .font(.headline) + .foregroundColor(.primary) + .padding(.horizontal) + .padding(.top) + + ForEach(filteredGames) { game in + GameListRow( + game: game, + startemu: $startemu, + games: games, + isViewingGameInfo: $isViewingGameInfo, + isSelectingGameUpdate: $isSelectingGameUpdate, + isSelectingGameDLC: $isSelectingGameDLC, + gameRequirements: $gameRequirements, + gameInfo: $gameInfo + ) + .padding(.horizontal, 3) + .padding(.vertical, 8) + } + } + } else { + ForEach(filteredGames) { game in + GameListRow( + game: game, + startemu: $startemu, + games: games, + isViewingGameInfo: $isViewingGameInfo, + isSelectingGameUpdate: $isSelectingGameUpdate, + isSelectingGameDLC: $isSelectingGameDLC, + gameRequirements: $gameRequirements, + gameInfo: $gameInfo + ) + .padding(.horizontal, 3) + .padding(.vertical, 8) + } + } + + Spacer(minLength: 50) + } + } + } + + private var firmwareSection: some View { + Group { + if firmwareversion == "0" { + Button { + DispatchQueue.main.async { + firmwareInstaller.toggle() + } + } label: { + Label("Install Firmware", systemImage: "square.and.arrow.down") + } + + } else { + Menu("Applets") { + Button { + let game = Game(containerFolder: URL(string: "none")!, fileType: .item, fileURL: URL(string: "MiiMaker")!, titleName: "Mii Maker", titleId: "0", developer: "Nintendo", version: firmwareversion) + self.startemu = game + } label: { + Label("Launch Mii Maker", systemImage: "person.crop.circle") + } + + } + } + } + } + + // MARK: - Game Management Functions + private func addToRecentGames(_ game: Game) { recentGames.removeAll { $0.titleId == game.titleId } - recentGames.insert(game, at: 0) if recentGames.count > 5 { @@ -336,7 +332,7 @@ struct GameLibraryView: View { let data = try encoder.encode(recentGames) recentGamesData = data } catch { - print("Error saving recent games: \(error)") + // print("Error saving recent games: \(error)") } } @@ -345,28 +341,161 @@ struct GameLibraryView: View { let decoder = JSONDecoder() recentGames = try decoder.decode([Game].self, from: recentGamesData) } catch { - print("Error loading recent games: \(error)") + // print("Error loading recent games: \(error)") recentGames = [] } } - // MARK: - Delete Game Function - func deleteGame(game: Game) { + private func deleteGame(game: Game) { let fileManager = FileManager.default do { try fileManager.removeItem(at: game.fileURL) Ryujinx.shared.games.removeAll { $0.id == game.id } Ryujinx.shared.games = Ryujinx.shared.loadGames() } catch { - print("Error deleting game: \(error)") + // print("Error deleting game: \(error)") + } + } + + // MARK: - Import Handlers + + private func handleFileImport(result: Result) { + if isSelectingGameFile { + switch result { + case .success(let url): + guard url.startAccessingSecurityScopedResource() else { + // print("Failed to access security-scoped resource") + return + } + defer { url.stopAccessingSecurityScopedResource() } + + do { + let fileManager = FileManager.default + let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first! + let romsDirectory = documentsDirectory.appendingPathComponent("roms") + + if !fileManager.fileExists(atPath: romsDirectory.path) { + try fileManager.createDirectory(at: romsDirectory, withIntermediateDirectories: true, attributes: nil) + } + + let destinationURL = romsDirectory.appendingPathComponent(url.lastPathComponent) + try fileManager.copyItem(at: url, to: destinationURL) + + Ryujinx.shared.games = Ryujinx.shared.loadGames() + } catch { + // print("Error copying game file: \(error)") + } + case .failure(let err): + print("File import failed: \(err.localizedDescription)") + } + } else { + switch result { + case .success(let url): + guard url.startAccessingSecurityScopedResource() else { + // print("Failed to access security-scoped resource") + return + } + + do { + let handle = try FileHandle(forReadingFrom: url) + let fileExtension = (url.pathExtension as NSString).utf8String + let extensionPtr = UnsafeMutablePointer(mutating: fileExtension) + + let gameInfo = get_game_info(handle.fileDescriptor, extensionPtr) + + let game = Game.convertGameInfoToGame(gameInfo: gameInfo, url: url) + + DispatchQueue.main.async { + startemu = game + } + } catch { + // print(error) + } + + case .failure(let err): + print("File import failed: \(err.localizedDescription)") + } + } + } + + private func handleFirmwareImport(result: Result) { + switch result { + case .success(let url): + do { + let fun = url.startAccessingSecurityScopedResource() + let path = url.path + + Ryujinx.shared.installFirmware(firmwarePath: path) + + firmwareversion = (Ryujinx.shared.fetchFirmwareVersion() == "" ? "0" : Ryujinx.shared.fetchFirmwareVersion()) + if fun { + url.stopAccessingSecurityScopedResource() + } + } + case .failure(let error): + print(error) + } + } + + // MARK: - Context Menus + + private func gameContextMenu(for game: Game) -> some View { + Group { + Section { + Button { + startemu = game + } label: { + Label("Play Now", systemImage: "play.fill") + } + + Button { + gameInfo = game + isViewingGameInfo.toggle() + } label: { + Label("Game Info", systemImage: "info.circle") + } + } + + Section { + Button { + gameInfo = game + isSelectingGameUpdate.toggle() + } label: { + Label("Update Manager", systemImage: "arrow.up.circle") + } + + Button { + gameInfo = game + isSelectingGameDLC.toggle() + } label: { + Label("DLC Manager", systemImage: "plus.circle") + } + } + + Section { + if #available(iOS 15, *) { + Button(role: .destructive) { + deleteGame(game: game) + } label: { + Label("Delete Game", systemImage: "trash") + } + } else { + Button(action: { + deleteGame(game: game) + }) { + Label("Delete Game", systemImage: "trash") + .foregroundColor(.red) + } + } + + } } } } -// MARK: - Game Model extension Game: Codable { - enum CodingKeys: String, CodingKey { - case titleName, titleId, developer, version, fileURL + private enum CodingKeys: String, CodingKey { + case titleName, titleId, developer, version, fileURL, containerFolder, fileType } public init(from decoder: Decoder) throws { @@ -376,9 +505,8 @@ extension Game: Codable { developer = try container.decode(String.self, forKey: .developer) version = try container.decode(String.self, forKey: .version) fileURL = try container.decode(URL.self, forKey: .fileURL) - - self.containerFolder = fileURL.deletingLastPathComponent() - self.fileType = .item + containerFolder = try container.decode(URL.self, forKey: .containerFolder) + fileType = try container.decode(UTType.self, forKey: .fileType) } public func encode(to encoder: Encoder) throws { @@ -388,10 +516,215 @@ extension Game: Codable { try container.encode(developer, forKey: .developer) try container.encode(version, forKey: .version) try container.encode(fileURL, forKey: .fileURL) + try container.encode(containerFolder, forKey: .containerFolder) + try container.encode(fileType, forKey: .fileType) } } -// MARK: - Game List Item + +// MARK: - Empty Library View +struct EmptyGameLibraryView: View { + @Binding var isSelectingGameFile: Bool + @Binding var isImporting: Bool + + var body: some View { + VStack(spacing: 24) { + Spacer() + + Image(systemName: "gamecontroller.fill") + .font(.system(size: 70)) + .foregroundColor(.blue.opacity(0.7)) + .padding(.bottom) + + Text("No Games Found") + .font(.title2.bold()) + .foregroundColor(.primary) + + Text("Add ROM files to get started with your gaming experience") + .font(.subheadline) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + .padding(.horizontal, 40) + + Button { + isSelectingGameFile = true + isImporting = true + } label: { + Label("Add Game", systemImage: "plus") + .font(.headline) + .padding(.horizontal, 24) + .padding(.vertical, 12) + .background(Color.blue) + .foregroundColor(.white) + .cornerRadius(10) + } + .padding(.top) + + Spacer() + } + .padding() + } +} + +// MARK: - Library Header +struct GameLibraryHeader: View { + let totalGames: Int + let recentGames: Int + let firmwareVersion: String + + var body: some View { + HStack(spacing: 16) { + // Stats cards + StatCard( + icon: "gamecontroller.fill", + title: "Total Games", + value: "\(totalGames)", + color: .blue + ) + + StatCard( + icon: "clock.fill", + title: "Recent", + value: "\(recentGames)", + color: .green + ) + + StatCard( + icon: "cpu", + title: "Firmware", + value: firmwareVersion == "0" ? "None" : firmwareVersion, + color: firmwareVersion == "0" ? .red : .orange + ) + } + .padding(.horizontal) + .padding(.top, 8) + .padding(.bottom, 4) + } +} + +struct StatCard: View { + let icon: String + let title: String + let value: String + let color: Color + + var body: some View { + VStack(alignment: .leading, spacing: 4) { + HStack { + Image(systemName: icon) + .foregroundColor(color) + Text(title) + .font(.caption) + .foregroundColor(.secondary) + } + + Text(value) + .font(.system(size: 16, weight: .bold)) + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(10) + .background(color.opacity(0.1)) + .cornerRadius(10) + } +} + +// MARK: - Game Card View +struct GameCardView: View { + let game: Game + @Binding var startemu: Game? + @Binding var games: [Game] + @Binding var isViewingGameInfo: Bool + @Binding var isSelectingGameUpdate: Bool + @Binding var isSelectingGameDLC: Bool + @Binding var gameRequirements: [GameRequirements] + @Binding var gameInfo: Game? + @Environment(\.colorScheme) var colorScheme + + var gameRequirement: GameRequirements? { + gameRequirements.first(where: { $0.game_id == game.titleId }) + } + + var body: some View { + VStack(spacing: 0) { + // Game Icon + ZStack { + if let icon = game.icon { + Image(uiImage: icon) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 130, height: 130) + .cornerRadius(8) + } else { + RoundedRectangle(cornerRadius: 8) + .fill(colorScheme == .dark ? Color(.systemGray5) : Color(.systemGray6)) + .frame(width: 130, height: 130) + + Image(systemName: "gamecontroller.fill") + .font(.system(size: 40)) + .foregroundColor(.gray) + } + + // Play button overlay + Button { + startemu = game + } label: { + Circle() + .fill(Color.black.opacity(0.6)) + .frame(width: 40, height: 40) + .overlay( + Image(systemName: "play.fill") + .font(.system(size: 16)) + .foregroundColor(.white) + ) + } + .offset(x: 0, y: 0) + .opacity(0.8) + } + + // Game info + VStack(alignment: .leading, spacing: 4) { + Text(game.titleName) + .font(.system(size: 14, weight: .medium)) + .multilineTextAlignment(.leading) + .foregroundColor(.primary) + .lineLimit(1) + + Text(game.developer) + .font(.system(size: 12)) + .foregroundColor(.secondary) + .lineLimit(1) + + // Compatibility tag + if let req = gameRequirement { + HStack(spacing: 4) { + Text(req.compatibility) + .font(.system(size: 10, weight: .medium)) + .foregroundColor(.white) + .padding(.horizontal, 6) + .padding(.vertical, 2) + .background(req.color) + .cornerRadius(4) + + Text(req.device_memory) + .font(.system(size: 10, weight: .medium)) + .foregroundColor(.white) + .padding(.horizontal, 6) + .padding(.vertical, 2) + .background(Color.blue) + .cornerRadius(4) + } + } + } + .frame(width: 130, alignment: .leading) + .padding(.top, 8) + } + .onTapGesture { + startemu = game + } + } +} + +// MARK: - Game List Row struct GameListRow: View { let game: Game @Binding var startemu: Game? @@ -404,143 +737,357 @@ struct GameListRow: View { @State var gametoDelete: Game? @State var showGameDeleteConfirmation: Bool = false @Environment(\.colorScheme) var colorScheme + @Environment(\.verticalSizeClass) var verticalSizeClass: UserInterfaceSizeClass? + @Environment(\.horizontalSizeClass) var horizontalSizeClass: UserInterfaceSizeClass? @AppStorage("portal") var gamepo = false var body: some View { - Button(action: { - startemu = game - }) { - HStack(spacing: 16) { - // Game Icon - if let icon = game.icon { - Image(uiImage: icon) - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: 45, height: 45) - .cornerRadius(8) - } else { - ZStack { - RoundedRectangle(cornerRadius: 8) - .fill(colorScheme == .dark ? - Color(.systemGray5) : Color(.systemGray6)) - .frame(width: 45, height: 45) + if #available(iOS 15.0, *) { + Button(action: { + startemu = game + }) { + HStack(spacing: 16) { + // Game Icon + if let icon = game.icon { + Image(uiImage: icon) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 55, height: 55) + .cornerRadius(10) + } else { + ZStack { + RoundedRectangle(cornerRadius: 10) + .fill(colorScheme == .dark ? + Color(.systemGray5) : Color(.systemGray6)) + .frame(width: 55, height: 55) + + Image(systemName: "gamecontroller.fill") + .font(.system(size: 24)) + .foregroundColor(.gray) + } + } + + // Game Info + VStack(alignment: .leading, spacing: 4) { + Text(game.titleName) + .font(.system(size: 16, weight: .medium)) + .foregroundColor(.primary) + .multilineTextAlignment(.leading) - Image(systemName: "gamecontroller.fill") - .font(.system(size: 20)) - .foregroundColor(.gray) + HStack { + Text(game.developer) + .font(.system(size: 14)) + .foregroundColor(.secondary) + .multilineTextAlignment(.leading) + + if !game.version.isEmpty && game.version != "0" { + Text("•") + .foregroundColor(.secondary) + + Text("v\(game.version)") + .font(.system(size: 14)) + .foregroundColor(.secondary) + } + } + } + + Spacer() + + VStack(alignment: .leading) { + // Compatibility badges + HStack { + if let gameReq = gameRequirements.first(where: { $0.game_id == game.titleId }) { + let totalMemory = ProcessInfo.processInfo.physicalMemory + + HStack(spacing: 4) { + // Memory requirement badge + Text(gameReq.device_memory) + .font(.system(size: 11, weight: .medium)) + .foregroundColor(.white) + .padding(.horizontal, 4) + .padding(.vertical, 4) + .background( + Capsule() + .fill(gameReq.memoryInt <= Int(String(format: "%.0f", Double(totalMemory) / 1_000_000_000)) ?? 0 ? Color.blue : Color.red) + ) + + // Compatibility badge + Text(gameReq.compatibility) + .font(.system(size: 11, weight: .medium)) + .foregroundColor(.white) + .padding(.horizontal, 4) + .padding(.vertical, 4) + .background( + Capsule() + .fill(gameReq.color) + ) + } + } + + // Play button + Image(systemName: "play.circle.fill") + .font(.title3) + .foregroundColor(.blue) + } } } - - // Game Info - VStack(alignment: .leading, spacing: 2) { - Text(game.titleName) - .font(.body) - .foregroundColor(.primary) + .padding(.horizontal, 10) + .padding(.vertical, 4) + .frame(width: .infinity, height: .infinity) + } + .contentShape(Rectangle()) + .contextMenu { + Section { + Button { + startemu = game + } label: { + Label("Play Now", systemImage: "play.fill") + } - Text(game.developer) - .font(.subheadline) - .foregroundColor(.secondary) - } - - - Spacer() - - if let game = gameRequirements.first(where: { $0.game_id == game.titleId }) { - let totalMemory = ProcessInfo.processInfo.physicalMemory - - VStack(spacing: 10) { - Capsule().fill(game.memoryInt <= Int(String(format: "%.0f", Double(totalMemory) / 1_000_000_000)) ?? 0 ? Color.green : Color.red) - .frame(width: 70 / 1.5, height: 35 / 1.5) - .overlay { - Text(game.device_memory) - .foregroundStyle(.white) - .font(.system(size: 11)) - } + Button { + gameInfo = game + isViewingGameInfo.toggle() - Capsule().fill(game.color) - .frame(width: 70 / 1.5, height: 35 / 1.5) - .overlay { - Text(game.compatibility) - .foregroundStyle(.white) - .font(.system(size: 10)) - } + if game.titleName.lowercased() == "portal" || game.titleName.lowercased() == "portal 2" { + gamepo = true + } + } label: { + Label("Game Info", systemImage: "info.circle") } } - Image(systemName: "play.circle.fill") - .font(.title2) - .foregroundColor(.accentColor) - .opacity(0.8) - } - } - .contextMenu { - Section { - Button { - startemu = game - } label: { - Label("Play Now", systemImage: "play.fill") - } - - Button { - gameInfo = game - isViewingGameInfo.toggle() - - if game.titleName.lowercased() == "portal" { - gamepo = true - } else if game.titleName.lowercased() == "portal 2" { - gamepo = true + Section { + Button { + gameInfo = game + isSelectingGameUpdate.toggle() + } label: { + Label("Update Manager", systemImage: "arrow.up.circle") + } + + Button { + gameInfo = game + isSelectingGameDLC.toggle() + } label: { + Label("DLC Manager", systemImage: "plus.circle") + } + } + + Section { + Button(role: .destructive) { + gametoDelete = game + showGameDeleteConfirmation.toggle() + } label: { + Label("Delete", systemImage: "trash") } - } label: { - Label("Game Info", systemImage: "info.circle") } } - - Section { - Button { - gameInfo = game - isSelectingGameUpdate.toggle() - } label: { - Label("Game Update Manager", systemImage: "chevron.up.circle") - } - - Button { - gameInfo = game - isSelectingGameDLC.toggle() - } label: { - Label("Game DLC Manager", systemImage: "plus.viewfinder") - } - } - - Section { + .swipeActions(edge: .trailing) { Button(role: .destructive) { gametoDelete = game showGameDeleteConfirmation.toggle() } label: { Label("Delete", systemImage: "trash") } + + Button { + gameInfo = game + isViewingGameInfo.toggle() + } label: { + Label("Info", systemImage: "info.circle") + } + .tint(.blue) } - } - .confirmationDialog("Are you sure you want to delete this game?", isPresented: $showGameDeleteConfirmation) { - Button("Delete", role: .destructive) { - if let game = gametoDelete { - deleteGame(game: game) + .swipeActions(edge: .leading) { + Button { + startemu = game + } label: { + Label("Play", systemImage: "play.fill") + } + .tint(.green) + } + .confirmationDialog("Are you sure you want to delete this game?", isPresented: $showGameDeleteConfirmation) { + Button("Delete", role: .destructive) { + if let game = gametoDelete { + deleteGame(game: game) + } + } + Button("Cancel", role: .cancel) {} + } message: { + Text("Are you sure you want to delete \(gametoDelete?.titleName ?? "this game")?") + } + .listRowInsets(EdgeInsets()) + .background( + RoundedRectangle(cornerRadius: 12) + .fill(colorScheme == .dark ? Color(.systemGray6) : Color(.systemGray6).opacity(0.5)) + ) + } else { + Button(action: { + startemu = game + }) { + HStack(spacing: 16) { + // Game Icon + if let icon = game.icon { + Image(uiImage: icon) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 55, height: 55) + .cornerRadius(10) + } else { + ZStack { + RoundedRectangle(cornerRadius: 10) + .fill(colorScheme == .dark ? + Color(.systemGray5) : Color(.systemGray6)) + .frame(width: 55, height: 55) + + Image(systemName: "gamecontroller.fill") + .font(.system(size: 24)) + .foregroundColor(.gray) + } + } + + // Game Info + VStack(alignment: .leading, spacing: 4) { + Text(game.titleName) + .font(.system(size: 16, weight: .medium)) + .foregroundColor(.primary) + .multilineTextAlignment(.leading) + + HStack { + Text(game.developer) + .font(.system(size: 14)) + .foregroundColor(.secondary) + .multilineTextAlignment(.leading) + + if !game.version.isEmpty && game.version != "0" { + Text("•") + .foregroundColor(.secondary) + + Text("v\(game.version)") + .font(.system(size: 14)) + .foregroundColor(.secondary) + } + } + } + + Spacer() + + VStack(alignment: .leading) { + // Compatibility badges + HStack { + if let gameReq = gameRequirements.first(where: { $0.game_id == game.titleId }) { + let totalMemory = ProcessInfo.processInfo.physicalMemory + + HStack(spacing: 4) { + // Memory requirement badge + Text(gameReq.device_memory) + .font(.system(size: 11, weight: .medium)) + .foregroundColor(.white) + .padding(.horizontal, 4) + .padding(.vertical, 4) + .background( + Capsule() + .fill(gameReq.memoryInt <= Int(String(format: "%.0f", Double(totalMemory) / 1_000_000_000)) ?? 0 ? Color.blue : Color.red) + ) + + // Compatibility badge + Text(gameReq.compatibility) + .font(.system(size: 11, weight: .medium)) + .foregroundColor(.white) + .padding(.horizontal, 4) + .padding(.vertical, 4) + .background( + Capsule() + .fill(gameReq.color) + ) + } + } + + // Play button + Image(systemName: "play.circle.fill") + .font(.title3) + .foregroundColor(.blue) + } + } + } + .padding(.horizontal, 10) + .padding(.vertical, 4) + .frame(width: .infinity, height: .infinity) + } + .contentShape(Rectangle()) + .contextMenu { + Section { + Button { + startemu = game + } label: { + Label("Play Now", systemImage: "play.fill") + } + + Button { + gameInfo = game + isViewingGameInfo.toggle() + + if game.titleName.lowercased() == "portal" || game.titleName.lowercased() == "portal 2" { + gamepo = true + } + } label: { + Label("Game Info", systemImage: "info.circle") + } + } + + Section { + Button { + gameInfo = game + isSelectingGameUpdate.toggle() + } label: { + Label("Update Manager", systemImage: "arrow.up.circle") + } + + Button { + gameInfo = game + isSelectingGameDLC.toggle() + } label: { + Label("DLC Manager", systemImage: "plus.circle") + } + } + + Section { + Button { + gametoDelete = game + showGameDeleteConfirmation.toggle() + } label: { + Label("Delete", systemImage: "trash") + .foregroundColor(.red) + } } } - Button("Cancel", role: .cancel) {} - } message: { - Text("Are you sure you want to delete \(gametoDelete?.titleName ?? "this game")?") + .alert(isPresented: $showGameDeleteConfirmation) { + Alert( + title: Text("Are you sure you want to delete this game?"), + message: Text("Are you sure you want to delete \(gametoDelete?.titleName ?? "this game")?"), + primaryButton: .destructive(Text("Delete")) { + if let game = gametoDelete { + deleteGame(game: game) + } + }, + secondaryButton: .cancel() + ) + } + .listRowInsets(EdgeInsets()) + .background( + RoundedRectangle(cornerRadius: 12) + .fill(colorScheme == .dark ? Color(.systemGray6) : Color(.systemGray6).opacity(0.5)) + ) } } - private func deleteGame(game: Game) { let fileManager = FileManager.default do { try fileManager.removeItem(at: game.fileURL) games.removeAll { $0.id == game.id } } catch { - print("Error deleting game: \(error)") + // print("Error deleting game: \(error)") } } } @@ -552,7 +1099,7 @@ struct GameRequirements: Codable { var memoryInt: Int { var devicemem = device_memory devicemem.removeLast(2) - print(devicemem) + // print(devicemem) return Int(devicemem) ?? 0 } diff --git a/src/MeloNX/MeloNX/App/Views/Main/JIT/JITPopover.swift b/src/MeloNX/MeloNX/App/Views/Main/JIT/JITPopover.swift index b5e219d9d..b779a0381 100644 --- a/src/MeloNX/MeloNX/App/Views/Main/JIT/JITPopover.swift +++ b/src/MeloNX/MeloNX/App/Views/Main/JIT/JITPopover.swift @@ -9,7 +9,7 @@ import SwiftUI struct JITPopover: View { var onJITEnabled: () -> Void - @Environment(\.dismiss) var dismiss + @Environment(\.presentationMode) var presentationMode @State var isJIT: Bool = false var body: some View { @@ -35,7 +35,7 @@ struct JITPopover: View { if isJIT { - dismiss() + presentationMode.wrappedValue.dismiss() onJITEnabled() Ryujinx.shared.ryuIsJITEnabled() diff --git a/src/MeloNX/MeloNX/App/Views/Main/Logging/Logs.swift b/src/MeloNX/MeloNX/App/Views/Main/Logging/Logs.swift index d7951c1ee..e3755aa6c 100644 --- a/src/MeloNX/MeloNX/App/Views/Main/Logging/Logs.swift +++ b/src/MeloNX/MeloNX/App/Views/Main/Logging/Logs.swift @@ -6,25 +6,20 @@ // import SwiftUI +import Combine struct LogFileView: View { - @State private var logs: [String] = [] + @StateObject var logsModel = LogViewModel() @State private var showingLogs = false public var isfps: Bool private let fileManager = FileManager.default - private let maxDisplayLines = 10 - - private var dateFormatter: DateFormatter { - let formatter = DateFormatter() - formatter.dateFormat = "yyyy-MM-dd_HH-mm-ss" - return formatter - } + private let maxDisplayLines = 4 var body: some View { VStack(alignment: .leading, spacing: 4) { - ForEach(logs.suffix(maxDisplayLines), id: \.self) { log in + ForEach(logsModel.logs.suffix(maxDisplayLines), id: \.self) { log in Text(log) .font(.caption) .foregroundColor(.white) @@ -34,85 +29,38 @@ struct LogFileView: View { .transition(.opacity) } } - .onAppear { - startLogFileWatching() - } - .onChange(of: logs) { newLogs in - print("Logs updated: \(newLogs.count) entries") - } - } - - private func getLatestLogFile() -> URL? { - let logsDirectory = URL.documentsDirectory.appendingPathComponent("Logs") - let currentDate = Date() - - do { - try fileManager.createDirectory(at: logsDirectory, withIntermediateDirectories: true) - - let logFiles = try fileManager.contentsOfDirectory(at: logsDirectory, includingPropertiesForKeys: [.creationDateKey]) - .filter { - let filename = $0.lastPathComponent - guard filename.hasPrefix("MeloNX_") && filename.hasSuffix(".log") else { - return false - } - - let dateString = filename.replacingOccurrences(of: "MeloNX_", with: "").replacingOccurrences(of: ".log", with: "") - guard let logDate = dateFormatter.date(from: dateString) else { - return false - } - - return Calendar.current.isDate(logDate, inSameDayAs: currentDate) - } - - let sortedLogFiles = logFiles.sorted { - $0.lastPathComponent > $1.lastPathComponent - } - - return sortedLogFiles.first - } catch { - print("Error finding log files: \(error)") - return nil - } - } - - private func readLatestLogFile() { - guard let logFileURL = getLatestLogFile() else { - print("no logs?") - return - } - print(logFileURL) - - do { - let logContents = try String(contentsOf: logFileURL) - let allLines = logContents.components(separatedBy: .newlines) - - DispatchQueue.global(qos: .userInteractive).async { - self.logs = Array(allLines) - } - } catch { - print("Error reading log file: \(error)") - } - } - - private func startLogFileWatching() { - showingLogs = true - - Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { timer in - if showingLogs { - self.readLatestLogFile() - } - - if isfps { - sleep(1) - if get_current_fps() != 0 { - stopLogFileWatching() - timer.invalidate() - } - } - } + .padding() } private func stopLogFileWatching() { showingLogs = false } } + + +class LogViewModel: ObservableObject { + @Published var logs: [String] = [] + private var cancellables = Set() + + init() { + _ = LogCapture.shared + + NotificationCenter.default.publisher(for: .newLogCaptured) + .receive(on: RunLoop.main) + .sink { [weak self] _ in + self?.updateLogs() + } + .store(in: &cancellables) + + updateLogs() + } + + func updateLogs() { + logs = LogCapture.shared.capturedLogs + } + + func clearLogs() { + LogCapture.shared.capturedLogs = [] + updateLogs() + } +} diff --git a/src/MeloNX/MeloNX/App/Views/Main/SettingsView/SettingsView.swift b/src/MeloNX/MeloNX/App/Views/Main/SettingsView/SettingsView.swift index 775358643..4be5c0d22 100644 --- a/src/MeloNX/MeloNX/App/Views/Main/SettingsView/SettingsView.swift +++ b/src/MeloNX/MeloNX/App/Views/Main/SettingsView/SettingsView.swift @@ -39,6 +39,8 @@ struct SettingsView: View { @AppStorage("performacehud") var performacehud: Bool = false + @AppStorage("swapBandA") var swapBandA: Bool = false + @AppStorage("oldWindowCode") var windowCode: Bool = false @AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0 @@ -60,479 +62,763 @@ struct SettingsView: View { @State private var searchText = "" @AppStorage("portal") var gamepo = false @StateObject var ryujinx = Ryujinx.shared + @Environment(\.colorScheme) var colorScheme + @Environment(\.verticalSizeClass) var verticalSizeClass: UserInterfaceSizeClass? + @Environment(\.horizontalSizeClass) var horizontalSizeClass: UserInterfaceSizeClass? + + @State private var selectedCategory: SettingsCategory = .graphics var filteredMemoryModes: [(String, String)] { guard !searchText.isEmpty else { return memoryManagerModes } return memoryManagerModes.filter { $0.1.localizedCaseInsensitiveContains(searchText) } } + enum SettingsCategory: String, CaseIterable, Identifiable { + case graphics = "Graphics" + case input = "Input" + case system = "System" + case misc = "Misc" + case advanced = "Advanced" + + var id: String { self.rawValue } + + var icon: String { + switch self { + case .graphics: return "paintbrush.fill" + case .input: return "gamecontroller.fill" + case .system: return "gearshape.fill" + case .misc: return "ellipsis.circle.fill" + case .advanced: return "terminal.fill" + } + } + } var body: some View { iOSNav { - List { + ZStack { + // Background color + Color(UIColor.systemBackground) + .ignoresSafeArea() - - // Graphics & Performance - Section { - Picker(selection: $config.aspectRatio) { - ForEach(AspectRatio.allCases, id: \.self) { ratio in - Text(ratio.displayName).tag(ratio) - } - } label: { - labelWithIcon("Aspect Ratio", iconName: "rectangle.expand.vertical") - } - .tint(.blue) - - Toggle(isOn: $config.disableShaderCache) { - labelWithIcon("Shader Cache", iconName: "memorychip") - } - .tint(.blue) - - Toggle(isOn: $config.disablevsync) { - labelWithIcon("Disable VSync", iconName: "arrow.triangle.2.circlepath") - } - .tint(.blue) - - - Toggle(isOn: $config.enableTextureRecompression) { - labelWithIcon("Texture Recompression", iconName: "rectangle.compress.vertical") - } - .tint(.blue) - - Toggle(isOn: $config.disableDockedMode) { - labelWithIcon("Docked Mode", iconName: "dock.rectangle") - } - .tint(.blue) - - Toggle(isOn: $config.macroHLE) { - labelWithIcon("Macro HLE", iconName: "gearshape") - }.tint(.blue) - - - VStack(alignment: .leading, spacing: 10) { - HStack { - labelWithIcon("Resolution Scale", iconName: "magnifyingglass") - .font(.headline) - Spacer() - Button { - showResolutionInfo.toggle() - } label: { - Image(systemName: "info.circle") - .symbolRenderingMode(.hierarchical) - .foregroundStyle(.secondary) - } - .buttonStyle(.plain) - .help("Learn more about Resolution Scale") - .alert(isPresented: $showResolutionInfo) { - Alert( - title: Text("Resolution Scale"), - message: Text("Adjust the internal rendering resolution. Higher values improve visuals but may reduce performance."), - dismissButton: .default(Text("OK")) - ) - } - } - - Slider(value: $config.resscale, in: 0.1...3.0, step: 0.05) { - Text("Resolution Scale") - } minimumValueLabel: { - Text("0.1x") - .font(.footnote) - .foregroundColor(.secondary) - } maximumValueLabel: { - Text("3.0x") - .font(.footnote) - .foregroundColor(.secondary) - } - Text("\(config.resscale, specifier: "%.2f")x") - .font(.subheadline) - .foregroundColor(.secondary) - } - .padding(.vertical, 8) - - VStack(alignment: .leading, spacing: 10) { - HStack { - labelWithIcon("Max Anisotropic Scale", iconName: "magnifyingglass") - .font(.headline) - Spacer() - Button { - showAnisotropicInfo.toggle() - } label: { - Image(systemName: "info.circle") - .symbolRenderingMode(.hierarchical) - .foregroundStyle(.secondary) - } - .buttonStyle(.plain) - .help("Learn more about Max Anisotropic Scale") - .alert(isPresented: $showAnisotropicInfo) { - Alert( - title: Text("Max Anisotripic Scale"), - message: Text("Adjust the internal Anisotropic resolution. Higher values improve visuals but may reduce performance. Default at 0 lets game decide."), - dismissButton: .default(Text("OK")) - ) - } - } - - Slider(value: $config.maxAnisotropy, in: 0...16.0, step: 0.1) { - Text("Resolution Scale") - } minimumValueLabel: { - Text("0x") - .font(.footnote) - .foregroundColor(.secondary) - } maximumValueLabel: { - Text("16.0x") - .font(.footnote) - .foregroundColor(.secondary) - } - Text("\(config.maxAnisotropy, specifier: "%.2f")x") - .font(.subheadline) - .foregroundColor(.secondary) - } - .padding(.vertical, 8) - - Toggle(isOn: $performacehud) { - labelWithIcon("Custom Performance Overlay", iconName: "speedometer") - } - .tint(.blue) - } header: { - Text("Graphics & Performance") - .font(.title3.weight(.semibold)) - .textCase(nil) - .headerProminence(.increased) - } footer: { - Text("Fine-tune graphics and performance to suit your device and preferences.") - } - - // Input Selector - Section { - if !controllersList.filter({ !currentControllers.contains($0) }).isEmpty { - DisclosureGroup("Unselected Controllers") { - ForEach(controllersList.filter { !currentControllers.contains($0) }) { controller in - var customBinding: Binding { - Binding( - get: { currentControllers.contains(controller) }, - set: { bool in - if !bool { - currentControllers.removeAll(where: { $0.id == controller.id }) - } else { - currentControllers.append(controller) - } - } - ) - } - - Toggle(isOn: customBinding) { - Text(controller.name) - .font(.body) - } - .tint(.blue) + VStack(spacing: 0) { + // Category selector + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 12) { + ForEach(SettingsCategory.allCases, id: \.id) { category in + CategoryButton( + title: category.rawValue, + icon: category.icon, + isSelected: selectedCategory == category + ) { + selectedCategory = category + } } } + .padding(.horizontal) + .padding(.vertical, 8) } + Divider() - - ForEach(currentControllers) { controller in - - var customBinding: Binding { - Binding( - get: { currentControllers.contains(controller) }, - set: { bool in - if !bool { - currentControllers.removeAll(where: { $0.id == controller.id }) - } else { - currentControllers.append(controller) - } - // toggleController(controller) - } - ) - } - - - if customBinding.wrappedValue { - DisclosureGroup { - Toggle(isOn: customBinding) { - Text(controller.name) - .font(.body) - } - .tint(.blue) - .onDrag({ NSItemProvider() }) - } label: { - - if let controller = currentControllers.firstIndex(where: { $0.id == controller.id } ) { - Text("Player \(controller + 1)") - .onAppear() { - // print(currentControllers.firstIndex(where: { $0.id == controller.id }) ?? 0) - print(currentControllers.count) - - if currentControllers.count > 2 { - print(currentControllers[1]) - print(currentControllers[2]) - } - } - } + // Settings content + ScrollView { + VStack(spacing: 24) { + // Device Info Card + deviceInfoCard + .padding(.horizontal) + .padding(.top) + + switch selectedCategory { + case .graphics: + graphicsSettings + case .input: + inputSettings + case .system: + systemSettings + case .advanced: + advancedSettings + case .misc: + miscSettings } + Spacer(minLength: 50) } + .padding(.bottom) } - .onMove { from, to in - currentControllers.move(fromOffsets: from, toOffset: to) - } - } header: { - Text("Input Selector") - .font(.title3.weight(.semibold)) - .textCase(nil) - .headerProminence(.increased) - } footer: { - Text("Select input devices and on-screen controls to play with. ") } + } + .navigationTitle("Settings") + .navigationBarTitleDisplayMode(.large) + // .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .automatic)) + .onAppear { + mVKPreFillBuffer = false - // Input Settings - Section { - Toggle(isOn: $config.handHeldController) { - labelWithIcon("Player 1 to Handheld Input", iconName: "formfitting.gamecontroller") - }.tint(.blue) - + if let configs = loadSettings() { + self.config = configs + } else { + saveSettings() + } + } + .onChange(of: config) { _ in + saveSettings() + } + } + } + + // MARK: - Device Info Card + + private var deviceInfoCard: some View { + VStack(spacing: 16) { + // JIT Status indicator + HStack { + Circle() + .fill(ryujinx.jitenabled ? Color.green : Color.red) + .frame(width: 12, height: 12) + + Text(ryujinx.jitenabled ? "JIT Enabled" : "JIT Not Acquired") + .font(.subheadline.weight(.medium)) + .foregroundColor(ryujinx.jitenabled ? .green : .red) + + Spacer() + + let totalMemory = ProcessInfo.processInfo.physicalMemory + let memoryText = ProcessInfo.processInfo.isiOSAppOnMac + ? String(format: "%.0f GB", Double(totalMemory) / (1024 * 1024 * 1024)) + : String(format: "%.0f GB", Double(totalMemory) / 1_000_000_000) + + Text("\(memoryText) RAM") + .font(.subheadline) + .foregroundColor(.secondary) + } + + // Device cards + if (horizontalSizeClass == .regular && verticalSizeClass == .regular) || (horizontalSizeClass == .regular && verticalSizeClass == .compact) { + HStack(spacing: 16) { + InfoCard( + title: "Device", + value: UIDevice.modelName, + icon: deviceIcon, + color: .blue + ) - Toggle(isOn: $stickButton) { - labelWithIcon("Show Stick Buttons", iconName: "l.joystick.press.down") - }.tint(.blue) + InfoCard( + title: "System", + value: "\(UIDevice.current.systemName) \(UIDevice.current.systemVersion)", + icon: "applelogo", + color: .gray + ) + InfoCard( + title: "Increased Memory Limit", + value: checkAppEntitlement("com.apple.developer.kernel.increased-memory-limit") ? "Enabled" : "Disabled", + icon: "memorychip.fill", + color: .orange + ) + } + } else { + VStack(spacing: 16) { + InfoCard( + title: "Device", + value: UIDevice.modelName, + icon: deviceIcon, + color: .blue + ) - Toggle(isOn: $ryuDemo) { - labelWithIcon("On-Screen Controller (Demo)", iconName: "hand.draw") + InfoCard( + title: "System", + value: "\(UIDevice.current.systemName) \(UIDevice.current.systemVersion)", + icon: "applelogo", + color: .gray + ) + + InfoCard( + title: "Increased Memory Limit", + value: checkAppEntitlement("com.apple.developer.kernel.increased-memory-limit") ? "Enabled" : "Disabled", + icon: "memorychip.fill", + color: .orange + ) + } + } + } + .padding() + .background( + RoundedRectangle(cornerRadius: 16) + .fill(colorScheme == .dark ? Color(.systemGray6) : Color.white) + .shadow(color: Color.black.opacity(0.05), radius: 5, x: 0, y: 2) + ) + .onAppear { + ryujinx.ryuIsJITEnabled() + } + } + + private var deviceIcon: String { + let model = UIDevice.modelName + if model.contains("iPad") { + return "ipad" + } else if model.contains("iPhone") { + return "iphone" + } else { + return "desktopcomputer" + } + } + + // MARK: - Graphics Settings + + private var graphicsSettings: some View { + SettingsSection(title: "Graphics & Performance") { + // Resolution scale card + SettingsCard { + VStack(alignment: .leading, spacing: 12) { + HStack { + labelWithIcon("Resolution Scale", iconName: "magnifyingglass") + .font(.headline) + Spacer() + Button { + showResolutionInfo.toggle() + } label: { + Image(systemName: "info.circle") + .foregroundColor(.secondary) + } + .buttonStyle(.plain) + .alert(isPresented: $showResolutionInfo) { + Alert( + title: Text("Resolution Scale"), + message: Text("Adjust the internal rendering resolution. Higher values improve visuals but may reduce performance."), + dismissButton: .default(Text("OK")) + ) + } } - .tint(.blue) - .disabled(true) - VStack(alignment: .leading, spacing: 10) { - HStack { - labelWithIcon("On-Screen Controller Scale", iconName: "magnifyingglass") - .font(.headline) - Spacer() - Button { - showControllerInfo.toggle() - } label: { - Image(systemName: "info.circle") - .symbolRenderingMode(.hierarchical) - .foregroundStyle(.secondary) - } - .buttonStyle(.plain) - .help("Learn more about On-Screen Controller Scale") - .alert(isPresented: $showControllerInfo) { - Alert( - title: Text("On-Screen Controller Scale"), - message: Text("Adjust the On-Screen Controller size."), - dismissButton: .default(Text("OK")) - ) - } - } + VStack(spacing: 8) { + Slider(value: $config.resscale, in: 0.1...3.0, step: 0.05) - Slider(value: $controllerScale, in: 0.1...3.0, step: 0.05) { - Text("Resolution Scale") - } minimumValueLabel: { + HStack { Text("0.1x") - .font(.footnote) + .font(.caption2) .foregroundColor(.secondary) - } maximumValueLabel: { + + Spacer() + + Text("\(config.resscale, specifier: "%.2f")x") + .font(.headline) + .foregroundColor(.blue) + + Spacer() + Text("3.0x") - .font(.footnote) + .font(.caption2) .foregroundColor(.secondary) } - Text("\(controllerScale, specifier: "%.2f")x") + } + } + } + + // Anisotropic filtering card + SettingsCard { + VStack(alignment: .leading, spacing: 12) { + HStack { + labelWithIcon("Max Anisotropic Filtering", iconName: "magnifyingglass") + .font(.headline) + Spacer() + Button { + showAnisotropicInfo.toggle() + } label: { + Image(systemName: "info.circle") + .foregroundColor(.secondary) + } + .buttonStyle(.plain) + .alert(isPresented: $showAnisotropicInfo) { + Alert( + title: Text("Max Anisotropic Filtering"), + message: Text("Adjust the internal Anisotropic filtering. Higher values improve texture quality at angles but may reduce performance. Default at 0 lets game decide."), + dismissButton: .default(Text("OK")) + ) + } + } + + VStack(spacing: 8) { + Slider(value: $config.maxAnisotropy, in: 0...16.0, step: 0.1) + + HStack { + Text("Off") + .font(.caption2) + .foregroundColor(.secondary) + + Spacer() + + Text("\(config.maxAnisotropy, specifier: "%.1f")x") + .font(.headline) + .foregroundColor(.blue) + + Spacer() + + Text("16x") + .font(.caption2) + .foregroundColor(.secondary) + } + } + } + } + + // Toggle options card + SettingsCard { + VStack(spacing: 4) { + SettingsToggle(isOn: $config.disableShaderCache, icon: "memorychip", label: "Shader Cache") + + Divider() + + SettingsToggle(isOn: $config.disablevsync, icon: "arrow.triangle.2.circlepath", label: "Disable VSync") + + Divider() + + SettingsToggle(isOn: $config.enableTextureRecompression, icon: "rectangle.compress.vertical", label: "Texture Recompression") + + Divider() + + SettingsToggle(isOn: $config.disableDockedMode, icon: "dock.rectangle", label: "Docked Mode") + + Divider() + + SettingsToggle(isOn: $config.macroHLE, icon: "gearshape", label: "Macro HLE") + + Divider() + + SettingsToggle(isOn: $performacehud, icon: "speedometer", label: "Performance Overlay") + } + } + + // Aspect ratio card + SettingsCard { + VStack(alignment: .leading, spacing: 12) { + labelWithIcon("Aspect Ratio", iconName: "rectangle.expand.vertical") + .font(.headline) + + if (horizontalSizeClass == .regular && verticalSizeClass == .regular) || (horizontalSizeClass == .regular && verticalSizeClass == .compact) { + Picker(selection: $config.aspectRatio) { + ForEach(AspectRatio.allCases, id: \.self) { ratio in + Text(ratio.displayName).tag(ratio) + } + } label: { + EmptyView() + } + .pickerStyle(.segmented) + } else { + Picker(selection: $config.aspectRatio) { + ForEach(AspectRatio.allCases, id: \.self) { ratio in + Text(ratio.displayName).tag(ratio) + } + } label: { + EmptyView() + } + .pickerStyle(.menu) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.vertical, 4) + } + } + } + } + } + + // MARK: - Input Settings + + private var inputSettings: some View { + SettingsSection(title: "Input Configuration") { + // Controller selection card + SettingsCard { + VStack(alignment: .leading, spacing: 12) { + Text("Controller Selection") + .font(.headline) + .foregroundColor(.primary) + Divider() + + if !currentControllers.isEmpty { + ForEach(currentControllers) { controller in + if currentControllers.firstIndex(of: controller) == currentControllers.count - 1 && currentControllers.count != 1 { + Divider() + } + + HStack { + Image(systemName: "gamecontroller.fill") + .foregroundColor(.blue) + + if let index = currentControllers.firstIndex(where: { $0.id == controller.id }) { + Text("Player \(index + 1): \(controller.name)") + .lineLimit(1) + } + + Spacer() + + Button { + toggleController(controller) + } label: { + Image(systemName: "xmark.circle.fill") + .foregroundColor(.secondary) + } + .buttonStyle(.plain) + } + + if currentControllers[0] != controller && currentControllers.firstIndex(of: controller) == currentControllers.count - 1 { + Divider() + } + } + .onMove { from, to in + currentControllers.move(fromOffsets: from, toOffset: to) + } + .environment(\.editMode, .constant(.active)) + } + + if !controllersList.filter({ !currentControllers.contains($0) }).isEmpty { + Divider() + + Menu { + ForEach(controllersList.filter { !currentControllers.contains($0) }) { controller in + Button { + currentControllers.append(controller) + } label: { + Text(controller.name) + } + } + } label: { + Label("Add Controller", systemImage: "plus.circle.fill") + .foregroundColor(.blue) + .frame(maxWidth: .infinity, alignment: .center) + .padding(.vertical, 6) + } + } + } + } + + // On-screen controls card + SettingsCard { + VStack(spacing: 4) { + SettingsToggle(isOn: $config.handHeldController, icon: "formfitting.gamecontroller", label: "Player 1 to Handheld") + + Divider() + + SettingsToggle(isOn: $stickButton, icon: "l.joystick.press.down", label: "Show Stick Buttons") + + Divider() + + SettingsToggle(isOn: $ryuDemo, icon: "hand.draw", label: "On-Screen Controller (Demo)") + .disabled(true) + + Divider() + + SettingsToggle(isOn: $swapBandA, icon: "rectangle.2.swap", label: "Swap Face Buttons (Physical Controller)") + } + } + + // Controller scale card + SettingsCard { + VStack(alignment: .leading, spacing: 12) { + HStack { + labelWithIcon("On-Screen Controller Scale", iconName: "magnifyingglass") + .font(.headline) + Spacer() + Button { + showControllerInfo.toggle() + } label: { + Image(systemName: "info.circle") + .foregroundColor(.secondary) + } + .buttonStyle(.plain) + .alert(isPresented: $showControllerInfo) { + Alert( + title: Text("On-Screen Controller Scale"), + message: Text("Adjust the On-Screen Controller size."), + dismissButton: .default(Text("OK")) + ) + } + } + + VStack(spacing: 8) { + Slider(value: $controllerScale, in: 0.1...3.0, step: 0.05) + + HStack { + Text("Smaller") + .font(.caption2) + .foregroundColor(.secondary) + + Spacer() + + Text("\(controllerScale, specifier: "%.2f")x") + .font(.headline) + .foregroundColor(.blue) + + Spacer() + + Text("Larger") + .font(.caption2) + .foregroundColor(.secondary) + } + } + } + } + } + } + + // MARK: - System Settings + + private var systemSettings: some View { + SettingsSection(title: "System Configuration") { + // Language and region card + SettingsCard { + VStack(alignment: .leading, spacing: 16) { + VStack(alignment: .leading, spacing: 8) { + labelWithIcon("System Language", iconName: "character.bubble") + .font(.headline) + + Picker(selection: $config.language) { + ForEach(SystemLanguage.allCases, id: \.self) { language in + Text(language.displayName).tag(language) + } + } label: { + EmptyView() + } + .pickerStyle(.menu) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.vertical, 4) + } + + Divider() + + VStack(alignment: .leading, spacing: 8) { + labelWithIcon("Region", iconName: "globe") + .font(.headline) + + Picker(selection: $config.regioncode) { + ForEach(SystemRegionCode.allCases, id: \.self) { region in + Text(region.displayName).tag(region) + } + } label: { + EmptyView() + } + .pickerStyle(.menu) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.vertical, 4) + } + } + } + + // CPU options card + SettingsCard { + VStack(alignment: .leading, spacing: 16) { + Text("CPU Configuration") + .font(.headline) + .foregroundColor(.primary) + + VStack(alignment: .leading, spacing: 8) { + Text("Memory Manager Mode") .font(.subheadline) .foregroundColor(.secondary) - } - .padding(.vertical, 8) - } header: { - Text("Input Settings") - .font(.title3.weight(.semibold)) - .textCase(nil) - .headerProminence(.increased) - } footer: { - Text("Configure input devices and on-screen controls for easier navigation and play.") - } - - // Language and Region Settings - Section { - Picker(selection: $config.language) { - ForEach(SystemLanguage.allCases, id: \.self) { ratio in - Text(ratio.displayName).tag(ratio) - } - } label: { - labelWithIcon("Language", iconName: "character.bubble") - } - - Picker(selection: $config.regioncode) { - ForEach(SystemRegionCode.allCases, id: \.self) { ratio in - Text(ratio.displayName).tag(ratio) - } - } label: { - labelWithIcon("Region", iconName: "globe") - } - - - // globe - } header: { - Text("Language and Region Settings") - .font(.title3.weight(.semibold)) - .textCase(nil) - .headerProminence(.increased) - } footer: { - Text("Configure the System Language and the Region.") - } - - // CPU Mode - Section { - if filteredMemoryModes.isEmpty { - Text("No matches for \"\(searchText)\"") - .foregroundColor(.secondary) - } else { + Picker(selection: $config.memoryManagerMode) { ForEach(filteredMemoryModes, id: \.0) { key, displayName in Text(displayName).tag(key) } } label: { - labelWithIcon("Memory Manager Mode", iconName: "gearshape") + EmptyView() } + .pickerStyle(.segmented) } - Toggle(isOn: $config.disablePTC) { - labelWithIcon("Disable PTC", iconName: "cpu") - }.tint(.blue) + Divider() + + SettingsToggle(isOn: $config.disablePTC, icon: "cpu", label: "Disable PTC") if let gpuInfo = getGPUInfo(), gpuInfo.hasPrefix("Apple M") { - if #available (iOS 16.4, *) { - Toggle(isOn: .constant(false)) { - labelWithIcon("Hypervisor", iconName: "bolt") - } - .tint(.blue) - .disabled(true) - .onAppear() { - print("CPU Info: \(gpuInfo)") - } - } else if checkAppEntitlement("com.apple.private.hypervisor") { - Toggle(isOn: $config.hypervisor) { - labelWithIcon("Hypervisor", iconName: "bolt") - } - .tint(.blue) - .onAppear() { - print("CPU Info: \(gpuInfo)") - } - } - } - } header: { - Text("CPU") - .font(.title3.weight(.semibold)) - .textCase(nil) - .headerProminence(.increased) - } footer: { - Text("Select how memory is managed. 'Host (fast)' is best for most users.") - } - - - Section { - - - Toggle(isOn: $config.expandRam) { - labelWithIcon("Expand Guest Ram (6GB)", iconName: "exclamationmark.bubble") - } - .tint(.red) - - Toggle(isOn: $config.ignoreMissingServices) { - labelWithIcon("Ignore Missing Services", iconName: "waveform.path") - } - .tint(.red) - } header: { - Text("Hacks") - .font(.title3.weight(.semibold)) - .textCase(nil) - .headerProminence(.increased) - } - - // Other Settings - Section { - - Toggle(isOn: $ssb) { - labelWithIcon("Screenshot Button", iconName: "square.and.arrow.up") - } - .tint(.blue) - - if #available(iOS 17.0.1, *) { - // You will stay in our hearts, JitStreamer EB. one of the first public JIT enablers, that didn't need a computer after initial install - /* - Toggle(isOn: $jitStreamerEB) { - labelWithIcon("JitStreamer EB", iconName: "bolt.heart") - } - .tint(.blue) - .contextMenu { - Button { - waitForVPN.toggle() - } label: { - Label { - Text("Wait for VPN") - } icon: { - if waitForVPN { - Image(systemName: "checkmark") - } - } - - } - Button { - if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, - let mainWindow = windowScene.windows.last { - let alertController = UIAlertController(title: "About JitStreamer EB", message: "JitStreamer EB is an Amazing Application to Enable JIT on the go, made by one of the best, most kind, helpful and nice developers of all time jkcoxson <3", preferredStyle: .alert) - - let learnMoreButton = UIAlertAction(title: "Learn More", style: .default) {_ in - UIApplication.shared.open(URL(string: "https://jkcoxson.com/jitstreamer")!) - } - alertController.addAction(learnMoreButton) - - let doneButton = UIAlertAction(title: "Done", style: .cancel, handler: nil) - alertController.addAction(doneButton) - - mainWindow.rootViewController?.present(alertController, animated: true) - } - } label: { - Text("About") - } - } - */ + Divider() - Toggle(isOn: $stikJIT) { - labelWithIcon("StikJIT", iconName: "bolt.heart") + if #available(iOS 16.4, *) { + SettingsToggle(isOn: .constant(false), icon: "bolt", label: "Hypervisor") + .disabled(true) + } else if checkAppEntitlement("com.apple.private.hypervisor") { + SettingsToggle(isOn: $config.hypervisor, icon: "bolt", label: "Hypervisor") } - .tint(.blue) - .contextMenu { - Button { - if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, - let mainWindow = windowScene.windows.last { - let alertController = UIAlertController(title: "About StikJIT", message: "StikJIT is a really amazing iOS Application to Enable JIT on the go on-device, made by the best, most kind, helpful and nice developers of all time jkcoxson and Blu <3", preferredStyle: .alert) - - let learnMoreButton = UIAlertAction(title: "Learn More", style: .default) {_ in - UIApplication.shared.open(URL(string: "https://github.com/0-Blu/StikJIT")!) - } - alertController.addAction(learnMoreButton) - - let doneButton = UIAlertAction(title: "Done", style: .cancel, handler: nil) - alertController.addAction(doneButton) - - mainWindow.rootViewController?.present(alertController, animated: true) - } - } label: { - Text("About") + } + } + } + + // Memory hacks card + SettingsCard { + VStack(spacing: 4) { + SettingsToggle(isOn: $config.expandRam, icon: "exclamationmark.bubble", label: "Expand Guest RAM (6GB)") + .accentColor(.red) + + Divider() + + SettingsToggle(isOn: $config.ignoreMissingServices, icon: "waveform.path", label: "Ignore Missing Services") + .accentColor(.red) + } + } + } + } + + // MARK: - Advanced Settings + + private var advancedSettings: some View { + SettingsSection(title: "Advanced Options") { + // Debug options card + SettingsCard { + VStack(spacing: 4) { + SettingsToggle(isOn: $showlogsloading, icon: "text.alignleft", label: "Show Logs While Loading") + + Divider() + + SettingsToggle(isOn: $showlogsgame, icon: "text.magnifyingglass", label: "Show Logs In-Game") + + Divider() + + SettingsToggle(isOn: $config.debuglogs, icon: "exclamationmark.bubble", label: "Debug Logs") + + Divider() + + SettingsToggle(isOn: $config.tracelogs, icon: "waveform.path", label: "Trace Logs") + } + } + + // Advanced toggles card + SettingsCard { + VStack(spacing: 4) { + SettingsToggle(isOn: $config.dfsIntegrityChecks, icon: "checkmark.shield", label: "Disable FS Integrity Checks") + + Divider() + + if MTLHud.shared.canMetalHud { + SettingsToggle(isOn: $metalHUDEnabled, icon: "speedometer", label: "Metal Performance HUD") + .onChange(of: metalHUDEnabled) { newValue in + MTLHud.shared.toggle() } - } - } else { - Toggle(isOn: $useTrollStore) { - labelWithIcon("TrollStore JIT", iconName: "troll.svg") - } - .tint(.blue) + + Divider() } - Toggle(isOn: $syncqsubmits) { - labelWithIcon("MVK: Synchronous Queue Submits", iconName: "line.diagonal") - }.tint(.blue) - .contextMenu() { + SettingsToggle(isOn: $ignoreJIT, icon: "cpu", label: "Ignore JIT Popup") + + Divider() + + Button { + finishedStorage = false + } label: { + HStack { + Image(systemName: "arrow.triangle.2.circlepath.circle.fill") + .foregroundColor(.blue) + Text("Show Setup Screen") + .foregroundColor(.blue) + Spacer() + } + .padding(.vertical, 8) + } + } + } + + // Additional args card + SettingsCard { + VStack(alignment: .leading, spacing: 12) { + Text("Additional Arguments") + .font(.headline) + .foregroundColor(.primary) + + if #available(iOS 15.0, *) { + TextField("Separate arguments with commas", text: Binding( + get: { + config.additionalArgs.joined(separator: ", ") + }, + set: { newValue in + config.additionalArgs = newValue + .split(separator: ",") + .map { $0.trimmingCharacters(in: .whitespaces) } + } + )) + .font(.system(.body, design: .monospaced)) + .textFieldStyle(.roundedBorder) + .textInputAutocapitalization(.none) + .disableAutocorrection(true) + .padding(.vertical, 4) + } else { + TextField("Separate arguments with commas", text: Binding( + get: { + config.additionalArgs.joined(separator: ", ") + }, + set: { newValue in + config.additionalArgs = newValue + .split(separator: ",") + .map { $0.trimmingCharacters(in: .whitespaces) } + } + )) + .font(.system(.body, design: .monospaced)) + .textFieldStyle(.roundedBorder) + .disableAutocorrection(true) + .padding(.vertical, 4) + } + } + } + + // Page size info card + SettingsCard { + HStack { + labelWithIcon("Page Size", iconName: "textformat.size") + Spacer() + Text("\(String(Int(getpagesize())))") + .font(.system(.body, design: .monospaced)) + .foregroundColor(.secondary) + } + } + + if gamepo { + SettingsCard { + Text("The cake is a lie") + .font(.system(.body, design: .monospaced)) + .foregroundColor(.secondary) + .frame(maxWidth: .infinity, alignment: .center) + } + } + } + } + + // MARK: - Miscellaneous Settings + + private var miscSettings: some View { + SettingsSection(title: "Miscellaneous Options") { + SettingsCard { + VStack(spacing: 4) { + // Screenshot button card + SettingsToggle(isOn: $ssb, icon: "square.and.arrow.up", label: "Screenshot Button") + + Divider() + + // JIT options + if #available(iOS 17.0.1, *) { + SettingsToggle(isOn: $stikJIT, icon: "bolt.heart", label: "StikJIT") + .contextMenu { + Button { + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let mainWindow = windowScene.windows.last { + let alertController = UIAlertController(title: "About StikJIT", message: "StikJIT is a really amazing iOS Application to Enable JIT on the go on-device, made by the best, most kind, helpful and nice developers of all time jkcoxson and Blu <3", preferredStyle: .alert) + + let learnMoreButton = UIAlertAction(title: "Learn More", style: .default) {_ in + UIApplication.shared.open(URL(string: "https://github.com/0-Blu/StikJIT")!) + } + alertController.addAction(learnMoreButton) + + let doneButton = UIAlertAction(title: "Done", style: .cancel, handler: nil) + alertController.addAction(doneButton) + + mainWindow.rootViewController?.present(alertController, animated: true) + } + } label: { + Text("About") + } + } + } else { + SettingsToggle(isOn: $useTrollStore, icon: "troll.svg", label: "TrollStore JIT") + } + + Divider() + + // MoltenVK Options + SettingsToggle(isOn: $syncqsubmits, icon: "line.diagonal", label: "MVK: Synchronous Queue Submits") + .contextMenu { Button { if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, let mainWindow = windowScene.windows.last { @@ -548,160 +834,27 @@ struct SettingsView: View { } } - DisclosureGroup { - Toggle(isOn: $showlogsloading) { - labelWithIcon("Show logs while loading", iconName: "text.alignleft") - }.tint(.blue) - - Toggle(isOn: $showlogsgame) { - labelWithIcon("Show logs in-game", iconName: "text.line.magnify") - }.tint(.blue) - - Toggle(isOn: $config.debuglogs) { - labelWithIcon("Debug Logs", iconName: "exclamationmark.bubble") - } - .tint(.blue) - - Toggle(isOn: $config.tracelogs) { - labelWithIcon("Trace Logs", iconName: "waveform.path") - } - .tint(.blue) - } label: { - Text("Logs") - } - } header: { - Text("Miscellaneous Options") - .font(.title3.weight(.semibold)) - .textCase(nil) - .headerProminence(.increased) - } footer: { - Text("Enable trace and debug logs for advanced troubleshooting (Note: This degrades performance),\nEnable Screenshot Button for better screenshots\nand Enable TrollStore for automatic TrollStore JIT.") - } - - // Info - Section { - let totalMemory = ProcessInfo.processInfo.physicalMemory - let model = getDeviceModel() - - let iconName = model.hasPrefix("iPad") ? "ipad.landscape" : - model.hasPrefix("iPhone") ? "iphone" : - "macwindow" - - labelWithIcon("JIT Acquisition: \(ryujinx.jitenabled ? "Acquired" : "Not Acquired" )", iconName: "bolt.fill") - .onAppear() { - print("JIY ;(((((") - ryujinx.ryuIsJITEnabled() - } - - labelWithIcon("Increased Memory Limit Entitlement: \(checkAppEntitlement("com.apple.developer.kernel.increased-memory-limit") ? "Enabled" : "Disabled")", iconName: "memorychip") - - labelWithIcon("Device: \(UIDevice.modelName)", iconName: iconName) - - if ProcessInfo.processInfo.isiOSAppOnMac { - labelWithIcon("Memory: \(String(format: "%.0f GB", Double(totalMemory) / (1024 * 1024 * 1024)))", iconName: "memorychip.fill") - } else { - labelWithIcon("Device Memory: \(String(format: "%.0f GB", Double(totalMemory) / 1_000_000_000))", iconName: "memorychip.fill") - } - - - labelWithIcon("\(UIDevice.current.systemName) \(UIDevice.current.systemVersion)", iconName: "applelogo") - - } header: { - Text("Information") - .font(.title3.weight(.semibold)) - .textCase(nil) - .headerProminence(.increased) - } footer: { - Text("Shows info about Memory, Entitlement and JIT.") - } - - // Advanced - Section { - DisclosureGroup { - - Toggle(isOn: $config.dfsIntegrityChecks) { - labelWithIcon("Disable FS Integrity Checks", iconName: "checkmark.shield") - }.tint(.blue) - - HStack { - labelWithIcon("Page Size", iconName: "textformat.size") - Spacer() - Text("\(String(Int(getpagesize())))") - .foregroundColor(.secondary) - } - - if MTLHud.shared.canMetalHud { - Toggle(isOn: $metalHUDEnabled) { - labelWithIcon("Metal Performance HUD", iconName: "speedometer") - } - .tint(.blue) - .onChange(of: metalHUDEnabled) { newValue in - MTLHud.shared.toggle() - } - } - - Toggle(isOn: $ignoreJIT) { - labelWithIcon("Ignore JIT Popup", iconName: "cpu") - }.tint(.blue) - - TextField("Additional Arguments", text: Binding( - get: { - config.additionalArgs.joined(separator: " ") - }, - set: { newValue in - config.additionalArgs = newValue - .split(separator: ",") - .map { $0.trimmingCharacters(in: .whitespaces) } - } - )) - .textInputAutocapitalization(.none) - .disableAutocorrection(true) - - - + if ryujinx.firmwareversion != "0" { + Divider() Button { - finishedStorage = false - + Ryujinx.shared.removeFirmware() } label: { - Text("Show Setup") - .font(.body) + HStack { + Text("Remove Firmware") + .foregroundColor(.blue) + Spacer() + } + .padding(.vertical, 8) } - - - } label: { - Text("Advanced Options") } - } header: { - Text("Advanced") - .font(.title3.weight(.semibold)) - .textCase(nil) - .headerProminence(.increased) - } footer: { - Text("For advanced users. See page size or add custom arguments for experimental features, \"Metal Performance HUD\" is not needed if you have it enabled in settings. \n \n\(gamepo ? "the cake is a lie" : "")") } - - } - .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always)) - .navigationTitle("Settings") - .navigationBarTitleDisplayMode(.inline) - .listStyle(.insetGrouped) - .onAppear { - mVKPreFillBuffer = false - - if let configs = loadSettings() { - self.config = configs - } else { - saveSettings() - } - } - .onChange(of: config) { _ in - saveSettings() } } - .navigationViewStyle(.stack) } + // MARK: - Helper Functions + private func toggleController(_ controller: Controller) { if currentControllers.contains(where: { $0.id == controller.id }) { currentControllers.removeAll(where: { $0.id == controller.id }) @@ -714,45 +867,29 @@ struct SettingsView: View { MeloNX.saveSettings(config: config) } - func getDeviceModel() -> String { - var systemInfo = utsname() - uname(&systemInfo) - let machineMirror = Mirror(reflecting: systemInfo.machine) - let identifier = machineMirror.children.reduce("") { identifier, element in - guard let value = element.value as? Int8, value != 0 else { return identifier } - return identifier + String(UnicodeScalar(UInt8(value))) - } - return identifier - } - - func getGPUInfo() -> String? { let device = MTLCreateSystemDefaultDevice() - - let gpu = device?.name - print("GPU: " + (gpu ?? "")) - return gpu + return device?.name } - @ViewBuilder private func labelWithIcon(_ text: String, iconName: String, flipimage: Bool? = nil) -> some View { HStack(spacing: 8) { - if iconName.hasSuffix(".svg"){ + if iconName.hasSuffix(".svg") { if let flipimage, flipimage { SVGView(svgName: iconName, color: .blue) - .symbolRenderingMode(.hierarchical) + // .symbolRenderingMode(.hierarchical) .frame(width: 20, height: 20) .rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0)) } else { SVGView(svgName: iconName, color: .blue) - .symbolRenderingMode(.hierarchical) + // .symbolRenderingMode(.hierarchical) .frame(width: 20, height: 20) } } else if !iconName.isEmpty { Image(systemName: iconName) - .symbolRenderingMode(.hierarchical) - .foregroundStyle(.blue) + // .symbolRenderingMode(.hierarchical) + .foregroundColor(.blue) } Text(text) } @@ -760,7 +897,6 @@ struct SettingsView: View { } } - struct SVGView: UIViewRepresentable { var svgName: String var color: Color = Color.black @@ -801,9 +937,9 @@ func saveSettings(config: Ryujinx.Configuration) { let fileURL = URL.documentsDirectory.appendingPathComponent("config.json") try data.write(to: fileURL) - print("Settings saved to: \(fileURL.path)") + // print("Settings saved to: \(fileURL.path)") } catch { - print("Failed to save settings: \(error)") + // print("Failed to save settings: \(error)") } } @@ -812,7 +948,7 @@ func loadSettings() -> Ryujinx.Configuration? { let fileURL = URL.documentsDirectory.appendingPathComponent("config.json") guard FileManager.default.fileExists(atPath: fileURL.path) else { - print("Config file does not exist at: \(fileURL.path)") + // print("Config file does not exist at: \(fileURL.path)") return nil } @@ -822,7 +958,140 @@ func loadSettings() -> Ryujinx.Configuration? { let configs = try decoder.decode(Ryujinx.Configuration.self, from: data) return configs } catch { - print("Failed to load settings: \(error)") + // print("Failed to load settings: \(error)") return nil } } + + +// MARK: - Supporting Views + +struct CategoryButton: View { + let title: String + let icon: String + let isSelected: Bool + let action: () -> Void + + var body: some View { + Button(action: action) { + VStack(spacing: 6) { + Image(systemName: icon) + .font(.system(size: 16, weight: isSelected ? .semibold : .regular)) + Text(title) + .font(.system(size: 12, weight: isSelected ? .semibold : .regular)) + } + .foregroundColor(isSelected ? .blue : .secondary) + .frame(width: 70, height: 56) + .background( + RoundedRectangle(cornerRadius: 12) + .fill(isSelected ? Color.blue.opacity(0.15) : Color.clear) + ) + } + } +} + +struct SettingsSection: View { + let title: String + let content: Content + + init(title: String, @ViewBuilder content: () -> Content) { + self.title = title + self.content = content() + } + + var body: some View { + VStack(alignment: .leading, spacing: 16) { + Text(title) + .font(.title2.weight(.bold)) + .padding(.horizontal) + + content + } + } +} + +struct SettingsCard: View { + @Environment(\.colorScheme) var colorScheme + let content: Content + + init(@ViewBuilder content: () -> Content) { + self.content = content() + } + + var body: some View { + content + .padding() + .background( + RoundedRectangle(cornerRadius: 12) + .fill(colorScheme == .dark ? Color(.systemGray6) : Color.white) + .shadow(color: Color.black.opacity(0.05), radius: 5, x: 0, y: 2) + ) + .padding(.horizontal) + } +} + +struct SettingsToggle: View { + let isOn: Binding + let icon: String + let label: String + var disabled: Bool = false + + var body: some View { + Toggle(isOn: isOn) { + HStack(spacing: 8) { + if icon.hasSuffix(".svg") { + SVGView(svgName: icon, color: .blue) + .frame(width: 20, height: 20) + } else { + Image(systemName: icon) + // .symbolRenderingMode(.hierarchical) + .foregroundColor(.blue) + } + + Text(label) + .font(.body) + } + } + .toggleStyle(SwitchToggleStyle(tint: .blue)) + .disabled(disabled) + .padding(.vertical, 6) + } + + func disabled(_ disabled: Bool) -> SettingsToggle { + var view = self + view.disabled = disabled + return view + } + + func accentColor(_ color: Color) -> SettingsToggle { + var view = self + return view + } +} + +struct InfoCard: View { + let title: String + let value: String + let icon: String + let color: Color + + var body: some View { + VStack(alignment: .leading, spacing: 4) { + HStack { + Image(systemName: icon) + .foregroundColor(color) + Text(title) + .font(.caption) + .foregroundColor(.secondary) + } + + Text(value) + .font(.system(size: 14, weight: .medium)) + .lineLimit(1) + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(10) + .background(color.opacity(0.1)) + .cornerRadius(8) + } +} diff --git a/src/MeloNX/MeloNX/App/Views/Main/Updates/App/MeloNXUpdateSheet.swift b/src/MeloNX/MeloNX/App/Views/Main/Updates/App/MeloNXUpdateSheet.swift index a9f98de55..d2297555e 100644 --- a/src/MeloNX/MeloNX/App/Views/Main/Updates/App/MeloNXUpdateSheet.swift +++ b/src/MeloNX/MeloNX/App/Views/Main/Updates/App/MeloNXUpdateSheet.swift @@ -33,18 +33,32 @@ struct MeloNXUpdateSheet: View { Spacer() - Button(action: { - if let url = URL(string: updateInfo.download_link) { - UIApplication.shared.open(url) + if #available(iOS 15.0, *) { + Button(action: { + if let url = URL(string: updateInfo.download_link) { + UIApplication.shared.open(url) + } + }) { + Text("Download Now") + .font(.title3) + .bold() + .frame(width: 300, height: 40) } - }) { - Text("Download Now") - .font(.title3) - .bold() - .frame(width: 300, height: 40) + .buttonStyle(.borderedProminent) + .frame(alignment: .bottom) + } else { + Button(action: { + if let url = URL(string: updateInfo.download_link) { + UIApplication.shared.open(url) + } + }) { + Text("Download Now") + .font(.title3) + .bold() + .frame(width: 300, height: 40) + } + .frame(alignment: .bottom) } - .buttonStyle(.borderedProminent) - .frame(alignment: .bottom) } .padding(.horizontal) .navigationTitle("Version \(updateInfo.version_number) Available!") diff --git a/src/MeloNX/MeloNX/App/Views/Main/Updates/Games/GameDLCManagerSheet.swift b/src/MeloNX/MeloNX/App/Views/Main/Updates/Games/GameDLCManagerSheet.swift index 653e7c9a6..b479e4c2f 100644 --- a/src/MeloNX/MeloNX/App/Views/Main/Updates/Games/GameDLCManagerSheet.swift +++ b/src/MeloNX/MeloNX/App/Views/Main/Updates/Games/GameDLCManagerSheet.swift @@ -46,7 +46,7 @@ struct DLCManagerSheet: View { @Binding var game: Game! @State private var isSelectingGameDLC = false @State private var dlcs: [DownloadableContentContainer] = [] - @Environment(\.dismiss) private var dismiss + @Environment(\.presentationMode) var presentationMode // MARK: - Body var body: some View { @@ -66,7 +66,7 @@ struct DLCManagerSheet: View { .toolbar { ToolbarItem(placement: .navigationBarLeading) { Button("Done") { - dismiss() + presentationMode.wrappedValue.dismiss() } } @@ -127,27 +127,56 @@ struct DLCManagerSheet: View { private func dlcRow(_ dlc: DownloadableContentContainer) -> some View { - Button { - toggleDLC(dlc) - } label: { - HStack { - Text(dlc.filename) - .foregroundStyle(.primary) - Spacer() - Image(systemName: dlc.isEnabled ? "checkmark.circle.fill" : "circle") - .foregroundStyle(dlc.isEnabled ? .primary : .secondary) - .imageScale(.large) - } - .contentShape(Rectangle()) - } - .buttonStyle(.plain) - .swipeActions(edge: .trailing) { - Button(role: .destructive) { - if let index = dlcs.firstIndex(where: { $0.id == dlc.id }) { - removeDLC(at: IndexSet(integer: index)) + Group { + if #available(iOS 15.0, *) { + Button { + toggleDLC(dlc) + } label: { + HStack { + Text(dlc.filename) + .foregroundColor(.primary) + Spacer() + Image(systemName: dlc.isEnabled ? "checkmark.circle.fill" : "circle") + .foregroundColor(dlc.isEnabled ? .primary : .secondary) + .imageScale(.large) + } + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .swipeActions(edge: .trailing) { + Button(role: .destructive) { + if let index = dlcs.firstIndex(where: { $0.id == dlc.id }) { + removeDLC(at: IndexSet(integer: index)) + } + } label: { + Label("Delete", systemImage: "trash") + } + } + } else { + Button { + toggleDLC(dlc) + } label: { + HStack { + Text(dlc.filename) + .foregroundColor(.primary) + Spacer() + Image(systemName: dlc.isEnabled ? "checkmark.circle.fill" : "circle") + .foregroundColor(dlc.isEnabled ? .primary : .secondary) + .imageScale(.large) + } + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .contextMenu { + Button { + if let index = dlcs.firstIndex(where: { $0.id == dlc.id }) { + removeDLC(at: IndexSet(integer: index)) + } + } label: { + Label("Delete", systemImage: "trash") + .foregroundColor(.red) + } } - } label: { - Label("Delete", systemImage: "trash") } } } @@ -261,7 +290,7 @@ private extension DLCManagerSheet { return result } catch { - print("Error loading DLCs: \(error)") + // print("Error loading DLCs: \(error)") return [] } } @@ -300,7 +329,7 @@ extension Array where Element: AnyObject { // MARK: - URL Extension extension URL { - @available(iOS, introduced: 15.0, deprecated: 16.0, message: "Use URL.documentsDirectory on iOS 16 and above") + @available(iOS, introduced: 14.0, deprecated: 16.0, message: "Use URL.documentsDirectory on iOS 16 and above") static var documentsDirectory: URL { let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! return documentDirectory diff --git a/src/MeloNX/MeloNX/App/Views/Main/Updates/Games/GameUpdateManagerSheet.swift b/src/MeloNX/MeloNX/App/Views/Main/Updates/Games/GameUpdateManagerSheet.swift index f4a236264..f6d83191b 100644 --- a/src/MeloNX/MeloNX/App/Views/Main/Updates/Games/GameUpdateManagerSheet.swift +++ b/src/MeloNX/MeloNX/App/Views/Main/Updates/Games/GameUpdateManagerSheet.swift @@ -14,7 +14,7 @@ struct UpdateManagerSheet: View { @Binding var game: Game? @State private var isSelectingGameUpdate = false @State private var jsonURL: URL? = nil - @Environment(\.dismiss) private var dismiss + @Environment(\.presentationMode) var presentationMode // MARK: - Models class UpdateItem: Identifiable, ObservableObject { @@ -51,7 +51,7 @@ struct UpdateManagerSheet: View { .toolbar { ToolbarItem(placement: .navigationBarLeading) { Button("Done") { - dismiss() + presentationMode.wrappedValue.dismiss() } } @@ -106,15 +106,26 @@ struct UpdateManagerSheet: View { } private func updateRow(_ update: UpdateItem) -> some View { + Group { + if #available(iOS 15, *) { + updateRowNew(update) + } else { + updateRowOld(update) + } + } + } + + @available(iOS 15, *) + private func updateRowNew(_ update: UpdateItem) -> some View { Button { toggleSelection(update) } label: { HStack { Text(update.filename) - .foregroundStyle(.primary) + .foregroundColor(.primary) Spacer() Image(systemName: update.isSelected ? "checkmark.circle.fill" : "circle") - .foregroundStyle(update.isSelected ? .primary : .secondary) + .foregroundColor(update.isSelected ? .primary : .secondary) .imageScale(.large) } .contentShape(Rectangle()) @@ -131,6 +142,31 @@ struct UpdateManagerSheet: View { } } + private func updateRowOld(_ update: UpdateItem) -> some View { + Button { + toggleSelection(update) + } label: { + HStack { + Text(update.filename) + .foregroundColor(.primary) + Spacer() + Image(systemName: update.isSelected ? "checkmark.circle.fill" : "circle") + .foregroundColor(update.isSelected ? .primary : .secondary) + .imageScale(.large) + } + .contentShape(Rectangle()) + } + .contextMenu { + Button { + if let index = updates.firstIndex(where: { $0.path == update.path }) { + removeUpdate(at: IndexSet(integer: index)) + } + } label: { + Label("Delete", systemImage: "trash") + } + } + } + // MARK: - Functions private func loadData() { guard let game = game else { return } @@ -246,12 +282,12 @@ struct UpdateManagerSheet: View { updates = updates.map { item in var mutableItem = item mutableItem.isSelected = item.path == update.path && !update.isSelected - print(mutableItem.isSelected) - print(update.isSelected) + // print(mutableItem.isSelected) + // print(update.isSelected) return mutableItem } - print(updates) + // print(updates) saveJSON() } diff --git a/src/MeloNX/MeloNX/App/Views/MeloNXApp.swift b/src/MeloNX/MeloNX/App/Views/MeloNXApp.swift index e4d74c70e..c800312b2 100644 --- a/src/MeloNX/MeloNX/App/Views/MeloNXApp.swift +++ b/src/MeloNX/MeloNX/App/Views/MeloNXApp.swift @@ -29,13 +29,6 @@ struct MeloNXApp: App { @State var finished = false @AppStorage("hasbeenfinished") var finishedStorage: Bool = false - init() { - let fixMethod = class_getInstanceMethod(UIDocumentPickerViewController.self, #selector(UIDocumentPickerViewController.fix_init(forOpeningContentTypes:asCopy:)))! - let origMethod = class_getInstanceMethod(UIDocumentPickerViewController.self, #selector(UIDocumentPickerViewController.init(forOpeningContentTypes:asCopy:)))! - method_exchangeImplementations(origMethod, fixMethod) - } - - var body: some Scene { WindowGroup { if finishedStorage { @@ -80,18 +73,18 @@ struct MeloNXApp: App { #endif guard let url = URL(string: urlString) else { - print("Invalid URL") + // print("Invalid URL") return } let task = URLSession.shared.dataTask(with: url) { data, response, error in if let error = error { - print("Error checking for new version: \(error)") + // print("Error checking for new version: \(error)") return } guard let data = data else { - print("No data received") + // print("No data received") return } @@ -106,7 +99,7 @@ struct MeloNXApp: App { } } } catch { - print("Failed to decode response: \(error)") + // print("Failed to decode response: \(error)") } } diff --git a/src/MeloNX/MeloNX/App/Views/Setup/SetupView.swift b/src/MeloNX/MeloNX/App/Views/Setup/SetupView.swift index 9a55bc66c..50caa99aa 100644 --- a/src/MeloNX/MeloNX/App/Views/Setup/SetupView.swift +++ b/src/MeloNX/MeloNX/App/Views/Setup/SetupView.swift @@ -54,12 +54,17 @@ struct SetupView: View { ) { result in handleFirmwareImport(result: result) } - .alert(alertMessage, isPresented: $showAlert) { - Button("OK", role: .cancel) {} + .alert(isPresented: $showAlert) { + Alert(title: Text(alertMessage), dismissButton: .default(Text("OK"))) } - .alert("Skip Setup?", isPresented: $showSkipAlert) { - Button("Skip", role: .destructive) { finished = true } - Button("Cancel", role: .cancel) {} + .alert(isPresented: $showSkipAlert) { + Alert( + title: Text("Skip Setup?"), + primaryButton: .destructive(Text("Skip")) { + finished = true + }, + secondaryButton: .cancel() + ) } .onAppear { initialize() @@ -390,7 +395,7 @@ struct SetupView: View { let iconFileName = iconFiles.last else { - print("Could not find icons in bundle") + // print("Could not find icons in bundle") return "" } diff --git a/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/libMoltenVK.dylib b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/libMoltenVK.dylib index 4f3386894..8996b7ca5 100755 Binary files a/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/libMoltenVK.dylib and b/src/MeloNX/MeloNX/Dependencies/Dynamic Libraries/libMoltenVK.dylib differ diff --git a/src/MeloNX/MeloNX/Dependencies/XCFrameworks/MoltenVK.xcframework/Info.plist b/src/MeloNX/MeloNX/Dependencies/XCFrameworks/MoltenVK.xcframework/Info.plist deleted file mode 100644 index d0cb291b6..000000000 --- a/src/MeloNX/MeloNX/Dependencies/XCFrameworks/MoltenVK.xcframework/Info.plist +++ /dev/null @@ -1,27 +0,0 @@ - - - - - AvailableLibraries - - - BinaryPath - MoltenVK.framework/MoltenVK - LibraryIdentifier - ios-arm64 - LibraryPath - MoltenVK.framework - SupportedArchitectures - - arm64 - - SupportedPlatform - ios - - - CFBundlePackageType - XFWK - XCFrameworkFormatVersion - 1.0 - - diff --git a/src/MeloNX/MeloNX/Dependencies/XCFrameworks/MoltenVK.xcframework/ios-arm64/MoltenVK.framework/Info.plist b/src/MeloNX/MeloNX/Dependencies/XCFrameworks/MoltenVK.xcframework/ios-arm64/MoltenVK.framework/Info.plist deleted file mode 100644 index 2e0914e03..000000000 Binary files a/src/MeloNX/MeloNX/Dependencies/XCFrameworks/MoltenVK.xcframework/ios-arm64/MoltenVK.framework/Info.plist and /dev/null differ diff --git a/src/MeloNX/MeloNX/Dependencies/XCFrameworks/MoltenVK.xcframework/ios-arm64/MoltenVK.framework/MoltenVK b/src/MeloNX/MeloNX/Dependencies/XCFrameworks/MoltenVK.xcframework/ios-arm64/MoltenVK.framework/MoltenVK deleted file mode 100755 index 4f3386894..000000000 Binary files a/src/MeloNX/MeloNX/Dependencies/XCFrameworks/MoltenVK.xcframework/ios-arm64/MoltenVK.framework/MoltenVK and /dev/null differ diff --git a/src/Ryujinx.Headless.SDL2/Options.cs b/src/Ryujinx.Headless.SDL2/Options.cs index 2707328b4..d45aa0215 100644 --- a/src/Ryujinx.Headless.SDL2/Options.cs +++ b/src/Ryujinx.Headless.SDL2/Options.cs @@ -207,7 +207,7 @@ namespace Ryujinx.Headless.SDL2 [Option("aspect-ratio", Required = false, Default = AspectRatio.Fixed16x9, HelpText = "Aspect Ratio applied to the renderer window.")] public AspectRatio AspectRatio { get; set; } - [Option("backend-threading", Required = false, Default = BackendThreading.Auto, HelpText = "Whether or not backend threading is enabled. The \"Auto\" setting will determine whether threading should be enabled at runtime.")] + [Option("backend-threading", Required = false, Default = BackendThreading.On, HelpText = "Whether or not backend threading is enabled. The \"Auto\" setting will determine whether threading should be enabled at runtime.")] public BackendThreading BackendThreading { get; set; } [Option("disable-macro-hle", Required = false, HelpText = "Disables high-level emulation of Macro code. Leaving this enabled improves performance but may cause graphical glitches in some games.")]