Add games, delete games and game info view

This commit is contained in:
Bella 2025-02-09 00:15:37 +13:00
parent 9c014e6f87
commit a2c3f6d624
No known key found for this signature in database
GPG Key ID: 725FECA79EF56B97
7 changed files with 316 additions and 38 deletions

View File

@ -618,7 +618,7 @@
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = 95J8WZ4TN8; DEVELOPMENT_TEAM = 4TD3JXVDW7;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
ENABLE_TESTABILITY = NO; ENABLE_TESTABILITY = NO;
FRAMEWORK_SEARCH_PATHS = ( FRAMEWORK_SEARCH_PATHS = (
@ -626,6 +626,8 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/XCFrameworks", "$(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",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
); );
GCC_OPTIMIZATION_LEVEL = fast; GCC_OPTIMIZATION_LEVEL = fast;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@ -633,11 +635,13 @@
INFOPLIST_KEY_GCSupportsGameMode = YES; INFOPLIST_KEY_GCSupportsGameMode = YES;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.games"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.games";
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; 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_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
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 UIInterfaceOrientationPortrait";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportsDocumentBrowser = YES; INFOPLIST_KEY_UISupportsDocumentBrowser = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0; IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = ( 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",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
); );
MARKETING_VERSION = 0.0.8; MARKETING_VERSION = 0.0.8;
PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX; PRODUCT_BUNDLE_IDENTIFIER = xyz.belladev.MeloNX;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "MeloNX/App/Core/Headers/Ryujinx-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "MeloNX/App/Core/Headers/Ryujinx-Header.h";
@ -671,7 +679,7 @@
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = 95J8WZ4TN8; DEVELOPMENT_TEAM = 4TD3JXVDW7;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
ENABLE_TESTABILITY = YES; ENABLE_TESTABILITY = YES;
FRAMEWORK_SEARCH_PATHS = ( FRAMEWORK_SEARCH_PATHS = (
@ -679,6 +687,8 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/XCFrameworks", "$(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",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
); );
GCC_OPTIMIZATION_LEVEL = fast; GCC_OPTIMIZATION_LEVEL = fast;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@ -686,11 +696,13 @@
INFOPLIST_KEY_GCSupportsGameMode = YES; INFOPLIST_KEY_GCSupportsGameMode = YES;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.games"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.games";
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; 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_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
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 UIInterfaceOrientationPortrait";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportsDocumentBrowser = YES; INFOPLIST_KEY_UISupportsDocumentBrowser = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0; IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = ( 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",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
); );
MARKETING_VERSION = 0.0.8; MARKETING_VERSION = 0.0.8;
PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX; PRODUCT_BUNDLE_IDENTIFIER = xyz.belladev.MeloNX;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "MeloNX/App/Core/Headers/Ryujinx-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "MeloNX/App/Core/Headers/Ryujinx-Header.h";

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>SchemeUserState</key>
<dict>
<key>MeloNX.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>0</integer>
</dict>
<key>Ryujinx.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>1</integer>
</dict>
<key>com.Stossy11.MeloNX.RyujinxAg.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>2</integer>
</dict>
</dict>
</dict>
</plist>

View File

@ -13,7 +13,6 @@ public struct Game: Identifiable, Equatable {
var containerFolder: URL var containerFolder: URL
var fileType: UTType var fileType: UTType
var fileURL: URL var fileURL: URL
var titleName: String var titleName: String
@ -59,12 +58,8 @@ public struct Game: Identifiable, Equatable {
gameTemp.icon = UIImage(data: imageData) gameTemp.icon = UIImage(data: imageData)
} else { } else {
print("Invalid image size.") print("Invalid image size.")
} }
return gameTemp return gameTemp
} }
func createImage(from gameInfo: GameInfo) -> UIImage? { func createImage(from gameInfo: GameInfo) -> UIImage? {
@ -82,7 +77,6 @@ public struct Game: Identifiable, Equatable {
let imageData = Data(bytes: gameInfoValue.ImageData, count: imageSize) let imageData = Data(bytes: gameInfoValue.ImageData, count: imageSize)
// Create a UIImage (or NSImage on macOS) // Create a UIImage (or NSImage on macOS)
print(imageData) print(imageData)
return UIImage(data: imageData) return UIImage(data: imageData)

View File

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

View File

@ -8,6 +8,10 @@
import SwiftUI import SwiftUI
import UniformTypeIdentifiers 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 { struct GameLibraryView: View {
@Binding var startemu: Game? @Binding var startemu: Game?
@ -22,6 +26,9 @@ struct GameLibraryView: View {
@State var firmwareversion = "0" @State var firmwareversion = "0"
@State var isImporting: Bool = false @State var isImporting: Bool = false
@State var startgame = false @State var startgame = false
@State var isSelectingGameFile = false
@State var isViewingGameInfo: Bool = false
@State var gameInfo: Game?
var filteredGames: [Game] { var filteredGames: [Game] {
@ -88,7 +95,7 @@ struct GameLibraryView: View {
LazyVStack(spacing: 2) { LazyVStack(spacing: 2) {
ForEach(filteredGames) { game in ForEach(filteredGames) { game in
GameListRow(game: game, startemu: $startemu) GameListRow(game: game, startemu: $startemu, games: $games, isViewingGameInfo: $isViewingGameInfo, gameInfo: $gameInfo)
.onTapGesture { .onTapGesture {
addToRecentGames(game) addToRecentGames(game)
} }
@ -98,7 +105,7 @@ struct GameLibraryView: View {
} else { } else {
LazyVStack(spacing: 2) { LazyVStack(spacing: 2) {
ForEach(filteredGames) { game in ForEach(filteredGames) { game in
GameListRow(game: game, startemu: $startemu) GameListRow(game: game, startemu: $startemu, games: $games, isViewingGameInfo: $isViewingGameInfo, gameInfo: $gameInfo)
.onTapGesture { .onTapGesture {
addToRecentGames(game) addToRecentGames(game)
} }
@ -111,17 +118,13 @@ struct GameLibraryView: View {
loadGames() loadGames()
loadRecentGames() loadRecentGames()
let firmware = Ryujinx.shared.fetchFirmwareVersion() let firmware = Ryujinx.shared.fetchFirmwareVersion()
firmwareversion = (firmware == "" ? "0" : firmware) firmwareversion = (firmware == "" ? "0" : firmware)
} }
.fileImporter(isPresented: $firmwareInstaller, allowedContentTypes: [.item]) { result in .fileImporter(isPresented: $firmwareInstaller, allowedContentTypes: [.item]) { result in
switch result { switch result {
case .success(let url): case .success(let url):
do { do {
let fun = url.startAccessingSecurityScopedResource() let fun = url.startAccessingSecurityScopedResource()
let path = url.path let path = url.path
@ -132,16 +135,22 @@ struct GameLibraryView: View {
url.stopAccessingSecurityScopedResource() url.stopAccessingSecurityScopedResource()
} }
} }
case .failure(let error): case .failure(let error):
print(error) print(error)
} }
} }
} }
.toolbar { .toolbar {
ToolbarItem(placement: .topBarLeading) {
Button {
isSelectingGameFile.toggle()
} label: {
Image(systemName: "plus")
}
}
ToolbarItem(placement: .topBarLeading) { ToolbarItem(placement: .topBarLeading) {
Menu { Menu {
Text("Firmware Version: \(firmwareversion)") Text("Firmware Version: \(firmwareversion)")
.tint(.white) .tint(.white)
@ -164,7 +173,6 @@ struct GameLibraryView: View {
Text("Remove Firmware") Text("Remove Firmware")
} }
Button { Button {
let game = Game(containerFolder: URL(string: "none")!, fileType: .item, fileURL: URL(string: "MiiMaker")!, titleName: "Mii Maker", titleId: "0", developer: "Nintendo", version: firmwareversion) 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 { Button {
let documentsUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! let documentsUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let sharedurl = documentsUrl.absoluteString.replacingOccurrences(of: "file://", with: "shareddocuments://") let sharedurl = documentsUrl.absoluteString.replacingOccurrences(of: "file://", with: "shareddocuments://")
@ -198,7 +204,6 @@ struct GameLibraryView: View {
Image(systemName: "ellipsis.circle") Image(systemName: "ellipsis.circle")
.foregroundColor(.blue) .foregroundColor(.blue)
} }
} }
} }
} }
@ -231,14 +236,53 @@ struct GameLibraryView: View {
} catch { } catch {
print(error) print(error)
} }
case .failure(let err): case .failure(let err):
print("File import failed: \(err.localizedDescription)") 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 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 }
let romsDirectory = documentsDirectory.appendingPathComponent("roms") let romsDirectory = documentsDirectory.appendingPathComponent("roms")
// Check if "roms" folder exists; if not, create it // Check if "roms" folder exists; if not, create it
if !fileManager.fileExists(atPath: romsDirectory.path) { if (!fileManager.fileExists(atPath: romsDirectory.path)) {
do { do {
try fileManager.createDirectory(at: romsDirectory, withIntermediateDirectories: true, attributes: nil) try fileManager.createDirectory(at: romsDirectory, withIntermediateDirectories: true, attributes: nil)
} catch { } catch {
@ -314,8 +359,21 @@ struct GameLibraryView: View {
print("Error loading games from roms folder: \(error)") 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 { extension Game: Codable {
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
case titleName, titleId, developer, version, fileURL case titleName, titleId, developer, version, fileURL
@ -344,6 +402,7 @@ extension Game: Codable {
} }
} }
// MARK: -Recent Game Card
struct RecentGameCard: View { struct RecentGameCard: View {
let game: Game let game: Game
@Binding var startemu: Game? @Binding var startemu: Game?
@ -390,9 +449,15 @@ struct RecentGameCard: View {
} }
} }
// MARK: -Game List Item
struct GameListRow: View { struct GameListRow: View {
let game: Game let game: Game
@Binding var startemu: 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 @Environment(\.colorScheme) var colorScheme
var body: some View { var body: some View {
@ -442,20 +507,52 @@ struct GameListRow: View {
.padding(.vertical, 8) .padding(.vertical, 8)
.background(Color(.systemBackground)) .background(Color(.systemBackground))
.contextMenu { .contextMenu {
Button { Section {
startemu = game Button {
} label: { startemu = game
Label("Play Now", systemImage: "play.fill") } label: {
Label("Play Now", systemImage: "play.fill")
}
Button {
gameInfo = game
isViewingGameInfo.toggle()
} label: {
Label("Game Info", systemImage: "info.circle")
}
} }
Button { Section {
let pasteboard = UIPasteboard.general Button(role: .destructive) {
pasteboard.string = game.titleId gametoDelete = game
} label: { showGameDeleteConfirmation.toggle()
Label("Game ID: \(game.titleId)", systemImage: "info.circle") } label: {
Label("Delete", systemImage: "trash")
}
} }
} }
} }
.buttonStyle(.plain) .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)")
}
} }
} }

View File

@ -6,5 +6,48 @@
<string>1d0e26921bac938456ee7210ff4f2fa701dc16c02de1760e0aa757db28818ec7</string> <string>1d0e26921bac938456ee7210ff4f2fa701dc16c02de1760e0aa757db28818ec7</string>
<key>UIFileSharingEnabled</key> <key>UIFileSharingEnabled</key>
<true/> <true/>
<key>UTExportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeIdentifier</key>
<string>com.nintendo.switch-package</string>
<key>UTTypeDescription</key>
<string>Nintendo Switch Package</string>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
<string>public.archive</string>
</array>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>nsp</string>
</array>
<key>public.mime-type</key>
<string>application/x-nsp</string>
</dict>
</dict>
<dict>
<key>UTTypeIdentifier</key>
<string>com.nintendo.switch-cartridge</string>
<key>UTTypeDescription</key>
<string>Nintendo Switch Cartridge</string>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
<string>public.archive</string>
</array>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>xci</string>
</array>
<key>public.mime-type</key>
<string>application/x-xci</string>
</dict>
</dict>
</array>
</dict> </dict>
</plist> </plist>