forked from MeloNX/MeloNX
Updated JitStreamer Implementation, Reimplemented Texture Chunks, Reworked Alerts and more
This commit is contained in:
parent
ceab2f0ac8
commit
54cb7eb953
@ -41,4 +41,6 @@ ESCAPED_PATH=$(echo "$DOTNET_PATH" | sed 's/\//\\\//g')
|
|||||||
# Update the xcconfig file
|
# Update the xcconfig file
|
||||||
sed -i '' "s/^DOTNET = .*/DOTNET = $ESCAPED_PATH/g" "$XCCONFIG_FILE"
|
sed -i '' "s/^DOTNET = .*/DOTNET = $ESCAPED_PATH/g" "$XCCONFIG_FILE"
|
||||||
|
|
||||||
|
$DOTNET_PATH clean
|
||||||
|
|
||||||
echo "Updated MeloNX.xcconfig with DOTNET path: $DOTNET_PATH"
|
echo "Updated MeloNX.xcconfig with DOTNET path: $DOTNET_PATH"
|
||||||
|
@ -8,6 +8,6 @@
|
|||||||
// Configuration settings file format documentation can be found at:
|
// Configuration settings file format documentation can be found at:
|
||||||
// https://help.apple.com/xcode/#/dev745c5c974
|
// https://help.apple.com/xcode/#/dev745c5c974
|
||||||
|
|
||||||
VERSION = 1.6.0
|
VERSION = 1.7.0
|
||||||
|
|
||||||
DOTNET = /usr/local/share/dotnet/dotnet
|
DOTNET = /usr/local/share/dotnet/dotnet
|
||||||
|
@ -116,7 +116,7 @@
|
|||||||
"Dependencies/Dynamic Libraries/Ryujinx.Headless.SDL2.dylib" = (
|
"Dependencies/Dynamic Libraries/Ryujinx.Headless.SDL2.dylib" = (
|
||||||
CodeSignOnCopy,
|
CodeSignOnCopy,
|
||||||
);
|
);
|
||||||
"Dependencies/Dynamic Libraries/RyujinxKeyboard.framework" = (
|
"Dependencies/Dynamic Libraries/RyujinxHelper.framework" = (
|
||||||
CodeSignOnCopy,
|
CodeSignOnCopy,
|
||||||
RemoveHeadersOnCopy,
|
RemoveHeadersOnCopy,
|
||||||
);
|
);
|
||||||
@ -177,7 +177,7 @@
|
|||||||
"Dependencies/Dynamic Libraries/libavutil.dylib",
|
"Dependencies/Dynamic Libraries/libavutil.dylib",
|
||||||
"Dependencies/Dynamic Libraries/libMoltenVK.dylib",
|
"Dependencies/Dynamic Libraries/libMoltenVK.dylib",
|
||||||
"Dependencies/Dynamic Libraries/Ryujinx.Headless.SDL2.dylib",
|
"Dependencies/Dynamic Libraries/Ryujinx.Headless.SDL2.dylib",
|
||||||
"Dependencies/Dynamic Libraries/RyujinxKeyboard.framework",
|
"Dependencies/Dynamic Libraries/RyujinxHelper.framework",
|
||||||
Dependencies/XCFrameworks/libavcodec.xcframework,
|
Dependencies/XCFrameworks/libavcodec.xcframework,
|
||||||
Dependencies/XCFrameworks/libavfilter.xcframework,
|
Dependencies/XCFrameworks/libavfilter.xcframework,
|
||||||
Dependencies/XCFrameworks/libavformat.xcframework,
|
Dependencies/XCFrameworks/libavformat.xcframework,
|
||||||
@ -484,6 +484,7 @@
|
|||||||
/* Begin PBXTargetDependency section */
|
/* Begin PBXTargetDependency section */
|
||||||
4E2953AC2D803BC9000497CD /* PBXTargetDependency */ = {
|
4E2953AC2D803BC9000497CD /* PBXTargetDependency */ = {
|
||||||
isa = PBXTargetDependency;
|
isa = PBXTargetDependency;
|
||||||
|
platformFilter = ios;
|
||||||
target = BD43C6212D1B248D003BBC42 /* com.Stossy11.MeloNX.RyujinxAg */;
|
target = BD43C6212D1B248D003BBC42 /* com.Stossy11.MeloNX.RyujinxAg */;
|
||||||
targetProxy = 4E2953AB2D803BC9000497CD /* PBXContainerItemProxy */;
|
targetProxy = 4E2953AB2D803BC9000497CD /* PBXContainerItemProxy */;
|
||||||
};
|
};
|
||||||
@ -712,6 +713,14 @@
|
|||||||
"$(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",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
);
|
);
|
||||||
GCC_OPTIMIZATION_LEVEL = z;
|
GCC_OPTIMIZATION_LEVEL = z;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@ -842,10 +851,24 @@
|
|||||||
"$(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",
|
||||||
|
"$(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)";
|
MARKETING_VERSION = "$(VERSION)";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
|
PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
|
SUPPORTS_MACCATALYST = NO;
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "MeloNX/App/Core/Headers/Ryujinx-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "MeloNX/App/Core/Headers/Ryujinx-Header.h";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
@ -924,6 +947,14 @@
|
|||||||
"$(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",
|
||||||
|
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||||
);
|
);
|
||||||
GCC_OPTIMIZATION_LEVEL = z;
|
GCC_OPTIMIZATION_LEVEL = z;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@ -1054,10 +1085,24 @@
|
|||||||
"$(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",
|
||||||
|
"$(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)";
|
MARKETING_VERSION = "$(VERSION)";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
|
PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
|
SUPPORTS_MACCATALYST = NO;
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "MeloNX/App/Core/Headers/Ryujinx-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "MeloNX/App/Core/Headers/Ryujinx-Header.h";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
|
Binary file not shown.
@ -64,7 +64,10 @@
|
|||||||
debugDocumentVersioning = "YES"
|
debugDocumentVersioning = "YES"
|
||||||
debugServiceExtension = "internal"
|
debugServiceExtension = "internal"
|
||||||
enableGPUValidationMode = "1"
|
enableGPUValidationMode = "1"
|
||||||
allowLocationSimulation = "YES">
|
allowLocationSimulation = "YES"
|
||||||
|
viewDebuggingEnabled = "No"
|
||||||
|
consoleMode = "0"
|
||||||
|
structuredConsoleMode = "2">
|
||||||
<BuildableProductRunnable
|
<BuildableProductRunnable
|
||||||
runnableDebuggingMode = "0">
|
runnableDebuggingMode = "0">
|
||||||
<BuildableReference
|
<BuildableReference
|
||||||
|
@ -6,40 +6,121 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import Network
|
||||||
|
import UIKit
|
||||||
|
|
||||||
func enableJITEB() {
|
func enableJITEB() {
|
||||||
guard let bundleID = Bundle.main.bundleIdentifier else {
|
if UserDefaults.standard.bool(forKey: "waitForVPN") {
|
||||||
return
|
waitForVPNConnection { connected in
|
||||||
|
if connected {
|
||||||
|
enableJITEBRequest()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
enableJITEBRequest()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func enableJITEBRequest() {
|
||||||
|
let pid = Int(getpid())
|
||||||
|
print(pid)
|
||||||
|
|
||||||
let address = URL(string: "http://[fd00::]:9172/launch_app/\(bundleID)")!
|
let address = URL(string: "http://[fd00::]:9172/attach/\(pid)")!
|
||||||
|
var request = URLRequest(url: address)
|
||||||
|
request.httpMethod = "POST"
|
||||||
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||||
|
|
||||||
let task = URLSession.shared.dataTask(with: address) { data, response, error in
|
let task = URLSession.shared.dataTask(with: request) { data, response, error in
|
||||||
if error != nil {
|
if let error = error {
|
||||||
|
presentAlert(title: "Request Error", message: error.localizedDescription)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
if let data = data, let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
|
||||||
let lastWindow = windowScene.windows.last {
|
showLaunchAppAlert(jsonData: data, in: windowScene.windows.last!.rootViewController!)
|
||||||
showLaunchAppAlert(jsonData: data!, in: lastWindow.rootViewController!)
|
|
||||||
} else {
|
} else {
|
||||||
fatalError("Unable to get Window")
|
fatalError("Unable to get Window")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
task.resume()
|
task.resume()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func waitForVPNConnection(timeout: TimeInterval = 30, interval: TimeInterval = 1, _ completion: @escaping (Bool) -> Void) {
|
||||||
|
let startTime = Date()
|
||||||
|
let timer = DispatchSource.makeTimerSource(queue: DispatchQueue.global(qos: .background))
|
||||||
|
|
||||||
|
timer.schedule(deadline: .now(), repeating: interval)
|
||||||
|
|
||||||
|
timer.setEventHandler {
|
||||||
|
pingSite { connected in
|
||||||
|
if connected {
|
||||||
|
timer.cancel()
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
completion(true)
|
||||||
|
}
|
||||||
|
} else if Date().timeIntervalSince(startTime) > timeout {
|
||||||
|
timer.cancel()
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
completion(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
timer.resume()
|
||||||
|
}
|
||||||
|
|
||||||
|
func pingSite(host: String = "http://[fd00::]:9172/hello", completion: @escaping (Bool) -> Void) {
|
||||||
|
guard let url = URL(string: host) else {
|
||||||
|
completion(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let config = URLSessionConfiguration.default
|
||||||
|
config.timeoutIntervalForRequest = 2.0
|
||||||
|
config.timeoutIntervalForResource = 2.0
|
||||||
|
|
||||||
|
let session = URLSession(configuration: config)
|
||||||
|
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
request.httpMethod = "GET"
|
||||||
|
|
||||||
|
let task = session.dataTask(with: request) { _, response, error in
|
||||||
|
if let error = error {
|
||||||
|
print("Ping failed: \(error.localizedDescription)")
|
||||||
|
completion(false)
|
||||||
|
} else if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 {
|
||||||
|
completion(true)
|
||||||
|
} else {
|
||||||
|
let httpResponse = response as? HTTPURLResponse
|
||||||
|
completion(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
task.resume()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func presentAlert(title: String, message: String, completion: (() -> Void)? = nil) {
|
||||||
|
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||||
|
let lastWindow = windowScene.windows.last {
|
||||||
|
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||||
|
alert.addAction(UIAlertAction(title: "OK", style: .default) { _ in
|
||||||
|
completion?()
|
||||||
|
})
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
lastWindow.rootViewController?.present(alert, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct LaunchApp: Codable {
|
struct LaunchApp: Codable {
|
||||||
let ok: Bool
|
let success: Bool
|
||||||
let error: String?
|
let message: String
|
||||||
let launching: Bool
|
|
||||||
let position: Int?
|
|
||||||
let mounting: Bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func showLaunchAppAlert(jsonData: Data, in viewController: UIViewController) {
|
func showLaunchAppAlert(jsonData: Data, in viewController: UIViewController) {
|
||||||
@ -48,28 +129,23 @@ func showLaunchAppAlert(jsonData: Data, in viewController: UIViewController) {
|
|||||||
|
|
||||||
var message = ""
|
var message = ""
|
||||||
|
|
||||||
if let error = result.error {
|
if !result.success {
|
||||||
message = "Error: \(error)"
|
message += "\n\(result.message)"
|
||||||
} else if result.mounting {
|
|
||||||
message = "App is mounting..."
|
|
||||||
} else if result.launching {
|
let alert = UIAlertController(title: "JIT Error", message: message, preferredStyle: .alert)
|
||||||
message = "App is launching..."
|
alert.addAction(UIAlertAction(title: "OK", style: .default))
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
viewController.present(alert, animated: true)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
message = "App launch status unknown."
|
print("Hopefully JIT is enabled now...")
|
||||||
}
|
Ryujinx.shared.ryuIsJITEnabled()
|
||||||
|
|
||||||
if let position = result.position {
|
|
||||||
message += "\nPosition: \(position)"
|
|
||||||
}
|
|
||||||
|
|
||||||
let alert = UIAlertController(title: "Launch Status", message: message, preferredStyle: .alert)
|
|
||||||
alert.addAction(UIAlertAction(title: "OK", style: .default))
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
viewController.present(alert, animated: true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
|
print(String(data: jsonData, encoding: .utf8))
|
||||||
let alert = UIAlertController(title: "Decoding Error", message: error.localizedDescription, preferredStyle: .alert)
|
let alert = UIAlertController(title: "Decoding Error", message: error.localizedDescription, preferredStyle: .alert)
|
||||||
alert.addAction(UIAlertAction(title: "OK", style: .default))
|
alert.addAction(UIAlertAction(title: "OK", style: .default))
|
||||||
|
|
||||||
|
@ -189,6 +189,24 @@ enum VirtualControllerButton: Int {
|
|||||||
case dPadRight
|
case dPadRight
|
||||||
case leftTrigger
|
case leftTrigger
|
||||||
case rightTrigger
|
case rightTrigger
|
||||||
|
|
||||||
|
var isTrigger: Bool {
|
||||||
|
switch self {
|
||||||
|
case .leftTrigger, .rightTrigger, .leftShoulder, .rightShoulder:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var isSmall: Bool {
|
||||||
|
switch self {
|
||||||
|
case .back, .start, .guide:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ThumbstickType: Int {
|
enum ThumbstickType: Int {
|
||||||
|
@ -31,7 +31,7 @@ struct iOSNav<Content: View>: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class Ryujinx {
|
class Ryujinx : ObservableObject {
|
||||||
private var isRunning = false
|
private var isRunning = false
|
||||||
|
|
||||||
let virtualController = VirtualController()
|
let virtualController = VirtualController()
|
||||||
@ -45,6 +45,10 @@ class Ryujinx {
|
|||||||
|
|
||||||
@Published var defMLContentSize: CGFloat?
|
@Published var defMLContentSize: CGFloat?
|
||||||
|
|
||||||
|
var thread: Thread!
|
||||||
|
|
||||||
|
@Published var jitenabled = false
|
||||||
|
|
||||||
var shouldMetal: Bool {
|
var shouldMetal: Bool {
|
||||||
metalLayer == nil
|
metalLayer == nil
|
||||||
}
|
}
|
||||||
@ -145,7 +149,7 @@ class Ryujinx {
|
|||||||
|
|
||||||
self.config = config
|
self.config = config
|
||||||
|
|
||||||
RunLoop.current.perform { [self] in
|
thread = Thread { [self] in
|
||||||
|
|
||||||
isRunning = true
|
isRunning = true
|
||||||
|
|
||||||
@ -178,6 +182,10 @@ class Ryujinx {
|
|||||||
Self.log("Emulation failed to start: \(error)")
|
Self.log("Emulation failed to start: \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
thread.qualityOfService = .background
|
||||||
|
thread.name = "MeloNX"
|
||||||
|
thread.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -192,6 +200,7 @@ class Ryujinx {
|
|||||||
self.metalLayer = nil
|
self.metalLayer = nil
|
||||||
|
|
||||||
stop_emulation()
|
stop_emulation()
|
||||||
|
thread.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
var running: Bool {
|
var running: Bool {
|
||||||
@ -500,6 +509,11 @@ class Ryujinx {
|
|||||||
static func log(_ message: String) {
|
static func log(_ message: String) {
|
||||||
print("[Ryujinx] \(message)")
|
print("[Ryujinx] \(message)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ryuIsJITEnabled() {
|
||||||
|
jitenabled = isJITEnabled()
|
||||||
|
print("JIT \(jitenabled)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -53,7 +53,7 @@ struct ContentView: View {
|
|||||||
private let animationDuration: Double = 1.0
|
private let animationDuration: Double = 1.0
|
||||||
@State private var isAnimating = false
|
@State private var isAnimating = false
|
||||||
@State var isLoading = true
|
@State var isLoading = true
|
||||||
@State var jitNotEnabled = false
|
@StateObject var ryujinx = Ryujinx.shared
|
||||||
|
|
||||||
// MARK: - SDL
|
// MARK: - SDL
|
||||||
var sdlInitFlags: UInt32 = SDL_INIT_EVENTS | SDL_INIT_GAMECONTROLLER | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO | SDL_INIT_VIDEO
|
var sdlInitFlags: UInt32 = SDL_INIT_EVENTS | SDL_INIT_GAMECONTROLLER | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO | SDL_INIT_VIDEO
|
||||||
@ -79,14 +79,16 @@ struct ContentView: View {
|
|||||||
|
|
||||||
_settings = State(initialValue: defaultSettings)
|
_settings = State(initialValue: defaultSettings)
|
||||||
|
|
||||||
|
print(SDL_CONTROLLER_BUTTON_LEFTSTICK.rawValue)
|
||||||
|
|
||||||
initializeSDL()
|
initializeSDL()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Body
|
// MARK: - Body
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if game != nil && (!jitNotEnabled || ignoreJIT) {
|
if game != nil && (ryujinx.jitenabled || ignoreJIT) {
|
||||||
gameView
|
gameView
|
||||||
} else if game != nil && jitNotEnabled {
|
} else if game != nil && !ryujinx.jitenabled {
|
||||||
jitErrorView
|
jitErrorView
|
||||||
} else {
|
} else {
|
||||||
mainMenuView
|
mainMenuView
|
||||||
@ -117,9 +119,16 @@ struct ContentView: View {
|
|||||||
|
|
||||||
private var jitErrorView: some View {
|
private var jitErrorView: some View {
|
||||||
Text("")
|
Text("")
|
||||||
.sheet(isPresented: $jitNotEnabled) {
|
.sheet(isPresented:Binding(
|
||||||
|
get: { !ryujinx.jitenabled },
|
||||||
|
set: { newValue in
|
||||||
|
ryujinx.jitenabled = newValue
|
||||||
|
|
||||||
|
ryujinx.ryuIsJITEnabled()
|
||||||
|
})
|
||||||
|
) {
|
||||||
JITPopover() {
|
JITPopover() {
|
||||||
jitNotEnabled = false
|
ryujinx.jitenabled = false
|
||||||
}
|
}
|
||||||
.interactiveDismissDisabled()
|
.interactiveDismissDisabled()
|
||||||
}
|
}
|
||||||
@ -308,9 +317,9 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func refreshControllersList() {
|
private func refreshControllersList() {
|
||||||
controllersList = Ryujinx.shared.getConnectedControllers()
|
controllersList = ryujinx.getConnectedControllers()
|
||||||
|
|
||||||
if let onscreen = controllersList.first(where: { $0.name == Ryujinx.shared.virtualController.controllername }) {
|
if let onscreen = controllersList.first(where: { $0.name == ryujinx.virtualController.controllername }) {
|
||||||
self.onscreencontroller = onscreen
|
self.onscreencontroller = onscreen
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -343,7 +352,7 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
try Ryujinx.shared.start(with: config)
|
try ryujinx.start(with: config)
|
||||||
} catch {
|
} catch {
|
||||||
print("Error: \(error.localizedDescription)")
|
print("Error: \(error.localizedDescription)")
|
||||||
}
|
}
|
||||||
@ -351,7 +360,8 @@ struct ContentView: View {
|
|||||||
|
|
||||||
private func configureEnvironmentVariables() {
|
private func configureEnvironmentVariables() {
|
||||||
if mVKPreFillBuffer {
|
if mVKPreFillBuffer {
|
||||||
setenv("MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS", "2", 1)
|
mVKPreFillBuffer = false
|
||||||
|
// setenv("MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS", "2", 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if syncqsubmits {
|
if syncqsubmits {
|
||||||
@ -366,8 +376,8 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func checkJitStatus() {
|
private func checkJitStatus() {
|
||||||
jitNotEnabled = !isJITEnabled()
|
ryujinx.ryuIsJITEnabled()
|
||||||
if jitNotEnabled {
|
if !ryujinx.jitenabled {
|
||||||
if useTrollStore {
|
if useTrollStore {
|
||||||
askForJIT()
|
askForJIT()
|
||||||
} else if jitStreamerEB {
|
} else if jitStreamerEB {
|
||||||
@ -382,9 +392,9 @@ struct ContentView: View {
|
|||||||
if let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
|
if let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
|
||||||
components.host == "game" {
|
components.host == "game" {
|
||||||
if let text = components.queryItems?.first(where: { $0.name == "id" })?.value {
|
if let text = components.queryItems?.first(where: { $0.name == "id" })?.value {
|
||||||
game = Ryujinx.shared.games.first(where: { $0.titleId == text })
|
game = ryujinx.games.first(where: { $0.titleId == text })
|
||||||
} else if let text = components.queryItems?.first(where: { $0.name == "name" })?.value {
|
} else if let text = components.queryItems?.first(where: { $0.name == "name" })?.value {
|
||||||
game = Ryujinx.shared.games.first(where: { $0.titleName == text })
|
game = ryujinx.games.first(where: { $0.titleName == text })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,101 +11,155 @@ import SwiftUIJoystick
|
|||||||
import CoreMotion
|
import CoreMotion
|
||||||
|
|
||||||
struct ControllerView: View {
|
struct ControllerView: View {
|
||||||
|
// MARK: - Properties
|
||||||
|
@AppStorage("On-ScreenControllerScale") private var controllerScale: Double = 1.0
|
||||||
|
@AppStorage("stick-button") private var stickButton = false
|
||||||
|
@State private var isPortrait = true
|
||||||
|
@Environment(\.verticalSizeClass) var verticalSizeClass
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Body
|
||||||
var body: some View {
|
var body: some View {
|
||||||
GeometryReader { geometry in
|
Group {
|
||||||
if geometry.size.height > geometry.size.width && UIDevice.current.userInterfaceIdiom != .pad {
|
let isPad = UIDevice.current.userInterfaceIdiom == .pad
|
||||||
VStack {
|
|
||||||
|
if isPortrait && !isPad {
|
||||||
Spacer()
|
portraitLayout
|
||||||
VStack {
|
} else {
|
||||||
HStack {
|
landscapeLayout
|
||||||
VStack {
|
}
|
||||||
ShoulderButtonsViewLeft()
|
}
|
||||||
ZStack {
|
.padding()
|
||||||
Joystick()
|
.onChange(of: verticalSizeClass) { _ in
|
||||||
DPadView()
|
updateOrientation()
|
||||||
}
|
}
|
||||||
}
|
.onAppear(perform: updateOrientation)
|
||||||
Spacer()
|
}
|
||||||
VStack {
|
|
||||||
ShoulderButtonsViewRight()
|
// MARK: - Layouts
|
||||||
ZStack {
|
private var portraitLayout: some View {
|
||||||
Joystick(iscool: true) // hope this works
|
VStack {
|
||||||
ABXYView()
|
Spacer()
|
||||||
}
|
VStack(spacing: 20) {
|
||||||
}
|
HStack(spacing: 30) {
|
||||||
|
VStack(spacing: 15) {
|
||||||
|
ShoulderButtonsViewLeft()
|
||||||
|
ZStack {
|
||||||
|
Joystick()
|
||||||
|
DPadView()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
HStack {
|
|
||||||
ButtonView(button: .start) // Adding the + button
|
VStack(spacing: 15) {
|
||||||
.padding(.horizontal, 40)
|
ShoulderButtonsViewRight()
|
||||||
ButtonView(button: .back) // Adding the - button
|
ZStack {
|
||||||
.padding(.horizontal, 40)
|
Joystick(iscool: true)
|
||||||
|
ABXYView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
HStack(spacing: 60) {
|
||||||
// could be landscape
|
HStack {
|
||||||
VStack {
|
ButtonView(button: .leftStick)
|
||||||
|
.padding()
|
||||||
Spacer()
|
ButtonView(button: .start)
|
||||||
VStack {
|
}
|
||||||
HStack {
|
|
||||||
|
HStack {
|
||||||
// gotta fuckin add + and - now
|
ButtonView(button: .back)
|
||||||
VStack {
|
ButtonView(button: .rightStick)
|
||||||
ShoulderButtonsViewLeft()
|
.padding()
|
||||||
ZStack {
|
|
||||||
Joystick()
|
|
||||||
DPadView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
HStack {
|
|
||||||
// Spacer()
|
|
||||||
VStack {
|
|
||||||
// Spacer()
|
|
||||||
ButtonView(button: .back) // Adding the - button
|
|
||||||
}
|
|
||||||
Spacer()
|
|
||||||
VStack {
|
|
||||||
// Spacer()
|
|
||||||
ButtonView(button: .start) // Adding the + button
|
|
||||||
}
|
|
||||||
// Spacer()
|
|
||||||
}
|
|
||||||
VStack {
|
|
||||||
ShoulderButtonsViewRight()
|
|
||||||
ZStack {
|
|
||||||
Joystick(iscool: true) // hope this work s
|
|
||||||
ABXYView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
// .padding(.bottom, geometry.size.height / 11) // also extremally broken (
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding()
|
}
|
||||||
|
|
||||||
|
private var landscapeLayout: some View {
|
||||||
|
VStack {
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
VStack(spacing: 15) {
|
||||||
|
ShoulderButtonsViewLeft()
|
||||||
|
ZStack {
|
||||||
|
Joystick()
|
||||||
|
DPadView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
centerButtons
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
VStack(spacing: 15) {
|
||||||
|
ShoulderButtonsViewRight()
|
||||||
|
ZStack {
|
||||||
|
Joystick(iscool: true)
|
||||||
|
ABXYView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var centerButtons: some View {
|
||||||
|
Group {
|
||||||
|
if stickButton {
|
||||||
|
VStack {
|
||||||
|
HStack(spacing: 50) {
|
||||||
|
ButtonView(button: .leftStick)
|
||||||
|
.padding()
|
||||||
|
Spacer()
|
||||||
|
ButtonView(button: .rightStick)
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
.padding(.top, 30)
|
||||||
|
|
||||||
|
HStack(spacing: 50) {
|
||||||
|
ButtonView(button: .back)
|
||||||
|
Spacer()
|
||||||
|
ButtonView(button: .start)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.bottom, 20)
|
||||||
|
} else {
|
||||||
|
HStack(spacing: 50) {
|
||||||
|
ButtonView(button: .back)
|
||||||
|
Spacer()
|
||||||
|
ButtonView(button: .start)
|
||||||
|
}
|
||||||
|
.padding(.bottom, 20)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Methods
|
||||||
|
|
||||||
|
private func updateOrientation() {
|
||||||
|
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||||
|
let window = windowScene.windows.first {
|
||||||
|
isPortrait = window.bounds.size.height > window.bounds.size.width
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
struct ShoulderButtonsViewLeft: View {
|
struct ShoulderButtonsViewLeft: View {
|
||||||
@State var width: CGFloat = 160
|
@State private var width: CGFloat = 160
|
||||||
@State var height: CGFloat = 20
|
@State private var height: CGFloat = 20
|
||||||
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
|
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack {
|
HStack(spacing: 20) {
|
||||||
ButtonView(button: .leftTrigger)
|
ButtonView(button: .leftTrigger)
|
||||||
.padding(.horizontal)
|
|
||||||
ButtonView(button: .leftShoulder)
|
ButtonView(button: .leftShoulder)
|
||||||
.padding(.horizontal)
|
|
||||||
}
|
}
|
||||||
.frame(width: width, height: height)
|
.frame(width: width, height: height)
|
||||||
.onAppear() {
|
.onAppear {
|
||||||
if UIDevice.current.systemName.contains("iPadOS") {
|
if UIDevice.current.systemName.contains("iPadOS") {
|
||||||
width *= 1.2
|
width *= 1.2
|
||||||
height *= 1.2
|
height *= 1.2
|
||||||
@ -118,19 +172,17 @@ struct ShoulderButtonsViewLeft: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct ShoulderButtonsViewRight: View {
|
struct ShoulderButtonsViewRight: View {
|
||||||
@State var width: CGFloat = 160
|
@State private var width: CGFloat = 160
|
||||||
@State var height: CGFloat = 20
|
@State private var height: CGFloat = 20
|
||||||
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
|
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack {
|
HStack(spacing: 20) {
|
||||||
ButtonView(button: .rightShoulder)
|
ButtonView(button: .rightShoulder)
|
||||||
.padding(.horizontal)
|
|
||||||
ButtonView(button: .rightTrigger)
|
ButtonView(button: .rightTrigger)
|
||||||
.padding(.horizontal)
|
|
||||||
}
|
}
|
||||||
.frame(width: width, height: height)
|
.frame(width: width, height: height)
|
||||||
.onAppear() {
|
.onAppear {
|
||||||
if UIDevice.current.systemName.contains("iPadOS") {
|
if UIDevice.current.systemName.contains("iPadOS") {
|
||||||
width *= 1.2
|
width *= 1.2
|
||||||
height *= 1.2
|
height *= 1.2
|
||||||
@ -143,21 +195,21 @@ struct ShoulderButtonsViewRight: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct DPadView: View {
|
struct DPadView: View {
|
||||||
@State var size: CGFloat = 145
|
@State private var size: CGFloat = 145
|
||||||
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
|
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack(spacing: 5) {
|
||||||
ButtonView(button: .dPadUp)
|
ButtonView(button: .dPadUp)
|
||||||
HStack {
|
HStack(spacing: 20) {
|
||||||
ButtonView(button: .dPadLeft)
|
ButtonView(button: .dPadLeft)
|
||||||
Spacer(minLength: 20)
|
Spacer(minLength: 20)
|
||||||
ButtonView(button: .dPadRight)
|
ButtonView(button: .dPadRight)
|
||||||
}
|
}
|
||||||
ButtonView(button: .dPadDown)
|
ButtonView(button: .dPadDown)
|
||||||
.padding(.horizontal)
|
|
||||||
}
|
}
|
||||||
.frame(width: size, height: size)
|
.frame(width: size, height: size)
|
||||||
.onAppear() {
|
.onAppear {
|
||||||
if UIDevice.current.systemName.contains("iPadOS") {
|
if UIDevice.current.systemName.contains("iPadOS") {
|
||||||
size *= 1.2
|
size *= 1.2
|
||||||
}
|
}
|
||||||
@ -168,22 +220,21 @@ struct DPadView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct ABXYView: View {
|
struct ABXYView: View {
|
||||||
@State var size: CGFloat = 145
|
@State private var size: CGFloat = 145
|
||||||
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
|
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack(spacing: 5) {
|
||||||
ButtonView(button: .X)
|
ButtonView(button: .X)
|
||||||
HStack {
|
HStack(spacing: 20) {
|
||||||
ButtonView(button: .Y)
|
ButtonView(button: .Y)
|
||||||
Spacer(minLength: 20)
|
Spacer(minLength: 20)
|
||||||
ButtonView(button: .A)
|
ButtonView(button: .A)
|
||||||
}
|
}
|
||||||
ButtonView(button: .B)
|
ButtonView(button: .B)
|
||||||
.padding(.horizontal)
|
|
||||||
}
|
}
|
||||||
.frame(width: size, height: size)
|
.frame(width: size, height: size)
|
||||||
.onAppear() {
|
.onAppear {
|
||||||
if UIDevice.current.systemName.contains("iPadOS") {
|
if UIDevice.current.systemName.contains("iPadOS") {
|
||||||
size *= 1.2
|
size *= 1.2
|
||||||
}
|
}
|
||||||
@ -195,58 +246,90 @@ struct ABXYView: View {
|
|||||||
|
|
||||||
struct ButtonView: View {
|
struct ButtonView: View {
|
||||||
var button: VirtualControllerButton
|
var button: VirtualControllerButton
|
||||||
@State var width: CGFloat = 45
|
@State private var width: CGFloat = 45
|
||||||
@State var height: CGFloat = 45
|
@State private var height: CGFloat = 45
|
||||||
@State var isPressed = false
|
@State private var isPressed = false
|
||||||
@AppStorage("onscreenhandheld") var onscreenjoy: Bool = false
|
@AppStorage("onscreenhandheld") var onscreenjoy: Bool = false
|
||||||
@Environment(\.colorScheme) var colorScheme
|
|
||||||
@Environment(\.presentationMode) var presentationMode
|
@Environment(\.presentationMode) var presentationMode
|
||||||
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
|
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
|
||||||
|
@State private var debounceTimer: Timer?
|
||||||
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Image(systemName: buttonText)
|
Image(systemName: buttonText)
|
||||||
.resizable()
|
.resizable()
|
||||||
|
.scaledToFit()
|
||||||
.frame(width: width, height: height)
|
.frame(width: width, height: height)
|
||||||
.foregroundColor(colorScheme == .dark ? Color.gray : Color.gray)
|
.foregroundColor(true ? Color.white.opacity(0.9) : Color.black.opacity(0.9))
|
||||||
.opacity(isPressed ? 0.4 : 0.7)
|
.background(
|
||||||
|
Group {
|
||||||
|
if !button.isTrigger {
|
||||||
|
Circle()
|
||||||
|
.fill(true ? Color.gray.opacity(0.4) : Color.gray.opacity(0.3))
|
||||||
|
.frame(width: width * 1.25, height: height * 1.25)
|
||||||
|
} else {
|
||||||
|
Image(systemName: buttonText)
|
||||||
|
.resizable()
|
||||||
|
.scaledToFit()
|
||||||
|
.frame(width: width * 1.25, height: height * 1.25)
|
||||||
|
.foregroundColor(true ? Color.gray.opacity(0.4) : Color.gray.opacity(0.3))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.opacity(isPressed ? 0.6 : 1.0)
|
||||||
.gesture(
|
.gesture(
|
||||||
DragGesture(minimumDistance: 0)
|
DragGesture(minimumDistance: 0)
|
||||||
.onChanged { _ in
|
.onChanged { _ in
|
||||||
if !self.isPressed {
|
handleButtonPress()
|
||||||
self.isPressed = true
|
|
||||||
Ryujinx.shared.virtualController.setButtonState(1, for: button)
|
|
||||||
Haptics.shared.play(.heavy)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.onEnded { _ in
|
.onEnded { _ in
|
||||||
self.isPressed = false
|
handleButtonRelease()
|
||||||
Ryujinx.shared.virtualController.setButtonState(0, for: button)
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.onAppear() {
|
.onAppear {
|
||||||
if button == .leftTrigger || button == .rightTrigger || button == .leftShoulder || button == .rightShoulder {
|
configureSizeForButton()
|
||||||
width = 65
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if button == .back || button == .start || button == .guide {
|
|
||||||
width = 35
|
|
||||||
height = 35
|
|
||||||
}
|
|
||||||
|
|
||||||
if UIDevice.current.systemName.contains("iPadOS") {
|
|
||||||
width *= 1.2
|
|
||||||
height *= 1.2
|
|
||||||
}
|
|
||||||
|
|
||||||
width *= CGFloat(controllerScale)
|
|
||||||
height *= CGFloat(controllerScale)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func handleButtonPress() {
|
||||||
|
if !isPressed {
|
||||||
|
isPressed = true
|
||||||
|
|
||||||
|
debounceTimer?.invalidate()
|
||||||
|
|
||||||
|
Ryujinx.shared.virtualController.setButtonState(1, for: button)
|
||||||
|
|
||||||
|
Haptics.shared.play(.medium)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleButtonRelease() {
|
||||||
|
if isPressed {
|
||||||
|
isPressed = false
|
||||||
|
|
||||||
|
debounceTimer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: false) { _ in
|
||||||
|
Ryujinx.shared.virtualController.setButtonState(0, for: button)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func configureSizeForButton() {
|
||||||
|
if button.isTrigger {
|
||||||
|
width = 70
|
||||||
|
height = 40
|
||||||
|
} else if button.isSmall {
|
||||||
|
width = 35
|
||||||
|
height = 35
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust for iPad
|
||||||
|
if UIDevice.current.systemName.contains("iPadOS") {
|
||||||
|
width *= 1.2
|
||||||
|
height *= 1.2
|
||||||
|
}
|
||||||
|
|
||||||
|
width *= CGFloat(controllerScale)
|
||||||
|
height *= CGFloat(controllerScale)
|
||||||
|
}
|
||||||
|
|
||||||
private var buttonText: String {
|
private var buttonText: String {
|
||||||
switch button {
|
switch button {
|
||||||
@ -258,6 +341,10 @@ struct ButtonView: View {
|
|||||||
return "x.circle.fill"
|
return "x.circle.fill"
|
||||||
case .Y:
|
case .Y:
|
||||||
return "y.circle.fill"
|
return "y.circle.fill"
|
||||||
|
case .leftStick:
|
||||||
|
return "l.joystick.press.down.fill"
|
||||||
|
case .rightStick:
|
||||||
|
return "r.joystick.press.down.fill"
|
||||||
case .dPadUp:
|
case .dPadUp:
|
||||||
return "arrowtriangle.up.circle.fill"
|
return "arrowtriangle.up.circle.fill"
|
||||||
case .dPadDown:
|
case .dPadDown:
|
||||||
@ -267,7 +354,7 @@ struct ButtonView: View {
|
|||||||
case .dPadRight:
|
case .dPadRight:
|
||||||
return "arrowtriangle.right.circle.fill"
|
return "arrowtriangle.right.circle.fill"
|
||||||
case .leftTrigger:
|
case .leftTrigger:
|
||||||
return"zl.rectangle.roundedtop.fill"
|
return "zl.rectangle.roundedtop.fill"
|
||||||
case .rightTrigger:
|
case .rightTrigger:
|
||||||
return "zr.rectangle.roundedtop.fill"
|
return "zr.rectangle.roundedtop.fill"
|
||||||
case .leftShoulder:
|
case .leftShoulder:
|
||||||
@ -275,16 +362,11 @@ struct ButtonView: View {
|
|||||||
case .rightShoulder:
|
case .rightShoulder:
|
||||||
return "r.rectangle.roundedbottom.fill"
|
return "r.rectangle.roundedbottom.fill"
|
||||||
case .start:
|
case .start:
|
||||||
return "plus.circle.fill" // System symbol for +
|
return "plus.circle.fill"
|
||||||
case .back:
|
case .back:
|
||||||
return "minus.circle.fill" // System symbol for -
|
return "minus.circle.fill"
|
||||||
case .guide:
|
case .guide:
|
||||||
return "house.circle.fill"
|
return "house.circle.fill"
|
||||||
// This should be all the cases
|
|
||||||
default:
|
|
||||||
return ""
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ import SwiftUIJoystick
|
|||||||
|
|
||||||
public struct Joystick: View {
|
public struct Joystick: View {
|
||||||
@State var iscool: Bool? = nil
|
@State var iscool: Bool? = nil
|
||||||
|
@Environment(\.colorScheme) var colorScheme
|
||||||
@ObservedObject public var joystickMonitor = JoystickMonitor()
|
@ObservedObject public var joystickMonitor = JoystickMonitor()
|
||||||
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
|
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
|
||||||
var dragDiameter: CGFloat {
|
var dragDiameter: CGFloat {
|
||||||
@ -36,8 +36,13 @@ public struct Joystick: View {
|
|||||||
.hidden()
|
.hidden()
|
||||||
},
|
},
|
||||||
foreground: {
|
foreground: {
|
||||||
Circle().fill(Color.gray)
|
Circle()
|
||||||
.opacity(0.7)
|
.fill(colorScheme == .dark ? Color.white.opacity(0.7) : Color.black.opacity(0.7))
|
||||||
|
.background(
|
||||||
|
Circle()
|
||||||
|
.fill(colorScheme == .dark ? Color.gray.opacity(0.3) : Color.gray.opacity(0.2))
|
||||||
|
.frame(width: (dragDiameter / 4) * 1.2, height: (dragDiameter / 4) * 1.2)
|
||||||
|
)
|
||||||
},
|
},
|
||||||
locksInPlace: false)
|
locksInPlace: false)
|
||||||
.onChange(of: self.joystickMonitor.xyPoint) { newValue in
|
.onChange(of: self.joystickMonitor.xyPoint) { newValue in
|
||||||
|
@ -29,6 +29,7 @@ struct GameLibraryView: View {
|
|||||||
@State var isViewingGameInfo: Bool = false
|
@State var isViewingGameInfo: Bool = false
|
||||||
@State var isSelectingGameUpdate: Bool = false
|
@State var isSelectingGameUpdate: Bool = false
|
||||||
@State var isSelectingGameDLC: Bool = false
|
@State var isSelectingGameDLC: Bool = false
|
||||||
|
@StateObject var ryujinx = Ryujinx.shared
|
||||||
@State var gameInfo: Game?
|
@State var gameInfo: Game?
|
||||||
var games: Binding<[Game]> {
|
var games: Binding<[Game]> {
|
||||||
Binding(
|
Binding(
|
||||||
@ -203,6 +204,13 @@ struct GameLibraryView: View {
|
|||||||
.foregroundColor(.blue)
|
.foregroundColor(.blue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ToolbarItem(placement: .topBarLeading) {
|
||||||
|
if ryujinx.jitenabled {
|
||||||
|
Image(systemName: "checkmark")
|
||||||
|
.foregroundStyle(.green)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: startemu) { game in
|
.onChange(of: startemu) { game in
|
||||||
guard let game else { return }
|
guard let game else { return }
|
||||||
|
@ -37,6 +37,8 @@ struct JITPopover: View {
|
|||||||
if isJIT {
|
if isJIT {
|
||||||
dismiss()
|
dismiss()
|
||||||
onJITEnabled()
|
onJITEnabled()
|
||||||
|
|
||||||
|
Ryujinx.shared.ryuIsJITEnabled()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -96,13 +96,14 @@ struct LogFileView: View {
|
|||||||
|
|
||||||
private func startLogFileWatching() {
|
private func startLogFileWatching() {
|
||||||
showingLogs = true
|
showingLogs = true
|
||||||
self.readLatestLogFile()
|
|
||||||
Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { timer in
|
Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { timer in
|
||||||
if showingLogs {
|
if showingLogs {
|
||||||
self.readLatestLogFile()
|
self.readLatestLogFile()
|
||||||
}
|
}
|
||||||
|
|
||||||
if isfps {
|
if isfps {
|
||||||
|
sleep(1)
|
||||||
if get_current_fps() != 0 {
|
if get_current_fps() != 0 {
|
||||||
stopLogFileWatching()
|
stopLogFileWatching()
|
||||||
timer.invalidate()
|
timer.invalidate()
|
||||||
|
@ -48,11 +48,15 @@ struct SettingsView: View {
|
|||||||
|
|
||||||
@AppStorage("showlogsgame") var showlogsgame: Bool = false
|
@AppStorage("showlogsgame") var showlogsgame: Bool = false
|
||||||
|
|
||||||
|
@AppStorage("stick-button") var stickButton = false
|
||||||
|
@AppStorage("waitForVPN") var waitForVPN = false
|
||||||
|
|
||||||
@State private var showResolutionInfo = false
|
@State private var showResolutionInfo = false
|
||||||
@State private var showAnisotropicInfo = false
|
@State private var showAnisotropicInfo = false
|
||||||
@State private var showControllerInfo = false
|
@State private var showControllerInfo = false
|
||||||
@State private var searchText = ""
|
@State private var searchText = ""
|
||||||
@AppStorage("portal") var gamepo = false
|
@AppStorage("portal") var gamepo = false
|
||||||
|
@StateObject var ryujinx = Ryujinx.shared
|
||||||
|
|
||||||
var filteredMemoryModes: [(String, String)] {
|
var filteredMemoryModes: [(String, String)] {
|
||||||
guard !searchText.isEmpty else { return memoryManagerModes }
|
guard !searchText.isEmpty else { return memoryManagerModes }
|
||||||
@ -286,6 +290,11 @@ struct SettingsView: View {
|
|||||||
}.tint(.blue)
|
}.tint(.blue)
|
||||||
|
|
||||||
|
|
||||||
|
Toggle(isOn: $stickButton) {
|
||||||
|
labelWithIcon("Show Stick Buttons", iconName: "l.joystick.press.down")
|
||||||
|
}.tint(.blue)
|
||||||
|
|
||||||
|
|
||||||
Toggle(isOn: $ryuDemo) {
|
Toggle(isOn: $ryuDemo) {
|
||||||
labelWithIcon("On-Screen Controller (Demo)", iconName: "hand.draw")
|
labelWithIcon("On-Screen Controller (Demo)", iconName: "hand.draw")
|
||||||
}
|
}
|
||||||
@ -451,10 +460,22 @@ struct SettingsView: View {
|
|||||||
}
|
}
|
||||||
.tint(.blue)
|
.tint(.blue)
|
||||||
.contextMenu {
|
.contextMenu {
|
||||||
|
Button {
|
||||||
|
waitForVPN.toggle()
|
||||||
|
} label: {
|
||||||
|
Label {
|
||||||
|
Text("Wait for VPN")
|
||||||
|
} icon: {
|
||||||
|
if waitForVPN {
|
||||||
|
Image(systemName: "checkmark")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Button {
|
Button {
|
||||||
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||||
let mainWindow = windowScene.windows.last {
|
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 iOS developers of all time jkcoxson <3", preferredStyle: .alert)
|
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
|
let learnMoreButton = UIAlertAction(title: "Learn More", style: .default) {_ in
|
||||||
UIApplication.shared.open(URL(string: "https://jkcoxson.com/jitstreamer")!)
|
UIApplication.shared.open(URL(string: "https://jkcoxson.com/jitstreamer")!)
|
||||||
@ -539,7 +560,11 @@ struct SettingsView: View {
|
|||||||
model.hasPrefix("iPhone") ? "iphone" :
|
model.hasPrefix("iPhone") ? "iphone" :
|
||||||
"macwindow"
|
"macwindow"
|
||||||
|
|
||||||
labelWithIcon("JIT Acquisition: \(isJITEnabled() ? "Acquired" : "Not Acquired" )", iconName: "bolt.fill")
|
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("Increased Memory Limit Entitlement: \(checkAppEntitlement("com.apple.developer.kernel.increased-memory-limit") ? "Enabled" : "Disabled")", iconName: "memorychip")
|
||||||
|
|
||||||
@ -566,10 +591,6 @@ struct SettingsView: View {
|
|||||||
Section {
|
Section {
|
||||||
DisclosureGroup {
|
DisclosureGroup {
|
||||||
|
|
||||||
Toggle(isOn: $mVKPreFillBuffer) {
|
|
||||||
labelWithIcon("MVK: Pre-Fill Metal Command Buffers", iconName: "gearshape")
|
|
||||||
}.tint(.blue)
|
|
||||||
|
|
||||||
Toggle(isOn: $config.dfsIntegrityChecks) {
|
Toggle(isOn: $config.dfsIntegrityChecks) {
|
||||||
labelWithIcon("Disable FS Integrity Checks", iconName: "checkmark.shield")
|
labelWithIcon("Disable FS Integrity Checks", iconName: "checkmark.shield")
|
||||||
}.tint(.blue)
|
}.tint(.blue)
|
||||||
@ -637,6 +658,8 @@ struct SettingsView: View {
|
|||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.listStyle(.insetGrouped)
|
.listStyle(.insetGrouped)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
|
mVKPreFillBuffer = false
|
||||||
|
|
||||||
if let configs = loadSettings() {
|
if let configs = loadSettings() {
|
||||||
self.config = configs
|
self.config = configs
|
||||||
} else {
|
} else {
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import UniformTypeIdentifiers
|
import UniformTypeIdentifiers
|
||||||
|
|
||||||
|
// MARK: - Models
|
||||||
struct DownloadableContentNca: Codable, Hashable {
|
struct DownloadableContentNca: Codable, Hashable {
|
||||||
var fullPath: String
|
var fullPath: String
|
||||||
var titleId: UInt
|
var titleId: UInt
|
||||||
@ -20,9 +21,18 @@ struct DownloadableContentNca: Codable, Hashable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct DownloadableContentContainer: Codable, Hashable {
|
struct DownloadableContentContainer: Codable, Hashable, Identifiable {
|
||||||
|
var id: String { containerPath }
|
||||||
var containerPath: String
|
var containerPath: String
|
||||||
var downloadableContentNcaList: [DownloadableContentNca]
|
var downloadableContentNcaList: [DownloadableContentNca]
|
||||||
|
|
||||||
|
var filename: String {
|
||||||
|
(containerPath as NSString).lastPathComponent
|
||||||
|
}
|
||||||
|
|
||||||
|
var isEnabled: Bool {
|
||||||
|
downloadableContentNcaList.first?.enabled == true
|
||||||
|
}
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
enum CodingKeys: String, CodingKey {
|
||||||
case containerPath = "path"
|
case containerPath = "path"
|
||||||
@ -30,136 +40,265 @@ struct DownloadableContentContainer: Codable, Hashable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - View
|
||||||
struct DLCManagerSheet: View {
|
struct DLCManagerSheet: View {
|
||||||
|
// MARK: - Properties
|
||||||
@Binding var game: Game!
|
@Binding var game: Game!
|
||||||
@State private var isSelectingGameDLC = false
|
@State private var isSelectingGameDLC = false
|
||||||
@State private var dlcs: [DownloadableContentContainer] = []
|
@State private var dlcs: [DownloadableContentContainer] = []
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
|
// MARK: - Body
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
iOSNav {
|
||||||
let withIndex = dlcs.enumerated().map { $0 }
|
List {
|
||||||
List(withIndex, id: \.element.containerPath) { index, dlc in
|
if dlcs.isEmpty {
|
||||||
Button(action: {
|
emptyStateView
|
||||||
let toggle = dlcs[index].downloadableContentNcaList.first?.enabled ?? true
|
} else {
|
||||||
dlcs[index].downloadableContentNcaList.mutableForEach { $0.enabled = !toggle }
|
ForEach(dlcs) { dlc in
|
||||||
Self.saveDlcs(game, dlc: dlcs)
|
dlcRow(dlc)
|
||||||
}) {
|
|
||||||
HStack {
|
|
||||||
Text((dlc.containerPath as NSString).lastPathComponent)
|
|
||||||
.foregroundStyle(Color(uiColor: .label))
|
|
||||||
Spacer()
|
|
||||||
if dlc.downloadableContentNcaList.first?.enabled == true {
|
|
||||||
Image(systemName: "checkmark.circle.fill")
|
|
||||||
.foregroundStyle(Color.accentColor)
|
|
||||||
.font(.system(size: 24))
|
|
||||||
} else {
|
|
||||||
Image(systemName: "circle")
|
|
||||||
.foregroundStyle(Color(uiColor: .secondaryLabel))
|
|
||||||
.font(.system(size: 24))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.contextMenu {
|
|
||||||
Button {
|
|
||||||
let path = URL.documentsDirectory.appendingPathComponent(dlc.containerPath)
|
|
||||||
try? FileManager.default.removeItem(atPath: path.path)
|
|
||||||
dlcs.remove(at: index)
|
|
||||||
Self.saveDlcs(game, dlc: dlcs)
|
|
||||||
} label: {
|
|
||||||
Text("Remove DLC")
|
|
||||||
}
|
}
|
||||||
|
.onDelete(perform: removeDLCs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle("\(game.titleName) DLCs")
|
.navigationTitle("\(game.titleName) DLCs")
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
Button("Add", systemImage: "plus") {
|
ToolbarItem(placement: .navigationBarLeading) {
|
||||||
isSelectingGameDLC = true
|
Button("Done") {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
|
Button {
|
||||||
|
isSelectingGameDLC = true
|
||||||
|
} label: {
|
||||||
|
Label("Add DLC", systemImage: "plus")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onAppear {
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
.fileImporter(
|
||||||
dlcs = Self.loadDlc(game)
|
isPresented: $isSelectingGameDLC,
|
||||||
}
|
allowedContentTypes: [.item],
|
||||||
.fileImporter(isPresented: $isSelectingGameDLC, allowedContentTypes: [.item], allowsMultipleSelection: true) { result in
|
allowsMultipleSelection: true,
|
||||||
switch result {
|
onCompletion: handleFileImport
|
||||||
case .success(let urls):
|
)
|
||||||
for url in urls {
|
}
|
||||||
guard url.startAccessingSecurityScopedResource() else {
|
|
||||||
print("Failed to access security-scoped resource")
|
// MARK: - Views
|
||||||
return
|
private var emptyStateView: some View {
|
||||||
}
|
Group {
|
||||||
defer { url.stopAccessingSecurityScopedResource() }
|
if #available(iOS 17, *) {
|
||||||
|
ContentUnavailableView(
|
||||||
do {
|
"No DLCs Found",
|
||||||
let fileManager = FileManager.default
|
systemImage: "puzzlepiece.extension",
|
||||||
let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
|
description: Text("Tap the + button to add game DLCs.")
|
||||||
let dlcDirectory = documentsDirectory.appendingPathComponent("dlc")
|
)
|
||||||
let romDlcDirectory = dlcDirectory.appendingPathComponent(game.titleId)
|
} else {
|
||||||
|
VStack(spacing: 20) {
|
||||||
if !fileManager.fileExists(atPath: dlcDirectory.path) {
|
Spacer()
|
||||||
try fileManager.createDirectory(at: dlcDirectory, withIntermediateDirectories: true, attributes: nil)
|
|
||||||
}
|
Image(systemName: "puzzlepiece.extension")
|
||||||
|
.font(.system(size: 64))
|
||||||
if !fileManager.fileExists(atPath: romDlcDirectory.path) {
|
.foregroundColor(.secondary)
|
||||||
try fileManager.createDirectory(at: romDlcDirectory, withIntermediateDirectories: true, attributes: nil)
|
|
||||||
}
|
Text("No DLCs Found")
|
||||||
|
.font(.title2)
|
||||||
let dlcContent = Ryujinx.shared.getDlcNcaList(titleId: game.titleId, path: url.path)
|
.fontWeight(.semibold)
|
||||||
guard !dlcContent.isEmpty else { return }
|
|
||||||
|
Text("Tap the + button to add game DLCs.")
|
||||||
let destinationURL = romDlcDirectory.appendingPathComponent(url.lastPathComponent)
|
.font(.subheadline)
|
||||||
try? fileManager.copyItem(at: url, to: destinationURL)
|
.foregroundColor(.secondary)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
let container = DownloadableContentContainer(
|
.padding(.horizontal)
|
||||||
containerPath: Self.relativeDlcDirectoryPath(for: game, dlcPath: destinationURL),
|
|
||||||
downloadableContentNcaList: dlcContent
|
Spacer()
|
||||||
)
|
|
||||||
dlcs.append(container)
|
|
||||||
|
|
||||||
Self.saveDlcs(game, dlc: dlcs)
|
|
||||||
} catch {
|
|
||||||
print("Error copying game file: \(error)")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
case .failure(let err):
|
.frame(maxWidth: .infinity)
|
||||||
print("File import failed: \(err.localizedDescription)")
|
.listRowInsets(EdgeInsets())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Label("Delete", systemImage: "trash")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Functions
|
||||||
|
private func loadData() {
|
||||||
|
dlcs = Self.loadDlc(game)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func toggleDLC(_ dlc: DownloadableContentContainer) {
|
||||||
|
guard let index = dlcs.firstIndex(where: { $0.id == dlc.id }) else { return }
|
||||||
|
|
||||||
|
let toggle = !dlcs[index].isEnabled
|
||||||
|
dlcs[index].downloadableContentNcaList = dlcs[index].downloadableContentNcaList.map { nca in
|
||||||
|
var mutableNca = nca
|
||||||
|
mutableNca.enabled = toggle
|
||||||
|
return mutableNca
|
||||||
|
}
|
||||||
|
|
||||||
|
Self.saveDlcs(game, dlc: dlcs)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func removeDLCs(at offsets: IndexSet) {
|
||||||
|
offsets.forEach { removeDLC(at: IndexSet(integer: $0)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private func removeDLC(at indexSet: IndexSet) {
|
||||||
|
guard let index = indexSet.first else { return }
|
||||||
|
|
||||||
|
let dlcToRemove = dlcs[index]
|
||||||
|
let path = URL.documentsDirectory.appendingPathComponent(dlcToRemove.containerPath)
|
||||||
|
|
||||||
|
do {
|
||||||
|
try FileManager.default.removeItem(at: path)
|
||||||
|
dlcs.remove(at: index)
|
||||||
|
Self.saveDlcs(game, dlc: dlcs)
|
||||||
|
} catch {
|
||||||
|
print("Failed to remove DLC: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleFileImport(result: Result<[URL], Error>) {
|
||||||
|
switch result {
|
||||||
|
case .success(let urls):
|
||||||
|
for url in urls {
|
||||||
|
importDLC(from: url)
|
||||||
|
}
|
||||||
|
case .failure(let error):
|
||||||
|
print("File import failed: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func importDLC(from url: URL) {
|
||||||
|
guard url.startAccessingSecurityScopedResource() else {
|
||||||
|
print("Failed to access security-scoped resource")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer { url.stopAccessingSecurityScopedResource() }
|
||||||
|
|
||||||
|
do {
|
||||||
|
let fileManager = FileManager.default
|
||||||
|
let dlcDirectory = URL.documentsDirectory.appendingPathComponent("dlc")
|
||||||
|
let gameDlcDirectory = dlcDirectory.appendingPathComponent(game.titleId)
|
||||||
|
|
||||||
|
try fileManager.createDirectory(at: gameDlcDirectory, withIntermediateDirectories: true)
|
||||||
|
|
||||||
|
// Copy the DLC file
|
||||||
|
let destinationURL = gameDlcDirectory.appendingPathComponent(url.lastPathComponent)
|
||||||
|
try? fileManager.removeItem(at: destinationURL)
|
||||||
|
try fileManager.copyItem(at: url, to: destinationURL)
|
||||||
|
|
||||||
|
// Fetch DLC metadata from Ryujinx
|
||||||
|
let dlcContent = Ryujinx.shared.getDlcNcaList(titleId: game.titleId, path: destinationURL.path)
|
||||||
|
guard !dlcContent.isEmpty else {
|
||||||
|
print("No valid DLC content found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let newDlcContainer = DownloadableContentContainer(
|
||||||
|
containerPath: Self.relativeDlcDirectoryPath(for: game, dlcPath: destinationURL),
|
||||||
|
downloadableContentNcaList: dlcContent
|
||||||
|
)
|
||||||
|
|
||||||
|
dlcs.append(newDlcContainer)
|
||||||
|
Self.saveDlcs(game, dlc: dlcs)
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
print("Error importing DLC: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Helper Methods
|
||||||
private extension DLCManagerSheet {
|
private extension DLCManagerSheet {
|
||||||
static func loadDlc(_ game: Game) -> [DownloadableContentContainer] {
|
static func loadDlc(_ game: Game) -> [DownloadableContentContainer] {
|
||||||
let jsonURL = dlcJsonPath(for: game)
|
let jsonURL = dlcJsonPath(for: game)
|
||||||
try? FileManager.default.createDirectory(at: jsonURL.deletingLastPathComponent(), withIntermediateDirectories: true)
|
|
||||||
guard let data = try? Data(contentsOf: jsonURL),
|
do {
|
||||||
var result = try? JSONDecoder().decode([DownloadableContentContainer].self, from: data)
|
try FileManager.default.createDirectory(at: jsonURL.deletingLastPathComponent(), withIntermediateDirectories: true)
|
||||||
else { return [] }
|
|
||||||
|
guard FileManager.default.fileExists(atPath: jsonURL.path),
|
||||||
result = result.filter { container in
|
let data = try? Data(contentsOf: jsonURL),
|
||||||
let path = URL.documentsDirectory.appendingPathComponent(container.containerPath)
|
var result = try? JSONDecoder().decode([DownloadableContentContainer].self, from: data)
|
||||||
return FileManager.default.fileExists(atPath: path.path)
|
else { return [] }
|
||||||
|
|
||||||
|
result = result.filter { container in
|
||||||
|
let path = URL.documentsDirectory.appendingPathComponent(container.containerPath)
|
||||||
|
return FileManager.default.fileExists(atPath: path.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
} catch {
|
||||||
|
print("Error loading DLCs: \(error)")
|
||||||
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static func saveDlcs(_ game: Game, dlc: [DownloadableContentContainer]) {
|
static func saveDlcs(_ game: Game, dlc: [DownloadableContentContainer]) {
|
||||||
guard let data = try? JSONEncoder().encode(dlc) else { return }
|
do {
|
||||||
try? data.write(to: dlcJsonPath(for: game))
|
let data = try JSONEncoder().encode(dlc)
|
||||||
|
try data.write(to: dlcJsonPath(for: game))
|
||||||
|
} catch {
|
||||||
|
print("Error saving DLCs: \(error)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static func relativeDlcDirectoryPath(for game: Game, dlcPath: URL) -> String {
|
static func relativeDlcDirectoryPath(for game: Game, dlcPath: URL) -> String {
|
||||||
"dlc/\(game.titleId)/\(dlcPath.lastPathComponent)"
|
"dlc/\(game.titleId)/\(dlcPath.lastPathComponent)"
|
||||||
}
|
}
|
||||||
|
|
||||||
static func dlcJsonPath(for game: Game) -> URL {
|
static func dlcJsonPath(for game: Game) -> URL {
|
||||||
URL.documentsDirectory.appendingPathComponent("games").appendingPathComponent(game.titleId).appendingPathComponent("dlc.json")
|
URL.documentsDirectory
|
||||||
|
.appendingPathComponent("games")
|
||||||
|
.appendingPathComponent(game.titleId)
|
||||||
|
.appendingPathComponent("dlc.json")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Array Extension
|
||||||
|
extension Array where Element: AnyObject {
|
||||||
|
mutating func mutableForEach(_ body: (inout Element) -> Void) {
|
||||||
|
for index in indices {
|
||||||
|
var element = self[index]
|
||||||
|
body(&element)
|
||||||
|
self[index] = element
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - URL Extension
|
||||||
extension URL {
|
extension URL {
|
||||||
@available(iOS, introduced: 15.0, deprecated: 16.0, message: "Use URL.documentsDirectory on iOS 16 and above")
|
@available(iOS, introduced: 15.0, deprecated: 16.0, message: "Use URL.documentsDirectory on iOS 16 and above")
|
||||||
static var documentsDirectory: URL {
|
static var documentsDirectory: URL {
|
||||||
|
@ -9,149 +9,169 @@ import SwiftUI
|
|||||||
import UniformTypeIdentifiers
|
import UniformTypeIdentifiers
|
||||||
|
|
||||||
struct UpdateManagerSheet: View {
|
struct UpdateManagerSheet: View {
|
||||||
@State private var items: [String] = []
|
// MARK: - Properties
|
||||||
@State private var paths: [URL] = []
|
@State private var updates: [UpdateItem] = []
|
||||||
@State private var selectedItem: String? = nil
|
|
||||||
@Binding var game: Game?
|
@Binding var game: Game?
|
||||||
@State private var isSelectingGameUpdate = false
|
@State private var isSelectingGameUpdate = false
|
||||||
@State private var jsonURL: URL? = nil
|
@State private var jsonURL: URL? = nil
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
|
// MARK: - Models
|
||||||
|
class UpdateItem: Identifiable, ObservableObject {
|
||||||
|
let id = UUID()
|
||||||
|
let url: URL
|
||||||
|
let filename: String
|
||||||
|
let path: String
|
||||||
|
|
||||||
|
@Published var isSelected: Bool = false
|
||||||
|
|
||||||
|
init(url: URL, filename: String, path: String, isSelected: Bool = false) {
|
||||||
|
self.url = url
|
||||||
|
self.filename = filename
|
||||||
|
self.path = path
|
||||||
|
self.isSelected = isSelected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Body
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
iOSNav {
|
||||||
List(paths, id: \..self, selection: $selectedItem) { item in
|
List {
|
||||||
Button(action: {
|
if updates.isEmpty {
|
||||||
selectItem(item.lastPathComponent)
|
emptyStateView
|
||||||
}) {
|
} else {
|
||||||
HStack {
|
ForEach(updates) { update in
|
||||||
Text(item.lastPathComponent)
|
updateRow(update)
|
||||||
.foregroundStyle(Color(uiColor: .label))
|
}
|
||||||
Spacer()
|
.onDelete(perform: removeUpdates)
|
||||||
if selectedItem == "updates/\(game!.titleId)/\(item.lastPathComponent)" {
|
}
|
||||||
Image(systemName: "checkmark.circle.fill")
|
}
|
||||||
.foregroundStyle(Color.accentColor)
|
.navigationTitle("\(game?.titleName ?? "Game") Updates")
|
||||||
.font(.system(size: 24))
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
} else {
|
.toolbar {
|
||||||
Image(systemName: "circle")
|
ToolbarItem(placement: .navigationBarLeading) {
|
||||||
.foregroundStyle(Color(uiColor: .secondaryLabel))
|
Button("Done") {
|
||||||
.font(.system(size: 24))
|
dismiss()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.contextMenu {
|
|
||||||
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
Button {
|
Button {
|
||||||
removeUpdate(item)
|
isSelectingGameUpdate = true
|
||||||
} label: {
|
} label: {
|
||||||
Text("Remove Update")
|
Label("Add Update", systemImage: "plus")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
print(URL.documentsDirectory.appendingPathComponent("games").appendingPathComponent(game!.titleId).appendingPathComponent("updates.json"))
|
loadData()
|
||||||
|
|
||||||
loadJSON(URL.documentsDirectory.appendingPathComponent("games").appendingPathComponent(game!.titleId).appendingPathComponent("updates.json"))
|
|
||||||
}
|
|
||||||
.navigationTitle("\(game!.titleName) Updates")
|
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
|
||||||
.toolbar {
|
|
||||||
Button("Add", systemImage: "plus") {
|
|
||||||
isSelectingGameUpdate = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.fileImporter(isPresented: $isSelectingGameUpdate, allowedContentTypes: [.item]) { result in
|
.fileImporter(isPresented: $isSelectingGameUpdate, allowedContentTypes: [.item], onCompletion: handleFileImport)
|
||||||
switch result {
|
}
|
||||||
case .success(let url):
|
|
||||||
guard url.startAccessingSecurityScopedResource() else {
|
// MARK: - Views
|
||||||
print("Failed to access security-scoped resource")
|
private var emptyStateView: some View {
|
||||||
return
|
Group {
|
||||||
}
|
if #available(iOS 17, *) {
|
||||||
defer { url.stopAccessingSecurityScopedResource() }
|
ContentUnavailableView(
|
||||||
|
"No Updates Found",
|
||||||
let gameInfo = game!
|
systemImage: "arrow.down.circle",
|
||||||
|
description: Text("Tap the + button to add game updates.")
|
||||||
do {
|
)
|
||||||
let fileManager = FileManager.default
|
} else {
|
||||||
let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
|
VStack(spacing: 20) {
|
||||||
let updatedDirectory = documentsDirectory.appendingPathComponent("updates")
|
Spacer()
|
||||||
let romUpdatedDirectory = updatedDirectory.appendingPathComponent(gameInfo.titleId)
|
|
||||||
|
|
||||||
if !fileManager.fileExists(atPath: updatedDirectory.path) {
|
Image(systemName: "arrow.down.circle")
|
||||||
try fileManager.createDirectory(at: updatedDirectory, withIntermediateDirectories: true, attributes: nil)
|
.font(.system(size: 64))
|
||||||
}
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
if !fileManager.fileExists(atPath: romUpdatedDirectory.path) {
|
Text("No Updates Found")
|
||||||
try fileManager.createDirectory(at: romUpdatedDirectory, withIntermediateDirectories: true, attributes: nil)
|
.font(.title2)
|
||||||
}
|
.fontWeight(.semibold)
|
||||||
|
|
||||||
let destinationURL = romUpdatedDirectory.appendingPathComponent(url.lastPathComponent)
|
Text("Tap the + button to add game updates.")
|
||||||
try? fileManager.copyItem(at: url, to: destinationURL)
|
.font(.subheadline)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
items.append("updates/" + gameInfo.titleId + "/" + url.lastPathComponent)
|
.multilineTextAlignment(.center)
|
||||||
selectItem(url.lastPathComponent)
|
.padding(.horizontal)
|
||||||
Ryujinx.shared.games = Ryujinx.shared.loadGames()
|
|
||||||
loadJSON(jsonURL!)
|
Spacer()
|
||||||
} catch {
|
|
||||||
print("Error copying game file: \(error)")
|
|
||||||
}
|
}
|
||||||
case .failure(let err):
|
.frame(maxWidth: .infinity)
|
||||||
print("File import failed: \(err.localizedDescription)")
|
.listRowInsets(EdgeInsets())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeUpdate(_ game: URL) {
|
private func updateRow(_ update: UpdateItem) -> some View {
|
||||||
let gameString = "updates/\(self.game!.titleId)/\(game.lastPathComponent)"
|
Button {
|
||||||
paths.removeAll { $0 == game }
|
toggleSelection(update)
|
||||||
items.removeAll { $0 == gameString }
|
} label: {
|
||||||
|
HStack {
|
||||||
if selectedItem == gameString {
|
Text(update.filename)
|
||||||
selectedItem = nil
|
.foregroundStyle(.primary)
|
||||||
|
Spacer()
|
||||||
|
Image(systemName: update.isSelected ? "checkmark.circle.fill" : "circle")
|
||||||
|
.foregroundStyle(update.isSelected ? .primary : .secondary)
|
||||||
|
.imageScale(.large)
|
||||||
|
}
|
||||||
|
.contentShape(Rectangle())
|
||||||
}
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
do {
|
.swipeActions(edge: .trailing) {
|
||||||
try FileManager.default.removeItem(at: game)
|
Button(role: .destructive) {
|
||||||
} catch {
|
if let index = updates.firstIndex(where: { $0.path == update.path }) {
|
||||||
print(error)
|
removeUpdate(at: IndexSet(integer: index))
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Label("Delete", systemImage: "trash")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
saveJSON(selectedItem: selectedItem ?? "")
|
|
||||||
Ryujinx.shared.games = Ryujinx.shared.loadGames()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveJSON(selectedItem: String?) {
|
// MARK: - Functions
|
||||||
|
private func loadData() {
|
||||||
|
guard let game = game else { return }
|
||||||
|
|
||||||
|
let documentsDirectory = URL.documentsDirectory
|
||||||
|
jsonURL = documentsDirectory
|
||||||
|
.appendingPathComponent("games")
|
||||||
|
.appendingPathComponent(game.titleId)
|
||||||
|
.appendingPathComponent("updates.json")
|
||||||
|
|
||||||
|
loadJSON()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadJSON() {
|
||||||
guard let jsonURL = jsonURL else { return }
|
guard let jsonURL = jsonURL else { return }
|
||||||
do {
|
|
||||||
let jsonDict = ["paths": items, "selected": selectedItem ?? self.selectedItem ?? ""] as [String: Any]
|
|
||||||
let newData = try JSONSerialization.data(withJSONObject: jsonDict, options: .prettyPrinted)
|
|
||||||
try newData.write(to: jsonURL)
|
|
||||||
} catch {
|
|
||||||
print("Failed to update JSON: \(error)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadJSON(_ json: URL) {
|
|
||||||
self.jsonURL = json
|
|
||||||
|
|
||||||
guard let jsonURL else { return }
|
|
||||||
|
|
||||||
do {
|
do {
|
||||||
|
if !FileManager.default.fileExists(atPath: jsonURL.path) {
|
||||||
|
createDefaultJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let data = try Data(contentsOf: jsonURL)
|
let data = try Data(contentsOf: jsonURL)
|
||||||
if let jsonDict = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
|
if let jsonDict = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
|
||||||
let list = jsonDict["paths"] as? [String]
|
let paths = jsonDict["paths"] as? [String],
|
||||||
{
|
let selected = jsonDict["selected"] as? String {
|
||||||
|
|
||||||
let filteredList = list.filter { relativePath in
|
let filteredPaths = paths.filter { relativePath in
|
||||||
let path = URL.documentsDirectory.appendingPathComponent(relativePath)
|
let path = URL.documentsDirectory.appendingPathComponent(relativePath)
|
||||||
return FileManager.default.fileExists(atPath: path.path)
|
return FileManager.default.fileExists(atPath: path.path)
|
||||||
}
|
}
|
||||||
|
|
||||||
let urls: [URL] = filteredList.map { relativePath in
|
updates = filteredPaths.map { relativePath in
|
||||||
URL.documentsDirectory.appendingPathComponent(relativePath)
|
let url = URL.documentsDirectory.appendingPathComponent(relativePath)
|
||||||
|
return UpdateItem(
|
||||||
|
url: url,
|
||||||
|
filename: url.lastPathComponent,
|
||||||
|
path: relativePath,
|
||||||
|
isSelected: selected == relativePath
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
items = filteredList
|
|
||||||
paths = urls
|
|
||||||
selectedItem = jsonDict["selected"] as? String
|
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
print("Failed to read JSON: \(error)")
|
print("Failed to read JSON: \(error)")
|
||||||
@ -159,42 +179,119 @@ struct UpdateManagerSheet: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func createDefaultJSON() {
|
private func createDefaultJSON() {
|
||||||
guard let jsonURL = jsonURL else { return }
|
guard let jsonURL = jsonURL else { return }
|
||||||
let defaultData: [String: Any] = ["selected": "", "paths": []]
|
|
||||||
do {
|
do {
|
||||||
|
try FileManager.default.createDirectory(at: jsonURL.deletingLastPathComponent(), withIntermediateDirectories: true)
|
||||||
|
|
||||||
|
let defaultData: [String: Any] = ["selected": "", "paths": []]
|
||||||
let newData = try JSONSerialization.data(withJSONObject: defaultData, options: .prettyPrinted)
|
let newData = try JSONSerialization.data(withJSONObject: defaultData, options: .prettyPrinted)
|
||||||
try newData.write(to: jsonURL)
|
try newData.write(to: jsonURL)
|
||||||
items = []
|
updates = []
|
||||||
selectedItem = ""
|
|
||||||
} catch {
|
} catch {
|
||||||
print("Failed to create default JSON: \(error)")
|
print("Failed to create default JSON: \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func selectItem(_ item: String) {
|
private func handleFileImport(result: Result<URL, Error>) {
|
||||||
let newSelection = "updates/\(game!.titleId)/\(item)"
|
switch result {
|
||||||
|
case .success(let selectedURL):
|
||||||
guard let jsonURL else { return }
|
guard let game = game,
|
||||||
|
selectedURL.startAccessingSecurityScopedResource() else {
|
||||||
do {
|
print("Failed to access security-scoped resource")
|
||||||
let data = try Data(contentsOf: jsonURL)
|
return
|
||||||
try FileManager.default.createDirectory(at: jsonURL.deletingLastPathComponent(), withIntermediateDirectories: true)
|
|
||||||
var jsonDict = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] ?? [:]
|
|
||||||
|
|
||||||
if let currentSelected = jsonDict["selected"] as? String, currentSelected == newSelection {
|
|
||||||
jsonDict["selected"] = ""
|
|
||||||
selectedItem = ""
|
|
||||||
} else {
|
|
||||||
jsonDict["selected"] = "\(newSelection)"
|
|
||||||
selectedItem = newSelection
|
|
||||||
}
|
}
|
||||||
|
|
||||||
jsonDict["paths"] = items
|
defer { selectedURL.stopAccessingSecurityScopedResource() }
|
||||||
|
|
||||||
|
do {
|
||||||
|
let fileManager = FileManager.default
|
||||||
|
let updatesDirectory = URL.documentsDirectory.appendingPathComponent("updates")
|
||||||
|
let gameUpdatesDirectory = updatesDirectory.appendingPathComponent(game.titleId)
|
||||||
|
|
||||||
|
// Create directories if needed
|
||||||
|
try fileManager.createDirectory(at: gameUpdatesDirectory, withIntermediateDirectories: true)
|
||||||
|
|
||||||
|
// Copy the file
|
||||||
|
let destinationURL = gameUpdatesDirectory.appendingPathComponent(selectedURL.lastPathComponent)
|
||||||
|
try? fileManager.removeItem(at: destinationURL) // Remove if exists
|
||||||
|
try fileManager.copyItem(at: selectedURL, to: destinationURL)
|
||||||
|
|
||||||
|
// Add to updates
|
||||||
|
let relativePath = "updates/\(game.titleId)/\(selectedURL.lastPathComponent)"
|
||||||
|
let newUpdate = UpdateItem(
|
||||||
|
url: destinationURL,
|
||||||
|
filename: selectedURL.lastPathComponent,
|
||||||
|
path: relativePath
|
||||||
|
)
|
||||||
|
|
||||||
|
updates.append(newUpdate)
|
||||||
|
toggleSelection(newUpdate)
|
||||||
|
|
||||||
|
// Reload games
|
||||||
|
Ryujinx.shared.games = Ryujinx.shared.loadGames()
|
||||||
|
} catch {
|
||||||
|
print("Error copying update file: \(error)")
|
||||||
|
}
|
||||||
|
|
||||||
|
case .failure(let error):
|
||||||
|
print("File import failed: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func toggleSelection(_ update: UpdateItem) {
|
||||||
|
print("toggle selection \(update.path)")
|
||||||
|
|
||||||
|
updates = updates.map { item in
|
||||||
|
var mutableItem = item
|
||||||
|
mutableItem.isSelected = item.path == update.path && !update.isSelected
|
||||||
|
print(mutableItem.isSelected)
|
||||||
|
print(update.isSelected)
|
||||||
|
return mutableItem
|
||||||
|
}
|
||||||
|
|
||||||
|
print(updates)
|
||||||
|
|
||||||
|
saveJSON()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func removeUpdates(at offsets: IndexSet) {
|
||||||
|
offsets.forEach { removeUpdate(at: IndexSet(integer: $0)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private func removeUpdate(at indexSet: IndexSet) {
|
||||||
|
guard let index = indexSet.first else { return }
|
||||||
|
|
||||||
|
let updateToRemove = updates[index]
|
||||||
|
|
||||||
|
do {
|
||||||
|
// Remove the file
|
||||||
|
try FileManager.default.removeItem(at: updateToRemove.url)
|
||||||
|
|
||||||
|
// Remove from updates array
|
||||||
|
updates.remove(at: index)
|
||||||
|
|
||||||
|
// Save changes
|
||||||
|
saveJSON()
|
||||||
|
|
||||||
|
// Reload games
|
||||||
|
Ryujinx.shared.games = Ryujinx.shared.loadGames()
|
||||||
|
} catch {
|
||||||
|
print("Failed to remove update: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func saveJSON() {
|
||||||
|
guard let jsonURL = jsonURL else { return }
|
||||||
|
|
||||||
|
do {
|
||||||
|
let paths = updates.map { $0.path }
|
||||||
|
let selected = updates.first(where: { $0.isSelected })?.path ?? ""
|
||||||
|
|
||||||
|
let jsonDict = ["paths": paths, "selected": selected] as [String: Any]
|
||||||
let newData = try JSONSerialization.data(withJSONObject: jsonDict, options: .prettyPrinted)
|
let newData = try JSONSerialization.data(withJSONObject: jsonDict, options: .prettyPrinted)
|
||||||
try newData.write(to: jsonURL)
|
try newData.write(to: jsonURL)
|
||||||
Ryujinx.shared.games = Ryujinx.shared.loadGames()
|
|
||||||
} catch {
|
} catch {
|
||||||
print("Failed to update JSON: \(error)")
|
print("Failed to update JSON: \(error)")
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@ struct SetupView: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
iOSNav {
|
iOSNav {
|
||||||
ZStack {
|
ZStack {
|
||||||
if UIDevice.current.userInterfaceIdiom == .pad {
|
if UIDevice.current.systemName.contains("iPadOS") {
|
||||||
iPadSetupView(
|
iPadSetupView(
|
||||||
finished: $finished,
|
finished: $finished,
|
||||||
isImportingKeys: $isImportingKeys,
|
isImportingKeys: $isImportingKeys,
|
||||||
@ -65,8 +65,9 @@ struct SetupView: View {
|
|||||||
initialize()
|
initialize()
|
||||||
finished = false
|
finished = false
|
||||||
keysImported = Ryujinx.shared.checkIfKeysImported()
|
keysImported = Ryujinx.shared.checkIfKeysImported()
|
||||||
print((Double(Ryujinx.shared.fetchFirmwareVersion()) ?? 0))
|
|
||||||
firmImported = (Ryujinx.shared.fetchFirmwareVersion() != "0")
|
let firmware = Ryujinx.shared.fetchFirmwareVersion()
|
||||||
|
firmImported = (firmware == "" ? "0" : firmware) != "0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -369,9 +370,8 @@ struct SetupView: View {
|
|||||||
|
|
||||||
Ryujinx.shared.installFirmware(firmwarePath: fileURL.path)
|
Ryujinx.shared.installFirmware(firmwarePath: fileURL.path)
|
||||||
|
|
||||||
print(Double(Ryujinx.shared.fetchFirmwareVersion()) ?? 0)
|
let firmware = Ryujinx.shared.fetchFirmwareVersion()
|
||||||
|
firmImported = (firmware == "" ? "0" : firmware) != "0"
|
||||||
firmImported = (Ryujinx.shared.fetchFirmwareVersion() != "0")
|
|
||||||
alertMessage = "Firmware installed successfully"
|
alertMessage = "Firmware installed successfully"
|
||||||
showAlert = true
|
showAlert = true
|
||||||
|
|
||||||
|
Binary file not shown.
@ -0,0 +1,6 @@
|
|||||||
|
framework module RyujinxHelper {
|
||||||
|
umbrella header "RyujinxHelper.h"
|
||||||
|
export *
|
||||||
|
|
||||||
|
module * { export * }
|
||||||
|
}
|
Binary file not shown.
@ -4,22 +4,22 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>files</key>
|
<key>files</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>Headers/RyujinxKeyboard.h</key>
|
<key>Headers/RyujinxHelper.h</key>
|
||||||
<data>
|
<data>
|
||||||
5P7GN4g050n199pV6/+SpfMBgJc=
|
5P7GN4g050n199pV6/+SpfMBgJc=
|
||||||
</data>
|
</data>
|
||||||
<key>Info.plist</key>
|
<key>Info.plist</key>
|
||||||
<data>
|
<data>
|
||||||
hYdI/ktAKwjBSfaJpt6Yc8UKLCY=
|
UOH9NuuEcz5NQiQlrM2LNFaG2pI=
|
||||||
</data>
|
</data>
|
||||||
<key>Modules/module.modulemap</key>
|
<key>Modules/module.modulemap</key>
|
||||||
<data>
|
<data>
|
||||||
0kFAMoTn+4Q1J/dM6uMLe3EhbL0=
|
JDij7psMD6pZZpigUfkSQldib+I=
|
||||||
</data>
|
</data>
|
||||||
</dict>
|
</dict>
|
||||||
<key>files2</key>
|
<key>files2</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>Headers/RyujinxKeyboard.h</key>
|
<key>Headers/RyujinxHelper.h</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>hash2</key>
|
<key>hash2</key>
|
||||||
<data>
|
<data>
|
||||||
@ -30,7 +30,7 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>hash2</key>
|
<key>hash2</key>
|
||||||
<data>
|
<data>
|
||||||
K+ZyxKhTI4bMVZuHBIspvd2PFqvCOlVUFYmwF96O5NQ=
|
5t/lQcpkzC5bwJqFQqIf6h1ldlhHouYzDawRVrnUeyM=
|
||||||
</data>
|
</data>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
@ -1,6 +0,0 @@
|
|||||||
framework module RyujinxKeyboard {
|
|
||||||
umbrella header "RyujinxKeyboard.h"
|
|
||||||
export *
|
|
||||||
|
|
||||||
module * { export * }
|
|
||||||
}
|
|
@ -769,7 +769,7 @@ namespace Ryujinx.Graphics.Vulkan
|
|||||||
|
|
||||||
private void SetData(ReadOnlySpan<byte> data, int layer, int level, int layers, int levels, bool singleSlice, Rectangle<int>? region = null)
|
private void SetData(ReadOnlySpan<byte> data, int layer, int level, int layers, int levels, bool singleSlice, Rectangle<int>? region = null)
|
||||||
{
|
{
|
||||||
const int MaxChunkSize = 1024 * 1024 * 16; // 16MB chunks
|
const int MaxChunkSize = 1024 * 1024;
|
||||||
|
|
||||||
int bufferDataLength = GetBufferDataLength(data.Length);
|
int bufferDataLength = GetBufferDataLength(data.Length);
|
||||||
|
|
||||||
@ -786,21 +786,39 @@ namespace Ryujinx.Graphics.Vulkan
|
|||||||
|
|
||||||
for (int i = 0; i < layers; i++)
|
for (int i = 0; i < layers; i++)
|
||||||
{
|
{
|
||||||
int currentLayer = layer + i;
|
|
||||||
int currentLayerSize = Math.Min(layerSize, data.Length - offset);
|
|
||||||
var layerData = data.Slice(offset, currentLayerSize);
|
|
||||||
|
|
||||||
ProcessChunk(layerData, currentLayer, level, 1, levels, true);
|
|
||||||
offset += layerSize;
|
|
||||||
|
|
||||||
if (offset >= data.Length)
|
if (offset >= data.Length)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
int currentLayer = layer + i;
|
||||||
|
int currentLayerSize = Math.Min(layerSize, data.Length - offset);
|
||||||
|
|
||||||
|
if (currentLayerSize <= 0)
|
||||||
|
break;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var layerData = data.Slice(offset, currentLayerSize);
|
||||||
|
ProcessChunk(layerData, currentLayer, level, 1, levels, true);
|
||||||
|
offset += layerSize;
|
||||||
|
}
|
||||||
|
catch (ArgumentOutOfRangeException)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (region.HasValue)
|
else if (region.HasValue)
|
||||||
{
|
{
|
||||||
var rect = region.Value;
|
var rect = region.Value;
|
||||||
|
|
||||||
|
if (rect.Width <= 0 || rect.Height <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
int dataPerPixel = data.Length / (rect.Width * rect.Height);
|
int dataPerPixel = data.Length / (rect.Width * rect.Height);
|
||||||
|
|
||||||
|
if (dataPerPixel <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
int rowStride = rect.Width * dataPerPixel;
|
int rowStride = rect.Width * dataPerPixel;
|
||||||
|
|
||||||
int rowsPerChunk = Math.Max(1, MaxChunkSize / rowStride);
|
int rowsPerChunk = Math.Max(1, MaxChunkSize / rowStride);
|
||||||
@ -811,42 +829,63 @@ namespace Ryujinx.Graphics.Vulkan
|
|||||||
while (currentY < rect.Y + originalHeight)
|
while (currentY < rect.Y + originalHeight)
|
||||||
{
|
{
|
||||||
int chunkHeight = Math.Min(rowsPerChunk, rect.Y + originalHeight - currentY);
|
int chunkHeight = Math.Min(rowsPerChunk, rect.Y + originalHeight - currentY);
|
||||||
|
|
||||||
|
if (chunkHeight <= 0)
|
||||||
|
break;
|
||||||
|
|
||||||
var chunkRegion = new Rectangle<int>(rect.X, currentY, rect.Width, chunkHeight);
|
var chunkRegion = new Rectangle<int>(rect.X, currentY, rect.Width, chunkHeight);
|
||||||
|
|
||||||
int chunkSize = chunkHeight * rowStride;
|
int chunkSize = chunkHeight * rowStride;
|
||||||
int safeChunkSize = Math.Min(chunkSize, data.Length - offset);
|
|
||||||
var chunkData = data.Slice(offset, safeChunkSize);
|
|
||||||
|
|
||||||
ProcessChunk(chunkData, layer, level, 1, 1, true, chunkRegion);
|
|
||||||
|
|
||||||
currentY += chunkHeight;
|
|
||||||
offset += chunkSize;
|
|
||||||
|
|
||||||
if (offset >= data.Length)
|
if (offset >= data.Length)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
int safeChunkSize = Math.Min(chunkSize, data.Length - offset);
|
||||||
|
|
||||||
|
if (safeChunkSize <= 0)
|
||||||
|
break;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var chunkData = data.Slice(offset, safeChunkSize);
|
||||||
|
ProcessChunk(chunkData, layer, level, 1, 1, true, chunkRegion);
|
||||||
|
|
||||||
|
currentY += chunkHeight;
|
||||||
|
offset += chunkSize;
|
||||||
|
}
|
||||||
|
catch (ArgumentOutOfRangeException)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
ProcessChunk(data, layer, level, layers, levels, singleSlice, region);
|
ProcessChunk(data, layer, level, layers, levels, singleSlice, region);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ProcessChunk(ReadOnlySpan<byte> chunkData, int chunkLayer, int chunkLevel, int chunkLayers, int chunkLevels, bool chunkSingleSlice, Rectangle<int>? chunkRegion = null)
|
||||||
|
{
|
||||||
|
int chunkBufferLength = GetBufferDataLength(chunkData.Length);
|
||||||
|
|
||||||
void ProcessChunk(ReadOnlySpan<byte> chunkData, int chunkLayer, int chunkLevel, int chunkLayers, int chunkLevels, bool chunkSingleSlice, Rectangle<int>? chunkRegion = null)
|
if (chunkBufferLength <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
using var bufferHolder = _gd.BufferManager.Create(_gd, chunkBufferLength);
|
||||||
|
|
||||||
|
using (var imageAuto = GetImage())
|
||||||
{
|
{
|
||||||
int chunkBufferLength = GetBufferDataLength(chunkData.Length);
|
bool loadInline = Storage.HasCommandBufferDependency(_gd.PipelineInternal.CurrentCommandBuffer);
|
||||||
|
var cbs = loadInline ? _gd.PipelineInternal.CurrentCommandBuffer : _gd.PipelineInternal.GetPreloadCommandBuffer();
|
||||||
|
|
||||||
using var bufferHolder = _gd.BufferManager.Create(_gd, chunkBufferLength);
|
if (loadInline)
|
||||||
|
{
|
||||||
using (var imageAuto = GetImage())
|
_gd.PipelineInternal.EndRenderPass();
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
bool loadInline = Storage.HasCommandBufferDependency(_gd.PipelineInternal.CurrentCommandBuffer);
|
|
||||||
var cbs = loadInline ? _gd.PipelineInternal.CurrentCommandBuffer : _gd.PipelineInternal.GetPreloadCommandBuffer();
|
|
||||||
|
|
||||||
if (loadInline)
|
|
||||||
{
|
|
||||||
_gd.PipelineInternal.EndRenderPass();
|
|
||||||
}
|
|
||||||
|
|
||||||
CopyDataToBuffer(bufferHolder.GetDataStorage(0, chunkBufferLength), chunkData);
|
CopyDataToBuffer(bufferHolder.GetDataStorage(0, chunkBufferLength), chunkData);
|
||||||
|
|
||||||
var buffer = bufferHolder.GetBuffer(cbs.CommandBuffer).Get(cbs).Value;
|
var buffer = bufferHolder.GetBuffer(cbs.CommandBuffer).Get(cbs).Value;
|
||||||
@ -854,6 +893,11 @@ namespace Ryujinx.Graphics.Vulkan
|
|||||||
|
|
||||||
if (chunkRegion.HasValue)
|
if (chunkRegion.HasValue)
|
||||||
{
|
{
|
||||||
|
var region = chunkRegion.Value;
|
||||||
|
|
||||||
|
if (region.Width <= 0 || region.Height <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
CopyFromOrToBuffer(
|
CopyFromOrToBuffer(
|
||||||
cbs.CommandBuffer,
|
cbs.CommandBuffer,
|
||||||
buffer,
|
buffer,
|
||||||
@ -862,10 +906,10 @@ namespace Ryujinx.Graphics.Vulkan
|
|||||||
false,
|
false,
|
||||||
chunkLayer,
|
chunkLayer,
|
||||||
chunkLevel,
|
chunkLevel,
|
||||||
chunkRegion.Value.X,
|
region.X,
|
||||||
chunkRegion.Value.Y,
|
region.Y,
|
||||||
chunkRegion.Value.Width,
|
region.Width,
|
||||||
chunkRegion.Value.Height);
|
region.Height);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -881,7 +925,11 @@ namespace Ryujinx.Graphics.Vulkan
|
|||||||
chunkLevels,
|
chunkLevels,
|
||||||
chunkSingleSlice);
|
chunkSingleSlice);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -239,6 +239,7 @@ namespace Ryujinx.HLE.HOS.Applets
|
|||||||
StringLengthMax = _keyboardForegroundConfig.StringLengthMax,
|
StringLengthMax = _keyboardForegroundConfig.StringLengthMax,
|
||||||
InitialText = initialText,
|
InitialText = initialText,
|
||||||
};
|
};
|
||||||
|
|
||||||
_device.UIHandler.DisplayInputDialog(args, inputText =>
|
_device.UIHandler.DisplayInputDialog(args, inputText =>
|
||||||
{
|
{
|
||||||
Console.WriteLine($"User entered: {inputText}");
|
Console.WriteLine($"User entered: {inputText}");
|
||||||
|
@ -561,7 +561,7 @@ namespace Ryujinx.HLE.HOS.Services.Hid
|
|||||||
|
|
||||||
context.ResponseData.Write((int)_gyroscopeZeroDriftMode);
|
context.ResponseData.Write((int)_gyroscopeZeroDriftMode);
|
||||||
|
|
||||||
Logger.Stub?.PrintStub(LogClass.ServiceHid, new { appletResourceUserId, sixAxisSensorHandle, _gyroscopeZeroDriftMode });
|
// Logger.Stub?.PrintStub(LogClass.ServiceHid, new { appletResourceUserId, sixAxisSensorHandle, _gyroscopeZeroDriftMode });
|
||||||
|
|
||||||
return ResultCode.Success;
|
return ResultCode.Success;
|
||||||
}
|
}
|
||||||
|
@ -251,15 +251,16 @@ namespace Ryujinx.Headless.SDL2
|
|||||||
[UnmanagedCallersOnly(EntryPoint = "get_current_fps")]
|
[UnmanagedCallersOnly(EntryPoint = "get_current_fps")]
|
||||||
public static unsafe int GetFPS()
|
public static unsafe int GetFPS()
|
||||||
{
|
{
|
||||||
if (_window != null) {
|
if (_window == null || _window.Device == null)
|
||||||
Switch Device = _window.Device;
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
int intValue = (int)Device.Statistics.GetGameFrameRate();
|
Switch Device = _window.Device;
|
||||||
|
|
||||||
return intValue;
|
int intValue = (int)Device.Statistics.GetGameFrameRate();
|
||||||
}
|
|
||||||
return 0;
|
return intValue;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[UnmanagedCallersOnly(EntryPoint = "initialize")]
|
[UnmanagedCallersOnly(EntryPoint = "initialize")]
|
||||||
|
@ -486,7 +486,12 @@ namespace Ryujinx.Headless.SDL2
|
|||||||
|
|
||||||
public bool DisplayMessageDialog(string title, string message)
|
public bool DisplayMessageDialog(string title, string message)
|
||||||
{
|
{
|
||||||
SDL_ShowSimpleMessageBox(SDL_MessageBoxFlags.SDL_MESSAGEBOX_INFORMATION, title, message, WindowHandle);
|
if (OperatingSystem.IsIOS())
|
||||||
|
{
|
||||||
|
AlertHelper.ShowAlert(title, message, false);
|
||||||
|
} else {
|
||||||
|
SDL_ShowSimpleMessageBox(SDL_MessageBoxFlags.SDL_MESSAGEBOX_INFORMATION, title, message, WindowHandle);
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -7,13 +7,16 @@ namespace Ryujinx.Headless.SDL2
|
|||||||
{
|
{
|
||||||
public static class AlertHelper
|
public static class AlertHelper
|
||||||
{
|
{
|
||||||
[DllImport("RyujinxKeyboard.framework/RyujinxKeyboard", CallingConvention = CallingConvention.Cdecl)]
|
[DllImport("RyujinxHelper.framework/RyujinxHelper", CallingConvention = CallingConvention.Cdecl)]
|
||||||
public static extern void showKeyboardAlert(string title, string message, string placeholder);
|
public static extern void showKeyboardAlert(string title, string message, string placeholder);
|
||||||
|
|
||||||
[DllImport("RyujinxKeyboard.framework/RyujinxKeyboard", CallingConvention = CallingConvention.Cdecl)]
|
[DllImport("RyujinxHelper.framework/RyujinxHelper", CallingConvention = CallingConvention.Cdecl)]
|
||||||
|
public static extern void showAlert(string title, string message, bool showCancel);
|
||||||
|
|
||||||
|
[DllImport("RyujinxHelper.framework/RyujinxHelper", CallingConvention = CallingConvention.Cdecl)]
|
||||||
private static extern IntPtr getKeyboardInput();
|
private static extern IntPtr getKeyboardInput();
|
||||||
|
|
||||||
[DllImport("RyujinxKeyboard.framework/RyujinxKeyboard", CallingConvention = CallingConvention.Cdecl)]
|
[DllImport("RyujinxHelper.framework/RyujinxHelper", CallingConvention = CallingConvention.Cdecl)]
|
||||||
private static extern void clearKeyboardInput();
|
private static extern void clearKeyboardInput();
|
||||||
|
|
||||||
public static void ShowAlertWithTextInput(string title, string message, string placeholder, Action<string> onTextEntered)
|
public static void ShowAlertWithTextInput(string title, string message, string placeholder, Action<string> onTextEntered)
|
||||||
@ -38,5 +41,10 @@ namespace Ryujinx.Headless.SDL2
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static void ShowAlert(string title, string message, bool cancel) {
|
||||||
|
showAlert(title, message, cancel);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user