diff --git a/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/PVGame+Spotlight.swift b/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/PVGame+Spotlight.swift index 34ec3e5608..21abc97353 100644 --- a/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/PVGame+Spotlight.swift +++ b/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/PVGame+Spotlight.swift @@ -31,6 +31,10 @@ public extension PVGame { #if os(iOS) || os(macOS) || targetEnvironment(macCatalyst) var spotlightContentSet: CSSearchableItemAttributeSet { + guard !isInvalidated else { + return CSSearchableItemAttributeSet() + } + let systemName = self.systemName var description = "\(systemName ?? "")" @@ -90,11 +94,13 @@ public extension PVGame { } var spotlightUniqueIdentifier: String { + guard !self.isInvalidated else { return "invalid" } return "org.provenance-emu.game.\(md5Hash)" } #endif var spotlightActivity: NSUserActivity { + guard !self.isInvalidated else { return NSUserActivity() } let activity = NSUserActivity(activityType: "org.provenance-emu.game.play") activity.title = title activity.userInfo = ["md5": md5Hash] diff --git a/PVUI/Sources/PVSwiftUI/Components/GameContextMenu.swift b/PVUI/Sources/PVSwiftUI/Components/GameContextMenu.swift index 9ac3f5cb3c..3eea0cfaee 100644 --- a/PVUI/Sources/PVSwiftUI/Components/GameContextMenu.swift +++ b/PVUI/Sources/PVSwiftUI/Components/GameContextMenu.swift @@ -79,11 +79,15 @@ struct GameContextMenu: SwiftUI.View { } label: { Label("Move to System", systemImage: "folder.fill.badge.plus") } if #available(iOS 15, tvOS 15, macOS 12, *) { Button(role: .destructive) { - rootDelegate?.attemptToDelete(game: game) + Task.detached { @MainActor in + rootDelegate?.attemptToDelete(game: game) + } } label: { Label("Delete", systemImage: "trash") } } else { Button { - rootDelegate?.attemptToDelete(game: game) + Task.detached { @MainActor in + rootDelegate?.attemptToDelete(game: game) + } } label: { Label("Delete", systemImage: "trash") } } } @@ -94,6 +98,7 @@ struct GameContextMenu: SwiftUI.View { extension GameContextMenu { func showMoreInfo(forGame game: PVGame) { + guard !game.isInvalidated else { return } let moreInfoCollectionVC = GameMoreInfoViewController(game: game) if let rootDelegate = rootDelegate as? UIViewController { @@ -108,6 +113,7 @@ extension GameContextMenu { } func promptUserMD5CopiedToClipboard(forGame game: PVGame) { + guard !game.isInvalidated else { return } // Get the MD5 of the game let md5 = game.md5 // Copy to pasteboard @@ -119,6 +125,7 @@ extension GameContextMenu { } func pasteArtwork(forGame game: PVGame) { + guard !game.isInvalidated else { return } #if !os(tvOS) DLOG("Attempting to paste artwork for game: \(game.title)") let pasteboard = UIPasteboard.general @@ -157,8 +164,15 @@ extension GameContextMenu { } func share(game: PVGame) { + guard !game.isInvalidated else { return } + #if !os(tvOS) + DLOG("Attempting to share game: \(game.title)") #warning("TODO: Share button action") self.rootDelegate?.showUnderConstructionAlert() + #else + DLOG("Sharing not supported on this platform") + rootDelegate?.showMessage("Sharing is not supported on this platform.", title: "Not Supported") + #endif } private func saveArtwork(image: UIImage, forGame game: PVGame) { @@ -201,6 +215,7 @@ extension GameContextMenu { } private func clearCustomArtwork(forGame game: PVGame) { + guard !game.isInvalidated else { return } DLOG("GameContextMenu: Attempting to clear custom artwork for game: \(game.title)") do { try RomDatabase.sharedInstance.writeTransaction { @@ -218,6 +233,7 @@ extension GameContextMenu { } private func resetCorePreferences(forGame game: PVGame) { + guard !game.isInvalidated else { return } let hasGamePreference = game.userPreferredCoreID != nil let hasSystemPreference = game.system.userPreferredCoreID != nil diff --git a/PVUI/Sources/PVSwiftUI/Components/SystemPickerView.swift b/PVUI/Sources/PVSwiftUI/Components/SystemPickerView.swift index e6d51b4427..700ea5604a 100644 --- a/PVUI/Sources/PVSwiftUI/Components/SystemPickerView.swift +++ b/PVUI/Sources/PVSwiftUI/Components/SystemPickerView.swift @@ -19,29 +19,34 @@ struct SystemPickerView: View { @Binding var isPresented: Bool private var availableSystems: [PVSystem] { - PVEmulatorConfiguration.systems.filter { $0.identifier != game.systemIdentifier } + PVEmulatorConfiguration.systems.filter { + $0.identifier != game.systemIdentifier && + !(AppState.shared.isAppStore && $0.appStoreDisabled) + } } var body: some View { - NavigationView { - List { - ForEach(availableSystems) { system in - Button { - moveGame(to: system) - isPresented = false - } label: { - SystemRowView(system: system) + if !availableSystems.isEmpty { + NavigationView { + List { + ForEach(availableSystems) { system in + Button { + moveGame(to: system) + isPresented = false + } label: { + SystemRowView(system: system) + } } } + .navigationTitle("Select System") + .navigationBarItems(trailing: Button("Cancel") { + isPresented = false + }) + }.onAppear { + DLOG("Loading systems for game: \(game.title)") + let systemsList = PVEmulatorConfiguration.systems.map{ $0.identifier }.joined(separator: ", ") + ILOG("Systemslist: \(systemsList)") } - .navigationTitle("Select System") - .navigationBarItems(trailing: Button("Cancel") { - isPresented = false - }) - }.onAppear { - DLOG("Loading systems for game: \(game.title)") - let systemsList = PVEmulatorConfiguration.systems.map{ $0.identifier }.joined(separator: ", ") - ILOG("Systemslist: \(systemsList)") } } diff --git a/PVUI/Sources/PVSwiftUI/Consoles/ConsoleGamesView+GameContextMenuDelegate.swift b/PVUI/Sources/PVSwiftUI/Consoles/ConsoleGamesView+GameContextMenuDelegate.swift index e6b2f0e336..fca00ef1cf 100644 --- a/PVUI/Sources/PVSwiftUI/Consoles/ConsoleGamesView+GameContextMenuDelegate.swift +++ b/PVUI/Sources/PVSwiftUI/Consoles/ConsoleGamesView+GameContextMenuDelegate.swift @@ -8,7 +8,10 @@ import SwiftUI internal struct SystemMoveState: Identifiable { - var id: String { game.id } + var id: String { + guard !game.isInvalidated else { return "" } + return game.id + } let game: PVGame var isPresenting: Bool = true } diff --git a/PVUI/Sources/PVSwiftUI/Consoles/ConsoleGamesView+GamepadNavigation.swift b/PVUI/Sources/PVSwiftUI/Consoles/ConsoleGamesView+GamepadNavigation.swift index 2b1994c295..fdf93b2da1 100644 --- a/PVUI/Sources/PVSwiftUI/Consoles/ConsoleGamesView+GamepadNavigation.swift +++ b/PVUI/Sources/PVSwiftUI/Consoles/ConsoleGamesView+GamepadNavigation.swift @@ -124,6 +124,7 @@ extension ConsoleGamesView { } internal func currentSectionForGame(_ game: PVGame) -> HomeSectionType { + guard !game.isInvalidated else { return .allGames } // If we're in favorites section, ONLY return favorites if the game is actually in favorites if gamesViewModel.focusedSection == .favorites { return gamesViewModel.favorites.contains(where: { $0.id == game.id }) ? .favorites : .allGames diff --git a/PVUI/Sources/PVSwiftUI/Consoles/ConsoleGamesView.swift b/PVUI/Sources/PVSwiftUI/Consoles/ConsoleGamesView.swift index 06b5664ae0..720ec135f6 100644 --- a/PVUI/Sources/PVSwiftUI/Consoles/ConsoleGamesView.swift +++ b/PVUI/Sources/PVSwiftUI/Consoles/ConsoleGamesView.swift @@ -171,18 +171,10 @@ struct ConsoleGamesView: SwiftUI.View { !gamesViewModel.favorites.filter("systemIdentifier == %@", console.identifier).isEmpty } - private var favoritesArray: [PVGame] { - Array(gamesViewModel.favorites.filter("systemIdentifier == %@", console.identifier)) - } - private var hasRecentlyPlayedGames: Bool { !gamesViewModel.recentlyPlayedGames.isEmpty } - private var recentlyPlayedGamesArray: [PVGame] { - gamesViewModel.recentlyPlayedGames.compactMap { $0.game } - } - private func loadGame(_ game: PVGame) { Task.detached { @MainActor in await rootDelegate?.root_load(game, sender: self, core: nil, saveState: nil) @@ -211,8 +203,10 @@ struct ConsoleGamesView: SwiftUI.View { constrainHeight: false, sectionContext: .allGames, isFocused: Binding( - get: { gamesViewModel.focusedItemInSection == game.id }, - set: { if $0 { gamesViewModel.focusedItemInSection = game.id } } + get: { !game.isInvalidated && + gamesViewModel.focusedSection == .allGames && + gamesViewModel.focusedItemInSection == game.id }, + set: { if $0 && !game.isInvalidated { gamesViewModel.focusedItemInSection = game.id} } ) ) { Task.detached { @MainActor in @@ -238,11 +232,12 @@ struct ConsoleGamesView: SwiftUI.View { sectionContext: .allGames, isFocused: Binding( get: { + !game.isInvalidated && gamesViewModel.focusedSection == .allGames && gamesViewModel.focusedItemInSection == game.id }, set: { - if $0 { + if $0 && !game.isInvalidated { gamesViewModel.focusedSection = .allGames gamesViewModel.focusedItemInSection = game.id } @@ -276,8 +271,12 @@ struct ConsoleGamesView: SwiftUI.View { viewType: .row, sectionContext: .allGames, isFocused: Binding( - get: { gamesViewModel.focusedItemInSection == game.id }, - set: { if $0 { gamesViewModel.focusedItemInSection = game.id } } + get: { + !game.isInvalidated && + gamesViewModel.focusedSection == .allGames && + gamesViewModel.focusedItemInSection == game.id + }, + set: { if $0 && !game.isInvalidated { gamesViewModel.focusedItemInSection = game.id} } ) ) { Task.detached { @MainActor in @@ -300,8 +299,11 @@ struct ConsoleGamesView: SwiftUI.View { viewType: .row, sectionContext: .allGames, isFocused: Binding( - get: { gamesViewModel.focusedItemInSection == game.id }, - set: { if $0 { gamesViewModel.focusedItemInSection = game.id } } + get: { + !game.isInvalidated && + gamesViewModel.focusedSection == .allGames && + gamesViewModel.focusedItemInSection == game.id }, + set: { if $0 && !game.isInvalidated { gamesViewModel.focusedItemInSection = game.id} } )) { loadGame(game) @@ -495,66 +497,74 @@ extension ConsoleGamesView { @ViewBuilder private func gameItem(_ game: PVGame, section: HomeSectionType) -> some View { - GameItemView( - game: game, - constrainHeight: true, - viewType: .cell, - sectionContext: section, - isFocused: Binding( - get: { - gamesViewModel.focusedSection == section && - gamesViewModel.focusedItemInSection == game.id - }, - set: { - if $0 { - gamesViewModel.focusedSection = section - gamesViewModel.focusedItemInSection = game.id + if !game.isInvalidated { + + GameItemView( + game: game, + constrainHeight: true, + viewType: .cell, + sectionContext: section, + isFocused: Binding( + get: { + !game.isInvalidated && + gamesViewModel.focusedSection == section && + gamesViewModel.focusedItemInSection == game.id + }, + set: { + if $0 && !game.isInvalidated { + gamesViewModel.focusedSection = section + gamesViewModel.focusedItemInSection = game.id + } } + ) + ) { + Task.detached { @MainActor in + await rootDelegate?.root_load(game, sender: self, core: nil, saveState: nil) } - ) - ) { - Task.detached { @MainActor in - await rootDelegate?.root_load(game, sender: self, core: nil, saveState: nil) } - } - .id(game.id) - .focusableIfAvailable() - .contextMenu { - GameContextMenu( - game: game, - rootDelegate: rootDelegate, - contextMenuDelegate: self - ) + .id(game.id) + .focusableIfAvailable() + .contextMenu { + GameContextMenu( + game: game, + rootDelegate: rootDelegate, + contextMenuDelegate: self + ) + } } } @ViewBuilder private func saveStateItem(_ saveState: PVSaveState) -> some View { - GameItemView( - game: saveState.game, - saveState: saveState, - constrainHeight: true, - viewType: .cell, - sectionContext: .recentSaveStates, - isFocused: Binding( - get: { - gamesViewModel.focusedSection == .recentSaveStates && - gamesViewModel.focusedItemInSection == saveState.id - }, - set: { - if $0 { - gamesViewModel.focusedSection = .recentSaveStates - gamesViewModel.focusedItemInSection = saveState.id + if !saveState.isInvalidated && !saveState.game.isInvalidated { + GameItemView( + game: saveState.game, + saveState: saveState, + constrainHeight: true, + viewType: .cell, + sectionContext: .recentSaveStates, + isFocused: Binding( + get: { + !saveState.isInvalidated && + gamesViewModel.focusedSection == .recentSaveStates && + gamesViewModel.focusedItemInSection == saveState.id + }, + set: { + if $0 && !saveState.isInvalidated { + gamesViewModel.focusedSection = .recentSaveStates + gamesViewModel.focusedItemInSection = saveState.id + } } + ) + ) { + Task.detached { @MainActor in + guard !saveState.isInvalidated, !saveState.game.isInvalidated else { return } + await rootDelegate?.root_load(saveState.game, sender: self, core: saveState.core, saveState: saveState) } - ) - ) { - Task.detached { @MainActor in - await rootDelegate?.root_load(saveState.game, sender: self, core: saveState.core, saveState: saveState) } + .id(saveState.id) + .focusableIfAvailable() } - .id(saveState.id) - .focusableIfAvailable() } } diff --git a/PVUI/Sources/PVSwiftUI/GameItem/GameItemViewCell.swift b/PVUI/Sources/PVSwiftUI/GameItem/GameItemViewCell.swift index 2072f4b498..7005518778 100644 --- a/PVUI/Sources/PVSwiftUI/GameItem/GameItemViewCell.swift +++ b/PVUI/Sources/PVSwiftUI/GameItem/GameItemViewCell.swift @@ -19,21 +19,23 @@ struct GameItemViewCell: SwiftUI.View { @State private var textMaxWidth: CGFloat = PVRowHeight var body: some SwiftUI.View { - VStack(alignment: .leading, spacing: 3) { - GameItemThumbnail(artwork: artwork, gameTitle: game.title, boxartAspectRatio: game.boxartAspectRatio) - if showGameTitles { - VStack(alignment: .leading, spacing: 0) { - GameItemTitle(text: game.title, viewType: viewType) - GameItemSubtitle(text: game.publishDate, viewType: viewType) + if !game.isInvalidated { + VStack(alignment: .leading, spacing: 3) { + GameItemThumbnail(artwork: artwork, gameTitle: game.title, boxartAspectRatio: game.boxartAspectRatio) + if showGameTitles { + VStack(alignment: .leading, spacing: 0) { + GameItemTitle(text: game.title, viewType: viewType) + GameItemSubtitle(text: game.publishDate, viewType: viewType) + } + .frame(width: textMaxWidth) } - .frame(width: textMaxWidth) } - } - .if(constrainHeight) { view in - view.frame(height: PVRowHeight) - } - .onPreferenceChange(ArtworkDynamicWidthPreferenceKey.self) { - textMaxWidth = $0 + .if(constrainHeight) { view in + view.frame(height: PVRowHeight) + } + .onPreferenceChange(ArtworkDynamicWidthPreferenceKey.self) { + textMaxWidth = $0 + } } } } diff --git a/PVUI/Sources/PVSwiftUI/GameItem/GameItemViewRow.swift b/PVUI/Sources/PVSwiftUI/GameItem/GameItemViewRow.swift index 9e867d84ad..902f28ae8f 100644 --- a/PVUI/Sources/PVSwiftUI/GameItem/GameItemViewRow.swift +++ b/PVUI/Sources/PVSwiftUI/GameItem/GameItemViewRow.swift @@ -22,13 +22,15 @@ struct GameItemViewRow: SwiftUI.View { var viewType: GameItemViewType var body: some SwiftUI.View { - HStack(alignment: .center, spacing: 10) { - GameItemThumbnail(artwork: artwork, gameTitle: game.title, boxartAspectRatio: game.boxartAspectRatio) - VStack(alignment: .leading, spacing: 0) { - GameItemTitle(text: game.title, viewType: viewType) - GameItemSubtitle(text: game.publishDate, viewType: viewType) + if !game.isInvalidated { + HStack(alignment: .center, spacing: 10) { + GameItemThumbnail(artwork: artwork, gameTitle: game.title, boxartAspectRatio: game.boxartAspectRatio) + VStack(alignment: .leading, spacing: 0) { + GameItemTitle(text: game.title, viewType: viewType) + GameItemSubtitle(text: game.publishDate, viewType: viewType) + } } + .frame(height: 50.0) } - .frame(height: 50.0) } } diff --git a/PVUI/Sources/PVSwiftUI/Home/HomeContinueSection.swift b/PVUI/Sources/PVSwiftUI/Home/HomeContinueSection.swift index 7bd98ec8f4..a3d2be6798 100644 --- a/PVUI/Sources/PVSwiftUI/Home/HomeContinueSection.swift +++ b/PVUI/Sources/PVSwiftUI/Home/HomeContinueSection.swift @@ -420,6 +420,7 @@ private struct ContinueItemWrapper: View { } }, isFocused: { + guard !saveState.isInvalidated else { return false } let shouldShowFocus = gamepadManager.isControllerConnected let isFocused = parentFocusedSection == .recentSaveStates && parentFocusedItem == saveState.id diff --git a/PVUI/Sources/PVSwiftUI/Home/HomeView.swift b/PVUI/Sources/PVSwiftUI/Home/HomeView.swift index 73070a70e6..4efd9739bf 100644 --- a/PVUI/Sources/PVSwiftUI/Home/HomeView.swift +++ b/PVUI/Sources/PVSwiftUI/Home/HomeView.swift @@ -206,29 +206,32 @@ struct HomeView: SwiftUI.View { private func showGamesList(_ games: Results) -> some View { LazyVStack(spacing: 8) { ForEach(games, id: \.self) { game in - GameItemView( - game: game, - constrainHeight: false, - viewType: .cell, - sectionContext: .allGames, - isFocused: Binding( - get: { - focusedSection == .allGames && - focusedItemInSection == game.id - }, - set: { - if $0 { - focusedSection = .allGames - focusedItemInSection = game.id + if !game.isInvalidated { + GameItemView( + game: game, + constrainHeight: false, + viewType: .cell, + sectionContext: .allGames, + isFocused: Binding( + get: { + !game.isInvalidated && + focusedSection == .allGames && + focusedItemInSection == game.id + }, + set: { + if $0 { + focusedSection = .allGames + focusedItemInSection = game.id + } } + ) + ) { + Task.detached { @MainActor in + await rootDelegate?.root_load(game, sender: self, core: nil, saveState: nil) } - ) - ) { - Task.detached { @MainActor in - await rootDelegate?.root_load(game, sender: self, core: nil, saveState: nil) } + .contextMenu { GameContextMenu(game: game, rootDelegate: rootDelegate) } } - .contextMenu { GameContextMenu(game: game, rootDelegate: rootDelegate) } } } } @@ -259,6 +262,7 @@ struct HomeView: SwiftUI.View { sectionContext: .allGames, isFocused: Binding( get: { + !game.isInvalidated && focusedSection == .allGames && focusedItemInSection == game.id }, @@ -526,6 +530,7 @@ struct HomeView: SwiftUI.View { sectionContext: .recentlyPlayedGames, isFocused: Binding( get: { + !game.isInvalidated && focusedSection == .recentlyPlayedGames && focusedItemInSection == game.id }, @@ -561,6 +566,7 @@ struct HomeView: SwiftUI.View { sectionContext: .favorites, isFocused: Binding( get: { + !favorite.isInvalidated && focusedSection == .favorites && focusedItemInSection == favorite.id }, @@ -595,6 +601,7 @@ struct HomeView: SwiftUI.View { sectionContext: .mostPlayed, isFocused: Binding( get: { + !playedGame.isInvalidated && focusedSection == .mostPlayed && focusedItemInSection == playedGame.id }, diff --git a/PVUI/Sources/PVUIBase/State Management/AppState.swift b/PVUI/Sources/PVUIBase/State Management/AppState.swift index 33471274bb..cdf3f5ca23 100644 --- a/PVUI/Sources/PVUIBase/State Management/AppState.swift +++ b/PVUI/Sources/PVUIBase/State Management/AppState.swift @@ -251,6 +251,10 @@ struct TimeoutError: Error { @available(iOS 9.0, macOS 11.0, macCatalyst 11.0, *) extension PVGame { func asShortcut(isFavorite: Bool) -> UIApplicationShortcutItem { + guard !isInvalidated else { + return UIApplicationShortcutItem(type: "kInvalidatedShortcut", localizedTitle: "Invalidated", localizedSubtitle: "Game is no longer valid", icon: .init(type: .play), userInfo: [:]) + } + let icon: UIApplicationShortcutIcon = isFavorite ? .init(type: .favorite) : .init(type: .play) return UIApplicationShortcutItem(type: "kRecentGameShortcut", localizedTitle: title,