diff --git a/src/MeloNX/MeloNX/App/Views/GamesList/GameInfoSheet.swift b/src/MeloNX/MeloNX/App/Views/GamesList/GameInfoSheet.swift index f8fe23945..5c4f9c3c8 100644 --- a/src/MeloNX/MeloNX/App/Views/GamesList/GameInfoSheet.swift +++ b/src/MeloNX/MeloNX/App/Views/GamesList/GameInfoSheet.swift @@ -14,45 +14,52 @@ struct GameInfoSheet: View { 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") - } + List { + Section {} + header: { + VStack(alignment: .center) { + 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() } - } 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) + VStack(alignment: .center) { + Text("**\(game.titleName)** | \(game.titleId.capitalized)") + .multilineTextAlignment(.center) + Text(game.developer) + .font(.caption) + .foregroundStyle(.secondary) + } + .padding(.vertical, 3) } - .padding(.vertical, 3) - - VStack(alignment: .leading, spacing: 5) { - Text("Information") - .font(.title2) - .bold() - - Text("**Version:** \(game.version)") - Text("**Title ID:** \(game.titleId)") + .frame(maxWidth: .infinity) + } + + Section { + HStack { + Text("**Version**") + Spacer() + Text(game.version) + .foregroundStyle(Color.secondary) + } + HStack { + Text("**Title ID**") .contextMenu { Button { UIPasteboard.general.string = game.titleId @@ -60,15 +67,32 @@ struct GameInfoSheet: View { Text("Copy Title ID") } } - Text("**Game Size:** \(fetchFileSize(for: game.fileURL) ?? 0) bytes") - Text("**File Type:** .\(getFileType(game.fileURL))") - Text("**Game URL:** \(trimGameURL(game.fileURL))") + Spacer() + Text(game.titleId) + .foregroundStyle(Color.secondary) } + HStack { + Text("**Game Size**") + Spacer() + Text("\(fetchFileSize(for: game.fileURL) ?? 0) bytes") + .foregroundStyle(Color.secondary) + } + HStack { + Text("**File Type**") + Spacer() + Text(getFileType(game.fileURL)) + .foregroundStyle(Color.secondary) + } + VStack(alignment: .leading, spacing: 4) { + Text("**Game URL**") + Text(trimGameURL(game.fileURL)) + .foregroundStyle(Color.secondary) + } + } header: { + Text("Information") } - - Spacer() + .headerProminence(.increased) } - .padding(.horizontal, 5) .navigationTitle(game.titleName) .navigationBarTitleDisplayMode(.inline) .toolbar { @@ -103,10 +127,6 @@ struct GameInfoSheet: View { } func getFileType(_ url: URL) -> String { - let path = url.path - if let range = path.range(of: ".") { - return String(path[range.upperBound...]) - } - return "Unknown" + url.pathExtension } } diff --git a/src/MeloNX/MeloNX/App/Views/GamesList/GameListView.swift b/src/MeloNX/MeloNX/App/Views/GamesList/GameListView.swift index da759a4b4..96a1e51fb 100644 --- a/src/MeloNX/MeloNX/App/Views/GamesList/GameListView.swift +++ b/src/MeloNX/MeloNX/App/Views/GamesList/GameListView.swift @@ -42,72 +42,54 @@ struct GameLibraryView: View { } return Ryujinx.shared.games.filter { $0.titleName.localizedCaseInsensitiveContains(searchText) || - $0.developer.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) + List { + if Ryujinx.shared.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) } - - if Ryujinx.shared.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") + .frame(maxWidth: .infinity) + .padding(.top, 40) + } else { + if !isSearching && !recentGames.isEmpty { + VStack(alignment: .leading, spacing: 12) { + Text("Recent") .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) + .padding(.horizontal) - ScrollView(.horizontal, showsIndicators: false) { - LazyHStack(spacing: 16) { - ForEach(recentGames) { game in - RecentGameCard(game: game, startemu: $startemu) - .onTapGesture { - addToRecentGames(game) - startemu = game - } - } - } - .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, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, gameInfo: $gameInfo) + ScrollView(.horizontal, showsIndicators: false) { + LazyHStack(spacing: 16) { + ForEach(recentGames) { game in + RecentGameCard(game: game, startemu: $startemu) .onTapGesture { addToRecentGames(game) + startemu = game } } } + .padding(.horizontal) } - } else { + } + + 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, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, gameInfo: $gameInfo) @@ -117,42 +99,51 @@ struct GameLibraryView: View { } } } - } - } - .onAppear { - 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 - - Ryujinx.shared.installFirmware(firmwarePath: path) - - firmwareversion = (Ryujinx.shared.fetchFirmwareVersion() == "" ? "0" : Ryujinx.shared.fetchFirmwareVersion()) - if fun { - url.stopAccessingSecurityScopedResource() - } + } else { + ForEach(filteredGames) { game in + GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, gameInfo: $gameInfo) + .onTapGesture { + addToRecentGames(game) + } } - case .failure(let error): - print(error) } } } + .navigationTitle("Games") + .navigationBarTitleDisplayMode(.large) + .onAppear { + 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 + + Ryujinx.shared.installFirmware(firmwarePath: path) + + firmwareversion = (Ryujinx.shared.fetchFirmwareVersion() == "" ? "0" : Ryujinx.shared.fetchFirmwareVersion()) + if fun { + url.stopAccessingSecurityScopedResource() + } + } + case .failure(let error): + print(error) + } + } .toolbar { - ToolbarItem(placement: .topBarLeading) { + ToolbarItem(placement: .topBarTrailing) { Button { isSelectingGameFile.toggle() } label: { Image(systemName: "plus") } } - + ToolbarItem(placement: .topBarLeading) { Menu { Text("Firmware Version: \(firmwareversion)") @@ -215,7 +206,6 @@ struct GameLibraryView: View { } } } - .background(Color(.systemGroupedBackground)) .searchable(text: $searchText) .onChange(of: searchText) { _ in isSearching = !searchText.isEmpty @@ -296,7 +286,6 @@ struct GameLibraryView: View { } } - private func addToRecentGames(_ game: Game) { recentGames.removeAll { $0.id == game.id } @@ -329,8 +318,7 @@ struct GameLibraryView: View { } } - -// MARK: - Delete Game Function + // MARK: - Delete Game Function func deleteGame(game: Game) { let fileManager = FileManager.default do { @@ -343,7 +331,7 @@ struct GameLibraryView: View { } } -// MARK: -Game Model +// MARK: - Game Model extension Game: Codable { enum CodingKeys: String, CodingKey { case titleName, titleId, developer, version, fileURL @@ -372,7 +360,7 @@ extension Game: Codable { } } -// MARK: -Recent Game Card +// MARK: - Recent Game Card struct RecentGameCard: View { let game: Game @Binding var startemu: Game? @@ -393,7 +381,7 @@ struct RecentGameCard: View { ZStack { RoundedRectangle(cornerRadius: 12) .fill(colorScheme == .dark ? - Color(.systemGray5) : Color(.systemGray6)) + Color(.systemGray5) : Color(.systemGray6)) .frame(width: 140, height: 140) Image(systemName: "gamecontroller.fill") @@ -419,7 +407,7 @@ struct RecentGameCard: View { } } -// MARK: -Game List Item +// MARK: - Game List Item struct GameListRow: View { let game: Game @Binding var startemu: Game? @@ -447,7 +435,7 @@ struct GameListRow: View { ZStack { RoundedRectangle(cornerRadius: 8) .fill(colorScheme == .dark ? - Color(.systemGray5) : Color(.systemGray6)) + Color(.systemGray5) : Color(.systemGray6)) .frame(width: 45, height: 45) Image(systemName: "gamecontroller.fill") @@ -474,9 +462,6 @@ struct GameListRow: View { .foregroundColor(.accentColor) .opacity(0.8) } - .padding(.horizontal) - .padding(.vertical, 8) - .background(Color(.systemBackground)) .contextMenu { Section { Button { @@ -533,4 +518,3 @@ struct GameListRow: View { } } } - diff --git a/src/MeloNX/MeloNX/App/Views/Updates/GameUpdateManagerSheet.swift b/src/MeloNX/MeloNX/App/Views/Updates/GameUpdateManagerSheet.swift index da5209654..0d7e8fa68 100644 --- a/src/MeloNX/MeloNX/App/Views/Updates/GameUpdateManagerSheet.swift +++ b/src/MeloNX/MeloNX/App/Views/Updates/GameUpdateManagerSheet.swift @@ -15,39 +15,45 @@ struct UpdateManagerSheet: View { @Binding var game: Game? @State private var isSelectingGameUpdate = false @State private var jsonURL: URL? = nil - + var body: some View { NavigationView { - VStack { - List(paths, id: \..self) { item in - Button(action: { - selectItem(item.lastPathComponent) - }) { - HStack { - Text(item.lastPathComponent) - if selectedItem == "\(game!.titleId)/\(item.lastPathComponent)" { - Spacer() - Image(systemName: "checkmark") - } - } - } - .contextMenu { - Button { - removeUpdate(item) - } label: { - Text("Remove Update") + List(paths, id: \..self, selection: $selectedItem) { item in + Button(action: { + selectItem(item.lastPathComponent) + }) { + HStack { + Text(item.lastPathComponent) + .foregroundStyle(Color(uiColor: .label)) + Spacer() + if selectedItem == "updates/\(game!.titleId)/\(item.lastPathComponent)" { + Image(systemName: "checkmark.circle.fill") + .foregroundStyle(Color.accentColor) + .font(.system(size: 24)) + } else { + Image(systemName: "circle") + .foregroundStyle(Color(uiColor: .secondaryLabel)) + .font(.system(size: 24)) } } } + .contextMenu { + Button { + removeUpdate(item) + } label: { + Text("Remove Update") + } + } } - .onAppear() { + .onAppear { print(URL.documentsDirectory.appendingPathComponent("games").appendingPathComponent(game!.titleId).appendingPathComponent("updates.json")) loadJSON(URL.documentsDirectory.appendingPathComponent("games").appendingPathComponent(game!.titleId).appendingPathComponent("updates.json")) } .navigationTitle("\(game!.titleName) Updates") + .navigationBarTitleDisplayMode(.inline) .toolbar { - Button("+") { + Button("Add", systemImage: "plus") { isSelectingGameUpdate = true } } @@ -80,7 +86,8 @@ struct UpdateManagerSheet: View { let destinationURL = romUpdatedDirectory.appendingPathComponent(url.lastPathComponent) try? fileManager.copyItem(at: url, to: destinationURL) - Ryujinx.shared.setTitleUpdate(titleId: gameInfo.titleId, updatePath: "\(gameInfo.titleId)/" + url.lastPathComponent) + items.append("updates/" + gameInfo.titleId + "/" + url.lastPathComponent) + selectItem(url.lastPathComponent) Ryujinx.shared.games = Ryujinx.shared.loadGames() loadJSON(jsonURL!) } catch { @@ -93,7 +100,7 @@ struct UpdateManagerSheet: View { } func removeUpdate(_ game: URL) { - let gameString = "\(self.game!.titleId)/\(game.lastPathComponent)" + let gameString = "updates/\(self.game!.titleId)/\(game.lastPathComponent)" paths.removeAll { $0 == game } items.removeAll { $0 == gameString } @@ -108,6 +115,7 @@ struct UpdateManagerSheet: View { } saveJSON(selectedItem: selectedItem ?? "") + Ryujinx.shared.games = Ryujinx.shared.loadGames() } func saveJSON(selectedItem: String) { @@ -122,26 +130,28 @@ struct UpdateManagerSheet: View { } func loadJSON(_ json: URL) { - self.jsonURL = json - print("Failed to read JSO") - guard let jsonURL = jsonURL else { return } - print("Failed to read JSOK") + guard let jsonURL else { return } do { let data = try Data(contentsOf: jsonURL) if let jsonDict = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], - let list = jsonDict["paths"] as? [String] { - var urls: [URL] = [] - - for path in list { - urls.append(URL.documentsDirectory.appendingPathComponent("updates").appendingPathComponent(path)) + let list = jsonDict["paths"] as? [String] + { + + let filteredList = list.filter { relativePath in + let path = URL.documentsDirectory.appendingPathComponent(relativePath) + return FileManager.default.fileExists(atPath: path.path) } - - self.items = list - self.paths = urls - self.selectedItem = jsonDict["selected"] as? String + + let urls: [URL] = filteredList.map { relativePath in + URL.documentsDirectory.appendingPathComponent(relativePath) + } + + items = filteredList + paths = urls + selectedItem = jsonDict["selected"] as? String } } catch { print("Failed to read JSON: \(error)") @@ -155,17 +165,17 @@ struct UpdateManagerSheet: View { do { let newData = try JSONSerialization.data(withJSONObject: defaultData, options: .prettyPrinted) try newData.write(to: jsonURL) - self.items = [] - self.selectedItem = "" + items = [] + selectedItem = "" } catch { print("Failed to create default JSON: \(error)") } } func selectItem(_ item: String) { - let newSelection = "\(game!.titleId)/\(item)" - - guard let jsonURL = jsonURL else { return } + let newSelection = "updates/\(game!.titleId)/\(item)" + + guard let jsonURL else { return } do { let data = try Data(contentsOf: jsonURL) @@ -175,17 +185,17 @@ struct UpdateManagerSheet: View { jsonDict["selected"] = "" selectedItem = "" } else { - jsonDict["selected"] = newSelection + jsonDict["selected"] = "\(newSelection)" selectedItem = newSelection } jsonDict["paths"] = items - + let newData = try JSONSerialization.data(withJSONObject: jsonDict, options: .prettyPrinted) try newData.write(to: jsonURL) + Ryujinx.shared.games = Ryujinx.shared.loadGames() } catch { print("Failed to update JSON: \(error)") } } - } diff --git a/src/Ryujinx.Headless.SDL2/Program.cs b/src/Ryujinx.Headless.SDL2/Program.cs index a5f799fac..5c6b84c11 100644 --- a/src/Ryujinx.Headless.SDL2/Program.cs +++ b/src/Ryujinx.Headless.SDL2/Program.cs @@ -751,7 +751,8 @@ namespace Ryujinx.Headless.SDL2 if (File.Exists(titleUpdateMetadataPath)) { - updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath, _titleSerializerContext.TitleUpdateMetadata).Selected; + string updatePathRelative = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath, _titleSerializerContext.TitleUpdateMetadata).Selected; + updatePath = Path.Combine(AppDataManager.BaseDirPath, updatePathRelative); if (File.Exists(updatePath)) {