Skip to content

Commit

Permalink
savestate view focused on 1 game
Browse files Browse the repository at this point in the history
Signed-off-by: Joseph Mattiello <git@joemattiello.com>
  • Loading branch information
JoeMatt committed Dec 3, 2024
1 parent c6032d4 commit 4515934
Show file tree
Hide file tree
Showing 9 changed files with 222 additions and 190 deletions.
126 changes: 65 additions & 61 deletions PVUI/Sources/PVSwiftUI/Consoles/ConsoleGamesView.swift

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ public class ContinuesMagementViewModel: ObservableObject {

// Observe save states size
driver.savesSizePublisher
.map { Int($0 / 1024 / 1024) } // Convert to MB
.map { Int($0) }
.receive(on: DispatchQueue.main)
.assign(to: \.savesTotalSize, on: headerViewModel)
.store(in: &cancellables)
Expand Down Expand Up @@ -284,11 +284,6 @@ public class ContinuesMagementViewModel: ObservableObject {
)

setupObservers()

// Subscribe to numberOfSaves changes
driver.numberOfSavesPublisher
.assign(to: \.numberOfSaves, on: headerViewModel)
.store(in: &cancellables)
}

/// Select all save states
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,7 @@ public struct ContinuesManagementHeaderView: View {
.font(.subheadline)
.foregroundColor(.white)

Text("\(viewModel.numberOfSaves) Save States - \(viewModel.savesTotalSize) MB")
.font(.subheadline)
Text("\(viewModel.numberOfSaves) Save States - \(String(format: "%.2f", Float(viewModel.savesTotalSize) / 1024.0 / 1024.0)) MB") .font(.subheadline)
.foregroundColor(.grey.gEEE)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,14 +121,14 @@ public struct ContinuesManagementContentView: View {
ContinuesManagementContentView(viewModel: viewModel)
.frame(height: 400)
.onAppear {
mockDriver.loadSaveStates(forGameId: "1")
mockDriver.gameId = "1" // Set the game ID filter
}

/// Edit mode
ContinuesManagementContentView(viewModel: viewModel)
.frame(height: 400)
.onAppear {
mockDriver.loadSaveStates(forGameId: "1")
mockDriver.gameId = "1" // Set the game ID filter
viewModel.controlsViewModel.isEditing = true
}
}
Expand All @@ -154,7 +154,7 @@ public struct ContinuesManagementContentView: View {
.frame(height: 400)
.padding()
.onAppear {
mockDriver.loadSaveStates(forGameId: "1")
mockDriver.gameId = "1" // Set the game ID filter
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,36 @@ import UIKit
@Observable
public class MockSaveStateDriver: SaveStateDriver {

private var saveStates: [SaveStateRowViewModel] = []
public let saveStatesSubject: CurrentValueSubject<[SaveStateRowViewModel], Never> = CurrentValueSubject([])
/// The game ID to filter save states by
public var gameId: String? {
didSet {
updateSaveStates()
}
}

public let saveStatesSubject = CurrentValueSubject<[SaveStateRowViewModel], Never>([])
public var saveStatesPublisher: AnyPublisher<[SaveStateRowViewModel], Never> {
saveStatesSubject.eraseToAnyPublisher()
}

public var numberOfSavesPublisher: AnyPublisher<Int, Never> {
saveStatesSubject.map { $0.count }.eraseToAnyPublisher()
saveStatesPublisher.map { $0.count }.eraseToAnyPublisher()
}

private var mockSaveSizes: [String: UInt64] = [:]

public var savesSizePublisher: AnyPublisher<UInt64, Never> {
saveStatesSubject.map { saveStates in
saveStates.reduce(0) { $0 + (self.mockSaveSizes[$1.id] ?? 0) }
saveStatesPublisher.map { states in
states.reduce(into: 0) { total, state in
if let size = self.mockSaveSizes[state.id] {
total += size
}
}
}.eraseToAnyPublisher()
}

private var allSaveStates: [SaveStateRowViewModel] = []
/// Mock dictionary to store save state sizes separately from the view models
private var mockSaveSizes: [String: UInt64] = [:]

/// Game metadata
public let gameTitle: String
public let systemTitle: String
Expand Down Expand Up @@ -66,84 +78,87 @@ public class MockSaveStateDriver: SaveStateDriver {
isFavorite: index % 2 == 0
)
}
saveStates = mockStates
saveStatesSubject.send(saveStates)
allSaveStates = mockStates
updateSaveStates()
}
}

public func getAllSaveStates() -> [SaveStateRowViewModel] {
saveStates
public init(mockSaveStates: [SaveStateRowViewModel] = []) {
self.gameTitle = "Test Game"
self.systemTitle = "Test System"
self.savesTotalSize = 0
self.gameUIImage = nil
self.allSaveStates = mockSaveStates
// Initialize mock sizes for provided save states
mockSaveStates.forEach { state in
mockSaveSizes[state.id] = UInt64.random(in: 1_000_000...10_000_000)
}
updateSaveStates()
}

public func getSaveStates(forGameId gameID: String) -> [SaveStateRowViewModel] {
saveStates.filter { $0.gameID == gameID }
private func updateSaveStates() {
if let gameId = gameId {
let filtered = allSaveStates.filter { $0.gameID == gameId }
saveStatesSubject.send(filtered)
} else {
saveStatesSubject.send(allSaveStates)
}
}

public func getAllSaveStates() -> [SaveStateRowViewModel] {
if let gameId = gameId {
return allSaveStates.filter { $0.gameID == gameId }
}
return allSaveStates
}

public func update(saveState: SaveStateRowViewModel) {
if let index = saveStates.firstIndex(where: { $0.id == saveState.id }) {
saveStates[index] = saveState
saveStatesSubject.send(saveStates)
if let index = allSaveStates.firstIndex(where: { $0.id == saveState.id }) {
allSaveStates[index] = saveState
updateSaveStates()
}
}

public func delete(saveStates: [SaveStateRowViewModel]) {
self.saveStates.removeAll(where: { saveState in
// Remove sizes for deleted save states
saveStates.forEach { state in
mockSaveSizes.removeValue(forKey: state.id)
}
allSaveStates.removeAll(where: { saveState in
saveStates.contains(where: { $0.id == saveState.id })
})
saveStatesSubject.send(self.saveStates)
}

/// Creates mock save states for testing
private func createMockSaveStates() -> [SaveStateRowViewModel] {
let dates = (-5...0).map { days in
Date().addingTimeInterval(TimeInterval(days * 24 * 3600))
}

return dates.enumerated().map { index, date in
let saveState = SaveStateRowViewModel(
gameID: "1",
gameTitle: "Test Game",
saveDate: date,
thumbnailImage: Image(systemName: "gamecontroller"),
description: index % 2 == 0 ? "Save \(index + 1)" : nil
)

saveState.isAutoSave = index % 3 == 0
saveState.isFavorite = index % 4 == 0
saveState.isPinned = index % 5 == 0

return saveState
}
updateSaveStates()
}

public func updateDescription(saveStateId: String, description: String?) {
if let index = saveStates.firstIndex(where: { $0.id == saveStateId }) {
saveStates[index].description = description
saveStatesSubject.send(saveStates)
if let index = allSaveStates.firstIndex(where: { $0.id == saveStateId }) {
var updated = allSaveStates[index]
updated.description = description
allSaveStates[index] = updated
updateSaveStates()
}
}

public func setPin(saveStateId: String, isPinned: Bool) {
if let index = saveStates.firstIndex(where: { $0.id == saveStateId }) {
saveStates[index].isPinned = isPinned
saveStatesSubject.send(saveStates)
if let index = allSaveStates.firstIndex(where: { $0.id == saveStateId }) {
var updated = allSaveStates[index]
updated.isPinned = isPinned
allSaveStates[index] = updated
updateSaveStates()
}
}

public func setFavorite(saveStateId: String, isFavorite: Bool) {
if let index = saveStates.firstIndex(where: { $0.id == saveStateId }) {
saveStates[index].isFavorite = isFavorite
saveStatesSubject.send(saveStates)
if let index = allSaveStates.firstIndex(where: { $0.id == saveStateId }) {
var updated = allSaveStates[index]
updated.isFavorite = isFavorite
allSaveStates[index] = updated
updateSaveStates()
}
}

public func share(saveStateId: String) -> URL? {
// Implementation for sharing save state
// Mock implementation returns nil
return nil
}

public func loadSaveStates(forGameId gameID: String) {
let states = getAllSaveStates()
saveStatesSubject.send(states)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,28 @@ import PVLibrary
import SwiftUI

public class RealmSaveStateDriver: SaveStateDriver {
/// The game ID to filter save states by
public var gameId: String? {
didSet {
updateSaveStates()
}
}

private let realm: Realm
private var notificationToken: NotificationToken?

/// Publisher for save state changes
public let saveStatesSubject = CurrentValueSubject<[SaveStateRowViewModel], Never>([])
public var saveStatesPublisher: AnyPublisher<[SaveStateRowViewModel], Never> {
saveStatesSubject.eraseToAnyPublisher()
}

public var numberOfSaveStates: Int {
saveStatesSubject.value.count
}

/// Publisher for number of saves
public var numberOfSavesPublisher: AnyPublisher<Int, Never> {
saveStatesSubject.map { $0.count }.eraseToAnyPublisher()
saveStatesPublisher.map { $0.count }.eraseToAnyPublisher()
}

/// Publisher for total size of all save states
public var savesSizePublisher: AnyPublisher<UInt64, Never> {
saveStatesSubject.map { saveStates in
self.realm.objects(PVSaveState.self)
Expand All @@ -29,22 +37,42 @@ public class RealmSaveStateDriver: SaveStateDriver {
}.eraseToAnyPublisher()
}

public init(realm: Realm? = nil) throws {
self.realm = try realm ?? Realm()

// Observe Realm changes
let token = self.realm.objects(PVSaveState.self).observe { [weak self] changes in
self?.handleRealmChanges(changes)
}
self.notificationToken = token
public init(realm: Realm) {
self.realm = realm
setupObservers()
}

deinit {
notificationToken?.invalidate()
}

private func setupObservers() {
let results = realm.objects(PVSaveState.self)
notificationToken = results.observe { [weak self] changes in
self?.updateSaveStates()
}
updateSaveStates()
}

private func updateSaveStates() {
var results = realm.objects(PVSaveState.self)

if let gameId = gameId {
results = results.filter("game.id == %@", gameId)
}

let viewModels = convertRealmResults(results)
saveStatesSubject.send(viewModels)
}

public func getAllSaveStates() -> [SaveStateRowViewModel] {
convertRealmResults(realm.objects(PVSaveState.self))
var results = realm.objects(PVSaveState.self)

if let gameId = gameId {
results = results.filter("game.id == %@", gameId)
}

return convertRealmResults(results)
}

public func getSaveStates(forGameId gameID: String) -> [SaveStateRowViewModel] {
Expand Down Expand Up @@ -87,13 +115,12 @@ public class RealmSaveStateDriver: SaveStateDriver {
realmSaveState.userDescription = saveState.description
realmSaveState.isPinned = saveState.isPinned
realmSaveState.isFavorite = saveState.isFavorite
realmSaveState.isAutosave = saveState.isAutoSave
}
}

public func delete(saveStates: [SaveStateRowViewModel]) {
let saveStateIds = saveStates.map { $0.id }
let realmSaveStates = realm.objects(PVSaveState.self).filter("id IN %@", saveStateIds)
let ids = saveStates.map { $0.id }
let realmSaveStates = realm.objects(PVSaveState.self).filter("id IN %@", ids)

try? realm.write {
realm.delete(realmSaveStates)
Expand All @@ -105,31 +132,20 @@ public class RealmSaveStateDriver: SaveStateDriver {
saveStatesSubject.send(states)
}

private var notificationToken: NotificationToken?

private func handleRealmChanges(_ changes: RealmCollectionChange<Results<PVSaveState>>) {
switch changes {
case .initial(let results), .update(let results, _, _, _):
saveStatesSubject.send(convertRealmResults(results))
case .error(let error):
print("Error observing Realm changes: \(error)")
}
}

private func convertRealmResults(_ results: Results<PVSaveState>) -> [SaveStateRowViewModel] {
results
.filter {
$0.game != nil
}
.map { realmSaveState in

let thumbnailImage: SwiftUI.Image
if let uiImage = realmSaveState.fetchUIImage() {
thumbnailImage = .init(uiImage: uiImage)
} else {
thumbnailImage = .init(uiImage: UIImage.missingArtworkImage(gameTitle: realmSaveState.game?.title ?? "Deleted", ratio: 1))
}

let viewModel = SaveStateRowViewModel(
id: realmSaveState.id,
gameID: realmSaveState.game.id,
Expand All @@ -141,7 +157,7 @@ public class RealmSaveStateDriver: SaveStateDriver {
isPinned: realmSaveState.isPinned,
isFavorite: realmSaveState.isFavorite
)

return viewModel
}
}
Expand Down
Loading

0 comments on commit 4515934

Please sign in to comment.