Merge pull request 'Update manager fix' (#8) from XITRIX/MeloNX:Update-manager-fix into XC-ios-ht

Reviewed-on: MeloNX/MeloNX#8
This commit is contained in:
stossy11 2025-02-16 20:14:58 +00:00
commit b3bb9cefcf
4 changed files with 198 additions and 183 deletions

View File

@ -14,45 +14,52 @@ struct GameInfoSheet: View {
var body: some View { var body: some View {
iOSNav { iOSNav {
VStack { List {
if let icon = game.icon { Section {}
Image(uiImage: icon) header: {
.resizable() VStack(alignment: .center) {
.aspectRatio(contentMode: .fit) if let icon = game.icon {
.frame(width: 250, height: 250) Image(uiImage: icon)
.cornerRadius(10) .resizable()
.padding() .aspectRatio(contentMode: .fit)
.contextMenu { .frame(width: 250, height: 250)
Button { .cornerRadius(10)
UIImageWriteToSavedPhotosAlbum(icon, nil, nil, nil) .padding()
} label: { .contextMenu {
Label("Save to Photos", systemImage: "square.and.arrow.down") 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 { VStack(alignment: .center) {
Image(systemName: "questionmark.circle") Text("**\(game.titleName)** | \(game.titleId.capitalized)")
.resizable() .multilineTextAlignment(.center)
.aspectRatio(contentMode: .fit) Text(game.developer)
.frame(width: 150, height: 150) .font(.caption)
.padding() .foregroundStyle(.secondary)
}
.padding(.vertical, 3)
}
.frame(maxWidth: .infinity)
} }
VStack(alignment: .leading) { Section {
VStack(alignment: .leading) { HStack {
Text("**\(game.titleName)** | \(game.titleId.capitalized)") Text("**Version**")
Text(game.developer) Spacer()
.font(.caption) Text(game.version)
.foregroundStyle(.secondary) .foregroundStyle(Color.secondary)
} }
.padding(.vertical, 3) HStack {
Text("**Title ID**")
VStack(alignment: .leading, spacing: 5) {
Text("Information")
.font(.title2)
.bold()
Text("**Version:** \(game.version)")
Text("**Title ID:** \(game.titleId)")
.contextMenu { .contextMenu {
Button { Button {
UIPasteboard.general.string = game.titleId UIPasteboard.general.string = game.titleId
@ -60,15 +67,32 @@ struct GameInfoSheet: View {
Text("Copy Title ID") Text("Copy Title ID")
} }
} }
Text("**Game Size:** \(fetchFileSize(for: game.fileURL) ?? 0) bytes") Spacer()
Text("**File Type:** .\(getFileType(game.fileURL))") Text(game.titleId)
Text("**Game URL:** \(trimGameURL(game.fileURL))") .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")
} }
.headerProminence(.increased)
Spacer()
} }
.padding(.horizontal, 5)
.navigationTitle(game.titleName) .navigationTitle(game.titleName)
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbar { .toolbar {
@ -103,10 +127,6 @@ struct GameInfoSheet: View {
} }
func getFileType(_ url: URL) -> String { func getFileType(_ url: URL) -> String {
let path = url.path url.pathExtension
if let range = path.range(of: ".") {
return String(path[range.upperBound...])
}
return "Unknown"
} }
} }

View File

@ -42,72 +42,54 @@ struct GameLibraryView: View {
} }
return Ryujinx.shared.games.filter { return Ryujinx.shared.games.filter {
$0.titleName.localizedCaseInsensitiveContains(searchText) || $0.titleName.localizedCaseInsensitiveContains(searchText) ||
$0.developer.localizedCaseInsensitiveContains(searchText) $0.developer.localizedCaseInsensitiveContains(searchText)
} }
} }
var body: some View { var body: some View {
iOSNav { iOSNav {
ScrollView { List {
LazyVStack(alignment: .leading, spacing: 20) { if Ryujinx.shared.games.isEmpty {
if !isSearching { VStack(spacing: 16) {
Text("Games") Image(systemName: "gamecontroller.fill")
.font(.system(size: 34, weight: .bold)) .font(.system(size: 64))
.padding(.horizontal) .foregroundColor(.secondary.opacity(0.7))
.padding(.top, 12) .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)
if Ryujinx.shared.games.isEmpty { .padding(.top, 40)
VStack(spacing: 16) { } else {
Image(systemName: "gamecontroller.fill") if !isSearching && !recentGames.isEmpty {
.font(.system(size: 64)) VStack(alignment: .leading, spacing: 12) {
.foregroundColor(.secondary.opacity(0.7)) Text("Recent")
.padding(.top, 60)
Text("No Games Found")
.font(.title2.bold()) .font(.title2.bold())
.foregroundColor(.primary) .padding(.horizontal)
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) { ScrollView(.horizontal, showsIndicators: false) {
LazyHStack(spacing: 16) { LazyHStack(spacing: 16) {
ForEach(recentGames) { game in ForEach(recentGames) { game in
RecentGameCard(game: game, startemu: $startemu) 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)
.onTapGesture { .onTapGesture {
addToRecentGames(game) addToRecentGames(game)
startemu = game
} }
} }
} }
.padding(.horizontal)
} }
} else { }
VStack(alignment: .leading, spacing: 12) {
Text("All Games")
.font(.title2.bold())
.padding(.horizontal)
LazyVStack(spacing: 2) { LazyVStack(spacing: 2) {
ForEach(filteredGames) { game in ForEach(filteredGames) { game in
GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, gameInfo: $gameInfo) GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, gameInfo: $gameInfo)
@ -117,35 +99,44 @@ struct GameLibraryView: View {
} }
} }
} }
} } else {
} ForEach(filteredGames) { game in
.onAppear { GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, gameInfo: $gameInfo)
loadRecentGames() .onTapGesture {
addToRecentGames(game)
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)
} }
} }
} }
.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 { .toolbar {
ToolbarItem(placement: .topBarLeading) { ToolbarItem(placement: .topBarTrailing) {
Button { Button {
isSelectingGameFile.toggle() isSelectingGameFile.toggle()
} label: { } label: {
@ -215,7 +206,6 @@ struct GameLibraryView: View {
} }
} }
} }
.background(Color(.systemGroupedBackground))
.searchable(text: $searchText) .searchable(text: $searchText)
.onChange(of: searchText) { _ in .onChange(of: searchText) { _ in
isSearching = !searchText.isEmpty isSearching = !searchText.isEmpty
@ -296,7 +286,6 @@ struct GameLibraryView: View {
} }
} }
private func addToRecentGames(_ game: Game) { private func addToRecentGames(_ game: Game) {
recentGames.removeAll { $0.id == game.id } 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) { func deleteGame(game: Game) {
let fileManager = FileManager.default let fileManager = FileManager.default
do { do {
@ -343,7 +331,7 @@ struct GameLibraryView: View {
} }
} }
// MARK: -Game Model // 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
@ -372,7 +360,7 @@ extension Game: Codable {
} }
} }
// MARK: -Recent Game Card // MARK: - Recent Game Card
struct RecentGameCard: View { struct RecentGameCard: View {
let game: Game let game: Game
@Binding var startemu: Game? @Binding var startemu: Game?
@ -393,7 +381,7 @@ struct RecentGameCard: View {
ZStack { ZStack {
RoundedRectangle(cornerRadius: 12) RoundedRectangle(cornerRadius: 12)
.fill(colorScheme == .dark ? .fill(colorScheme == .dark ?
Color(.systemGray5) : Color(.systemGray6)) Color(.systemGray5) : Color(.systemGray6))
.frame(width: 140, height: 140) .frame(width: 140, height: 140)
Image(systemName: "gamecontroller.fill") Image(systemName: "gamecontroller.fill")
@ -419,7 +407,7 @@ struct RecentGameCard: View {
} }
} }
// MARK: -Game List Item // MARK: - Game List Item
struct GameListRow: View { struct GameListRow: View {
let game: Game let game: Game
@Binding var startemu: Game? @Binding var startemu: Game?
@ -447,7 +435,7 @@ struct GameListRow: View {
ZStack { ZStack {
RoundedRectangle(cornerRadius: 8) RoundedRectangle(cornerRadius: 8)
.fill(colorScheme == .dark ? .fill(colorScheme == .dark ?
Color(.systemGray5) : Color(.systemGray6)) Color(.systemGray5) : Color(.systemGray6))
.frame(width: 45, height: 45) .frame(width: 45, height: 45)
Image(systemName: "gamecontroller.fill") Image(systemName: "gamecontroller.fill")
@ -474,9 +462,6 @@ struct GameListRow: View {
.foregroundColor(.accentColor) .foregroundColor(.accentColor)
.opacity(0.8) .opacity(0.8)
} }
.padding(.horizontal)
.padding(.vertical, 8)
.background(Color(.systemBackground))
.contextMenu { .contextMenu {
Section { Section {
Button { Button {
@ -533,4 +518,3 @@ struct GameListRow: View {
} }
} }
} }

View File

@ -18,36 +18,42 @@ struct UpdateManagerSheet: View {
var body: some View { var body: some View {
NavigationView { NavigationView {
VStack { List(paths, id: \..self, selection: $selectedItem) { item in
List(paths, id: \..self) { item in Button(action: {
Button(action: { selectItem(item.lastPathComponent)
selectItem(item.lastPathComponent) }) {
}) { HStack {
HStack { Text(item.lastPathComponent)
Text(item.lastPathComponent) .foregroundStyle(Color(uiColor: .label))
if selectedItem == "\(game!.titleId)/\(item.lastPathComponent)" { Spacer()
Spacer() if selectedItem == "updates/\(game!.titleId)/\(item.lastPathComponent)" {
Image(systemName: "checkmark") Image(systemName: "checkmark.circle.fill")
} .foregroundStyle(Color.accentColor)
} .font(.system(size: 24))
} } else {
.contextMenu { Image(systemName: "circle")
Button { .foregroundStyle(Color(uiColor: .secondaryLabel))
removeUpdate(item) .font(.system(size: 24))
} label: {
Text("Remove Update")
} }
} }
} }
.contextMenu {
Button {
removeUpdate(item)
} label: {
Text("Remove Update")
}
}
} }
.onAppear() { .onAppear {
print(URL.documentsDirectory.appendingPathComponent("games").appendingPathComponent(game!.titleId).appendingPathComponent("updates.json")) print(URL.documentsDirectory.appendingPathComponent("games").appendingPathComponent(game!.titleId).appendingPathComponent("updates.json"))
loadJSON(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") .navigationTitle("\(game!.titleName) Updates")
.navigationBarTitleDisplayMode(.inline)
.toolbar { .toolbar {
Button("+") { Button("Add", systemImage: "plus") {
isSelectingGameUpdate = true isSelectingGameUpdate = true
} }
} }
@ -80,7 +86,8 @@ struct UpdateManagerSheet: View {
let destinationURL = romUpdatedDirectory.appendingPathComponent(url.lastPathComponent) let destinationURL = romUpdatedDirectory.appendingPathComponent(url.lastPathComponent)
try? fileManager.copyItem(at: url, to: destinationURL) 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() Ryujinx.shared.games = Ryujinx.shared.loadGames()
loadJSON(jsonURL!) loadJSON(jsonURL!)
} catch { } catch {
@ -93,7 +100,7 @@ struct UpdateManagerSheet: View {
} }
func removeUpdate(_ game: URL) { func removeUpdate(_ game: URL) {
let gameString = "\(self.game!.titleId)/\(game.lastPathComponent)" let gameString = "updates/\(self.game!.titleId)/\(game.lastPathComponent)"
paths.removeAll { $0 == game } paths.removeAll { $0 == game }
items.removeAll { $0 == gameString } items.removeAll { $0 == gameString }
@ -108,6 +115,7 @@ struct UpdateManagerSheet: View {
} }
saveJSON(selectedItem: selectedItem ?? "") saveJSON(selectedItem: selectedItem ?? "")
Ryujinx.shared.games = Ryujinx.shared.loadGames()
} }
func saveJSON(selectedItem: String) { func saveJSON(selectedItem: String) {
@ -122,26 +130,28 @@ struct UpdateManagerSheet: View {
} }
func loadJSON(_ json: URL) { func loadJSON(_ json: URL) {
self.jsonURL = json self.jsonURL = json
print("Failed to read JSO")
guard let jsonURL = jsonURL else { return } guard let jsonURL else { return }
print("Failed to read JSOK")
do { do {
let data = try Data(contentsOf: jsonURL) let data = try Data(contentsOf: jsonURL)
if let jsonDict = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], if let jsonDict = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
let list = jsonDict["paths"] as? [String] { let list = jsonDict["paths"] as? [String]
var urls: [URL] = [] {
for path in list { let filteredList = list.filter { relativePath in
urls.append(URL.documentsDirectory.appendingPathComponent("updates").appendingPathComponent(path)) let path = URL.documentsDirectory.appendingPathComponent(relativePath)
return FileManager.default.fileExists(atPath: path.path)
} }
self.items = list let urls: [URL] = filteredList.map { relativePath in
self.paths = urls URL.documentsDirectory.appendingPathComponent(relativePath)
self.selectedItem = jsonDict["selected"] as? String }
items = filteredList
paths = urls
selectedItem = jsonDict["selected"] as? String
} }
} catch { } catch {
print("Failed to read JSON: \(error)") print("Failed to read JSON: \(error)")
@ -155,17 +165,17 @@ struct UpdateManagerSheet: View {
do { do {
let newData = try JSONSerialization.data(withJSONObject: defaultData, options: .prettyPrinted) let newData = try JSONSerialization.data(withJSONObject: defaultData, options: .prettyPrinted)
try newData.write(to: jsonURL) try newData.write(to: jsonURL)
self.items = [] items = []
self.selectedItem = "" selectedItem = ""
} catch { } catch {
print("Failed to create default JSON: \(error)") print("Failed to create default JSON: \(error)")
} }
} }
func selectItem(_ item: String) { func selectItem(_ item: String) {
let newSelection = "\(game!.titleId)/\(item)" let newSelection = "updates/\(game!.titleId)/\(item)"
guard let jsonURL = jsonURL else { return } guard let jsonURL else { return }
do { do {
let data = try Data(contentsOf: jsonURL) let data = try Data(contentsOf: jsonURL)
@ -175,7 +185,7 @@ struct UpdateManagerSheet: View {
jsonDict["selected"] = "" jsonDict["selected"] = ""
selectedItem = "" selectedItem = ""
} else { } else {
jsonDict["selected"] = newSelection jsonDict["selected"] = "\(newSelection)"
selectedItem = newSelection selectedItem = newSelection
} }
@ -183,9 +193,9 @@ struct UpdateManagerSheet: View {
let newData = try JSONSerialization.data(withJSONObject: jsonDict, options: .prettyPrinted) let newData = try JSONSerialization.data(withJSONObject: jsonDict, options: .prettyPrinted)
try newData.write(to: jsonURL) try newData.write(to: jsonURL)
Ryujinx.shared.games = Ryujinx.shared.loadGames()
} catch { } catch {
print("Failed to update JSON: \(error)") print("Failed to update JSON: \(error)")
} }
} }
} }

View File

@ -751,7 +751,8 @@ namespace Ryujinx.Headless.SDL2
if (File.Exists(titleUpdateMetadataPath)) 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)) if (File.Exists(updatePath))
{ {