Skip to content

Commit 60eb1d3

Browse files
committed
fix pause menu set skin
Signed-off-by: Joseph Mattiello <git@joemattiello.com>
1 parent dc1eefd commit 60eb1d3

File tree

6 files changed

+139
-19
lines changed

6 files changed

+139
-19
lines changed

PVUI/Sources/PVUIBase/PVEmulatorVC/PauseTileMenuView.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1341,7 +1341,11 @@ struct PauseTileMenuView: View {
13411341
}
13421342
.sheet(isPresented: $showingSkinCatalog) {
13431343
NavigationStack {
1344-
SkinCatalogBrowserView(preselectedSystem: activeSystemIdentifier.skinCatalogSystemCode)
1344+
SkinCatalogBrowserView(
1345+
preselectedSystem: activeSystemIdentifier.skinCatalogSystemCode,
1346+
activationContextSystemIdentifier: activeSystemIdentifier,
1347+
activationContextGameId: emulatorVC.game.flatMap { $0.id.isEmpty ? nil : $0.id }
1348+
)
13451349
.toolbar {
13461350
ToolbarItem(placement: .topBarTrailing) {
13471351
Button(String(localized: "Done")) { showingSkinCatalog = false }

PVUI/Sources/PVUIBase/PVEmulatorVC/RetroMenuView.swift

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import PVLogging
1212
import PVSettings
1313
import PVSupport
1414
import PVLibrary
15+
import PVPrimitives
1516
import PVFeatureFlags
1617
import UniformTypeIdentifiers
1718
import PVThemes
@@ -45,6 +46,21 @@ struct RetroMenuView: View {
4546

4647
private var palette: UXThemePalette { themeManager.currentPalette }
4748

49+
/// Same resolution as the pause tile menu so catalog activation matches the running emulator’s `SystemIdentifier`.
50+
private var skinCatalogActiveSystemIdentifier: SystemIdentifier {
51+
let game = emulatorVC.game
52+
let candidates: [String?] = [
53+
game.system?.identifier,
54+
game.systemIdentifier.isEmpty ? nil : game.systemIdentifier,
55+
emulatorVC.core.systemIdentifier
56+
]
57+
let parsed = candidates.compactMap { raw -> SystemIdentifier? in
58+
guard let raw, !raw.isEmpty else { return nil }
59+
return SystemIdentifier(rawValue: raw)
60+
}
61+
return parsed.first(where: { $0 != .RetroArch }) ?? parsed.first ?? .RetroArch
62+
}
63+
4864
/// Dismisses the menu without resuming emulation - use when opening sub-sheets that should keep the game paused
4965
private func dismissMenuForSubSheet() {
5066
dismissMenuForSubSheetThen {}
@@ -1626,7 +1642,9 @@ struct RetroMenuView: View {
16261642
}) {
16271643
NavigationStack {
16281644
SkinCatalogBrowserView(
1629-
preselectedSystem: emulatorVC.game.system?.systemIdentifier.skinCatalogSystemCode ?? nil
1645+
preselectedSystem: skinCatalogActiveSystemIdentifier.skinCatalogSystemCode,
1646+
activationContextSystemIdentifier: skinCatalogActiveSystemIdentifier,
1647+
activationContextGameId: emulatorVC.game.id.isEmpty ? nil : emulatorVC.game.id
16301648
)
16311649
.toolbar {
16321650
ToolbarItem(placement: .topBarTrailing) {

PVUI/Sources/PVUIBase/SwiftUI/DeltaSkins/Views/Catalog/SkinCatalogBrowserView.swift

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,29 @@ public struct SkinCatalogBrowserView: View {
2020

2121
#if os(tvOS)
2222
private let preselectedSystem: String?
23-
public init(preselectedSystem: String? = nil) {
23+
private let activationContextSystemIdentifier: SystemIdentifier?
24+
private let activationContextGameId: String?
25+
public init(
26+
preselectedSystem: String? = nil,
27+
activationContextSystemIdentifier: SystemIdentifier? = nil,
28+
activationContextGameId: String? = nil
29+
) {
2430
self.preselectedSystem = preselectedSystem
31+
self.activationContextSystemIdentifier = activationContextSystemIdentifier
32+
self.activationContextGameId = activationContextGameId
2533
}
2634
public var body: some View {
27-
TVOSSkinCatalogBrowserView(preselectedSystem: preselectedSystem)
35+
TVOSSkinCatalogBrowserView(
36+
preselectedSystem: preselectedSystem,
37+
activationContextSystemIdentifier: activationContextSystemIdentifier,
38+
activationContextGameId: activationContextGameId
39+
)
2840
}
2941
#else
3042

43+
private let activationContextSystemIdentifier: SystemIdentifier?
44+
private let activationContextGameId: String?
45+
3146
// MARK: - State
3247

3348
@State private var entries: [SkinCatalogEntry] = []
@@ -66,7 +81,14 @@ public struct SkinCatalogBrowserView: View {
6681
///
6782
/// When `preselectedSystem` is non-nil the filter bar is shown automatically
6883
/// so the user immediately sees which system is active and can change it.
69-
public init(preselectedSystem: String? = nil) {
84+
/// Pass `activationContextSystemIdentifier` / `activationContextGameId` when presenting from gameplay so “Set as active skin” updates the running session.
85+
public init(
86+
preselectedSystem: String? = nil,
87+
activationContextSystemIdentifier: SystemIdentifier? = nil,
88+
activationContextGameId: String? = nil
89+
) {
90+
self.activationContextSystemIdentifier = activationContextSystemIdentifier
91+
self.activationContextGameId = activationContextGameId
7092
_selectedSystem = State(initialValue: preselectedSystem)
7193
_showingFilters = State(initialValue: preselectedSystem != nil)
7294
// Pre-select the device filter that matches the current hardware so
@@ -81,6 +103,15 @@ public struct SkinCatalogBrowserView: View {
81103
_selectedDevice = State(initialValue: defaultDevice)
82104
}
83105

106+
@ViewBuilder
107+
private func catalogDetailDestination(for entry: SkinCatalogEntry) -> some View {
108+
SkinCatalogDetailView(
109+
entry: entry,
110+
activationContextSystemIdentifier: activationContextSystemIdentifier,
111+
activationContextGameId: activationContextGameId
112+
)
113+
}
114+
84115
// MARK: - Body
85116

86117
public var body: some View {
@@ -314,7 +345,7 @@ public struct SkinCatalogBrowserView: View {
314345
) {
315346
ForEach(entries) { entry in
316347
let isInstalled = isSkinInstalled(entry)
317-
NavigationLink(destination: SkinCatalogDetailView(entry: entry)) {
348+
NavigationLink(destination: catalogDetailDestination(for: entry)) {
318349
CatalogSkinCard(entry: entry, glowIntensity: glowIntensity, isInstalled: isInstalled)
319350
}
320351
.buttonStyle(PlainButtonStyle())

PVUI/Sources/PVUIBase/SwiftUI/DeltaSkins/Views/Catalog/SkinCatalogDetailView.swift

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ public struct SkinCatalogDetailView: View {
2222
// MARK: - Properties
2323

2424
let entry: SkinCatalogEntry
25+
/// When set (e.g. opened from pause while playing), preferences and notifications use this system so they match ``EmulatorWithSkinView``.
26+
private let activationContextSystemIdentifier: SystemIdentifier?
27+
/// When set, activation writes **game** scope so per-game prefs override any prior selection (system-only writes were ignored by the effective-skin chain).
28+
private let activationContextGameId: String?
2529

2630
// MARK: - State
2731

@@ -52,6 +56,18 @@ public struct SkinCatalogDetailView: View {
5256
case failed(String)
5357
}
5458

59+
// MARK: - Init
60+
61+
public init(
62+
entry: SkinCatalogEntry,
63+
activationContextSystemIdentifier: SystemIdentifier? = nil,
64+
activationContextGameId: String? = nil
65+
) {
66+
self.entry = entry
67+
self.activationContextSystemIdentifier = activationContextSystemIdentifier
68+
self.activationContextGameId = activationContextGameId.flatMap { $0.isEmpty ? nil : $0 }
69+
}
70+
5571
// MARK: - Body
5672

5773
public var body: some View {
@@ -347,7 +363,7 @@ public struct SkinCatalogDetailView: View {
347363
)
348364
.shadow(color: RetroTheme.retroPink.opacity(0.4), radius: 6)
349365

350-
if let system = primarySystemIdentifier {
366+
if let system = resolvedSystemForActivation {
351367
activateSkinButton(system: system)
352368
}
353369
}
@@ -604,6 +620,25 @@ public struct SkinCatalogDetailView: View {
604620
}.first
605621
}
606622

623+
/// System used for the activate button and for preference writes when the catalog was opened in-game.
624+
private var resolvedSystemForActivation: SystemIdentifier? {
625+
activationContextSystemIdentifier ?? primarySystemIdentifier
626+
}
627+
628+
/// Order matters: prefer runtime context first, then catalog-derived id, so `DeltaSkinManager.skins(for:)` finds the install under the emulator’s system when they differ.
629+
private var systemsToQueryForInstalledSkin: [SystemIdentifier] {
630+
var result: [SystemIdentifier] = []
631+
var seen = Set<SystemIdentifier>()
632+
func appendUnique(_ id: SystemIdentifier?) {
633+
guard let id, !seen.contains(id) else { return }
634+
seen.insert(id)
635+
result.append(id)
636+
}
637+
appendUnique(activationContextSystemIdentifier)
638+
appendUnique(primarySystemIdentifier)
639+
return result
640+
}
641+
607642
// MARK: - Installed Check
608643

609644
/// Sets `downloadState` to `.installed` when the catalog entry matches a
@@ -723,19 +758,33 @@ public struct SkinCatalogDetailView: View {
723758
// Force a rescan so the just-installed skin is found
724759
await DeltaSkinManager.shared.reloadSkins()
725760

726-
let skins = try await DeltaSkinManager.shared.skins(for: system)
761+
let querySystems = systemsToQueryForInstalledSkin.isEmpty ? [system] : systemsToQueryForInstalledSkin
762+
var matchedSkin: DeltaSkinProtocol?
763+
for sys in querySystems {
764+
let skins = try await DeltaSkinManager.shared.skins(for: sys)
765+
if let found = findMatchingInstalledSkin(for: entry, in: skins) {
766+
matchedSkin = found
767+
break
768+
}
769+
}
727770

728771
guard !Task.isCancelled else { return }
729772

730-
guard let skin = findMatchingInstalledSkin(for: entry, in: skins) else {
731-
// Log what we have vs what we're looking for
732-
ELOG("activateSkin: skin '\(entry.name)' (id: \(entry.id)) not found. Available: \(skins.map { "\($0.name) (\($0.identifier))" })")
773+
guard let skin = matchedSkin else {
774+
ELOG("activateSkin: skin '\(entry.name)' (id: \(entry.id)) not found after querying systems: \(querySystems.map(\.rawValue))")
733775
throw NSError(domain: "SkinCatalog", code: 1, userInfo: [NSLocalizedDescriptionKey: "Skin '\(entry.name)' not found after install. Try going back and re-entering."])
734776
}
735777

778+
// Must match EmulatorWithSkinView.systemId so selectionChangedNotification triggers a reload.
779+
let prefSystem = activationContextSystemIdentifier ?? primarySystemIdentifier ?? system
736780
let manager = DeltaSkinSelectionManager.shared
737-
await manager.setSkin(skin.identifier, for: system, orientation: .portrait, scope: .system)
738-
await manager.setSkin(skin.identifier, for: system, orientation: .landscape, scope: .system)
781+
if let gameId = activationContextGameId {
782+
await manager.setSkin(skin.identifier, for: prefSystem, gameId: gameId, orientation: .portrait, scope: .game)
783+
await manager.setSkin(skin.identifier, for: prefSystem, gameId: gameId, orientation: .landscape, scope: .game)
784+
} else {
785+
await manager.setSkin(skin.identifier, for: prefSystem, orientation: .portrait, scope: .system)
786+
await manager.setSkin(skin.identifier, for: prefSystem, orientation: .landscape, scope: .system)
787+
}
739788

740789
guard !Task.isCancelled else { return }
741790

@@ -744,7 +793,7 @@ public struct SkinCatalogDetailView: View {
744793
activationState = .activated
745794
}
746795
}
747-
ILOG("SkinCatalogDetailView: Activated skin '\(skin.name)' for \(system)")
796+
ILOG("SkinCatalogDetailView: Activated skin '\(skin.name)' for \(prefSystem) (gameId: \(activationContextGameId ?? "nil"))")
748797
} catch is CancellationError {
749798
// Task was cancelled (e.g. view dismissed) — don't update state
750799
return

PVUI/Sources/PVUIBase/SwiftUI/DeltaSkins/Views/Catalog/TVOSSkinCatalogBrowserView.swift

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ import PVLogging
2424
/// Download and install is handled by `SkinCatalogDetailView` via `DeltaSkinManager`.
2525
public struct TVOSSkinCatalogBrowserView: View {
2626

27+
private let activationContextSystemIdentifier: SystemIdentifier?
28+
private let activationContextGameId: String?
29+
2730
// MARK: - State
2831

2932
@State private var catalog: [SkinCatalogEntry] = []
@@ -39,8 +42,23 @@ public struct TVOSSkinCatalogBrowserView: View {
3942
// MARK: - Init
4043

4144
/// Creates the browser, optionally pre-filtering to a system.
42-
public init(preselectedSystem: String? = nil) {
45+
public init(
46+
preselectedSystem: String? = nil,
47+
activationContextSystemIdentifier: SystemIdentifier? = nil,
48+
activationContextGameId: String? = nil
49+
) {
4350
_selectedSystem = State(initialValue: preselectedSystem)
51+
self.activationContextSystemIdentifier = activationContextSystemIdentifier
52+
self.activationContextGameId = activationContextGameId
53+
}
54+
55+
@ViewBuilder
56+
private func catalogDetailDestination(for entry: SkinCatalogEntry) -> some View {
57+
SkinCatalogDetailView(
58+
entry: entry,
59+
activationContextSystemIdentifier: activationContextSystemIdentifier,
60+
activationContextGameId: activationContextGameId
61+
)
4462
}
4563

4664
// MARK: - Derived data
@@ -220,7 +238,7 @@ public struct TVOSSkinCatalogBrowserView: View {
220238
ScrollView(.horizontal, showsIndicators: false) {
221239
HStack(spacing: 24) {
222240
ForEach(skins) { entry in
223-
NavigationLink(destination: SkinCatalogDetailView(entry: entry)) {
241+
NavigationLink(destination: catalogDetailDestination(for: entry)) {
224242
TVOSSkinCard(entry: entry, glowIntensity: glowIntensity)
225243
}
226244
.retroFocusButtonStyle(focusScale: 1.08, cornerRadius: 16)
@@ -245,7 +263,7 @@ public struct TVOSSkinCatalogBrowserView: View {
245263
spacing: 30
246264
) {
247265
ForEach(searchResults) { entry in
248-
NavigationLink(destination: SkinCatalogDetailView(entry: entry)) {
266+
NavigationLink(destination: catalogDetailDestination(for: entry)) {
249267
TVOSSkinCard(entry: entry, glowIntensity: glowIntensity)
250268
}
251269
.retroFocusButtonStyle(focusScale: 1.08, cornerRadius: 16)

PVUI/Sources/PVUIBase/SwiftUI/DeltaSkins/Views/Display/EmulatorWithSkinView.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,8 @@ struct EmulatorWithSkinView: View {
9393
self.gameTitle = game.title
9494
self.systemName = game.system?.name
9595

96-
// Convert string system identifier to enum
97-
self.systemId = game.system?.systemIdentifier
96+
// Linked PVSystem is authoritative; fall back to persisted PVGame.systemIdentifier when the relationship is missing so skin reload notifications still match.
97+
self.systemId = game.system?.systemIdentifier ?? SystemIdentifier(rawValue: game.systemIdentifier)
9898

9999
// Get game ID for skin preferences (must match game.id used in skin selection)
100100
self.gameId = game.id

0 commit comments

Comments
 (0)