diff --git a/src/MeloNX/MeloNX.xcodeproj/project.pbxproj b/src/MeloNX/MeloNX.xcodeproj/project.pbxproj
index 7b86e1b25..012e89633 100644
--- a/src/MeloNX/MeloNX.xcodeproj/project.pbxproj
+++ b/src/MeloNX/MeloNX.xcodeproj/project.pbxproj
@@ -618,7 +618,7 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "";
- DEVELOPMENT_TEAM = 95J8WZ4TN8;
+ DEVELOPMENT_TEAM = 4TD3JXVDW7;
ENABLE_PREVIEWS = YES;
ENABLE_TESTABILITY = NO;
FRAMEWORK_SEARCH_PATHS = (
@@ -626,6 +626,8 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/XCFrameworks",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
);
GCC_OPTIMIZATION_LEVEL = fast;
GENERATE_INFOPLIST_FILE = YES;
@@ -633,11 +635,13 @@
INFOPLIST_KEY_GCSupportsGameMode = YES;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.games";
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
+ INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "MeloNX needs access to your Photo Library in order to save images";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UIRequiresFullScreen = YES;
- INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+ INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait";
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportsDocumentBrowser = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
@@ -651,9 +655,13 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
);
MARKETING_VERSION = 0.0.8;
- PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
+ PRODUCT_BUNDLE_IDENTIFIER = xyz.belladev.MeloNX;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "MeloNX/App/Core/Headers/Ryujinx-Header.h";
@@ -671,7 +679,7 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "";
- DEVELOPMENT_TEAM = 95J8WZ4TN8;
+ DEVELOPMENT_TEAM = 4TD3JXVDW7;
ENABLE_PREVIEWS = YES;
ENABLE_TESTABILITY = YES;
FRAMEWORK_SEARCH_PATHS = (
@@ -679,6 +687,8 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/XCFrameworks",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
);
GCC_OPTIMIZATION_LEVEL = fast;
GENERATE_INFOPLIST_FILE = YES;
@@ -686,11 +696,13 @@
INFOPLIST_KEY_GCSupportsGameMode = YES;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.games";
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
+ INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "MeloNX needs access to your Photo Library in order to save images";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UIRequiresFullScreen = YES;
- INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+ INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait";
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportsDocumentBrowser = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
@@ -704,9 +716,13 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
);
MARKETING_VERSION = 0.0.8;
- PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
+ PRODUCT_BUNDLE_IDENTIFIER = xyz.belladev.MeloNX;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "MeloNX/App/Core/Headers/Ryujinx-Header.h";
diff --git a/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/benlawrence.xcuserdatad/UserInterfaceState.xcuserstate b/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/benlawrence.xcuserdatad/UserInterfaceState.xcuserstate
new file mode 100644
index 000000000..d95c76d65
Binary files /dev/null and b/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/benlawrence.xcuserdatad/UserInterfaceState.xcuserstate differ
diff --git a/src/MeloNX/MeloNX.xcodeproj/xcuserdata/benlawrence.xcuserdatad/xcschemes/xcschememanagement.plist b/src/MeloNX/MeloNX.xcodeproj/xcuserdata/benlawrence.xcuserdatad/xcschemes/xcschememanagement.plist
new file mode 100644
index 000000000..1b8118340
--- /dev/null
+++ b/src/MeloNX/MeloNX.xcodeproj/xcuserdata/benlawrence.xcuserdatad/xcschemes/xcschememanagement.plist
@@ -0,0 +1,24 @@
+
+
+
+
+ SchemeUserState
+
+ MeloNX.xcscheme_^#shared#^_
+
+ orderHint
+ 0
+
+ Ryujinx.xcscheme_^#shared#^_
+
+ orderHint
+ 1
+
+ com.Stossy11.MeloNX.RyujinxAg.xcscheme_^#shared#^_
+
+ orderHint
+ 2
+
+
+
+
diff --git a/src/MeloNX/MeloNX/App/Models/Game.swift b/src/MeloNX/MeloNX/App/Models/Game.swift
index de6acc5c0..ee13dd3f6 100644
--- a/src/MeloNX/MeloNX/App/Models/Game.swift
+++ b/src/MeloNX/MeloNX/App/Models/Game.swift
@@ -13,7 +13,6 @@ public struct Game: Identifiable, Equatable {
var containerFolder: URL
var fileType: UTType
-
var fileURL: URL
var titleName: String
@@ -59,12 +58,8 @@ public struct Game: Identifiable, Equatable {
gameTemp.icon = UIImage(data: imageData)
} else {
print("Invalid image size.")
-
}
-
-
return gameTemp
-
}
func createImage(from gameInfo: GameInfo) -> UIImage? {
@@ -82,7 +77,6 @@ public struct Game: Identifiable, Equatable {
let imageData = Data(bytes: gameInfoValue.ImageData, count: imageSize)
// Create a UIImage (or NSImage on macOS)
-
print(imageData)
return UIImage(data: imageData)
diff --git a/src/MeloNX/MeloNX/App/Views/GamesList/GameInfoSheet.swift b/src/MeloNX/MeloNX/App/Views/GamesList/GameInfoSheet.swift
new file mode 100644
index 000000000..45dea7551
--- /dev/null
+++ b/src/MeloNX/MeloNX/App/Views/GamesList/GameInfoSheet.swift
@@ -0,0 +1,104 @@
+//
+// GameInfoSheet.swift
+// MeloNX
+//
+// Created by Bella on 08/02/2025.
+//
+
+import SwiftUI
+
+struct GameInfoSheet: View {
+ let game: Game
+
+ @Environment(\.dismiss) var dismiss
+
+ var body: some View {
+ iOSNav {
+ VStack {
+ if let icon = game.icon {
+ Image(uiImage: icon)
+ .resizable()
+ .aspectRatio(contentMode: .fit)
+ .frame(width: 250, height: 250)
+ .cornerRadius(10)
+ .padding()
+ .contextMenu {
+ Button {
+ UIImageWriteToSavedPhotosAlbum(icon, nil, nil, nil)
+ } label: {
+ Label("Save to Photos", systemImage: "square.and.arrow.down")
+ }
+ }
+ } else {
+ Image(systemName: "questionmark.circle")
+ .resizable()
+ .aspectRatio(contentMode: .fit)
+ .frame(width: 150, height: 150)
+ .padding()
+ }
+
+ VStack(alignment: .leading) {
+ VStack(alignment: .leading) {
+ Text("**\(game.titleName)** | \(game.titleId.capitalized)")
+ Text(game.developer)
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ }
+ .padding(.vertical, 3)
+
+ VStack(alignment: .leading, spacing: 5) {
+ Text("Information")
+ .font(.title2)
+ .bold()
+
+ Text("**Version:** \(game.version)")
+ Text("**Game Size:** \(fetchFileSize(for: game.fileURL) ?? 0) bytes")
+ Text("**File Type:** .\(getFileType(game.fileURL))")
+ Text("**Game URL:** \(trimGameURL(game.fileURL))")
+ }
+ }
+
+ Spacer()
+ }
+ .padding(.horizontal, 5)
+ .navigationTitle(game.titleName)
+ .navigationBarTitleDisplayMode(.inline)
+ .toolbar {
+ ToolbarItem(placement: .navigationBarTrailing) {
+ Button("Done") {
+ dismiss()
+ }
+ }
+ }
+ }
+ }
+
+ func fetchFileSize(for gamePath: URL) -> UInt64? {
+ let fileManager = FileManager.default
+ do {
+ let attributes = try fileManager.attributesOfItem(atPath: gamePath.path)
+ if let size = attributes[FileAttributeKey.size] as? UInt64 {
+ return size
+ }
+ } catch {
+ print("Error getting file size: \(error)")
+ }
+ return nil
+ }
+
+ func trimGameURL(_ url: URL) -> String {
+ let path = url.path
+ if let range = path.range(of: "/roms/") {
+ return String(path[range.lowerBound...])
+ }
+ return path
+ }
+
+ func getFileType(_ url: URL) -> String {
+ let path = url.path
+ if let range = path.range(of: ".") {
+ return String(path[range.upperBound...])
+ }
+ return "Unknown"
+ }
+}
diff --git a/src/MeloNX/MeloNX/App/Views/GamesList/GameListView.swift b/src/MeloNX/MeloNX/App/Views/GamesList/GameListView.swift
index 8aac6be9a..6ab186c0d 100644
--- a/src/MeloNX/MeloNX/App/Views/GamesList/GameListView.swift
+++ b/src/MeloNX/MeloNX/App/Views/GamesList/GameListView.swift
@@ -8,6 +8,10 @@
import SwiftUI
import UniformTypeIdentifiers
+extension UTType {
+ static let nsp = UTType(exportedAs: "com.nintendo.switch-package")
+ static let xci = UTType(exportedAs: "com.nintendo.switch-cartridge")
+}
struct GameLibraryView: View {
@Binding var startemu: Game?
@@ -22,6 +26,9 @@ struct GameLibraryView: View {
@State var firmwareversion = "0"
@State var isImporting: Bool = false
@State var startgame = false
+ @State var isSelectingGameFile = false
+ @State var isViewingGameInfo: Bool = false
+ @State var gameInfo: Game?
var filteredGames: [Game] {
@@ -88,7 +95,7 @@ struct GameLibraryView: View {
LazyVStack(spacing: 2) {
ForEach(filteredGames) { game in
- GameListRow(game: game, startemu: $startemu)
+ GameListRow(game: game, startemu: $startemu, games: $games, isViewingGameInfo: $isViewingGameInfo, gameInfo: $gameInfo)
.onTapGesture {
addToRecentGames(game)
}
@@ -98,7 +105,7 @@ struct GameLibraryView: View {
} else {
LazyVStack(spacing: 2) {
ForEach(filteredGames) { game in
- GameListRow(game: game, startemu: $startemu)
+ GameListRow(game: game, startemu: $startemu, games: $games, isViewingGameInfo: $isViewingGameInfo, gameInfo: $gameInfo)
.onTapGesture {
addToRecentGames(game)
}
@@ -111,17 +118,13 @@ struct GameLibraryView: View {
loadGames()
loadRecentGames()
-
let firmware = Ryujinx.shared.fetchFirmwareVersion()
firmwareversion = (firmware == "" ? "0" : firmware)
}
.fileImporter(isPresented: $firmwareInstaller, allowedContentTypes: [.item]) { result in
switch result {
-
case .success(let url):
-
do {
-
let fun = url.startAccessingSecurityScopedResource()
let path = url.path
@@ -132,16 +135,22 @@ struct GameLibraryView: View {
url.stopAccessingSecurityScopedResource()
}
}
-
case .failure(let error):
print(error)
}
}
}
.toolbar {
+ ToolbarItem(placement: .topBarLeading) {
+ Button {
+ isSelectingGameFile.toggle()
+ } label: {
+ Image(systemName: "plus")
+ }
+ }
+
ToolbarItem(placement: .topBarLeading) {
Menu {
-
Text("Firmware Version: \(firmwareversion)")
.tint(.white)
@@ -164,7 +173,6 @@ struct GameLibraryView: View {
Text("Remove Firmware")
}
-
Button {
let game = Game(containerFolder: URL(string: "none")!, fileType: .item, fileURL: URL(string: "MiiMaker")!, titleName: "Mii Maker", titleId: "0", developer: "Nintendo", version: firmwareversion)
@@ -182,8 +190,6 @@ struct GameLibraryView: View {
}
}
-
-
Button {
let documentsUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let sharedurl = documentsUrl.absoluteString.replacingOccurrences(of: "file://", with: "shareddocuments://")
@@ -198,7 +204,6 @@ struct GameLibraryView: View {
Image(systemName: "ellipsis.circle")
.foregroundColor(.blue)
}
-
}
}
}
@@ -231,14 +236,53 @@ struct GameLibraryView: View {
} catch {
print(error)
}
-
case .failure(let err):
print("File import failed: \(err.localizedDescription)")
}
}
-
-
+ .fileImporter(isPresented: $isSelectingGameFile, allowedContentTypes: [.nsp, .xci, .zip, .folder]) { result in
+ switch result {
+ case .success(let url):
+ guard url.startAccessingSecurityScopedResource() else {
+ print("Failed to access security-scoped resource")
+ return
+ }
+ defer { url.stopAccessingSecurityScopedResource() }
+
+ do {
+ let fileManager = FileManager.default
+ let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
+ let romsDirectory = documentsDirectory.appendingPathComponent("roms")
+
+ if !fileManager.fileExists(atPath: romsDirectory.path) {
+ try fileManager.createDirectory(at: romsDirectory, withIntermediateDirectories: true, attributes: nil)
+ }
+
+ let destinationURL = romsDirectory.appendingPathComponent(url.lastPathComponent)
+ try fileManager.copyItem(at: url, to: destinationURL)
+
+ loadGames()
+ } catch {
+ print("Error copying game file: \(error)")
+ }
+ case .failure(let err):
+ print("File import failed: \(err.localizedDescription)")
+ }
+ }
+ .sheet(isPresented: Binding(
+ get: { isViewingGameInfo && gameInfo != nil },
+ set: { newValue in
+ if !newValue {
+ isViewingGameInfo = false
+ gameInfo = nil
+ }
+ }
+ )) {
+ if let game = gameInfo {
+ GameInfoSheet(game: game)
+ }
+ }
}
@@ -274,14 +318,15 @@ struct GameLibraryView: View {
}
}
- private func loadGames() {
+// MARK: - loads games from roms
+ func loadGames() {
let fileManager = FileManager.default
guard let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else { return }
let romsDirectory = documentsDirectory.appendingPathComponent("roms")
// Check if "roms" folder exists; if not, create it
- if !fileManager.fileExists(atPath: romsDirectory.path) {
+ if (!fileManager.fileExists(atPath: romsDirectory.path)) {
do {
try fileManager.createDirectory(at: romsDirectory, withIntermediateDirectories: true, attributes: nil)
} catch {
@@ -314,8 +359,21 @@ struct GameLibraryView: View {
print("Error loading games from roms folder: \(error)")
}
}
+
+// MARK: - Delete Game Function
+ func deleteGame(game: Game) {
+ let fileManager = FileManager.default
+ do {
+ try fileManager.removeItem(at: game.fileURL)
+ games.removeAll { $0.id == game.id }
+ loadGames()
+ } catch {
+ print("Error deleting game: \(error)")
+ }
+ }
}
+// MARK: -Game Model
extension Game: Codable {
enum CodingKeys: String, CodingKey {
case titleName, titleId, developer, version, fileURL
@@ -344,6 +402,7 @@ extension Game: Codable {
}
}
+// MARK: -Recent Game Card
struct RecentGameCard: View {
let game: Game
@Binding var startemu: Game?
@@ -390,9 +449,15 @@ struct RecentGameCard: View {
}
}
+// MARK: -Game List Item
struct GameListRow: View {
let game: Game
@Binding var startemu: Game?
+ @Binding var games: [Game] // Add this binding
+ @Binding var isViewingGameInfo: Bool
+ @Binding var gameInfo: Game?
+ @State var gametoDelete: Game?
+ @State var showGameDeleteConfirmation: Bool = false
@Environment(\.colorScheme) var colorScheme
var body: some View {
@@ -442,20 +507,52 @@ struct GameListRow: View {
.padding(.vertical, 8)
.background(Color(.systemBackground))
.contextMenu {
- Button {
- startemu = game
- } label: {
- Label("Play Now", systemImage: "play.fill")
+ Section {
+ Button {
+ startemu = game
+ } label: {
+ Label("Play Now", systemImage: "play.fill")
+ }
+
+ Button {
+ gameInfo = game
+ isViewingGameInfo.toggle()
+ } label: {
+ Label("Game Info", systemImage: "info.circle")
+ }
}
- Button {
- let pasteboard = UIPasteboard.general
- pasteboard.string = game.titleId
- } label: {
- Label("Game ID: \(game.titleId)", systemImage: "info.circle")
+ Section {
+ Button(role: .destructive) {
+ gametoDelete = game
+ showGameDeleteConfirmation.toggle()
+ } label: {
+ Label("Delete", systemImage: "trash")
+ }
}
}
}
.buttonStyle(.plain)
+ .confirmationDialog("Are you sure you want to delete this game?", isPresented: $showGameDeleteConfirmation) {
+ Button("Delete", role: .destructive) {
+ if let game = gametoDelete {
+ deleteGame(game: game)
+ }
+ }
+ Button("Cancel", role: .cancel) {}
+ } message: {
+ Text("Are you sure you want to delete \(gametoDelete?.titleName ?? "this game")?")
+ }
+ }
+
+ private func deleteGame(game: Game) {
+ let fileManager = FileManager.default
+ do {
+ try fileManager.removeItem(at: game.fileURL)
+ games.removeAll { $0.id == game.id }
+ } catch {
+ print("Error deleting game: \(error)")
+ }
}
}
+
diff --git a/src/MeloNX/MeloNX/Info.plist b/src/MeloNX/MeloNX/Info.plist
index 1a90a55eb..2080cee6f 100644
--- a/src/MeloNX/MeloNX/Info.plist
+++ b/src/MeloNX/MeloNX/Info.plist
@@ -6,5 +6,48 @@
1d0e26921bac938456ee7210ff4f2fa701dc16c02de1760e0aa757db28818ec7
UIFileSharingEnabled
+ UTExportedTypeDeclarations
+
+
+ UTTypeIdentifier
+ com.nintendo.switch-package
+ UTTypeDescription
+ Nintendo Switch Package
+ UTTypeConformsTo
+
+ public.data
+ public.archive
+
+ UTTypeTagSpecification
+
+ public.filename-extension
+
+ nsp
+
+ public.mime-type
+ application/x-nsp
+
+
+
+ UTTypeIdentifier
+ com.nintendo.switch-cartridge
+ UTTypeDescription
+ Nintendo Switch Cartridge
+ UTTypeConformsTo
+
+ public.data
+ public.archive
+
+ UTTypeTagSpecification
+
+ public.filename-extension
+
+ xci
+
+ public.mime-type
+ application/x-xci
+
+
+