New UI Interface

This commit is contained in:
Stossy11 2024-12-09 18:17:58 +11:00
parent fdbcc483b3
commit e81ee8f8bf
15 changed files with 421 additions and 233 deletions

View File

@ -526,6 +526,7 @@
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UIRequiresFullScreen = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait";
INFOPLIST_KEY_UISupportsDocumentBrowser = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
@ -668,6 +669,7 @@
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UIRequiresFullScreen = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait";
INFOPLIST_KEY_UISupportsDocumentBrowser = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (

View File

@ -123,13 +123,8 @@ class VirtualController {
}
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
if UIDevice.current.systemName.contains("iPadOS") {
scaleFactor /= (160 * 1.2)
} else {
scaleFactor /= 160
}
var scaleFactor = 32767.0 / 160
let scaledX = Int16(min(32767.0, max(-32768.0, x * scaleFactor)))
let scaledY = Int16(min(32767.0, max(-32768.0, y * scaleFactor)))

View File

@ -39,7 +39,7 @@ class Ryujinx {
private init() {}
public struct Configuration : Codable {
public struct Configuration : Codable, Equatable {
var gamepath: String
var inputids: [String]
var resscale: Float

View 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?
}

View File

@ -10,6 +10,7 @@ import SwiftUI
import GameController
import Darwin
import UIKit
import MetalKit
// import SDL
struct MoltenVKSettings: Codable, Hashable {
@ -55,14 +56,12 @@ struct ContentView: View {
// MARK: - Body
var body: some View {
iOSNav {
if let game {
emulationView
} else {
mainMenuView
}
}
}
// MARK: - View Components
private var emulationView: some View {
@ -73,52 +72,9 @@ struct ContentView: View {
}
private var mainMenuView: some View {
HStack {
GameListView(startemu: $game)
.onAppear {
refreshControllersList()
}
settingsListView
}
}
private var settingsListView: some View {
List {
Section("Settings") {
NavigationLink("Config") {
SettingsView(config: $config, MoltenVKSettings: $settings)
MainTabView(startemu: $game, config: $config, MVKconfig: $settings, controllersList: $controllersList, currentControllers: $currentControllers, onscreencontroller: $onscreencontroller)
.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")
}
refreshControllersList()
}
}
@ -155,11 +111,9 @@ struct ContentView: View {
}
// controllerCallback!()
}
private func refreshControllersList() {
Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { _ in
controllersList = Ryujinx.shared.getConnectedControllers()
if let onscreen = controllersList.first(where: { $0.name == Ryujinx.shared.virtualController.controllername }) {
@ -175,16 +129,6 @@ struct ContentView: View {
currentControllers.append(controller)
}
}
}
private func toggleController(_ controller: Controller) {
if currentControllers.contains(where: { $0.id == controller.id }) {
currentControllers.removeAll(where: { $0.id == controller.id })
} else {
currentControllers.append(controller)
}
}
func showAlert(title: String, message: String, showOk: Bool, completion: @escaping (Bool) -> Void) {
DispatchQueue.main.async {

View File

@ -64,12 +64,12 @@ struct ControllerView: View {
// Spacer()
VStack {
// Spacer()
ButtonView(button: .start) // Adding the + button
ButtonView(button: .back) // Adding the + button
}
Spacer()
VStack {
// Spacer()
ButtonView(button: .back) // Adding the - button
ButtonView(button: .start) // Adding the - button
}
// Spacer()
}

View File

@ -5,39 +5,174 @@
// Created by Stossy11 on 3/11/2024.
//
// MARK: - This will most likely not be used in prod
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?
}
struct GameListView: View {
struct MainTabView: View {
@Binding var startemu: URL?
@State private var games: [Game] = []
@Binding var config: Ryujinx.Configuration
@Binding var MVKconfig: [MoltenVKSettings]
@Binding var controllersList: [Controller]
@Binding var currentControllers: [Controller]
@Binding var onscreencontroller: Controller
var body: some View {
List($games, id: \.id) { $game in
Button {
startemu = $game.wrappedValue.fileURL
} label: {
Text(game.titleName)
TabView {
GameLibraryView(startemu: $startemu)
.tabItem {
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")
}
}
.navigationTitle("Games")
.onAppear(perform: loadGames)
}
}
struct GameLibraryView: View {
@Binding var startemu: URL?
@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 {
iOSNav {
ScrollView {
LazyVStack(alignment: .leading, spacing: 20) {
if !isSearching {
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()
}
}
}
.background(Color(.systemGroupedBackground))
.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() {
@ -54,7 +189,7 @@ struct GameListView: View {
print("Failed to create roms directory: \(error)")
}
}
games = []
// Load games only from "roms" folder
do {
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)
}
}

View File

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

View File

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

View File

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

View File

@ -44,7 +44,6 @@ struct SettingsView: View {
Section(header: Title("Input Settings")) {
Toggle("List Input IDs", isOn: $config.listinputids)
Toggle("Nintendo Controller Layout", isOn: $config.nintendoinput)
Toggle("Ryujinx Demo On-Screen Controller", isOn: $ryuDemo)
// Toggle("Host Mapped Memory", isOn: $config.hostMappedMemory)
}
@ -89,10 +88,10 @@ struct SettingsView: View {
print(configs)
}
}
.navigationTitle("Settings")
.navigationBarItems(trailing: Button("Save") {
.onChange(of: config) { newValue in
print(newValue)
saveSettings()
})
}
}
func saveSettings() {

View File

@ -63,8 +63,16 @@ namespace Ryujinx.Common.Configuration
{
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);
if (Directory.Exists(portablePath))

View File

@ -116,10 +116,10 @@ private void CreateFonts(string uiThemeFontFamily)
if (OperatingSystem.IsIOS())
{
availableFonts = new string[] {
"Chalkboard",
"Chalkboard", // San Francisco is the default font on iOS
"Chalkboard", // Legacy iOS font
"Chalkboard" // Common system font
"SF Pro",
"New York",
"Helvetica Neue",
"Avenir"
};
}
else

View File

@ -958,7 +958,6 @@ namespace Ryujinx.Headless.SDL2
static void Load(Options option)
{
AppDataManager.Initialize(option.BaseDataDir);
if (_virtualFileSystem == null)
{