mirror of
https://git.eden-emu.dev/eden-emu/eden
synced 2026-04-10 05:28:56 +02:00
pomelo scraps
This commit is contained in:
parent
8c3be1098c
commit
5396913cc5
29 changed files with 2258 additions and 11 deletions
77
src/ios/AdvancedSettingsView.swift
Normal file
77
src/ios/AdvancedSettingsView.swift
Normal 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
0
src/ios/Air.swift
Normal file
0
src/ios/AirPlay.swift
Normal file
0
src/ios/AirPlay.swift
Normal file
19
src/ios/AppIconProvider.swift
Normal file
19
src/ios/AppIconProvider.swift
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
26
src/ios/BootOSView.swift
Normal 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
420
src/ios/ControllerView.swift
Normal file
420
src/ios/ControllerView.swift
Normal 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 ""
|
||||
}
|
||||
}
|
||||
}
|
||||
88
src/ios/CoreSettingsView.swift
Normal file
88
src/ios/CoreSettingsView.swift
Normal 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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
26
src/ios/DetectServer.swift
Normal file
26
src/ios/DetectServer.swift
Normal 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
|
||||
}
|
||||
31
src/ios/EmulationGame.swift
Normal file
31
src/ios/EmulationGame.swift
Normal 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
|
||||
}
|
||||
}
|
||||
96
src/ios/EmulationHandler.swift
Normal file
96
src/ios/EmulationHandler.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
133
src/ios/EmulationScreenView.swift
Normal file
133
src/ios/EmulationScreenView.swift
Normal 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
137
src/ios/EmulationView.swift
Normal 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
52
src/ios/EnableJIT.swift
Normal 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
254
src/ios/FileManager.swift
Normal 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
|
||||
}
|
||||
}
|
||||
49
src/ios/FolderMonitor.swift
Normal file
49
src/ios/FolderMonitor.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
40
src/ios/GameButtonListView.swift
Normal file
40
src/ios/GameButtonListView.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
182
src/ios/GameButtonView.swift
Normal file
182
src/ios/GameButtonView.swift
Normal 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
140
src/ios/GameListView.swift
Normal 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
23
src/ios/Haptics.swift
Normal 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
46
src/ios/InfoView.swift
Normal 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
|
||||
}
|
||||
}
|
||||
55
src/ios/JoystickView.swift
Normal file
55
src/ios/JoystickView.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
76
src/ios/KeyboardHostingController.swift
Normal file
76
src/ios/KeyboardHostingController.swift
Normal 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
191
src/ios/LibraryView.swift
Normal 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
23
src/ios/MetalView.swift
Normal 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
26
src/ios/NavView.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/ios/SettingsView.swift
Normal file
18
src/ios/SettingsView.swift
Normal 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 {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue