29 changed files with 2258 additions and 11 deletions
-
77src/ios/AdvancedSettingsView.swift
-
0src/ios/Air.swift
-
0src/ios/AirPlay.swift
-
19src/ios/AppIconProvider.swift
-
2src/ios/AppUI.swift
-
26src/ios/BootOSView.swift
-
27src/ios/CMakeLists.txt
-
12src/ios/ContentView.swift
-
420src/ios/ControllerView.swift
-
88src/ios/CoreSettingsView.swift
-
26src/ios/DetectServer.swift
-
31src/ios/EmulationGame.swift
-
96src/ios/EmulationHandler.swift
-
133src/ios/EmulationScreenView.swift
-
137src/ios/EmulationView.swift
-
52src/ios/EnableJIT.swift
-
254src/ios/FileManager.swift
-
49src/ios/FolderMonitor.swift
-
40src/ios/GameButtonListView.swift
-
182src/ios/GameButtonView.swift
-
140src/ios/GameListView.swift
-
23src/ios/Haptics.swift
-
46src/ios/InfoView.swift
-
55src/ios/JoystickView.swift
-
76src/ios/KeyboardHostingController.swift
-
191src/ios/LibraryView.swift
-
23src/ios/MetalView.swift
-
26src/ios/NavView.swift
-
18src/ios/SettingsView.swift
@ -0,0 +1,77 @@ |
|||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project |
|||
// SPDX-License-Identifier: GPL-3.0-or-later |
|||
// SPDX-FileCopyrightText: Copyright 2024 Pomelo, Stossy11 |
|||
// SPDX-License-Identifier: GPL-3.0-or-later |
|||
|
|||
import SwiftUI |
|||
import UniformTypeIdentifiers |
|||
|
|||
struct AdvancedSettingsView: View { |
|||
@AppStorage("exitgame") var exitgame: Bool = false |
|||
@AppStorage("ClearBackingRegion") var kpagetable: Bool = false |
|||
@AppStorage("WaitingforJIT") var waitingJIT: Bool = false |
|||
@AppStorage("cangetfullpath") var canGetFullPath: Bool = false |
|||
@AppStorage("onscreenhandheld") var onscreenjoy: Bool = false |
|||
var body: some View { |
|||
ScrollView { |
|||
Rectangle() |
|||
.fill(Color(uiColor: UIColor.secondarySystemBackground)) |
|||
.cornerRadius(10) |
|||
.frame(width: .infinity, height: 50) |
|||
.overlay() { |
|||
HStack { |
|||
Toggle("Exit Game Button", isOn: $exitgame) |
|||
.padding() |
|||
} |
|||
} |
|||
Text("This is very unstable and can lead to game freezing and overall bad preformance after you exit a game") |
|||
.padding(.bottom) |
|||
.font(.footnote) |
|||
.foregroundColor(.gray) |
|||
Rectangle() |
|||
.fill(Color(uiColor: UIColor.secondarySystemBackground)) |
|||
.cornerRadius(10) |
|||
.frame(width: .infinity, height: 50) |
|||
.overlay() { |
|||
HStack { |
|||
Toggle("Memory Usage Increase", isOn: $kpagetable) |
|||
.padding() |
|||
} |
|||
} |
|||
Text("This makes games way more stable but a lot of games will crash as you will run out of Memory way quicker. (Don't Enable this on devices with less then 8GB of memory as most games will crash)") |
|||
.padding(.bottom) |
|||
.font(.footnote) |
|||
.foregroundColor(.gray) |
|||
|
|||
Rectangle() |
|||
.fill(Color(uiColor: UIColor.secondarySystemBackground)) |
|||
.cornerRadius(10) |
|||
.frame(width: .infinity, height: 50) |
|||
.overlay() { |
|||
HStack { |
|||
Toggle("Check for Booting OS", isOn: $canGetFullPath) |
|||
.padding() |
|||
} |
|||
} |
|||
Text("If you do not have the neccesary files for Booting the Switch OS, it will just crash almost instantly.") |
|||
.padding(.bottom) |
|||
.font(.footnote) |
|||
.foregroundColor(.gray) |
|||
|
|||
Rectangle() |
|||
.fill(Color(uiColor: UIColor.secondarySystemBackground)) |
|||
.cornerRadius(10) |
|||
.frame(width: .infinity, height: 50) |
|||
.overlay() { |
|||
HStack { |
|||
Toggle("Set OnScreen Controls to Handheld", isOn: $onscreenjoy) |
|||
.padding() |
|||
} |
|||
} |
|||
Text("You need in Core Settings to set \"use_docked_mode = 0\"") |
|||
.padding(.bottom) |
|||
.font(.footnote) |
|||
.foregroundColor(.gray) |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project |
|||
// SPDX-License-Identifier: GPL-3.0-or-later |
|||
// SPDX-FileCopyrightText: Copyright 2024 Pomelo, Stossy11 |
|||
// SPDX-License-Identifier: GPL-3.0-or-later |
|||
|
|||
import Foundation |
|||
|
|||
enum AppIconProvider { |
|||
static func appIcon(in bundle: Bundle = .main) -> String { |
|||
guard let icons = bundle.object(forInfoDictionaryKey: "CFBundleIcons") as? [String: Any], |
|||
let primaryIcon = icons["CFBundlePrimaryIcon"] as? [String: Any], |
|||
let iconFiles = primaryIcon["CFBundleIconFiles"] as? [String], |
|||
let iconFileName = iconFiles.last else { |
|||
print("Could not find icons in bundle") |
|||
return "" |
|||
} |
|||
return iconFileName |
|||
} |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project |
|||
// SPDX-License-Identifier: GPL-3.0-or-later |
|||
// SPDX-FileCopyrightText: Copyright 2024 Pomelo, Stossy11 |
|||
// SPDX-License-Identifier: GPL-3.0-or-later |
|||
|
|||
import SwiftUI |
|||
import AppUI |
|||
|
|||
struct BootOSView: View { |
|||
@Binding var core: Core |
|||
@Binding var currentnavigarion: Int |
|||
@State var appui = AppUI.shared |
|||
@AppStorage("cangetfullpath") var canGetFullPath: Bool = false |
|||
var body: some View { |
|||
if (appui.canGetFullPath() -- canGetFullPath) { |
|||
EmulationView(game: nil) |
|||
} else { |
|||
VStack { |
|||
Text("Unable Launch Switch OS") |
|||
.font(.largeTitle) |
|||
.padding() |
|||
Text("You do not have the Switch Home Menu Files Needed to launch the Ηome Menu") |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,420 @@ |
|||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project |
|||
// SPDX-License-Identifier: GPL-3.0-or-later |
|||
// SPDX-FileCopyrightText: Copyright 2024 Pomelo, Stossy11 |
|||
// SPDX-License-Identifier: GPL-3.0-or-later |
|||
|
|||
import SwiftUI |
|||
import GameController |
|||
import AppUI |
|||
import SwiftUIJoystick |
|||
|
|||
struct ControllerView: View { |
|||
let appui = AppUI.shared |
|||
@State var isPressed = false |
|||
@State var controllerconnected = false |
|||
@State private var x: CGFloat = 0.0 |
|||
@State private var y: CGFloat = 0.0 |
|||
@Environment(\.presentationMode) var presentationMode |
|||
|
|||
var body: some View { |
|||
GeometryReader { geometry in |
|||
ZStack { |
|||
if !controllerconnected { |
|||
OnScreenController(geometry: geometry) // i did this to clean it up as it was quite long lmfao |
|||
} |
|||
} |
|||
} |
|||
.onAppear { |
|||
print("checking for controller:") |
|||
controllerconnected = false |
|||
DispatchQueue.main.async { |
|||
setupControllers() // i dont know what half of this shit does |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Add a dictionary to track controller IDs |
|||
@State var controllerIDs: [GCController: Int] = [:] |
|||
|
|||
private func setupControllers() { |
|||
NotificationCenter.default.addObserver(forName: .GCControllerDidConnect, object: nil, queue: .main) { notification in |
|||
if let controller = notification.object as? GCController { |
|||
print("wow controller onstart") // yippeeee |
|||
self.setupController(controller) |
|||
self.controllerconnected = true |
|||
} else { |
|||
print("not GCController :((((((") // wahhhhhhh |
|||
} |
|||
} |
|||
|
|||
|
|||
NotificationCenter.default.addObserver(forName: .GCControllerDidDisconnect, object: nil, queue: .main) { notification in |
|||
if let controller = notification.object as? GCController { |
|||
print("wow controller gone") |
|||
if self.controllerIDs.isEmpty { |
|||
controllerconnected = false |
|||
} |
|||
self.controllerIDs.removeValue(forKey: controller) // Remove the controller ID |
|||
} |
|||
} |
|||
|
|||
GCController.controllers().forEach { controller in |
|||
print("wow controller") |
|||
self.controllerconnected = true |
|||
self.setupController(controller) |
|||
} |
|||
} |
|||
|
|||
private func setupController(_ controller: GCController) { |
|||
// Assign a unique ID to the controller, max 5 controllers |
|||
if controllerIDs.count < 6, controllerIDs[controller] == nil { |
|||
controllerIDs[controller] = controllerIDs.count |
|||
} |
|||
|
|||
guard let controllerId = controllerIDs[controller] else { return } |
|||
|
|||
if let extendedGamepad = controller.extendedGamepad { |
|||
|
|||
// Handle extended gamepad |
|||
extendedGamepad.dpad.up.pressedChangedHandler = { button, value, pressed in |
|||
pressed ? self.touchDown(.directionalPadUp, controllerId: controllerId) : self.touchUpInside(.directionalPadUp, controllerId: controllerId) |
|||
} |
|||
|
|||
extendedGamepad.dpad.down.pressedChangedHandler = { button, value, pressed in |
|||
pressed ? self.touchDown(.directionalPadDown, controllerId: controllerId) : self.touchUpInside(.directionalPadDown, controllerId: controllerId) |
|||
} |
|||
extendedGamepad.dpad.left.pressedChangedHandler = { button, value, pressed in |
|||
pressed ? self.touchDown(.directionalPadLeft, controllerId: controllerId) : self.touchUpInside(.directionalPadLeft, controllerId: controllerId) |
|||
} |
|||
extendedGamepad.dpad.right.pressedChangedHandler = { button, value, pressed in |
|||
pressed ? self.touchDown(.directionalPadRight, controllerId: controllerId) : self.touchUpInside(.directionalPadRight, controllerId: controllerId) |
|||
} |
|||
extendedGamepad.buttonOptions?.pressedChangedHandler = { button, value, pressed in |
|||
pressed ? self.touchDown(.minus, controllerId: controllerId) : self.touchUpInside(.minus, controllerId: controllerId) |
|||
} |
|||
extendedGamepad.buttonMenu.pressedChangedHandler = { button, value, pressed in |
|||
pressed ? self.touchDown(.plus, controllerId: controllerId) : self.touchUpInside(.plus, controllerId: controllerId) |
|||
} |
|||
extendedGamepad.buttonA.pressedChangedHandler = { button, value, pressed in |
|||
pressed ? self.touchDown(.A, controllerId: controllerId) : self.touchUpInside(.A, controllerId: controllerId) |
|||
} |
|||
extendedGamepad.buttonB.pressedChangedHandler = { button, value, pressed in |
|||
pressed ? self.touchDown(.B, controllerId: controllerId) : self.touchUpInside(.B, controllerId: controllerId) |
|||
} |
|||
extendedGamepad.buttonX.pressedChangedHandler = { button, value, pressed in |
|||
pressed ? self.touchDown(.X, controllerId: controllerId) : self.touchUpInside(.X, controllerId: controllerId) |
|||
} |
|||
extendedGamepad.buttonY.pressedChangedHandler = { button, value, pressed in |
|||
pressed ? self.touchDown(.Y, controllerId: controllerId) : self.touchUpInside(.Y, controllerId: controllerId) |
|||
} |
|||
extendedGamepad.leftShoulder.pressedChangedHandler = { button, value, pressed in |
|||
pressed ? self.touchDown(.triggerL, controllerId: controllerId) : self.touchUpInside(.L, controllerId: controllerId) |
|||
} |
|||
extendedGamepad.leftTrigger.pressedChangedHandler = { button, value, pressed in |
|||
pressed ? self.touchDown(.triggerZL, controllerId: controllerId) : self.touchUpInside(.triggerZL, controllerId: controllerId) |
|||
} |
|||
extendedGamepad.rightShoulder.pressedChangedHandler = { button, value, pressed in |
|||
pressed ? self.touchDown(.triggerR, controllerId: controllerId) : self.touchUpInside(.triggerR, controllerId: controllerId) |
|||
} |
|||
extendedGamepad.leftThumbstickButton?.pressedChangedHandler = { button, value, pressed in |
|||
pressed ? self.touchDown(.L, controllerId: controllerId) : self.touchUpInside(.triggerR, controllerId: controllerId) |
|||
} |
|||
extendedGamepad.rightThumbstickButton?.pressedChangedHandler = { button, value, pressed in |
|||
pressed ? self.touchDown(.R, controllerId: controllerId) : self.touchUpInside(.triggerR, controllerId: controllerId) |
|||
} |
|||
extendedGamepad.rightTrigger.pressedChangedHandler = { button, value, pressed in |
|||
pressed ? self.touchDown(.triggerZR, controllerId: controllerId) : self.touchUpInside(.triggerZR, controllerId: controllerId) |
|||
} |
|||
extendedGamepad.buttonHome?.pressedChangedHandler = { button, value, pressed in |
|||
if pressed { |
|||
appui.exit() |
|||
presentationMode.wrappedValue.dismiss() |
|||
} |
|||
} |
|||
extendedGamepad.leftThumbstick.valueChangedHandler = { dpad, x, y in |
|||
self.appui.thumbstickMoved(analog: .left, x: x, y: y, controllerid: controllerId) |
|||
} |
|||
|
|||
extendedGamepad.rightThumbstick.valueChangedHandler = { dpad, x, y in |
|||
self.appui.thumbstickMoved(analog: .right, x: x, y: y, controllerid: controllerId) |
|||
} |
|||
|
|||
if let motion = controller.motion { |
|||
var lastTimestamp = Date().timeIntervalSince1970 // Initialize timestamp when motion starts |
|||
|
|||
motion.valueChangedHandler = { motion in |
|||
// Get current time |
|||
let currentTimestamp = Date().timeIntervalSince1970 |
|||
let deltaTimestamp = Int32((currentTimestamp - lastTimestamp) * 1000) // Difference in milliseconds |
|||
|
|||
// Update last timestamp |
|||
lastTimestamp = currentTimestamp |
|||
|
|||
// Get gyroscope data |
|||
let gyroX = motion.rotationRate.x |
|||
let gyroY = motion.rotationRate.y |
|||
let gyroZ = motion.rotationRate.z |
|||
|
|||
// Get accelerometer data |
|||
let accelX = motion.gravity.x + motion.userAcceleration.x |
|||
let accelY = motion.gravity.y + motion.userAcceleration.y |
|||
let accelZ = motion.gravity.z + motion.userAcceleration.z |
|||
|
|||
print("\(gyroX), \(gyroY), \(gyroZ), \(accelX), \(accelY), \(accelZ)") |
|||
|
|||
// Call your gyroMoved function with the motion data |
|||
appui.gyroMoved(x: Float(gyroX), y: Float(gyroY), z: Float(gyroZ), accelX: Float(accelX), accelY: Float(accelY), accelZ: Float(accelZ), controllerId: Int32(controllerId), deltaTimestamp: Int32(lastTimestamp)) |
|||
} |
|||
} |
|||
} else if let microGamepad = controller.microGamepad { |
|||
// Handle micro gamepad |
|||
microGamepad.dpad.up.pressedChangedHandler = { button, value, pressed in |
|||
pressed ? self.touchDown(.directionalPadUp, controllerId: controllerId) : self.touchUpInside(.directionalPadUp, controllerId: controllerId) |
|||
} |
|||
microGamepad.dpad.down.pressedChangedHandler = { button, value, pressed in |
|||
pressed ? self.touchDown(.directionalPadDown, controllerId: controllerId) : self.touchUpInside(.directionalPadDown, controllerId: controllerId) |
|||
} |
|||
microGamepad.dpad.left.pressedChangedHandler = { button, value, pressed in |
|||
pressed ? self.touchDown(.directionalPadLeft, controllerId: controllerId) : self.touchUpInside(.directionalPadLeft, controllerId: controllerId) |
|||
} |
|||
microGamepad.dpad.right.pressedChangedHandler = { button, value, pressed in |
|||
pressed ? self.touchDown(.directionalPadRight, controllerId: controllerId) : self.touchUpInside(.directionalPadRight, controllerId: controllerId) |
|||
} |
|||
microGamepad.buttonA.pressedChangedHandler = { button, value, pressed in |
|||
pressed ? self.touchDown(.A, controllerId: controllerId) : self.touchUpInside(.A, controllerId: controllerId) |
|||
} |
|||
microGamepad.buttonX.pressedChangedHandler = { button, value, pressed in |
|||
pressed ? self.touchDown(.X, controllerId: controllerId) : self.touchUpInside(.X, controllerId: controllerId) |
|||
} |
|||
} |
|||
} |
|||
|
|||
private func touchDown(_ button: VirtualControllerButtonType, controllerId: Int) { |
|||
appui.virtualControllerButtonDown(button: button, controllerid: controllerId) } |
|||
|
|||
private func touchUpInside(_ button: VirtualControllerButtonType, controllerId: Int) { |
|||
appui.virtualControllerButtonUp(button: button, controllerid: controllerId) |
|||
} |
|||
} |
|||
|
|||
struct OnScreenController: View { |
|||
@State var geometry: GeometryProxy |
|||
var body: some View { |
|||
if geometry.size.height > geometry.size.width && UIDevice.current.userInterfaceIdiom != .pad { |
|||
// portrait |
|||
VStack { |
|||
Spacer() |
|||
VStack { |
|||
HStack { |
|||
VStack { |
|||
ShoulderButtonsViewLeft() |
|||
ZStack { |
|||
Joystick() |
|||
DPadView() |
|||
} |
|||
} |
|||
.padding() |
|||
VStack { |
|||
ShoulderButtonsViewRight() |
|||
ZStack { |
|||
Joystick(iscool: true) // hope this works |
|||
ABXYView() |
|||
} |
|||
} |
|||
.padding() |
|||
} |
|||
HStack { |
|||
ButtonView(button: .plus).padding(.horizontal, 40) |
|||
ButtonView(button: .minus).padding(.horizontal, 40) |
|||
} |
|||
} |
|||
.padding(.bottom, geometry.size.height / 3.2) // very broken |
|||
} |
|||
} else { |
|||
// could be landscape |
|||
VStack { |
|||
HStack { |
|||
Spacer() |
|||
ButtonView(button: .home) |
|||
.padding(.horizontal) |
|||
} |
|||
Spacer() |
|||
VStack { |
|||
HStack { |
|||
|
|||
// gotta fuckin add + and - now |
|||
VStack { |
|||
ShoulderButtonsViewLeft() |
|||
ZStack { |
|||
Joystick() |
|||
DPadView() |
|||
} |
|||
} |
|||
HStack { |
|||
Spacer() |
|||
VStack { |
|||
Spacer() |
|||
ButtonView(button: .plus) // Adding the + button |
|||
} |
|||
VStack { |
|||
Spacer() |
|||
ButtonView(button: .minus) // Adding the - button |
|||
} |
|||
Spacer() |
|||
} |
|||
VStack { |
|||
ShoulderButtonsViewRight() |
|||
ZStack { |
|||
Joystick(iscool: true) // hope this work s |
|||
ABXYView() |
|||
} |
|||
} |
|||
} |
|||
|
|||
} |
|||
.padding(.bottom, geometry.size.height / 11) // also extremally broken ( |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
struct ShoulderButtonsViewLeft: View { |
|||
var body: some View { |
|||
HStack { |
|||
ButtonView(button: .triggerZL) |
|||
.padding(.horizontal) |
|||
ButtonView(button: .triggerL) |
|||
.padding(.horizontal) |
|||
} |
|||
.frame(width: 160, height: 20) |
|||
} |
|||
} |
|||
|
|||
struct ShoulderButtonsViewRight: View { |
|||
var body: some View { |
|||
HStack { |
|||
ButtonView(button: .triggerR) |
|||
.padding(.horizontal) |
|||
ButtonView(button: .triggerZR) |
|||
.padding(.horizontal) |
|||
} |
|||
.frame(width: 160, height: 20) |
|||
|
|||
} |
|||
} |
|||
|
|||
struct DPadView: View { |
|||
var body: some View { |
|||
VStack { |
|||
ButtonView(button: .directionalPadUp) |
|||
HStack { |
|||
ButtonView(button: .directionalPadLeft) |
|||
Spacer(minLength: 20) |
|||
ButtonView(button: .directionalPadRight) |
|||
} |
|||
ButtonView(button: .directionalPadDown) |
|||
.padding(.horizontal) |
|||
} |
|||
.frame(width: 145, height: 145) |
|||
} |
|||
} |
|||
|
|||
struct ABXYView: View { |
|||
var body: some View { |
|||
VStack { |
|||
ButtonView(button: .X) |
|||
HStack { |
|||
ButtonView(button: .Y) |
|||
Spacer(minLength: 20) |
|||
ButtonView(button: .A) |
|||
} |
|||
ButtonView(button: .B) |
|||
.padding(.horizontal) |
|||
} |
|||
.frame(width: 145, height: 145) |
|||
} |
|||
} |
|||
|
|||
struct ButtonView: View { |
|||
var button: VirtualControllerButtonType |
|||
@StateObject private var viewModel: EmulationViewModel = EmulationViewModel(game: nil) |
|||
let appui = AppUI.shared |
|||
@State var mtkView: MTKView? |
|||
@State var width: CGFloat = 45 |
|||
@State var height: CGFloat = 45 |
|||
@State var isPressed = false |
|||
var id: Int { |
|||
if onscreenjoy { |
|||
return 8 |
|||
} |
|||
return 0 |
|||
} |
|||
@AppStorage("onscreenhandheld") var onscreenjoy: Bool = false |
|||
@Environment(\.colorScheme) var colorScheme |
|||
@Environment(\.presentationMode) var presentationMode |
|||
|
|||
var body: some View { |
|||
Image(systemName: buttonText) |
|||
.resizable() |
|||
.frame(width: width, height: height) |
|||
.foregroundColor(colorScheme == .dark ? Color.gray : Color.gray) |
|||
.opacity(isPressed ? 0.5 : 1) |
|||
.gesture( |
|||
DragGesture(minimumDistance: 0) |
|||
.onChanged { _ in |
|||
if !self.isPressed { |
|||
self.isPressed = true |
|||
DispatchQueue.main.async { |
|||
if button == .home { |
|||
presentationMode.wrappedValue.dismiss() |
|||
appui.exit() |
|||
} else { |
|||
appui.virtualControllerButtonDown(button: button, controllerid: id) |
|||
Haptics.shared.play(.heavy) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
.onEnded { _ in |
|||
self.isPressed = false |
|||
DispatchQueue.main.async { |
|||
if button != .home { |
|||
appui.virtualControllerButtonUp(button: button, controllerid: id) |
|||
} |
|||
} |
|||
} |
|||
) |
|||
.onAppear() { |
|||
if button == .triggerL || button == .triggerZL || button == .triggerZR || button == .triggerR { |
|||
width = 65 |
|||
} |
|||
|
|||
|
|||
if button == .minus || button == .plus || button == .home { |
|||
width = 35 |
|||
height = 35 |
|||
} |
|||
} |
|||
} |
|||
|
|||
private var buttonText: String { |
|||
switch button { |
|||
case .A: return "a.circle.fill" |
|||
case .B: return "b.circle.fill" |
|||
case .X: return "x.circle.fill" |
|||
case .Y: return "y.circle.fill" |
|||
case .directionalPadUp: return "arrowtriangle.up.circle.fill" |
|||
case .directionalPadDown: return "arrowtriangle.down.circle.fill" |
|||
case .directionalPadLeft: return "arrowtriangle.left.circle.fill" |
|||
case .directionalPadRight: return "arrowtriangle.right.circle.fill" |
|||
case .triggerZL: return"zl.rectangle.roundedtop.fill" |
|||
case .triggerZR: return "zr.rectangle.roundedtop.fill" |
|||
case .triggerL: return "l.rectangle.roundedbottom.fill" |
|||
case .triggerR: return "r.rectangle.roundedbottom.fill" |
|||
case .plus: return "plus.circle.fill" |
|||
case .minus: return "minus.circle.fill" |
|||
case .home: return "house.circle.fill" |
|||
default: return "" |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,88 @@ |
|||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project |
|||
// SPDX-License-Identifier: GPL-3.0-or-later |
|||
// SPDX-FileCopyrightText: Copyright 2024 Pomelo, Stossy11 |
|||
// SPDX-License-Identifier: GPL-3.0-or-later |
|||
|
|||
import SwiftUI |
|||
import Foundation |
|||
import AppUI |
|||
|
|||
struct CoreSettingsView: View { |
|||
@State private var text: String = "" |
|||
@State private var isLoading: Bool = true |
|||
@Environment(\.presentationMode) var presentationMode |
|||
|
|||
var body: some View { |
|||
VStack { |
|||
if isLoading { |
|||
ProgressView() |
|||
.progressViewStyle(CircularProgressViewStyle()) |
|||
} else { |
|||
TextEditor(text: $text) |
|||
.padding() |
|||
|
|||
} |
|||
} |
|||
.toolbar { |
|||
ToolbarItem(placement: .navigationBarTrailing) { |
|||
Button { |
|||
let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] |
|||
let configfolder = documentDirectory.appendingPathComponent("config", conformingTo: .folder) |
|||
let fileURL = configfolder.appendingPathComponent("config.ini") |
|||
|
|||
presentationMode.wrappedValue.dismiss() |
|||
|
|||
do { |
|||
try FileManager.default.removeItem(at: fileURL) |
|||
} catch { |
|||
print("\(error.localizedDescription)") |
|||
} |
|||
|
|||
AppUI.shared.settingsSaved() |
|||
|
|||
} label: { |
|||
Text("Reset File") |
|||
} |
|||
} |
|||
} |
|||
.onAppear { |
|||
loadFile() |
|||
} |
|||
.onDisappear() { |
|||
saveFile() |
|||
} |
|||
} |
|||
|
|||
private func loadFile() { |
|||
let fileManager = FileManager.default |
|||
let documentDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0] |
|||
let configfolder = documentDirectory.appendingPathComponent("config", conformingTo: .folder) |
|||
let fileURL = configfolder.appendingPathComponent("config.ini") |
|||
|
|||
if fileManager.fileExists(atPath: fileURL.path) { |
|||
do { |
|||
text = try String(contentsOf: fileURL, encoding: .utf8) |
|||
} catch { |
|||
print("Error reading file: \(error)") |
|||
} |
|||
} else { |
|||
text = "" // Initialize with empty text if file doesn't exist |
|||
} |
|||
isLoading = false |
|||
} |
|||
|
|||
private func saveFile() { |
|||
let fileManager = FileManager.default |
|||
let documentDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0] |
|||
let configfolder = documentDirectory.appendingPathComponent("config", conformingTo: .folder) |
|||
let fileURL = configfolder.appendingPathComponent("config.ini") |
|||
|
|||
do { |
|||
try text.write(to: fileURL, atomically: true, encoding: .utf8) |
|||
AppUI.shared.settingsSaved() |
|||
print("File saved successfully!") |
|||
} catch { |
|||
print("Error saving file: \(error)") |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project |
|||
// SPDX-License-Identifier: GPL-3.0-or-later |
|||
// SPDX-FileCopyrightText: Copyright 2024 Pomelo, Stossy11 |
|||
// SPDX-License-Identifier: GPL-3.0-or-later |
|||
|
|||
import Foundation |
|||
|
|||
func isSideJITServerDetected(completion: @escaping (Result<Void, Error>) -> Void) { |
|||
let address = UserDefaults.standard.string(forKey: "sidejitserver") ?? "" |
|||
var SJSURL = address |
|||
if (address).isEmpty { |
|||
SJSURL = "http://sidejitserver._http._tcp.local:8080" |
|||
} |
|||
// Create a network operation at launch to Refresh SideJITServer |
|||
let url = URL(string: SJSURL)! |
|||
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in |
|||
if let error = error { |
|||
print("No SideJITServer on Network") |
|||
completion(.failure(error)) |
|||
return |
|||
} |
|||
completion(.success(())) |
|||
} |
|||
task.resume() |
|||
return |
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project |
|||
// SPDX-License-Identifier: GPL-3.0-or-later |
|||
// SPDX-FileCopyrightText: Copyright 2024 Pomelo, Stossy11 |
|||
// SPDX-License-Identifier: GPL-3.0-or-later |
|||
|
|||
import Foundation |
|||
|
|||
struct EmulationGame : Comparable, Hashable, Identifiable { |
|||
var id = UUID() |
|||
|
|||
let developer: String |
|||
let fileURL: URL |
|||
let imageData: Data |
|||
let title: String |
|||
|
|||
func hash(into hasher: inout Hasher) { |
|||
hasher.combine(id) |
|||
hasher.combine(developer) |
|||
hasher.combine(fileURL) |
|||
hasher.combine(imageData) |
|||
hasher.combine(title) |
|||
} |
|||
|
|||
static func < (lhs: EmulationGame, rhs: Yuzu) -> Bool { |
|||
lhs.title < rhs.title |
|||
} |
|||
|
|||
static func == (lhs: EmulationGame, rhs: Yuzu) -> Bool { |
|||
lhs.title == rhs.title |
|||
} |
|||
} |
|||
@ -0,0 +1,96 @@ |
|||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project |
|||
// SPDX-License-Identifier: GPL-3.0-or-later |
|||
// SPDX-FileCopyrightText: Copyright 2024 Pomelo, Stossy11 |
|||
// SPDX-License-Identifier: GPL-3.0-or-later |
|||
|
|||
import SwiftUI |
|||
import AppUI |
|||
import Metal |
|||
import Foundation |
|||
|
|||
class EmulationViewModel: ObservableObject { |
|||
@Published var isShowingCustomButton = true |
|||
@State var should = false |
|||
var device: MTLDevice? |
|||
@State var mtkView: MTKView = MTKView() |
|||
var CaLayer: CAMetalLayer? |
|||
private var sudachiGame: EmulationGame? |
|||
private let appui = AppUI.shared |
|||
private var thread: Thread! |
|||
private var isRunning = false |
|||
var doesneedresources = false |
|||
@State var iscustom: Bool = false |
|||
|
|||
init(game: EmulationGame?) { |
|||
self.device = MTLCreateSystemDefaultDevice() |
|||
self.sudachiGame = game |
|||
} |
|||
|
|||
func configureAppUI(with mtkView: MTKView) { |
|||
self.mtkView = mtkView |
|||
device = self.mtkView.device |
|||
guard !isRunning else { return } |
|||
isRunning = true |
|||
appui.configure(layer: mtkView.layer as! CAMetalLayer, with: mtkView.frame.size) |
|||
|
|||
iscustom = ((sudachiGame?.fileURL.startAccessingSecurityScopedResource()) != nil) |
|||
|
|||
DispatchQueue.global(qos: .userInitiated).async { [self] in |
|||
if let sudachiGame = self.sudachiGame { |
|||
self.appui.insert(game: sudachiGame.fileURL) |
|||
} else { |
|||
self.appui.bootOS() |
|||
} |
|||
} |
|||
|
|||
thread = .init(block: self.step) |
|||
thread.name = "Yuzu" |
|||
thread.qualityOfService = .userInteractive |
|||
thread.threadPriority = 0.9 |
|||
thread.start() |
|||
} |
|||
|
|||
private func step() { |
|||
while true { |
|||
appui.step() |
|||
} |
|||
} |
|||
|
|||
func customButtonTapped() { |
|||
stopEmulation() |
|||
} |
|||
|
|||
private func stopEmulation() { |
|||
if isRunning { |
|||
isRunning = false |
|||
appui.exit() |
|||
thread.cancel() |
|||
if iscustom { |
|||
sudachiGame?.fileURL.stopAccessingSecurityScopedResource() |
|||
} |
|||
} |
|||
} |
|||
|
|||
func handleOrientationChange() { |
|||
DispatchQueue.main.async { [weak self] in |
|||
guard let self = self else { return } |
|||
let interfaceOrientation = self.getInterfaceOrientation(from: UIDevice.current.orientation) |
|||
self.appui.orientationChanged(orientation: interfaceOrientation, with: self.mtkView.layer as! CAMetalLayer, size: mtkView.frame.size) |
|||
} |
|||
} |
|||
|
|||
private func getInterfaceOrientation(from deviceOrientation: UIDeviceOrientation) -> UIInterfaceOrientation { |
|||
switch deviceOrientation { |
|||
case .portrait: |
|||
return .portrait |
|||
case .portraitUpsideDown: |
|||
return .portraitUpsideDown |
|||
case .landscapeLeft: |
|||
return .landscapeRight |
|||
case .landscapeRight: |
|||
return .landscapeLeft |
|||
default: |
|||
return .unknown |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,133 @@ |
|||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project |
|||
// SPDX-License-Identifier: GPL-3.0-or-later |
|||
// SPDX-FileCopyrightText: Copyright 2024 Pomelo, Stossy11 |
|||
// SPDX-License-Identifier: GPL-3.0-or-later |
|||
|
|||
import SwiftUI |
|||
import AppUI |
|||
import MetalKit |
|||
|
|||
class EmulationScreenView: UIView { |
|||
var primaryScreen: UIView! |
|||
var portraitconstraints = [NSLayoutConstraint]() |
|||
var landscapeconstraints = [NSLayoutConstraint]() |
|||
var fullscreenconstraints = [NSLayoutConstraint]() |
|||
let appui = AppUI.shared |
|||
let userDefaults = UserDefaults.standard |
|||
|
|||
override init(frame: CGRect) { |
|||
super.init(frame: frame) |
|||
if UIDevice.current.userInterfaceIdiom == .pad { |
|||
setupAppUIScreenforiPad() |
|||
} else { |
|||
setupAppUIScreen() |
|||
} |
|||
} |
|||
|
|||
required init?(coder: NSCoder) { |
|||
super.init(coder: coder) |
|||
if UIDevice.current.userInterfaceIdiom == .pad { |
|||
setupAppUIScreenforiPad() |
|||
} else { |
|||
setupAppUIScreen() |
|||
} |
|||
|
|||
} |
|||
|
|||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { |
|||
super.touchesBegan(touches, with: event) |
|||
guard let touch = touches.first else { |
|||
return |
|||
} |
|||
|
|||
print("Location: \(touch.location(in: primaryScreen))") |
|||
appui.touchBegan(at: touch.location(in: primaryScreen), for: 0) |
|||
} |
|||
|
|||
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) { |
|||
super.touchesEnded(touches, with: event) |
|||
print("Touch Ended") |
|||
appui.touchEnded(for: 0) |
|||
} |
|||
|
|||
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) { |
|||
super.touchesMoved(touches, with: event) |
|||
guard let touch = touches.first else { |
|||
return |
|||
} |
|||
let location = touch.location(in: primaryScreen) |
|||
print("Location Moved: \(location)") |
|||
appui.touchMoved(at: location, for: 0) |
|||
} |
|||
|
|||
func setupAppUIScreenforiPad() { |
|||
primaryScreen = MTKView(frame: .zero, device: MTLCreateSystemDefaultDevice()) |
|||
primaryScreen.translatesAutoresizingMaskIntoConstraints = false |
|||
primaryScreen.clipsToBounds = true |
|||
primaryScreen.layer.borderColor = UIColor.secondarySystemBackground.cgColor |
|||
primaryScreen.layer.borderWidth = 3 |
|||
primaryScreen.layer.cornerCurve = .continuous |
|||
primaryScreen.layer.cornerRadius = 10 |
|||
addSubview(primaryScreen) |
|||
|
|||
|
|||
portraitconstraints = [ |
|||
primaryScreen.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: 10), |
|||
primaryScreen.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 10), |
|||
primaryScreen.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -10), |
|||
primaryScreen.heightAnchor.constraint(equalTo: primaryScreen.widthAnchor, multiplier: 9 / 16), |
|||
] |
|||
|
|||
landscapeconstraints = [ |
|||
primaryScreen.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: 50), |
|||
primaryScreen.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor, constant: -100), |
|||
primaryScreen.widthAnchor.constraint(equalTo: primaryScreen.heightAnchor, multiplier: 16 / 9), |
|||
primaryScreen.centerXAnchor.constraint(equalTo: safeAreaLayoutGuide.centerXAnchor), |
|||
] |
|||
|
|||
|
|||
updateConstraintsForOrientation() |
|||
} |
|||
|
|||
|
|||
|
|||
func setupAppUIScreen() { |
|||
primaryScreen = MTKView(frame: .zero, device: MTLCreateSystemDefaultDevice()) |
|||
primaryScreen.translatesAutoresizingMaskIntoConstraints = false |
|||
primaryScreen.clipsToBounds = true |
|||
primaryScreen.layer.borderColor = UIColor.secondarySystemBackground.cgColor |
|||
primaryScreen.layer.borderWidth = 3 |
|||
primaryScreen.layer.cornerCurve = .continuous |
|||
primaryScreen.layer.cornerRadius = 10 |
|||
addSubview(primaryScreen) |
|||
|
|||
|
|||
portraitconstraints = [ |
|||
primaryScreen.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: 10), |
|||
primaryScreen.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 10), |
|||
primaryScreen.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -10), |
|||
primaryScreen.heightAnchor.constraint(equalTo: primaryScreen.widthAnchor, multiplier: 9 / 16), |
|||
] |
|||
|
|||
landscapeconstraints = [ |
|||
primaryScreen.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: 10), |
|||
primaryScreen.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor, constant: -10), |
|||
primaryScreen.widthAnchor.constraint(equalTo: primaryScreen.heightAnchor, multiplier: 16 / 9), |
|||
primaryScreen.centerXAnchor.constraint(equalTo: safeAreaLayoutGuide.centerXAnchor), |
|||
] |
|||
|
|||
updateConstraintsForOrientation() |
|||
} |
|||
|
|||
override func layoutSubviews() { |
|||
super.layoutSubviews() |
|||
updateConstraintsForOrientation() |
|||
} |
|||
|
|||
private func updateConstraintsForOrientation() { |
|||
removeConstraints(portraitconstraints) |
|||
removeConstraints(landscapeconstraints) |
|||
let isPortrait = UIApplication.shared.statusBarOrientation.isPortrait |
|||
addConstraints(isPortrait ? portraitconstraints : landscapeconstraints) |
|||
} |
|||
} |
|||
@ -0,0 +1,137 @@ |
|||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project |
|||
// SPDX-License-Identifier: GPL-3.0-or-later |
|||
// SPDX-FileCopyrightText: Copyright 2024 Pomelo, Stossy11 |
|||
// SPDX-License-Identifier: GPL-3.0-or-later |
|||
|
|||
import SwiftUI |
|||
import AppUI |
|||
import Foundation |
|||
import GameController |
|||
import UIKit |
|||
import SwiftUIIntrospect |
|||
|
|||
struct EmulationView: View { |
|||
@StateObject private var viewModel: EmulationViewModel |
|||
@State var controllerconnected = false |
|||
@State var appui = AppUI.shared |
|||
var device: MTLDevice? = MTLCreateSystemDefaultDevice() |
|||
@State var CaLayer: CAMetalLayer? |
|||
@State var ShowPopup: Bool = false |
|||
@State var mtkview: MTKView? |
|||
@State private var thread: Thread! |
|||
@State var uiTabBarController: UITabBarController? |
|||
@State private var isFirstFrameShown = false |
|||
@State private var timer: Timer? |
|||
@Environment(\.scenePhase) var scenePhase |
|||
|
|||
init(game: EmulationGame?) { |
|||
_viewModel = StateObject(wrappedValue: EmulationViewModel(game: game)) |
|||
} |
|||
|
|||
var body: some View { |
|||
ZStack { |
|||
MetalView(device: device) { view in |
|||
DispatchQueue.main.async { |
|||
if let metalView = view as? MTKView { |
|||
mtkview = metalView |
|||
viewModel.configureAppUI(with: metalView) |
|||
} else { |
|||
print("Error: view is not of type MTKView") |
|||
} |
|||
} |
|||
} |
|||
.onRotate { size in |
|||
viewModel.handleOrientationChange() |
|||
} |
|||
ControllerView() |
|||
} |
|||
.overlay( |
|||
// Loading screen overlay on top of MetalView |
|||
Group { |
|||
if !isFirstFrameShown { |
|||
LoadingView() |
|||
} |
|||
} |
|||
.transition(.opacity) |
|||
) |
|||
.onAppear { |
|||
UIApplication.shared.isIdleTimerDisabled = true |
|||
startPollingFirstFrameShowed() |
|||
} |
|||
.onDisappear { |
|||
stopPollingFirstFrameShowed() |
|||
uiTabBarController?.tabBar.isHidden = false |
|||
viewModel.customButtonTapped() |
|||
} |
|||
.navigationBarBackButtonHidden(true) |
|||
.introspect(.tabView, on: .iOS(.v13, .v14, .v15, .v16, .v17)) { (tabBarController) in |
|||
tabBarController.tabBar.isHidden = true |
|||
uiTabBarController = tabBarController |
|||
} |
|||
} |
|||
|
|||
private func startPollingFirstFrameShowed() { |
|||
timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in |
|||
if appui.FirstFrameShowed() { |
|||
withAnimation { |
|||
isFirstFrameShown = true |
|||
} |
|||
stopPollingFirstFrameShowed() |
|||
} |
|||
} |
|||
} |
|||
|
|||
private func stopPollingFirstFrameShowed() { |
|||
timer?.invalidate() |
|||
timer = nil |
|||
print("Timer Invalidated") |
|||
} |
|||
} |
|||
|
|||
struct LoadingView: View { |
|||
var body: some View { |
|||
VStack { |
|||
ProgressView("Loading...") |
|||
// .font(.system(size: 90)) |
|||
.progressViewStyle(CircularProgressViewStyle()) |
|||
.padding() |
|||
Text("Please wait while the game loads.") |
|||
} |
|||
.frame(maxWidth: .infinity, maxHeight: .infinity) |
|||
.background(Color.black.opacity(0.8)) |
|||
.foregroundColor(.white) |
|||
} |
|||
} |
|||
|
|||
extension View { |
|||
func onRotate(perform action: @escaping (CGSize) -> Void) -> some View { |
|||
self.modifier(DeviceRotationModifier(action: action)) |
|||
} |
|||
} |
|||
|
|||
struct DeviceRotationModifier: ViewModifier { |
|||
let action: (CGSize) -> Void |
|||
@State var startedfirst: Bool = false |
|||
|
|||
func body(content: Content) -> some View { content |
|||
.background(GeometryReader { geometry in |
|||
Color.clear |
|||
.preference(key: SizePreferenceKey.self, value: geometry.size) |
|||
}) |
|||
.onPreferenceChange(SizePreferenceKey.self) { newSize in |
|||
if startedfirst { |
|||
action(newSize) |
|||
} else { |
|||
startedfirst = true |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
struct SizePreferenceKey: PreferenceKey { |
|||
static var defaultValue: CGSize = .zero |
|||
|
|||
static func reduce(value: inout CGSize, nextValue: () -> CGSize) { |
|||
value = nextValue() |
|||
} |
|||
} |
|||
@ -0,0 +1,52 @@ |
|||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project |
|||
// SPDX-License-Identifier: GPL-3.0-or-later |
|||
// SPDX-FileCopyrightText: Copyright 2024 Pomelo, Stossy11 |
|||
// SPDX-License-Identifier: GPL-3.0-or-later |
|||
|
|||
import Foundation |
|||
|
|||
enum SideJITServerErrorType: Error { |
|||
case invalidURL |
|||
case errorConnecting |
|||
case deviceNotFound |
|||
case other(String) |
|||
} |
|||
|
|||
func sendrequestsidejit(url: String, completion: @escaping (Result<Void, SideJITServerErrorType>) -> Void) { |
|||
let url = URL(string: url)! |
|||
let task = URLSession.shared.dataTask(with: url) {(data, response, error) in |
|||
if let error = error { |
|||
completion(.failure(.errorConnecting)) |
|||
return |
|||
} |
|||
guard let data = data, let datastring = String(data: data, encoding: .utf8) else { return } |
|||
if datastring == "Enabled JIT" { |
|||
completion(.success(())) |
|||
} else { |
|||
let errorType: SideJITServerErrorType = datastring == "Could not find device!" ? .deviceNotFound : .other(datastring) |
|||
completion(.failure(errorType)) |
|||
} |
|||
} |
|||
task.resume() |
|||
} |
|||
|
|||
func sendrefresh(url: String, completion: @escaping (Result<Void, SideJITServerErrorType>) -> Void) { |
|||
let url = URL(string: url)! |
|||
|
|||
let task = URLSession.shared.dataTask(with: url) {(data, response, error) in |
|||
if let error = error { |
|||
completion(.failure(.errorConnecting)) |
|||
return |
|||
} |
|||
|
|||
guard let data = data, let datastring = String(data: data, encoding: .utf8) else { return } |
|||
let inputText = "{\"OK\":\"Refreshed!\"}" |
|||
if datastring == inputText { |
|||
completion(.success(())) |
|||
} else { |
|||
let errorType: SideJITServerErrorType = datastring == "Could not find device!" ? .deviceNotFound : .other(datastring) |
|||
completion(.failure(errorType)) |
|||
} |
|||
} |
|||
task.resume() |
|||
} |
|||
@ -0,0 +1,254 @@ |
|||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project |
|||
// SPDX-License-Identifier: GPL-3.0-or-later |
|||
// SPDX-FileCopyrightText: Copyright 2024 Pomelo, Stossy11 |
|||
// SPDX-License-Identifier: GPL-3.0-or-later |
|||
|
|||
import SwiftUI |
|||
import Foundation |
|||
import UIKit |
|||
import AppUI |
|||
import Zip |
|||
|
|||
struct Core : Comparable, Hashable { |
|||
|
|||
let name = "Yuzu" |
|||
var games: [EmulationGame] |
|||
let root: URL |
|||
|
|||
static func < (lhs: Core, rhs: Core) -> Bool { |
|||
lhs.name < rhs.name |
|||
} |
|||
|
|||
func AddFirmware(at fileURL: URL) { |
|||
do { |
|||
let fileManager = FileManager.default |
|||
let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first! |
|||
let destinationURL = documentsDirectory.appendingPathComponent("nand/system/Contents/registered") |
|||
|
|||
|
|||
if !fileManager.fileExists(atPath: destinationURL.path) { |
|||
try fileManager.createDirectory(at: destinationURL, withIntermediateDirectories: true, attributes: nil) |
|||
} |
|||
|
|||
|
|||
try Zip.unzipFile(fileURL, destination: destinationURL, overwrite: true, password: nil) |
|||
print("File unzipped successfully to \(destinationURL.path)") |
|||
|
|||
} catch { |
|||
print("Failed to unzip file: \(error)") |
|||
} |
|||
} |
|||
} |
|||
|
|||
class YuzuFileManager { |
|||
static var shared = YuzuFileManager() |
|||
|
|||
func directories() -> [String : [String : String]] { |
|||
[ |
|||
"themes" : [:], |
|||
"amiibo" : [:], |
|||
"cache" : [:], |
|||
"config" : [:], |
|||
"crash_dumps" : [:], |
|||
"dump" : [:], |
|||
"keys" : [:], |
|||
"load" : [:], |
|||
"log" : [:], |
|||
"nand" : [:], |
|||
"play_time" : [:], |
|||
"roms" : [:], |
|||
"screenshots" : [:], |
|||
"sdmc" : [:], |
|||
"shader" : [:], |
|||
"tas" : [:], |
|||
"icons" : [:] |
|||
] |
|||
} |
|||
|
|||
func createdirectories() throws { |
|||
let documentdir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] |
|||
try directories().forEach() { directory, filename in |
|||
let directoryURL = documentdir.appendingPathComponent(directory) |
|||
|
|||
if !FileManager.default.fileExists(atPath: directoryURL.path) { |
|||
print("creating dir at \(directoryURL.path)") // yippee |
|||
try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: false, attributes: nil) |
|||
} |
|||
} |
|||
} |
|||
|
|||
func DetectKeys() -> (Bool, Bool) { |
|||
var prodkeys = false |
|||
var titlekeys = false |
|||
let filemanager = FileManager.default |
|||
let documentdir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] |
|||
let KeysFolderURL = documentdir.appendingPathComponent("keys") |
|||
prodkeys = filemanager.fileExists(atPath: KeysFolderURL.appendingPathComponent("prod.keys").path) |
|||
titlekeys = filemanager.fileExists(atPath: KeysFolderURL.appendingPathComponent("title.keys").path) |
|||
return (prodkeys, titlekeys) |
|||
} |
|||
} |
|||
|
|||
enum LibManError : Error { |
|||
case ripenum, urlgobyebye |
|||
} |
|||
|
|||
class LibraryManager { |
|||
static let shared = LibraryManager() |
|||
let documentdir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("roms", conformingTo: .folder) |
|||
|
|||
|
|||
func removerom(_ game: EmulationGame) throws { |
|||
do { |
|||
try FileManager.default.removeItem(at: game.fileURL) |
|||
} catch { |
|||
throw error |
|||
} |
|||
} |
|||
|
|||
func homebrewroms() -> [EmulationGame] { |
|||
// TODO(lizzie): this is horrible |
|||
var urls: [URL] = [] |
|||
let sdmc = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("sdmc", conformingTo: .folder) |
|||
let sdfolder = sdmc.appendingPathComponent("switch", conformingTo: .folder) |
|||
if FileManager.default.fileExists(atPath: sdfolder.path) { |
|||
if let dirContents = FileManager.default.enumerator(at: sdmc, includingPropertiesForKeys: nil, options: []) { |
|||
do { |
|||
try dirContents.forEach() { files in |
|||
if let file = files as? URL { |
|||
let getaboutfile = try file.resourceValues(forKeys: [.isRegularFileKey]) |
|||
if let isfile = getaboutfile.isRegularFile, isfile { |
|||
if ["nso", "nro"].contains(file.pathExtension.lowercased()) { |
|||
urls.append(file) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} catch { |
|||
if let dirContents = FileManager.default.enumerator(at: documentdir, includingPropertiesForKeys: nil, options: []) { |
|||
do { |
|||
try dirContents.forEach() { files in |
|||
if let file = files as? URL { |
|||
let getaboutfile = try file.resourceValues(forKeys: [.isRegularFileKey]) |
|||
if let isfile = getaboutfile.isRegularFile, isfile { |
|||
if ["nso", "nro"].contains(file.pathExtension.lowercased()) { |
|||
urls.append(file) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} catch { |
|||
print("damn") |
|||
if let dirContents = FileManager.default.enumerator(at: documentdir, includingPropertiesForKeys: nil, options: []) { |
|||
do { |
|||
try dirContents.forEach() { files in |
|||
if let file = files as? URL { |
|||
let getaboutfile = try file.resourceValues(forKeys: [.isRegularFileKey]) |
|||
if let isfile = getaboutfile.isRegularFile, isfile { |
|||
if ["nso", "nro"].contains(file.pathExtension.lowercased()) { |
|||
urls.append(file) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} catch { |
|||
return [] |
|||
} |
|||
} else { |
|||
return [] |
|||
} |
|||
|
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
if let dirContents = FileManager.default.enumerator(at: documentdir, includingPropertiesForKeys: nil, options: []) { |
|||
do { |
|||
try dirContents.forEach() { files in |
|||
if let file = files as? URL { |
|||
let getaboutfile = try file.resourceValues(forKeys: [.isRegularFileKey]) |
|||
if let isfile = getaboutfile.isRegularFile, isfile { |
|||
if ["nso", "nro"].contains(file.pathExtension.lowercased()) { |
|||
urls.append(file) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} catch { |
|||
return [] |
|||
} |
|||
} else { |
|||
return [] |
|||
} |
|||
func games(from urls: [URL]) -> [EmulationGame] { |
|||
var pomelogames: [EmulationGame] = [] |
|||
pomelogames = urls.reduce(into: [EmulationGame]()) { partialResult, element in |
|||
let iscustom = element.startAccessingSecurityScopedResource() |
|||
let information = AppUI.shared.information(for: element) |
|||
let game = EmulationGame(developer: information.developer, fileURL: element, imageData: information.iconData, title: information.title) |
|||
if iscustom { |
|||
element.stopAccessingSecurityScopedResource() |
|||
} |
|||
partialResult.append(game) |
|||
} |
|||
return pomelogames |
|||
} |
|||
return games(from: urls) |
|||
} |
|||
|
|||
func library() throws -> Core { |
|||
func getromsfromdir() throws -> [URL] { |
|||
guard let dirContents = FileManager.default.enumerator(at: documentdir, includingPropertiesForKeys: nil, options: []) else { |
|||
print("uhoh how unfortunate for some reason FileManager.default.enumerator aint workin") |
|||
throw LibManError.ripenum |
|||
} |
|||
let appui = AppUI.shared |
|||
var urls: [URL] = [] |
|||
try dirContents.forEach() { files in |
|||
if let file = files as? URL { |
|||
let getaboutfile = try file.resourceValues(forKeys: [.isRegularFileKey]) |
|||
if let isfile = getaboutfile.isRegularFile, isfile { |
|||
if ["nca", "nro", "nsp", "nso", "xci"].contains(file.pathExtension.lowercased()) { |
|||
urls.append(file) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
let sdmc = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("sdmc", conformingTo: .folder) |
|||
let sdfolder = sdmc.appendingPathComponent("switch", conformingTo: .folder) |
|||
if FileManager.default.fileExists(atPath: sdfolder.path) { |
|||
if let dirContents = FileManager.default.enumerator(at: sdmc, includingPropertiesForKeys: nil, options: []) { |
|||
try dirContents.forEach() { files in |
|||
if let file = files as? URL { |
|||
let getaboutfile = try file.resourceValues(forKeys: [.isRegularFileKey]) |
|||
if let isfile = getaboutfile.isRegularFile, isfile { |
|||
if ["nso", "nro"].contains(file.pathExtension.lowercased()) { |
|||
urls.append(file) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
appui.insert(games: urls) |
|||
return urls |
|||
} |
|||
|
|||
func games(from urls: [URL], core: inout Core) { |
|||
core.games = urls.reduce(into: [EmulationGame]()) { partialResult, element in |
|||
let iscustom = element.startAccessingSecurityScopedResource() |
|||
let information = AppUI.shared.information(for: element) |
|||
let game = EmulationGame(developer: information.developer, fileURL: element, imageData: information.iconData, title: information.title) |
|||
if iscustom { |
|||
element.stopAccessingSecurityScopedResource() |
|||
} |
|||
partialResult.append(game) |
|||
} |
|||
} |
|||
let directory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] |
|||
var YuzuCore = Core(games: [], root: directory) |
|||
games(from: try getromsfromdir(), core: &YuzuCore) |
|||
return YuzuCore |
|||
} |
|||
} |
|||
@ -0,0 +1,49 @@ |
|||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project |
|||
// SPDX-License-Identifier: GPL-3.0-or-later |
|||
// SPDX-FileCopyrightText: Copyright 2024 Pomelo, Stossy11 |
|||
// SPDX-License-Identifier: GPL-3.0-or-later |
|||
|
|||
import Foundation |
|||
|
|||
class FolderMonitor { |
|||
private var folderDescriptor: Int32 = -1 |
|||
private var folderMonitorSource: DispatchSourceFileSystemObject? |
|||
private let folderURL: URL |
|||
private let onFolderChange: () -> Void |
|||
init(folderURL: URL, onFolderChange: @escaping () -> Void) { |
|||
self.folderURL = folderURL |
|||
self.onFolderChange = onFolderChange |
|||
startMonitoring() |
|||
} |
|||
private func startMonitoring() { |
|||
folderDescriptor = open(folderURL.path, O_EVTONLY) |
|||
guard folderDescriptor != -1 else { |
|||
print("Failed to open folder descriptor.") |
|||
return |
|||
} |
|||
|
|||
folderMonitorSource = DispatchSource.makeFileSystemObjectSource( |
|||
fileDescriptor: folderDescriptor, |
|||
eventMask: .write, |
|||
queue: DispatchQueue.global() |
|||
) |
|||
folderMonitorSource?.setEventHandler { [weak self] in |
|||
self?.folderDidChange() |
|||
} |
|||
folderMonitorSource?.setCancelHandler { |
|||
close(self.folderDescriptor) |
|||
} |
|||
folderMonitorSource?.resume() |
|||
} |
|||
|
|||
private func folderDidChange() { |
|||
// Detect the change and call the refreshcore function |
|||
print("Folder changed! New file added or removed.") |
|||
DispatchQueue.main.async { [weak self] in |
|||
self?.onFolderChange() |
|||
} |
|||
} |
|||
deinit { |
|||
folderMonitorSource?.cancel() |
|||
} |
|||
} |
|||
@ -0,0 +1,40 @@ |
|||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project |
|||
// SPDX-License-Identifier: GPL-3.0-or-later |
|||
// SPDX-FileCopyrightText: Copyright 2024 Pomelo, TechGuy |
|||
// SPDX-License-Identifier: GPL-3.0-or-later |
|||
|
|||
import SwiftUI |
|||
import Foundation |
|||
import UIKit |
|||
|
|||
struct GameButtonListView: View { |
|||
var game: EmulationGame |
|||
@Environment(\.colorScheme) var colorScheme |
|||
|
|||
var body: some View { |
|||
HStack(spacing: 15) { |
|||
if let image = UIImage(data: game.imageData) { |
|||
Image(uiImage: image) |
|||
.resizable() |
|||
.frame(width: 60, height: 60) |
|||
.cornerRadius(8) |
|||
} else { |
|||
Image(systemName: "photo") |
|||
.resizable() |
|||
.frame(width: 60, height: 60) |
|||
.cornerRadius(8) |
|||
} |
|||
|
|||
VStack(alignment: .leading, spacing: 4) { |
|||
Text(game.title) |
|||
.font(.headline) |
|||
.foregroundColor(colorScheme == .dark ? Color.white : Color.black) |
|||
Text(game.developer) |
|||
.font(.subheadline) |
|||
.foregroundColor(.gray) |
|||
} |
|||
Spacer() |
|||
} |
|||
.padding(.vertical, 8) |
|||
} |
|||
} |
|||
@ -0,0 +1,182 @@ |
|||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project |
|||
// SPDX-License-Identifier: GPL-3.0-or-later |
|||
// SPDX-FileCopyrightText: Copyright 2024 Pomelo, Stossy11 |
|||
// SPDX-License-Identifier: GPL-3.0-or-later |
|||
|
|||
import SwiftUI |
|||
import Foundation |
|||
import UIKit |
|||
import UniformTypeIdentifiers |
|||
import Combine |
|||
|
|||
struct GameIconView: View { |
|||
var game: EmulationGame |
|||
@Binding var selectedGame: EmulationGame? |
|||
@State var startgame: Bool = false |
|||
@State var timesTapped: Int = 0 |
|||
|
|||
var isSelected: Bool { |
|||
selectedGame == game |
|||
} |
|||
|
|||
var body: some View { |
|||
NavigationLink( |
|||
destination: EmulationView(game: game).toolbar(.hidden, for: .tabBar), |
|||
isActive: $startgame, |
|||
label: { |
|||
EmptyView() |
|||
} |
|||
) |
|||
VStack(spacing: 5) { |
|||
if isSelected { |
|||
Text(game.title) |
|||
.foregroundColor(.blue) |
|||
.font(.title2) |
|||
} |
|||
if let uiImage = UIImage(data: game.imageData) { |
|||
Image(uiImage: uiImage) |
|||
.resizable() |
|||
.scaledToFit() |
|||
.frame(width: isSelected ? 200 : 180, height: isSelected ? 200 : 180) |
|||
.cornerRadius(10) |
|||
.overlay( |
|||
isSelected ? RoundedRectangle(cornerRadius: 10) |
|||
.stroke(Color.blue, lineWidth: 5) |
|||
: nil |
|||
) |
|||
.onTapGesture { |
|||
if isSelected { |
|||
startgame = true |
|||
print(isSelected) |
|||
} |
|||
if !isSelected { |
|||
selectedGame = game |
|||
} |
|||
} |
|||
} else { |
|||
Image(systemName: "questionmark") |
|||
.resizable() |
|||
.scaledToFit() |
|||
.frame(width: 200, height: 200) |
|||
.cornerRadius(10) |
|||
.onTapGesture { selectedGame = game } |
|||
} |
|||
} |
|||
.frame(width: 200, height: 250) |
|||
} |
|||
} |
|||
|
|||
struct BottomMenuView: View { |
|||
@State var core: Core |
|||
var body: some View { |
|||
HStack(spacing: 40) { |
|||
Button { |
|||
|
|||
} label: { |
|||
Circle() |
|||
.overlay { |
|||
Image(systemName: "message").font(.system(size: 30)).foregroundColor(.red) |
|||
} |
|||
.frame(width: 50, height: 50) |
|||
.foregroundColor(Color.init(uiColor: .lightGray)) |
|||
} |
|||
Button { |
|||
|
|||
} label: { |
|||
Circle() |
|||
.overlay { |
|||
Image(systemName: "photo").font(.system(size: 30)).foregroundColor(.blue) |
|||
} |
|||
.frame(width: 50, height: 50) |
|||
.foregroundColor(Color.init(uiColor: .lightGray)) |
|||
} |
|||
NavigationLink(destination: SettingsView(core: core)) { |
|||
Circle() |
|||
.overlay { |
|||
Image(systemName: "gearshape").foregroundColor(Color.init(uiColor: .darkGray)).font(.system(size: 30)) |
|||
} |
|||
.frame(width: 50, height: 50) |
|||
.foregroundColor(Color.init(uiColor: .lightGray)) |
|||
} |
|||
|
|||
Button { |
|||
|
|||
} label: { |
|||
Circle() |
|||
.overlay { |
|||
Image(systemName: "power").foregroundColor(Color.init(uiColor: .darkGray)).font(.system(size: 30)) |
|||
} |
|||
.frame(width: 50, height: 50) |
|||
.foregroundColor(Color.init(uiColor: .lightGray)) |
|||
} |
|||
} |
|||
.padding(.bottom, 20) |
|||
} |
|||
} |
|||
|
|||
struct HomeView: View { |
|||
@State private var selectedGame: EmulationGame? = nil |
|||
|
|||
@State var core: Core |
|||
|
|||
init(selectedGame: EmulationGame? = nil, core: Core) { |
|||
_core = State(wrappedValue: core) |
|||
self.selectedGame = selectedGame |
|||
refreshcore() |
|||
} |
|||
|
|||
var body: some View { |
|||
NavigationStack { |
|||
GeometryReader { geometry in |
|||
VStack { |
|||
GameCarouselView(core: core, selectedGame: $selectedGame) |
|||
Spacer() |
|||
BottomMenuView(core: core) |
|||
} |
|||
} |
|||
} |
|||
.background(Color.gray.opacity(0.1)) |
|||
.edgesIgnoringSafeArea(.all) |
|||
.onAppear { |
|||
refreshcore() |
|||
if let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first { |
|||
let romsFolderURL = documentsDirectory.appendingPathComponent("roms") |
|||
let folderMonitor = FolderMonitor(folderURL: romsFolderURL) { |
|||
do { |
|||
core = Core(games: [], root: documentsDirectory) |
|||
core = try LibraryManager.shared.library() |
|||
} catch { |
|||
print("Error refreshing core: \(error)") |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
func refreshcore() { |
|||
print("Loading library...") |
|||
do { |
|||
core = try LibraryManager.shared.library() |
|||
print(core.games) |
|||
} catch { |
|||
print("Failed to fetch library: \(error)") |
|||
return |
|||
} |
|||
} |
|||
} |
|||
|
|||
|
|||
struct GameCarouselView: View { |
|||
// let games: [EmulationGame] |
|||
@State var core: Core |
|||
@Binding var selectedGame: EmulationGame? |
|||
var body: some View { |
|||
ScrollView(.horizontal, showsIndicators: false) { |
|||
HStack(spacing: 20) { |
|||
ForEach(core.games) { game in |
|||
GameIconView(game: game, selectedGame: $selectedGame) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,140 @@ |
|||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project |
|||
// SPDX-License-Identifier: GPL-3.0-or-later |
|||
// SPDX-FileCopyrightText: Copyright 2024 Pomelo, Stossy11 |
|||
// SPDX-License-Identifier: GPL-3.0-or-later |
|||
|
|||
import SwiftUI |
|||
import Foundation |
|||
import UIKit |
|||
import UniformTypeIdentifiers |
|||
import AppUI |
|||
|
|||
struct GameListView: View { |
|||
@State var core: Core |
|||
@State private var searchText = "" |
|||
@State var game: Int = 1 |
|||
@State var startgame: Bool = false |
|||
@Binding var isGridView: Bool |
|||
@State var showAlert = false |
|||
@State var alertMessage: Alert? = nil |
|||
|
|||
var body: some View { |
|||
let filteredGames = core.games.filter { game in |
|||
guard let EmulationGame = game as? PoYuzume else { return false } |
|||
return searchText.isEmpty || EmulationGame.title.localizedCaseInsensitiveContains(searchText) |
|||
} |
|||
|
|||
ScrollView { |
|||
VStack { |
|||
VStack(alignment: .leading) { |
|||
|
|||
if isGridView { |
|||
LazyVGrid(columns: [GridItem(.adaptive(minimum: 160))], spacing: 10) { |
|||
ForEach(0..<filteredGames.count, id: \.self) { index in |
|||
let game = filteredGames[index] // Use filteredGames here |
|||
NavigationLink(destination: EmulationView(game: game).toolbar(.hidden, for: .tabBar)) { |
|||
// GameButtonView(game: game) |
|||
// .frame(maxWidth: .infinity, minHeight: 200) |
|||
} |
|||
.contextMenu { |
|||
Button(action: { |
|||
do { |
|||
try LibraryManager.shared.removerom(filteredGames[index]) |
|||
} catch { |
|||
showAlert = true |
|||
alertMessage = Alert(title: Text("Unable to Remove Game"), message: Text(error.localizedDescription)) |
|||
} |
|||
}) { |
|||
Text("Remove") |
|||
} |
|||
Button(action: { |
|||
if let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?.appending(path: "roms") { |
|||
UIApplication.shared.open(documentsURL, options: [:], completionHandler: nil) |
|||
} |
|||
}) { |
|||
if ProcessInfo.processInfo.isMacCatalystApp { |
|||
Text("Open in Finder") |
|||
} else { |
|||
Text("Open in Files") |
|||
} |
|||
} |
|||
NavigationLink(destination: EmulationView(game: game).toolbar(.hidden, for: .tabBar)) { |
|||
Text("Launch") |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} else { |
|||
LazyVStack() { |
|||
ForEach(0..<filteredGames.count, id: \.self) { index in |
|||
let game = filteredGames[index] // Use filteredGames here |
|||
NavigationLink(destination: EmulationView(game: game).toolbar(.hidden, for: .tabBar)) { |
|||
GameButtonListView(game: game) |
|||
.frame(maxWidth: .infinity, minHeight: 75) |
|||
} |
|||
.contextMenu { |
|||
Button(action: { |
|||
do { |
|||
try LibraryManager.shared.removerom(filteredGames[index]) |
|||
try FileManager.default.removeItem(atPath: game.fileURL.path) |
|||
} catch { |
|||
showAlert = true |
|||
alertMessage = Alert(title: Text("Unable to Remove Game"), message: Text(error.localizedDescription)) |
|||
} |
|||
}) { |
|||
Text("Remove") |
|||
} |
|||
|
|||
Button(action: { |
|||
if let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?.appending(path: "roms") { |
|||
UIApplication.shared.open(documentsURL, options: [:], completionHandler: nil) |
|||
} |
|||
}) { |
|||
if ProcessInfo.processInfo.isMacCatalystApp { |
|||
Text("Open in Finder") |
|||
} else { |
|||
Text("Open in Files") |
|||
} |
|||
} |
|||
NavigationLink(destination: EmulationView(game: game).toolbar(.hidden, for: .tabBar)) { |
|||
Text("Launch") |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
.searchable(text: $searchText) |
|||
.padding() |
|||
} |
|||
.onAppear { |
|||
refreshcore() |
|||
|
|||
if let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first { |
|||
let romsFolderURL = documentsDirectory.appendingPathComponent("roms") |
|||
|
|||
let folderMonitor = FolderMonitor(folderURL: romsFolderURL) { |
|||
do { |
|||
core = Core(games: [], root: documentsDirectory) |
|||
core = try LibraryManager.shared.library() |
|||
} catch { |
|||
print("Error refreshing core: \(error)") |
|||
} |
|||
} |
|||
} |
|||
} |
|||
.alert(isPresented: $showAlert) { |
|||
alertMessage ?? Alert(title: Text("Error Not Found")) |
|||
} |
|||
} |
|||
} |
|||
|
|||
func refreshcore() { |
|||
do { |
|||
core = try LibraryManager.shared.library() |
|||
} catch { |
|||
print("Failed to fetch library: \(error)") |
|||
return |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project |
|||
// SPDX-License-Identifier: GPL-3.0-or-later |
|||
// SPDX-FileCopyrightText: Copyright 2024 Pomelo, Stossy11 |
|||
// SPDX-License-Identifier: GPL-3.0-or-later |
|||
|
|||
import UIKit |
|||
import SwiftUI |
|||
import AppUI |
|||
|
|||
class Haptics { |
|||
static let shared = Haptics() |
|||
|
|||
private init() { } |
|||
|
|||
func play(_ feedbackStyle: UIImpactFeedbackGenerator.FeedbackStyle) { |
|||
print("haptics") |
|||
UIImpactFeedbackGenerator(style: feedbackStyle).impactOccurred() |
|||
} |
|||
|
|||
func notify(_ feedbackType: UINotificationFeedbackGenerator.FeedbackType) { |
|||
UINotificationFeedbackGenerator().notificationOccurred(feedbackType) |
|||
} |
|||
} |
|||
@ -0,0 +1,46 @@ |
|||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project |
|||
// SPDX-License-Identifier: GPL-3.0-or-later |
|||
// SPDX-FileCopyrightText: Copyright 2024 Yuzu, Stossy11 |
|||
// SPDX-License-Identifier: GPL-3.0-or-later |
|||
|
|||
import SwiftUI |
|||
|
|||
struct InfoView: View { |
|||
@AppStorage("entitlementNotExists") private var entitlementNotExists: Bool = false |
|||
@AppStorage("increaseddebugmem") private var increaseddebugmem: Bool = false |
|||
@AppStorage("extended-virtual-addressing") private var extended: Bool = false |
|||
let infoDictionary = Bundle.main.infoDictionary |
|||
|
|||
var body: some View { |
|||
ScrollView { |
|||
VStack { |
|||
Text("Welcome").font(.largeTitle) |
|||
Divider() |
|||
Text("Entitlements:").font(.title).font(Font.headline.weight(.bold)) |
|||
Spacer().frame(height: 10) |
|||
Group { |
|||
Text("Required:").font(.title2).font(Font.headline.weight(.bold)) |
|||
Spacer().frame(height: 10) |
|||
Text("Limit: \(String(describing: !entitlementNotExists))") |
|||
Spacer().frame(height: 10) |
|||
} |
|||
Group { |
|||
Spacer().frame(height: 10) |
|||
Text("Reccomended:").font(.title2).font(Font.headline.weight(.bold)) |
|||
Spacer().frame(height: 10) |
|||
Text("Limit: \(String(describing: increaseddebugmem))").padding() |
|||
Text("Extended: \(String(describing: extended))") |
|||
} |
|||
|
|||
} |
|||
.padding() |
|||
Text("Version: \(getAppVersion())").foregroundColor(.gray) |
|||
} |
|||
} |
|||
func getAppVersion() -> String { |
|||
guard let s = infoDictionary?["CFBundleShortVersionString"] as? String else { |
|||
return "Unknown" |
|||
} |
|||
return s |
|||
} |
|||
} |
|||
@ -0,0 +1,55 @@ |
|||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project |
|||
// SPDX-License-Identifier: GPL-3.0-or-later |
|||
// SPDX-FileCopyrightText: Copyright 2024 Pomelo, Stossy11 |
|||
// SPDX-License-Identifier: GPL-3.0-or-later |
|||
|
|||
import SwiftUI |
|||
import SwiftUIJoystick |
|||
import AppUI |
|||
|
|||
public struct Joystick: View { |
|||
@State var iscool: Bool? = nil |
|||
var id: Int { |
|||
if onscreenjoy { |
|||
return 8 |
|||
} |
|||
return 0 |
|||
} |
|||
@AppStorage("onscreenhandheld") var onscreenjoy: Bool = false |
|||
|
|||
let appui = AppUI.shared |
|||
|
|||
@ObservedObject public var joystickMonitor = JoystickMonitor() |
|||
private let dragDiameter: CGFloat = 160 |
|||
private let shape: JoystickShape = .circle |
|||
|
|||
public var body: some View { |
|||
VStack{ |
|||
JoystickBuilder( |
|||
monitor: self.joystickMonitor, |
|||
width: self.dragDiameter, |
|||
shape: .circle, |
|||
background: { |
|||
// Example Background |
|||
RoundedRectangle(cornerRadius: 8).fill(Color.gray.opacity(0)) |
|||
}, |
|||
foreground: { |
|||
// Example Thumb |
|||
Circle().fill(Color.gray) |
|||
}, |
|||
locksInPlace: false) |
|||
.onChange(of: self.joystickMonitor.xyPoint) { newValue in |
|||
let scaledX = Float(newValue.x) |
|||
let scaledY = Float(-newValue.y) // my dumbass broke this by having -y instead of y :/ (well it appears that with the new joystick code, its supposed to be -y) |
|||
joystickMonitor.objectWillChange |
|||
print("Joystick Position: (\(scaledX), \(scaledY))") |
|||
|
|||
if iscool != nil { |
|||
appui.thumbstickMoved(analog: .right, x: scaledX, y: scaledY, controllerid: id) |
|||
} else { |
|||
appui.thumbstickMoved(analog: .left, x: scaledX, y: scaledY, controllerid: id) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,76 @@ |
|||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project |
|||
// SPDX-License-Identifier: GPL-3.0-or-later |
|||
// SPDX-FileCopyrightText: Copyright 2024 Pomelo, Stossy11 |
|||
// SPDX-License-Identifier: GPL-3.0-or-later |
|||
|
|||
import SwiftUI |
|||
import UIKit |
|||
|
|||
class KeyboardHostingController<Content: View>: UIHostingController<Content> { |
|||
|
|||
override var canBecomeFirstResponder: Bool { |
|||
return true |
|||
} |
|||
|
|||
override func viewDidLoad() { |
|||
super.viewDidLoad() |
|||
becomeFirstResponder() // Make sure the view can become the first responder |
|||
} |
|||
|
|||
override var keyCommands: [UIKeyCommand]? { |
|||
return [ |
|||
UIKeyCommand(input: UIKeyCommand.inputUpArrow, modifierFlags: [], action: #selector(handleKeyCommand)), |
|||
UIKeyCommand(input: UIKeyCommand.inputDownArrow, modifierFlags: [], action: #selector(handleKeyCommand)), |
|||
UIKeyCommand(input: UIKeyCommand.inputLeftArrow, modifierFlags: [], action: #selector(handleKeyCommand)), |
|||
UIKeyCommand(input: UIKeyCommand.inputRightArrow, modifierFlags: [], action: #selector(handleKeyCommand)), |
|||
UIKeyCommand(input: "w", modifierFlags: [], action: #selector(handleKeyCommand)), |
|||
UIKeyCommand(input: "s", modifierFlags: [], action: #selector(handleKeyCommand)), |
|||
UIKeyCommand(input: "a", modifierFlags: [], action: #selector(handleKeyCommand)), |
|||
UIKeyCommand(input: "d", modifierFlags: [], action: #selector(handleKeyCommand)) |
|||
] |
|||
} |
|||
|
|||
@objc func handleKeyCommand(_ sender: UIKeyCommand) { |
|||
if let input = sender.input { |
|||
switch input { |
|||
case UIKeyCommand.inputUpArrow: |
|||
print("Up Arrow Pressed") |
|||
case UIKeyCommand.inputDownArrow: |
|||
print("Down Arrow Pressed") |
|||
case UIKeyCommand.inputLeftArrow: |
|||
print("Left Arrow Pressed") |
|||
case UIKeyCommand.inputRightArrow: |
|||
print("Right Arrow Pressed") |
|||
case "w": |
|||
print("W Key Pressed") |
|||
case "s": |
|||
print("S Key Pressed") |
|||
case "a": |
|||
print("A Key Pressed") |
|||
case "d": |
|||
print("D Key Pressed") |
|||
default: |
|||
break |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
|
|||
struct KeyboardSupportView: UIViewControllerRepresentable { |
|||
let content: Text |
|||
|
|||
func makeUIViewController(context: Context) -> KeyboardHostingController<Text> { |
|||
return KeyboardHostingController(rootView: content) |
|||
} |
|||
|
|||
func updateUIViewController(_ uiViewController: KeyboardHostingController<Text>, context: Context) { |
|||
// Handle any updates needed |
|||
} |
|||
} |
|||
|
|||
struct KeyboardView: View { |
|||
var body: some View { |
|||
KeyboardSupportView(content: Text("")) |
|||
} |
|||
} |
|||
@ -0,0 +1,191 @@ |
|||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project |
|||
// SPDX-License-Identifier: GPL-3.0-or-later |
|||
// SPDX-FileCopyrightText: Copyright 2024 Pomelo, Stossy11 |
|||
// SPDX-License-Identifier: GPL-3.0-or-later |
|||
|
|||
import SwiftUI |
|||
import CryptoKit |
|||
import AppUI |
|||
|
|||
struct LibraryView: View { |
|||
@Binding var core: Core |
|||
@State var isGridView: Bool = true |
|||
@State var doesitexist = (false, false) |
|||
@State var importedgame: EmulationGame? = nil |
|||
@State var importgame: Bool = false |
|||
@State var isimportingfirm: Bool = false |
|||
@State var launchGame: Bool = false |
|||
var body: some View { |
|||
NavigationStack { |
|||
if let importedgame = importedgame { |
|||
NavigationLink( |
|||
isActive: $launchGame, |
|||
destination: { |
|||
EmulationView(game: importedgame).toolbar(.hidden, for: .tabBar) |
|||
}, |
|||
label: { |
|||
EmptyView() // This keeps the link hidden |
|||
} |
|||
) |
|||
} |
|||
|
|||
VStack { |
|||
if doesitexist.0, doesitexist.1 { |
|||
HomeView(core: core) |
|||
} else { |
|||
let (doesKeyExist, doesProdExist) = doeskeysexist() |
|||
ScrollView { |
|||
Text("You Are Missing These Files:") |
|||
.font(.headline) |
|||
.foregroundColor(.red) |
|||
HStack { |
|||
if !doesProdExist { |
|||
Text("Prod.keys") |
|||
.font(.subheadline) |
|||
.foregroundColor(.red) |
|||
} |
|||
if !doesKeyExist { |
|||
Text("Title.keys") |
|||
.font(.subheadline) |
|||
.foregroundColor(.red) |
|||
} |
|||
} |
|||
Text("These goes into the Keys folder") |
|||
.font(.caption) |
|||
.foregroundColor(.red) |
|||
.padding(.bottom) |
|||
|
|||
if !LibraryManager.shared.homebrewroms().isEmpty { |
|||
Text("Homebrew Roms:") |
|||
.font(.headline) |
|||
LazyVGrid(columns: [GridItem(.adaptive(minimum: 160))], spacing: 10) { |
|||
ForEach(LibraryManager.shared.homebrewroms()) { game in |
|||
NavigationLink(destination: EmulationView(game: game).toolbar(.hidden, for: .tabBar)) { |
|||
// GameButtonView(game: game) |
|||
// .frame(maxWidth: .infinity, minHeight: 200) |
|||
} |
|||
.contextMenu { |
|||
NavigationLink(destination: EmulationView(game: game)) { |
|||
Text("Launch") |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
.refreshable { |
|||
doesitexist = doeskeysexist() |
|||
} |
|||
|
|||
|
|||
} |
|||
|
|||
} |
|||
.fileImporter(isPresented: $isimportingfirm, allowedContentTypes: [.zip], onCompletion: { result in |
|||
switch result { |
|||
case .success(let elements): |
|||
core.AddFirmware(at: elements) |
|||
case .failure(let error): |
|||
|
|||
print(error.localizedDescription) |
|||
} |
|||
}) |
|||
.fileImporter(isPresented: $importgame, allowedContentTypes: [.item], onCompletion: { result in |
|||
switch result { |
|||
case .success(let elements): |
|||
let iscustom = elements.startAccessingSecurityScopedResource() |
|||
let information = AppUI.shared.information(for: elements) |
|||
|
|||
let game = EmulationGame(developer: information.developer, fileURL: elements, |
|||
imageData: information.iconData, |
|||
title: information.title) |
|||
|
|||
importedgame = game |
|||
|
|||
|
|||
DispatchQueue.main.async { |
|||
|
|||
if iscustom { |
|||
elements.stopAccessingSecurityScopedResource() |
|||
} |
|||
|
|||
launchGame = true |
|||
} |
|||
case .failure(let error): |
|||
|
|||
print(error.localizedDescription) |
|||
} |
|||
}) |
|||
.onAppear() { |
|||
doesitexist = doeskeysexist() |
|||
} |
|||
.navigationBarTitle("Library", displayMode: .inline) |
|||
.toolbar { |
|||
ToolbarItem(placement: .navigationBarLeading) { // why did this take me so long to figure out lmfao |
|||
Button(action: { |
|||
isGridView.toggle() |
|||
}) { |
|||
Image(systemName: isGridView ? "rectangle.grid.1x2" : "square.grid.2x2") |
|||
.imageScale(.large) |
|||
.padding() |
|||
} |
|||
} |
|||
|
|||
ToolbarItem(placement: .navigationBarTrailing) { // funsies |
|||
Menu { |
|||
Button(action: { |
|||
importgame = true // this part took a while |
|||
|
|||
}) { |
|||
Text("Launch Game") |
|||
} |
|||
|
|||
Button(action: { |
|||
isimportingfirm = true |
|||
}) { |
|||
Text("Import Firmware") |
|||
} |
|||
} label: { |
|||
Image(systemName: "plus.circle.fill") |
|||
.imageScale(.large) |
|||
.padding() |
|||
} |
|||
|
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
|
|||
func doeskeysexist() -> (Bool, Bool) { |
|||
var doesprodexist = false |
|||
var doestitleexist = false |
|||
|
|||
|
|||
let title = core.root.appendingPathComponent("keys").appendingPathComponent("title.keys") |
|||
let prod = core.root.appendingPathComponent("keys").appendingPathComponent("prod.keys") |
|||
let fileManager = FileManager.default |
|||
let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0] |
|||
|
|||
if fileManager.fileExists(atPath: prod.path) { |
|||
doesprodexist = true |
|||
} else { |
|||
print("File does not exist") |
|||
} |
|||
|
|||
if fileManager.fileExists(atPath: title.path) { |
|||
doestitleexist = true |
|||
} else { |
|||
print("File does not exist") |
|||
} |
|||
|
|||
return (doestitleexist, doesprodexist) |
|||
} |
|||
} |
|||
|
|||
func getDeveloperNames() -> String { |
|||
guard let s = infoDictionary?["CFBundleIdentifier"] as? String else { |
|||
return "Unknown" |
|||
} |
|||
return s |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project |
|||
// SPDX-License-Identifier: GPL-3.0-or-later |
|||
// SPDX-FileCopyrightText: Copyright 2024 Pomelo, Stossy11 |
|||
// SPDX-License-Identifier: GPL-3.0-or-later |
|||
|
|||
import SwiftUI |
|||
import Metal |
|||
import AppUI |
|||
|
|||
struct MetalView: UIViewRepresentable { |
|||
let device: MTLDevice? |
|||
let configure: (UIView) -> Void |
|||
|
|||
func makeUIView(context: Context) -> EmulationScreenView { |
|||
let view = EmulationScreenView() |
|||
configure(view.primaryScreen) |
|||
return view |
|||
} |
|||
|
|||
func updateUIView(_ uiView: EmulationScreenView, context: Context) { |
|||
// |
|||
} |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project |
|||
// SPDX-License-Identifier: GPL-3.0-or-later |
|||
// SPDX-FileCopyrightText: Copyright 2024 Pomelo, Stossy11 |
|||
// SPDX-License-Identifier: GPL-3.0-or-later |
|||
|
|||
import SwiftUI |
|||
import AppUI |
|||
|
|||
struct NavView: View { |
|||
@Binding var core: Core |
|||
@State private var selectedTab = 0 |
|||
var body: some View { |
|||
TabView(selection: $selectedTab) { |
|||
LibraryView(core: $core) |
|||
.tabItem { Label("Library", systemImage: "rectangle.on.rectangle") } |
|||
.tag(0) |
|||
BootOSView(core: $core, currentnavigarion: $selectedTab) |
|||
.toolbar(.hidden, for: .tabBar) |
|||
.tabItem { Label("Boot OS", systemImage: "house") } |
|||
.tag(1) |
|||
SettingsView(core: core) |
|||
.tabItem { Label("Settings", systemImage: "gear") } |
|||
.tag(2) |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project |
|||
// SPDX-License-Identifier: GPL-3.0-or-later |
|||
// SPDX-FileCopyrightText: Copyright 2024 Pomelo, Stossy11 |
|||
// SPDX-License-Identifier: GPL-3.0-or-later |
|||
|
|||
import SwiftUI |
|||
|
|||
struct SettingsView: View { |
|||
@State var core: Core |
|||
@State var showprompt = false |
|||
|
|||
@AppStorage("icon") var iconused = 1 |
|||
var body: some View { |
|||
NavigationStack { |
|||
|
|||
} |
|||
} |
|||
} |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue