forked from MeloNX/MeloNX
New UI Interface
This commit is contained in:
parent
fdbcc483b3
commit
e81ee8f8bf
@ -526,6 +526,7 @@
|
|||||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||||
INFOPLIST_KEY_UIRequiresFullScreen = YES;
|
INFOPLIST_KEY_UIRequiresFullScreen = YES;
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait";
|
||||||
INFOPLIST_KEY_UISupportsDocumentBrowser = YES;
|
INFOPLIST_KEY_UISupportsDocumentBrowser = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@ -668,6 +669,7 @@
|
|||||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||||
INFOPLIST_KEY_UIRequiresFullScreen = YES;
|
INFOPLIST_KEY_UIRequiresFullScreen = YES;
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait";
|
||||||
INFOPLIST_KEY_UISupportsDocumentBrowser = YES;
|
INFOPLIST_KEY_UISupportsDocumentBrowser = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
Binary file not shown.
@ -123,13 +123,8 @@ class VirtualController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func thumbstickMoved(_ stick: ThumbstickType, x: Double, y: Double) {
|
func thumbstickMoved(_ stick: ThumbstickType, x: Double, y: Double) {
|
||||||
// Convert float values (-1.0 to 1.0) to SDL axis values (-32768 to 32767)
|
var scaleFactor = 32767.0 / 160
|
||||||
var scaleFactor = 32767.0
|
|
||||||
if UIDevice.current.systemName.contains("iPadOS") {
|
|
||||||
scaleFactor /= (160 * 1.2)
|
|
||||||
} else {
|
|
||||||
scaleFactor /= 160
|
|
||||||
}
|
|
||||||
let scaledX = Int16(min(32767.0, max(-32768.0, x * scaleFactor)))
|
let scaledX = Int16(min(32767.0, max(-32768.0, x * scaleFactor)))
|
||||||
let scaledY = Int16(min(32767.0, max(-32768.0, y * scaleFactor)))
|
let scaledY = Int16(min(32767.0, max(-32768.0, y * scaleFactor)))
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@ class Ryujinx {
|
|||||||
|
|
||||||
private init() {}
|
private init() {}
|
||||||
|
|
||||||
public struct Configuration : Codable {
|
public struct Configuration : Codable, Equatable {
|
||||||
var gamepath: String
|
var gamepath: String
|
||||||
var inputids: [String]
|
var inputids: [String]
|
||||||
var resscale: Float
|
var resscale: Float
|
||||||
|
24
src/MeloNX/MeloNX/Models/Game.swift
Normal file
24
src/MeloNX/MeloNX/Models/Game.swift
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
//
|
||||||
|
// GameInfo.swift
|
||||||
|
// MeloNX
|
||||||
|
//
|
||||||
|
// Created by Stossy11 on 9/12/2024.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import UniformTypeIdentifiers
|
||||||
|
|
||||||
|
public struct Game: Identifiable, Equatable {
|
||||||
|
public var id = UUID()
|
||||||
|
|
||||||
|
var containerFolder: URL
|
||||||
|
var fileType: UTType
|
||||||
|
|
||||||
|
var fileURL: URL
|
||||||
|
|
||||||
|
var titleName: String
|
||||||
|
var titleId: String
|
||||||
|
var developer: String
|
||||||
|
var version: String
|
||||||
|
var icon: Image?
|
||||||
|
}
|
@ -10,6 +10,7 @@ import SwiftUI
|
|||||||
import GameController
|
import GameController
|
||||||
import Darwin
|
import Darwin
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import MetalKit
|
||||||
// import SDL
|
// import SDL
|
||||||
|
|
||||||
struct MoltenVKSettings: Codable, Hashable {
|
struct MoltenVKSettings: Codable, Hashable {
|
||||||
@ -55,12 +56,10 @@ struct ContentView: View {
|
|||||||
|
|
||||||
// MARK: - Body
|
// MARK: - Body
|
||||||
var body: some View {
|
var body: some View {
|
||||||
iOSNav {
|
if let game {
|
||||||
if let game {
|
emulationView
|
||||||
emulationView
|
} else {
|
||||||
} else {
|
mainMenuView
|
||||||
mainMenuView
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,53 +72,10 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var mainMenuView: some View {
|
private var mainMenuView: some View {
|
||||||
HStack {
|
MainTabView(startemu: $game, config: $config, MVKconfig: $settings, controllersList: $controllersList, currentControllers: $currentControllers, onscreencontroller: $onscreencontroller)
|
||||||
GameListView(startemu: $game)
|
.onAppear() {
|
||||||
.onAppear {
|
refreshControllersList()
|
||||||
refreshControllersList()
|
|
||||||
}
|
|
||||||
|
|
||||||
settingsListView
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var settingsListView: some View {
|
|
||||||
List {
|
|
||||||
Section("Settings") {
|
|
||||||
NavigationLink("Config") {
|
|
||||||
SettingsView(config: $config, MoltenVKSettings: $settings)
|
|
||||||
.onAppear() {
|
|
||||||
virtualController?.disconnect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Section {
|
|
||||||
Button("Refresh", action: refreshControllersList)
|
|
||||||
ForEach(controllersList, id: \.self) { controller in
|
|
||||||
controllerRow(for: controller)
|
|
||||||
}
|
|
||||||
} header: {
|
|
||||||
Text("Controllers")
|
|
||||||
} footer: {
|
|
||||||
Text("If no controllers are selected, the keyboard will be used.")
|
|
||||||
.font(.footnote)
|
|
||||||
.foregroundColor(.gray)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func controllerRow(for controller: Controller) -> some View {
|
|
||||||
HStack {
|
|
||||||
Button(controller.name) {
|
|
||||||
toggleController(controller)
|
|
||||||
}
|
|
||||||
Spacer()
|
|
||||||
if currentControllers.contains(where: { $0.id == controller.id }) {
|
|
||||||
Image(systemName: "checkmark.circle.fill")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -155,36 +111,24 @@ struct ContentView: View {
|
|||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
// controllerCallback!()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func refreshControllersList() {
|
private func refreshControllersList() {
|
||||||
Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { _ in
|
controllersList = Ryujinx.shared.getConnectedControllers()
|
||||||
controllersList = Ryujinx.shared.getConnectedControllers()
|
|
||||||
|
if let onscreen = controllersList.first(where: { $0.name == Ryujinx.shared.virtualController.controllername }) {
|
||||||
if let onscreen = controllersList.first(where: { $0.name == Ryujinx.shared.virtualController.controllername }) {
|
self.onscreencontroller = onscreen
|
||||||
self.onscreencontroller = onscreen
|
|
||||||
}
|
|
||||||
|
|
||||||
controllersList.removeAll(where: { $0.id == "0"})
|
|
||||||
|
|
||||||
if controllersList.count > 2 {
|
|
||||||
let controller = controllersList[2]
|
|
||||||
currentControllers.append(controller)
|
|
||||||
} else if let controller = controllersList.first(where: { $0.id == onscreencontroller.id }), !controllersList.isEmpty {
|
|
||||||
currentControllers.append(controller)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
controllersList.removeAll(where: { $0.id == "0"})
|
||||||
private func toggleController(_ controller: Controller) {
|
|
||||||
if currentControllers.contains(where: { $0.id == controller.id }) {
|
if controllersList.count > 2 {
|
||||||
currentControllers.removeAll(where: { $0.id == controller.id })
|
let controller = controllersList[2]
|
||||||
} else {
|
currentControllers.append(controller)
|
||||||
|
} else if let controller = controllersList.first(where: { $0.id == onscreencontroller.id }), !controllersList.isEmpty {
|
||||||
currentControllers.append(controller)
|
currentControllers.append(controller)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func showAlert(title: String, message: String, showOk: Bool, completion: @escaping (Bool) -> Void) {
|
func showAlert(title: String, message: String, showOk: Bool, completion: @escaping (Bool) -> Void) {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
@ -64,12 +64,12 @@ struct ControllerView: View {
|
|||||||
// Spacer()
|
// Spacer()
|
||||||
VStack {
|
VStack {
|
||||||
// Spacer()
|
// Spacer()
|
||||||
ButtonView(button: .start) // Adding the + button
|
ButtonView(button: .back) // Adding the + button
|
||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
VStack {
|
VStack {
|
||||||
// Spacer()
|
// Spacer()
|
||||||
ButtonView(button: .back) // Adding the - button
|
ButtonView(button: .start) // Adding the - button
|
||||||
}
|
}
|
||||||
// Spacer()
|
// Spacer()
|
||||||
}
|
}
|
||||||
|
@ -5,41 +5,176 @@
|
|||||||
// Created by Stossy11 on 3/11/2024.
|
// Created by Stossy11 on 3/11/2024.
|
||||||
//
|
//
|
||||||
|
|
||||||
// MARK: - This will most likely not be used in prod
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import UniformTypeIdentifiers
|
import UniformTypeIdentifiers
|
||||||
|
|
||||||
public struct Game: Identifiable, Equatable {
|
|
||||||
public var id = UUID()
|
|
||||||
|
|
||||||
var containerFolder: URL
|
struct MainTabView: View {
|
||||||
var fileType: UTType
|
@Binding var startemu: URL?
|
||||||
|
@Binding var config: Ryujinx.Configuration
|
||||||
|
@Binding var MVKconfig: [MoltenVKSettings]
|
||||||
|
@Binding var controllersList: [Controller]
|
||||||
|
@Binding var currentControllers: [Controller]
|
||||||
|
|
||||||
var fileURL: URL
|
@Binding var onscreencontroller: Controller
|
||||||
|
|
||||||
var titleName: String
|
var body: some View {
|
||||||
var titleId: String
|
TabView {
|
||||||
var developer: String
|
GameLibraryView(startemu: $startemu)
|
||||||
var version: String
|
.tabItem {
|
||||||
var icon: Image?
|
Label("Games", systemImage: "gamecontroller.fill")
|
||||||
|
}
|
||||||
|
|
||||||
|
SelectControllerView(controllersList: $controllersList, currentControllers: $currentControllers, onscreencontroller: $onscreencontroller)
|
||||||
|
.tabItem {
|
||||||
|
Label("Controllers", systemImage: "gamecontroller.fill")
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsView(config: $config, MoltenVKSettings: $MVKconfig)
|
||||||
|
.tabItem {
|
||||||
|
Label("Settings", systemImage: "gear")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct GameListView: View {
|
struct GameLibraryView: View {
|
||||||
@Binding var startemu: URL?
|
@Binding var startemu: URL?
|
||||||
@State private var games: [Game] = []
|
@State private var games: [Game] = []
|
||||||
|
@State private var searchText = ""
|
||||||
|
@State private var isSearching = false
|
||||||
|
@AppStorage("recentGames") private var recentGamesData: Data = Data()
|
||||||
|
@State private var recentGames: [Game] = []
|
||||||
|
@Environment(\.colorScheme) var colorScheme
|
||||||
|
|
||||||
|
var filteredGames: [Game] {
|
||||||
|
if searchText.isEmpty {
|
||||||
|
return games
|
||||||
|
}
|
||||||
|
return games.filter {
|
||||||
|
$0.titleName.localizedCaseInsensitiveContains(searchText) ||
|
||||||
|
$0.developer.localizedCaseInsensitiveContains(searchText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List($games, id: \.id) { $game in
|
iOSNav {
|
||||||
Button {
|
ScrollView {
|
||||||
startemu = $game.wrappedValue.fileURL
|
LazyVStack(alignment: .leading, spacing: 20) {
|
||||||
} label: {
|
if !isSearching {
|
||||||
Text(game.titleName)
|
Text("Games")
|
||||||
|
.font(.system(size: 34, weight: .bold))
|
||||||
|
.padding(.horizontal)
|
||||||
|
.padding(.top, 12)
|
||||||
|
}
|
||||||
|
|
||||||
|
if games.isEmpty {
|
||||||
|
VStack(spacing: 16) {
|
||||||
|
Image(systemName: "gamecontroller.fill")
|
||||||
|
.font(.system(size: 64))
|
||||||
|
.foregroundColor(.secondary.opacity(0.7))
|
||||||
|
.padding(.top, 60)
|
||||||
|
Text("No Games Found")
|
||||||
|
.font(.title2.bold())
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
Text("Add ROM, Keys and Firmware to get started")
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.padding(.top, 40)
|
||||||
|
} else {
|
||||||
|
if !isSearching && !recentGames.isEmpty {
|
||||||
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
|
Text("Recent")
|
||||||
|
.font(.title2.bold())
|
||||||
|
.padding(.horizontal)
|
||||||
|
|
||||||
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
|
LazyHStack(spacing: 16) {
|
||||||
|
ForEach(recentGames) { game in
|
||||||
|
RecentGameCard(game: game, startemu: $startemu)
|
||||||
|
.onTapGesture {
|
||||||
|
addToRecentGames(game)
|
||||||
|
startemu = game.fileURL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
|
Text("All Games")
|
||||||
|
.font(.title2.bold())
|
||||||
|
.padding(.horizontal)
|
||||||
|
|
||||||
|
LazyVStack(spacing: 2) {
|
||||||
|
ForEach(filteredGames) { game in
|
||||||
|
GameListRow(game: game, startemu: $startemu)
|
||||||
|
.onTapGesture {
|
||||||
|
addToRecentGames(game)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LazyVStack(spacing: 2) {
|
||||||
|
ForEach(filteredGames) { game in
|
||||||
|
GameListRow(game: game, startemu: $startemu)
|
||||||
|
.onTapGesture {
|
||||||
|
addToRecentGames(game)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
loadGames()
|
||||||
|
loadRecentGames()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle("Games")
|
.background(Color(.systemGroupedBackground))
|
||||||
.onAppear(perform: loadGames)
|
.searchable(text: $searchText)
|
||||||
|
.onChange(of: searchText) { _ in
|
||||||
|
isSearching = !searchText.isEmpty
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func addToRecentGames(_ game: Game) {
|
||||||
|
recentGames.removeAll { $0.id == game.id }
|
||||||
|
|
||||||
|
recentGames.insert(game, at: 0)
|
||||||
|
|
||||||
|
if recentGames.count > 5 {
|
||||||
|
recentGames = Array(recentGames.prefix(5))
|
||||||
|
}
|
||||||
|
|
||||||
|
saveRecentGames()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func saveRecentGames() {
|
||||||
|
do {
|
||||||
|
let encoder = JSONEncoder()
|
||||||
|
let data = try encoder.encode(recentGames)
|
||||||
|
recentGamesData = data
|
||||||
|
} catch {
|
||||||
|
print("Error saving recent games: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadRecentGames() {
|
||||||
|
do {
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
recentGames = try decoder.decode([Game].self, from: recentGamesData)
|
||||||
|
} catch {
|
||||||
|
print("Error loading recent games: \(error)")
|
||||||
|
recentGames = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func loadGames() {
|
private func loadGames() {
|
||||||
let fileManager = FileManager.default
|
let fileManager = FileManager.default
|
||||||
guard let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else { return }
|
guard let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else { return }
|
||||||
@ -54,7 +189,7 @@ struct GameListView: View {
|
|||||||
print("Failed to create roms directory: \(error)")
|
print("Failed to create roms directory: \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
games = []
|
||||||
// Load games only from "roms" folder
|
// Load games only from "roms" folder
|
||||||
do {
|
do {
|
||||||
let files = try fileManager.contentsOfDirectory(at: romsDirectory, includingPropertiesForKeys: nil)
|
let files = try fileManager.contentsOfDirectory(at: romsDirectory, includingPropertiesForKeys: nil)
|
||||||
@ -98,3 +233,147 @@ struct GameListView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Make sure your Game model conforms to Codable
|
||||||
|
extension Game: Codable {
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case titleName, titleId, developer, version, fileURL
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
titleName = try container.decode(String.self, forKey: .titleName)
|
||||||
|
titleId = try container.decode(String.self, forKey: .titleId)
|
||||||
|
developer = try container.decode(String.self, forKey: .developer)
|
||||||
|
version = try container.decode(String.self, forKey: .version)
|
||||||
|
fileURL = try container.decode(URL.self, forKey: .fileURL)
|
||||||
|
|
||||||
|
// Initialize other properties
|
||||||
|
self.containerFolder = fileURL.deletingLastPathComponent()
|
||||||
|
self.fileType = .item
|
||||||
|
}
|
||||||
|
|
||||||
|
public func encode(to encoder: Encoder) throws {
|
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
|
try container.encode(titleName, forKey: .titleName)
|
||||||
|
try container.encode(titleId, forKey: .titleId)
|
||||||
|
try container.encode(developer, forKey: .developer)
|
||||||
|
try container.encode(version, forKey: .version)
|
||||||
|
try container.encode(fileURL, forKey: .fileURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RecentGameCard: View {
|
||||||
|
let game: Game
|
||||||
|
@Binding var startemu: URL?
|
||||||
|
@Environment(\.colorScheme) var colorScheme
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Button(action: {
|
||||||
|
startemu = game.fileURL
|
||||||
|
}) {
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
if let icon = game.icon {
|
||||||
|
icon
|
||||||
|
.resizable()
|
||||||
|
.aspectRatio(contentMode: .fill)
|
||||||
|
.frame(width: 140, height: 140)
|
||||||
|
.cornerRadius(12)
|
||||||
|
} else {
|
||||||
|
ZStack {
|
||||||
|
RoundedRectangle(cornerRadius: 12)
|
||||||
|
.fill(colorScheme == .dark ?
|
||||||
|
Color(.systemGray5) : Color(.systemGray6))
|
||||||
|
.frame(width: 140, height: 140)
|
||||||
|
|
||||||
|
Image(systemName: "gamecontroller.fill")
|
||||||
|
.font(.system(size: 40))
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
Text(game.titleName)
|
||||||
|
.font(.subheadline.bold())
|
||||||
|
.lineLimit(1)
|
||||||
|
|
||||||
|
Text(game.developer)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct GameListRow: View {
|
||||||
|
let game: Game
|
||||||
|
@Binding var startemu: URL?
|
||||||
|
@Environment(\.colorScheme) var colorScheme
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Button(action: {
|
||||||
|
startemu = game.fileURL
|
||||||
|
}) {
|
||||||
|
HStack(spacing: 16) {
|
||||||
|
// Game Icon
|
||||||
|
if let icon = game.icon {
|
||||||
|
icon
|
||||||
|
.resizable()
|
||||||
|
.aspectRatio(contentMode: .fill)
|
||||||
|
.frame(width: 45, height: 45)
|
||||||
|
.cornerRadius(8)
|
||||||
|
} else {
|
||||||
|
ZStack {
|
||||||
|
RoundedRectangle(cornerRadius: 8)
|
||||||
|
.fill(colorScheme == .dark ?
|
||||||
|
Color(.systemGray5) : Color(.systemGray6))
|
||||||
|
.frame(width: 45, height: 45)
|
||||||
|
|
||||||
|
Image(systemName: "gamecontroller.fill")
|
||||||
|
.font(.system(size: 20))
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Game Info
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
Text(game.titleName)
|
||||||
|
.font(.body)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
|
||||||
|
Text(game.developer)
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Image(systemName: "play.circle.fill")
|
||||||
|
.font(.title2)
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
.opacity(0.8)
|
||||||
|
}
|
||||||
|
.padding(.horizontal)
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
.background(Color(.systemBackground))
|
||||||
|
.contextMenu {
|
||||||
|
Button {
|
||||||
|
startemu = game.fileURL
|
||||||
|
} label: {
|
||||||
|
Label("Play Now", systemImage: "play.fill")
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
// Add info action
|
||||||
|
} label: {
|
||||||
|
Label("Game Info", systemImage: "info.circle")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,87 +0,0 @@
|
|||||||
//
|
|
||||||
// VulkanSDLView.swift
|
|
||||||
// MeloNX
|
|
||||||
//
|
|
||||||
// Created by Stossy11 on 3/11/2024.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
import MetalKit
|
|
||||||
|
|
||||||
/*
|
|
||||||
class SDLView: UIView {
|
|
||||||
var sdlwin: OpaquePointer?
|
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
|
||||||
super.init(frame: frame)
|
|
||||||
DispatchQueue.main.async { [self] in
|
|
||||||
makeSDLWindow()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
super.init(coder: coder)
|
|
||||||
DispatchQueue.main.async { [self] in
|
|
||||||
makeSDLWindow()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getWindowFlags() -> UInt32 {
|
|
||||||
return SDL_WINDOW_VULKAN.rawValue
|
|
||||||
}
|
|
||||||
|
|
||||||
private func makeSDLWindow() {
|
|
||||||
let width: Int32 = 1280 // Replace with the desired width
|
|
||||||
let height: Int32 = 720 // Replace with the desired height
|
|
||||||
|
|
||||||
let defaultFlags: UInt32 = SDL_WINDOW_SHOWN.rawValue
|
|
||||||
let fullscreenFlag: UInt32 = SDL_WINDOW_FULLSCREEN.rawValue // Or SDL_WINDOW_FULLSCREEN_DESKTOP if needed
|
|
||||||
|
|
||||||
// Create the SDL window
|
|
||||||
sdlwin = SDL_CreateWindow(
|
|
||||||
"Ryujinx",
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
defaultFlags | getWindowFlags() // | fullscreenFlag | getWindowFlags()
|
|
||||||
)
|
|
||||||
|
|
||||||
// Check if we successfully retrieved the SDL window
|
|
||||||
guard sdlwin != nil else {
|
|
||||||
print("Error creating SDL window: \(String(cString: SDL_GetError()))")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
print("SDL window created successfully.")
|
|
||||||
|
|
||||||
// Position SDL window over this UIView
|
|
||||||
self.syncSDLWindowPosition()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func syncSDLWindowPosition() {
|
|
||||||
guard let sdlwin = sdlwin else { return }
|
|
||||||
|
|
||||||
|
|
||||||
// Get the frame of the UIView in screen coordinates
|
|
||||||
let viewFrameInWindow = self.convert(self.bounds, to: nil)
|
|
||||||
|
|
||||||
// Set the SDL window position and size to match the UIView frame
|
|
||||||
SDL_SetWindowPosition(sdlwin, Int32(viewFrameInWindow.origin.x), Int32(viewFrameInWindow.origin.y))
|
|
||||||
SDL_SetWindowSize(sdlwin, Int32(viewFrameInWindow.width), Int32(viewFrameInWindow.height))
|
|
||||||
|
|
||||||
// Bring SDL window to the front
|
|
||||||
SDL_RaiseWindow(sdlwin)
|
|
||||||
|
|
||||||
print("SDL window positioned over SDLView.")
|
|
||||||
}
|
|
||||||
|
|
||||||
override func layoutSubviews() {
|
|
||||||
super.layoutSubviews()
|
|
||||||
// Adjust SDL window whenever layout changes
|
|
||||||
syncSDLWindowPosition()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
|||||||
//
|
|
||||||
// VulkanSDLViewRepresentable.swift
|
|
||||||
// MeloNX
|
|
||||||
//
|
|
||||||
// Created by Stossy11 on 3/11/2024.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
import SwiftUI
|
|
||||||
import GameController
|
|
||||||
/*
|
|
||||||
struct SDLViewRepresentable: UIViewRepresentable {
|
|
||||||
let configure: (UInt32) -> Void
|
|
||||||
func makeUIView(context: Context) -> SDLView {
|
|
||||||
// Configure (start ryu) before initialsing SDLView so SDLView can get the SDL_Window from Ryu
|
|
||||||
let view = SDLView(frame: .zero)
|
|
||||||
configure(SDL_GetWindowID(view.sdlwin))
|
|
||||||
return view
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateUIView(_ uiView: SDLView, context: Context) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
@ -0,0 +1,53 @@
|
|||||||
|
//
|
||||||
|
// SelectControllerView.swift
|
||||||
|
// MeloNX
|
||||||
|
//
|
||||||
|
// Created by Stossy11 on 9/12/2024.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct SelectControllerView: View {
|
||||||
|
|
||||||
|
@Binding var controllersList: [Controller]
|
||||||
|
@Binding var currentControllers: [Controller]
|
||||||
|
|
||||||
|
@Binding var onscreencontroller: Controller
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
List {
|
||||||
|
|
||||||
|
Section {
|
||||||
|
ForEach(controllersList, id: \.self) { controller in
|
||||||
|
controllerRow(for: controller)
|
||||||
|
}
|
||||||
|
} footer: {
|
||||||
|
Text("If no controllers are selected, the keyboard will be used.")
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private func controllerRow(for controller: Controller) -> some View {
|
||||||
|
HStack {
|
||||||
|
Button(controller.name) {
|
||||||
|
toggleController(controller)
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
if currentControllers.contains(where: { $0.id == controller.id }) {
|
||||||
|
Image(systemName: "checkmark.circle.fill")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func toggleController(_ controller: Controller) {
|
||||||
|
if currentControllers.contains(where: { $0.id == controller.id }) {
|
||||||
|
currentControllers.removeAll(where: { $0.id == controller.id })
|
||||||
|
} else {
|
||||||
|
currentControllers.append(controller)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -44,7 +44,6 @@ struct SettingsView: View {
|
|||||||
|
|
||||||
Section(header: Title("Input Settings")) {
|
Section(header: Title("Input Settings")) {
|
||||||
Toggle("List Input IDs", isOn: $config.listinputids)
|
Toggle("List Input IDs", isOn: $config.listinputids)
|
||||||
Toggle("Nintendo Controller Layout", isOn: $config.nintendoinput)
|
|
||||||
Toggle("Ryujinx Demo On-Screen Controller", isOn: $ryuDemo)
|
Toggle("Ryujinx Demo On-Screen Controller", isOn: $ryuDemo)
|
||||||
// Toggle("Host Mapped Memory", isOn: $config.hostMappedMemory)
|
// Toggle("Host Mapped Memory", isOn: $config.hostMappedMemory)
|
||||||
}
|
}
|
||||||
@ -89,10 +88,10 @@ struct SettingsView: View {
|
|||||||
print(configs)
|
print(configs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle("Settings")
|
.onChange(of: config) { newValue in
|
||||||
.navigationBarItems(trailing: Button("Save") {
|
print(newValue)
|
||||||
saveSettings()
|
saveSettings()
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveSettings() {
|
func saveSettings() {
|
||||||
|
@ -63,8 +63,16 @@ namespace Ryujinx.Common.Configuration
|
|||||||
{
|
{
|
||||||
appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||||
}
|
}
|
||||||
|
string userProfilePath;
|
||||||
|
if (OperatingSystem.IsIOS())
|
||||||
|
{
|
||||||
|
userProfilePath = appDataPath;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
userProfilePath = Path.Combine(appDataPath, DefaultBaseDir);
|
||||||
|
}
|
||||||
|
|
||||||
string userProfilePath = Path.Combine(appDataPath, DefaultBaseDir);
|
|
||||||
string portablePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, DefaultPortableDir);
|
string portablePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, DefaultPortableDir);
|
||||||
|
|
||||||
if (Directory.Exists(portablePath))
|
if (Directory.Exists(portablePath))
|
||||||
|
@ -116,10 +116,10 @@ private void CreateFonts(string uiThemeFontFamily)
|
|||||||
if (OperatingSystem.IsIOS())
|
if (OperatingSystem.IsIOS())
|
||||||
{
|
{
|
||||||
availableFonts = new string[] {
|
availableFonts = new string[] {
|
||||||
"Chalkboard",
|
"SF Pro",
|
||||||
"Chalkboard", // San Francisco is the default font on iOS
|
"New York",
|
||||||
"Chalkboard", // Legacy iOS font
|
"Helvetica Neue",
|
||||||
"Chalkboard" // Common system font
|
"Avenir"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
@ -958,7 +958,6 @@ namespace Ryujinx.Headless.SDL2
|
|||||||
|
|
||||||
static void Load(Options option)
|
static void Load(Options option)
|
||||||
{
|
{
|
||||||
AppDataManager.Initialize(option.BaseDataDir);
|
|
||||||
|
|
||||||
if (_virtualFileSystem == null)
|
if (_virtualFileSystem == null)
|
||||||
{
|
{
|
||||||
|
Loading…
x
Reference in New Issue
Block a user