pomelo scraps

This commit is contained in:
lizzie 2026-03-31 01:53:04 +00:00
parent 7cdd35f934
commit c686e37b90
29 changed files with 2258 additions and 11 deletions

View file

@ -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
src/ios/Air.swift Normal file
View file

0
src/ios/AirPlay.swift Normal file
View file

View file

@ -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
}
}

View file

@ -8,9 +8,7 @@ import Foundation
import QuartzCore.CAMetalLayer
public struct AppUI {
public static let shared = AppUI()
fileprivate let appUIObjC = AppUIObjC.shared()
public func configure(layer: CAMetalLayer, with size: CGSize) {

26
src/ios/BootOSView.swift Normal file
View file

@ -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")
}
}
}
}

View file

@ -17,8 +17,33 @@ add_executable(eden-ios
EmulationWindow.mm
VMA.cpp
PomeloApp.swift
EnableJIT.swift
EmulationGame.swift
JoystickView.swift
CoreSettingsView.swift
ContentView.swift
EmulationHandler.swift
DetectServer.swift
NavView.swift
PomeloApp.swift
SettingsView.swift
FileManager.swift
EmulationView.swift
LibraryView.swift
GameButtonListView.swift
KeyboardHostingController.swift
MetalView.swift
BootOSView.swift
ControllerView.swift
AppUI.swift
InfoView.swift
FolderMonitor.swift
AdvancedSettingsView.swift
GameButtonView.swift
AppIconProvider.swift
Haptics.swift
EmulationScreenView.swift
GameListView.swift
)
set(MACOSX_BUNDLE_GUI_IDENTIFIER "dev.eden-emu.eden")

View file

@ -4,16 +4,12 @@
// SPDX-License-Identifier: GPL-3.0-or-later
import SwiftUI
//import AppUI
import AppUI
struct ContentView: View {
// @State var core = Core(games: [], root: FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0])
@State var core = Core(games: [], root: FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0])
var body: some View {
// HomeView(core: core).onAppear() {
// Air.play(AnyView(
// Text("Select Game").font(.system(size: 100))
// ))
// // rest of death
// }
HomeView(core: core).onAppear() {
}
}
}

View file

@ -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 ""
}
}
}

View file

@ -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)")
}
}
}

View file

@ -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
}

View file

@ -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
}
}

View file

@ -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
}
}
}

View file

@ -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)
}
}

137
src/ios/EmulationView.swift Normal file
View file

@ -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()
}
}

52
src/ios/EnableJIT.swift Normal file
View file

@ -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()
}

254
src/ios/FileManager.swift Normal file
View file

@ -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
}
}

View file

@ -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()
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}
}
}
}

140
src/ios/GameListView.swift Normal file
View file

@ -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
}
}
}

23
src/ios/Haptics.swift Normal file
View file

@ -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)
}
}

46
src/ios/InfoView.swift Normal file
View file

@ -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
}
}

View file

@ -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)
}
}
}
}
}

View file

@ -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(""))
}
}

191
src/ios/LibraryView.swift Normal file
View file

@ -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
}

23
src/ios/MetalView.swift Normal file
View file

@ -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) {
//
}
}

26
src/ios/NavView.swift Normal file
View file

@ -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)
}
}
}

View file

@ -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 {
}
}
}