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