diff --git a/src/MeloNX/MeloNX.xcodeproj/project.pbxproj b/src/MeloNX/MeloNX.xcodeproj/project.pbxproj
index fcccb2b19..567e8252e 100644
--- a/src/MeloNX/MeloNX.xcodeproj/project.pbxproj
+++ b/src/MeloNX/MeloNX.xcodeproj/project.pbxproj
@@ -672,6 +672,8 @@
"$(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;
GENERATE_INFOPLIST_FILE = YES;
@@ -755,8 +757,12 @@
"$(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 = 1.2.0;
+ MARKETING_VERSION = 1.3.0;
PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
@@ -814,6 +820,8 @@
"$(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;
GENERATE_INFOPLIST_FILE = YES;
@@ -897,8 +905,12 @@
"$(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 = 1.2.0;
+ MARKETING_VERSION = 1.3.0;
PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
diff --git a/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/stossy11.xcuserdatad/UserInterfaceState.xcuserstate b/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/stossy11.xcuserdatad/UserInterfaceState.xcuserstate
index e66740c97..b67489187 100644
Binary files a/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/stossy11.xcuserdatad/UserInterfaceState.xcuserstate and b/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/stossy11.xcuserdatad/UserInterfaceState.xcuserstate differ
diff --git a/src/MeloNX/MeloNX.xcodeproj/xcuserdata/stossy11.xcuserdatad/xcschemes/xcschememanagement.plist b/src/MeloNX/MeloNX.xcodeproj/xcuserdata/stossy11.xcuserdatad/xcschemes/xcschememanagement.plist
index 62375ba69..fbd8d81c1 100644
--- a/src/MeloNX/MeloNX.xcodeproj/xcuserdata/stossy11.xcuserdatad/xcschemes/xcschememanagement.plist
+++ b/src/MeloNX/MeloNX.xcodeproj/xcuserdata/stossy11.xcuserdatad/xcschemes/xcschememanagement.plist
@@ -12,12 +12,12 @@
Ryujinx.xcscheme_^#shared#^_
orderHint
- 1
+ 3
com.Stossy11.MeloNX.RyujinxAg.xcscheme_^#shared#^_
orderHint
- 2
+ 4
SuppressBuildableAutocreation
diff --git a/src/MeloNX/MeloNX/App/Core/Ryujinx/Ryujinx.swift b/src/MeloNX/MeloNX/App/Core/Ryujinx/Ryujinx.swift
index a4a2558ee..bd09f1b74 100644
--- a/src/MeloNX/MeloNX/App/Core/Ryujinx/Ryujinx.swift
+++ b/src/MeloNX/MeloNX/App/Core/Ryujinx/Ryujinx.swift
@@ -484,23 +484,26 @@ class Ryujinx {
func repeatuntilfindLayer() {
- DispatchQueue.global(qos: .background).async {
+ Task { @MainActor in
while self.metalLayer == nil {
let layer = self.getMetalLayer(nil)
-
+
if layer != nil {
DispatchQueue.main.async {
self.metalLayer = layer
}
+ self.metalLayer = layer
break
}
-
+
Thread.sleep(forTimeInterval: 0.1)
+ try await Task.sleep(nanoseconds: 100_000_000)
}
}
}
-
+
+ @MainActor
func getMetalLayer(_ window: OpaquePointer?) -> CAMetalLayer? {
var window = window
if window == nil {
diff --git a/src/MeloNX/MeloNX/App/Models/Game.swift b/src/MeloNX/MeloNX/App/Models/Game.swift
index d9ed10422..96d27591d 100644
--- a/src/MeloNX/MeloNX/App/Models/Game.swift
+++ b/src/MeloNX/MeloNX/App/Models/Game.swift
@@ -9,7 +9,7 @@ import SwiftUI
import UniformTypeIdentifiers
public struct Game: Identifiable, Equatable, Hashable {
- public var id = UUID()
+ public var id: URL { fileURL }
var containerFolder: URL
var fileType: UTType
diff --git a/src/MeloNX/MeloNX/App/Views/GamesList/GameListView.swift b/src/MeloNX/MeloNX/App/Views/GamesList/GameListView.swift
index 721734ef7..5673ab7ea 100644
--- a/src/MeloNX/MeloNX/App/Views/GamesList/GameListView.swift
+++ b/src/MeloNX/MeloNX/App/Views/GamesList/GameListView.swift
@@ -39,14 +39,23 @@ struct GameLibraryView: View {
var filteredGames: [Game] {
if searchText.isEmpty {
- return Ryujinx.shared.games
+ return Ryujinx.shared.games.filter { game in
+ !realRecentGames.contains(where: { $0.fileURL == game.fileURL })
+ }
}
return Ryujinx.shared.games.filter {
$0.titleName.localizedCaseInsensitiveContains(searchText) ||
- $0.developer.localizedCaseInsensitiveContains(searchText)
+ $0.developer.localizedCaseInsensitiveContains(searchText)
}
}
-
+
+ var realRecentGames: [Game] {
+ let games = Ryujinx.shared.games
+ return recentGames.compactMap { recentGame in
+ games.first(where: { $0.fileURL == recentGame.fileURL })
+ }
+ }
+
var body: some View {
iOSNav {
List {
@@ -66,46 +75,32 @@ struct GameLibraryView: View {
.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) {
- 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, isSelectingGameDLC: $isSelectingGameDLC, gameInfo: $gameInfo)
- .onTapGesture {
- addToRecentGames(game)
+ if !isSearching && !realRecentGames.isEmpty {
+ Section {
+ ForEach(realRecentGames) { game in
+ GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, isSelectingGameDLC: $isSelectingGameDLC, gameInfo: $gameInfo)
+ .swipeActions(edge: .trailing, allowsFullSwipe: true) {
+ Button(role: .destructive) {
+ removeFromRecentGames(game)
+ } label: {
+ Label("Delete", systemImage: "trash")
}
- }
+ }
}
+ } header: {
+ Text("Recent")
+ }
+
+ Section {
+ ForEach(filteredGames) { game in
+ GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, isSelectingGameDLC: $isSelectingGameDLC, gameInfo: $gameInfo)
+ }
+ } header: {
+ Text("Others")
}
} else {
ForEach(filteredGames) { game in
GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, isSelectingGameDLC: $isSelectingGameDLC, gameInfo: $gameInfo)
- .onTapGesture {
- addToRecentGames(game)
- }
}
}
}
@@ -206,8 +201,13 @@ struct GameLibraryView: View {
}
}
}
+ .onChange(of: startemu) { game in
+ guard let game else { return }
+ addToRecentGames(game)
+ }
}
.searchable(text: $searchText)
+ .animation(.easeInOut, value: searchText)
.onChange(of: searchText) { _ in
isSearching = !searchText.isEmpty
}
@@ -291,8 +291,8 @@ struct GameLibraryView: View {
}
private func addToRecentGames(_ game: Game) {
- recentGames.removeAll { $0.id == game.id }
-
+ recentGames.removeAll { $0.titleId == game.titleId }
+
recentGames.insert(game, at: 0)
if recentGames.count > 5 {
@@ -301,7 +301,12 @@ struct GameLibraryView: View {
saveRecentGames()
}
-
+
+ private func removeFromRecentGames(_ game: Game) {
+ recentGames.removeAll { $0.titleId == game.titleId }
+ saveRecentGames()
+ }
+
private func saveRecentGames() {
do {
let encoder = JSONEncoder()
@@ -364,53 +369,6 @@ extension Game: Codable {
}
}
-// MARK: - Recent Game Card
-struct RecentGameCard: View {
- let game: Game
- @Binding var startemu: Game?
- @Environment(\.colorScheme) var colorScheme
-
- var body: some View {
- Button(action: {
- startemu = game
- }) {
- VStack(alignment: .leading, spacing: 8) {
- if let icon = game.icon {
- Image(uiImage: icon)
- .resizable()
- .aspectRatio(contentMode: .fill)
- .frame(width: 140, height: 140)
- .cornerRadius(12)
- } else {
- ZStack {
- RoundedRectangle(cornerRadius: 12)
- .fill(colorScheme == .dark ?
- Color(.systemGray5) : Color(.systemGray6))
- .frame(width: 140, height: 140)
-
- Image(systemName: "gamecontroller.fill")
- .font(.system(size: 40))
- .foregroundColor(.gray)
- }
- }
-
- VStack(alignment: .leading, spacing: 2) {
- Text(game.titleName)
- .font(.subheadline.bold())
- .lineLimit(1)
-
- Text(game.developer)
- .font(.caption)
- .foregroundColor(.secondary)
- .lineLimit(1)
- }
- .padding(.horizontal, 4)
- }
- }
- .buttonStyle(.plain)
- }
-}
-
// MARK: - Game List Item
struct GameListRow: View {
let game: Game
@@ -469,56 +427,54 @@ struct GameListRow: View {
.foregroundColor(.accentColor)
.opacity(0.8)
}
- .contextMenu {
- Section {
- Button {
- startemu = game
- } label: {
- Label("Play Now", systemImage: "play.fill")
- }
+ }
+ .contextMenu {
+ Section {
+ Button {
+ startemu = game
+ } label: {
+ Label("Play Now", systemImage: "play.fill")
+ }
+
+ Button {
+ gameInfo = game
+ isViewingGameInfo.toggle()
- Button {
- gameInfo = game
- isViewingGameInfo.toggle()
-
- if game.titleName.lowercased() == "portal" {
- gamepo = true
- } else if game.titleName.lowercased() == "portal 2" {
- gamepo = true
- }
- } label: {
- Label("Game Info", systemImage: "info.circle")
+ if game.titleName.lowercased() == "portal" {
+ gamepo = true
+ } else if game.titleName.lowercased() == "portal 2" {
+ gamepo = true
}
+ } label: {
+ Label("Game Info", systemImage: "info.circle")
+ }
+ }
+
+ Section {
+ Button {
+ gameInfo = game
+ isSelectingGameUpdate.toggle()
+ } label: {
+ Label("Game Update Manager", systemImage: "chevron.up.circle")
}
- Section {
- Button {
- gameInfo = game
- isSelectingGameUpdate.toggle()
- } label: {
- Label("Game Update Manager", systemImage: "chevron.up.circle")
- }
-
- Button {
- gameInfo = game
- isSelectingGameDLC.toggle()
- } label: {
- Label("Game DLC Manager", systemImage: "plus.viewfinder")
- }
+ Button {
+ gameInfo = game
+ isSelectingGameDLC.toggle()
+ } label: {
+ Label("Game DLC Manager", systemImage: "plus.viewfinder")
}
-
-
- Section {
- Button(role: .destructive) {
- gametoDelete = game
- showGameDeleteConfirmation.toggle()
- } label: {
- Label("Delete", systemImage: "trash")
- }
+ }
+
+ 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 {
diff --git a/src/MeloNX/MeloNX/App/Views/Updates/GameDLCManagerSheet.swift b/src/MeloNX/MeloNX/App/Views/Updates/GameDLCManagerSheet.swift
index 5e751dd48..589c16545 100644
--- a/src/MeloNX/MeloNX/App/Views/Updates/GameDLCManagerSheet.swift
+++ b/src/MeloNX/MeloNX/App/Views/Updates/GameDLCManagerSheet.swift
@@ -45,7 +45,7 @@ struct DLCManagerSheet: View {
Self.saveDlcs(game, dlc: dlcs)
}) {
HStack {
- Text(dlc.containerPath)
+ Text((dlc.containerPath as NSString).lastPathComponent)
.foregroundStyle(Color(uiColor: .label))
Spacer()
if dlc.downloadableContentNcaList.first?.enabled == true {