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 + + +