diff --git a/PVLibrary/Sources/PVLibrary/Importer/Models/ImportQueueItem.swift b/PVLibrary/Sources/PVLibrary/Importer/Models/ImportQueueItem.swift index 67ef108102..fe5c92a092 100644 --- a/PVLibrary/Sources/PVLibrary/Importer/Models/ImportQueueItem.swift +++ b/PVLibrary/Sources/PVLibrary/Importer/Models/ImportQueueItem.swift @@ -10,13 +10,17 @@ import PVPrimitives import Perception // Enum to define the possible statuses of each import -public enum ImportStatus: String { - case queued +public enum ImportStatus: Int, CustomStringConvertible, CaseIterable { + case conflict // Indicates additional action needed by user after successful import + case partial //indicates the item is waiting for associated files before it could be processed case processing - case success + + case queued + case failure - case conflict // Indicates additional action needed by user after successful import + + case success public var description: String { switch self { @@ -55,16 +59,20 @@ public enum ProcessingState { // ImportItem model to hold each file's metadata and progress @Perceptible public class ImportQueueItem: Identifiable, ObservableObject { + + // TODO: Make this more generic with AnySystem, some System? + public typealias System = PVSystem //AnySystem + public let id = UUID() public var url: URL public var fileType: FileType - public var systems: [PVSystem] // Can be set to the specific system type - public var userChosenSystem: PVSystem? + public var systems: [System] // Can be set to the specific system type + public var userChosenSystem: (System)? public var destinationUrl: URL? public var errorValue: String? //this is used when a single import has child items - e.g., m3u, cue, directory - public var childQueueItems:[ImportQueueItem] + public var childQueueItems: [ImportQueueItem] // Observable status for individual imports public var status: ImportStatus = .queued @@ -94,7 +102,7 @@ public class ImportQueueItem: Identifiable, ObservableObject { var md5: String? } - public func targetSystem() -> PVSystem? { + public func targetSystem() -> (any SystemProtocol)? { guard !systems.isEmpty else { return nil } @@ -105,7 +113,7 @@ public class ImportQueueItem: Identifiable, ObservableObject { if let chosenSystem = userChosenSystem { - var target:PVSystem? = nil + var target:(any SystemProtocol)? = nil for system in systems { if (chosenSystem.identifier == system.identifier) { @@ -139,3 +147,15 @@ public class ImportQueueItem: Identifiable, ObservableObject { return current } } + +extension ImportQueueItem: Equatable { + public static func == (lhs: ImportQueueItem, rhs: ImportQueueItem) -> Bool { + return lhs.url == rhs.url + } +} + +extension ImportQueueItem: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(url) + } +} diff --git a/PVLibrary/Sources/PVLibrary/Importer/Services/GameImporter/ArtworkImporter.swift b/PVLibrary/Sources/PVLibrary/Importer/Services/GameImporter/ArtworkImporter.swift index 7bebc00104..1227795216 100644 --- a/PVLibrary/Sources/PVLibrary/Importer/Services/GameImporter/ArtworkImporter.swift +++ b/PVLibrary/Sources/PVLibrary/Importer/Services/GameImporter/ArtworkImporter.swift @@ -5,24 +5,30 @@ // Created by David Proskin on 11/3/24. // +import PVPrimitives + protocol ArtworkImporting { - func setSystemsService(_ systemsService:GameImporterSystemsServicing) - func importArtworkItem(_ queueItem: ImportQueueItem) async -> PVGame? + // TODO: Make me more generic +// associatedtype MyGameImporterSystemsService: GameImporterSystemsServicing + typealias MyGameImporterSystemsService = GameImporterSystemsServicing + + func setSystemsService(_ systemsService: MyGameImporterSystemsService) + func importArtworkItem(_ queueItem: ImportQueueItem) async -> MyGameImporterSystemsService.GameType? } class ArtworkImporter : ArtworkImporting { - - private var gameImporterSystemsService:GameImporterSystemsServicing? + + private var gameImporterSystemsService: (any GameImporterSystemsServicing)? init() { } - func setSystemsService(_ systemsService:GameImporterSystemsServicing) { + func setSystemsService(_ systemsService: MyGameImporterSystemsService) { gameImporterSystemsService = systemsService } - func importArtworkItem(_ queueItem: ImportQueueItem) async -> PVGame? { + func importArtworkItem(_ queueItem: ImportQueueItem) async -> MyGameImporterSystemsService.GameType? { guard queueItem.fileType == .artwork else { return nil } @@ -108,7 +114,7 @@ class ArtworkImporter : ArtworkImporting { } catch { ELOG("Couldn't update game \(onlyMatch.title) with new artwork URL") } - return onlyMatch + return onlyMatch as? PVGame } else { ELOG("We got to the unlikely scenario where an extension is possibly a CD binary file, or belongs to a system, and had multiple games that matched the filename under more than one core.") return nil diff --git a/PVLibrary/Sources/PVLibrary/Importer/Services/GameImporter/CDFileHandler.swift b/PVLibrary/Sources/PVLibrary/Importer/Services/GameImporter/CDFileHandler.swift index 186fd45012..bd38168764 100644 --- a/PVLibrary/Sources/PVLibrary/Importer/Services/GameImporter/CDFileHandler.swift +++ b/PVLibrary/Sources/PVLibrary/Importer/Services/GameImporter/CDFileHandler.swift @@ -6,6 +6,7 @@ // import Foundation +import PVPrimitives protocol CDFileHandling { func findAssociatedBinFileNames(for cueFileItem: ImportQueueItem) throws -> [String] diff --git a/PVLibrary/Sources/PVLibrary/Importer/Services/GameImporter/GameImporter.swift b/PVLibrary/Sources/PVLibrary/Importer/Services/GameImporter/GameImporter.swift index 575052a3ce..f87c053387 100644 --- a/PVLibrary/Sources/PVLibrary/Importer/Services/GameImporter/GameImporter.swift +++ b/PVLibrary/Sources/PVLibrary/Importer/Services/GameImporter/GameImporter.swift @@ -102,22 +102,41 @@ public typealias GameImporterFinishedImportingGameHandler = (_ md5Hash: String, public typealias GameImporterFinishedGettingArtworkHandler = (_ artworkURL: String?) -> Void public protocol GameImporting { + + typealias ImportQueueItemType = ImportQueueItem + func initSystems() async var importStatus: String { get } - var importQueue: [ImportQueueItem] { get } + var importQueue: [ImportQueueItemType] { get } var processingState: ProcessingState { get } func addImport(_ item: ImportQueueItem) func addImports(forPaths paths: [URL]) + func addImports(forPaths paths: [URL], targetSystem: AnySystem) + func removeImports(at offsets: IndexSet) func startProcessing() - func sortImportQueueItems(_ importQueueItems: [ImportQueueItem]) -> [ImportQueueItem] + func sortImportQueueItems(_ importQueueItems: [ImportQueueItemType]) -> [ImportQueueItemType] - func importQueueContainsDuplicate(_ queue: [ImportQueueItem], ofItem queueItem: ImportQueueItem) -> Bool + func importQueueContainsDuplicate(_ queue: [ImportQueueItemType], ofItem queueItem: ImportQueueItemType) -> Bool + + var importStartedHandler: GameImporterImportStartedHandler? { get set } + /// Closure called when import completes + var completionHandler: GameImporterCompletionHandler? { get set } + /// Closure called when a game finishes importing + var finishedImportHandler: GameImporterFinishedImportingGameHandler? { get set } + /// Closure called when artwork finishes downloading + var finishedArtworkHandler: GameImporterFinishedGettingArtworkHandler? { get set } + + /// Spotlight Handerls + /// Closure called when spotlight completes + var spotlightCompletionHandler: GameImporterCompletionHandler? { get set } + /// Closure called when a game finishes importing + var spotlightFinishedImportHandler: GameImporterFinishedImportingGameHandler? { get set } } @@ -182,9 +201,9 @@ public final class GameImporter: GameImporting, ObservableObject { public var processingState: ProcessingState = .idle // Observable state for processing status internal var gameImporterFileService:GameImporterFileServicing - internal var gameImporterDatabaseService:GameImporterDatabaseServicing - internal var gameImporterSystemsService:GameImporterSystemsServicing - internal var gameImporterArtworkImporter:ArtworkImporting + internal var gameImporterDatabaseService:any GameImporterDatabaseServicing + internal var gameImporterSystemsService:any GameImporterSystemsServicing + internal var gameImporterArtworkImporter:any ArtworkImporting internal var cdRomFileHandler:CDFileHandling // MARK: - Paths @@ -198,7 +217,7 @@ public final class GameImporter: GameImporting, ObservableObject { /// Path to the BIOS directory public var biosPath: URL { get { Paths.biosesPath }} - public var databaseService: GameImporterDatabaseServicing { + public var databaseService: any GameImporterDatabaseServicing { return gameImporterDatabaseService } @@ -360,13 +379,13 @@ public final class GameImporter: GameImporting, ObservableObject { } } - public func addImports(forPaths paths: [URL], targetSystem: PVSystem) { + public func addImports(forPaths paths: [URL], targetSystem: AnySystem) { importQueueLock.lock() defer { importQueueLock.unlock() } for path in paths { var item = ImportQueueItem(url: path, fileType: .unknown) - item.userChosenSystem = targetSystem + item.userChosenSystem?.identifier = targetSystem.identifier self.addImportItemToQueue(item) } } diff --git a/PVLibrary/Sources/PVLibrary/Importer/Services/GameImporter/GameImporterDatabaseService.swift b/PVLibrary/Sources/PVLibrary/Importer/Services/GameImporter/GameImporterDatabaseService.swift index c904ca5e05..9d1e12e635 100644 --- a/PVLibrary/Sources/PVLibrary/Importer/Services/GameImporter/GameImporterDatabaseService.swift +++ b/PVLibrary/Sources/PVLibrary/Importer/Services/GameImporter/GameImporterDatabaseService.swift @@ -22,20 +22,31 @@ import Perception import SwiftUI public protocol GameImporterDatabaseServicing { + // TODO: Make me more generic +// associatedtype GameType: PVGameLibraryEntry + typealias GameType = PVGame // PVGameLibraryEntry + func setOpenVGDB(_ vgdb: OpenVGDB) func setRomsPath(url:URL) func importGameIntoDatabase(queueItem: ImportQueueItem) async throws func importBIOSIntoDatabase(queueItem: ImportQueueItem) async throws - func getUpdatedGameInfo(for game: PVGame, forceRefresh: Bool) -> PVGame - func getArtwork(forGame game: PVGame) async -> PVGame + func getUpdatedGameInfo(for game: GameType, forceRefresh: Bool) -> GameType + func getArtwork(forGame game: GameType) async -> GameType +} + +extension CharacterSet { + var GameImporterDatabaseServiceCharset: CharacterSet { + GameImporterDatabaseServiceCharset + } } +fileprivate let GameImporterDatabaseServiceCharset: CharacterSet = { + var c = CharacterSet.punctuationCharacters + c.remove(charactersIn: ",-+&.'") + return c +}() class GameImporterDatabaseService : GameImporterDatabaseServicing { - static var charset: CharacterSet = { - var c = CharacterSet.punctuationCharacters - c.remove(charactersIn: ",-+&.'") - return c - }() + var romsPath:URL? var openVGDB: OpenVGDB? @@ -81,7 +92,7 @@ class GameImporterDatabaseService : GameImporterDatabaseServicing { await saveRelativePath(existingGame, partialPath: partialPath, file: queueItem.url) } else { DLOG("No existing game found, starting import to database") - try await self.importToDatabaseROM(forItem: queueItem, system: targetSystem, relatedFiles: nil) + try await self.importToDatabaseROM(forItem: queueItem, system: targetSystem as! AnySystem, relatedFiles: nil) } } @@ -118,7 +129,7 @@ class GameImporterDatabaseService : GameImporterDatabaseServicing { } /// Imports a ROM to the database - internal func importToDatabaseROM(forItem queueItem: ImportQueueItem, system: PVSystem, relatedFiles: [URL]?) async throws { + internal func importToDatabaseROM(forItem queueItem: ImportQueueItem, system: AnySystem, relatedFiles: [URL]?) async throws { guard let _ = queueItem.destinationUrl else { //how did we get here, throw? @@ -134,6 +145,10 @@ class GameImporterDatabaseService : GameImporterDatabaseServicing { DLOG("Creating game object with title: \(title), partialPath: \(partialPath)") + guard let system = RomDatabase.sharedInstance.object(ofType: PVSystem.self, wherePrimaryKeyEquals: system.identifier) else { + throw GameImporterError.noSystemMatched + } + let file = PVFile(withURL: queueItem.destinationUrl!) let game = PVGame(withFile: file, system: system) game.romPath = partialPath @@ -371,7 +386,7 @@ class GameImporterDatabaseService : GameImporterDatabaseServicing { if resultsMaybe == nil || resultsMaybe!.isEmpty { //PVEmulatorConfiguration.supportedROMFileExtensions.contains(game.file.url.pathExtension.lowercased()) { let fileName: String = game.file.url.lastPathComponent // Remove any extraneous stuff in the rom name such as (U), (J), [T+Eng] etc - let nonCharRange: NSRange = (fileName as NSString).rangeOfCharacter(from: GameImporterDatabaseService.charset) + let nonCharRange: NSRange = (fileName as NSString).rangeOfCharacter(from: GameImporterDatabaseServiceCharset) var gameTitleLen: Int if nonCharRange.length > 0, nonCharRange.location > 1 { gameTitleLen = nonCharRange.location - 1 diff --git a/PVLibrary/Sources/PVLibrary/Importer/Services/GameImporter/GameImporterFileService.swift b/PVLibrary/Sources/PVLibrary/Importer/Services/GameImporter/GameImporterFileService.swift index c0b57cc09a..e08d2a6a44 100644 --- a/PVLibrary/Sources/PVLibrary/Importer/Services/GameImporter/GameImporterFileService.swift +++ b/PVLibrary/Sources/PVLibrary/Importer/Services/GameImporter/GameImporterFileService.swift @@ -8,6 +8,7 @@ import Foundation import PVSupport import RealmSwift +import PVPrimitives protocol GameImporterFileServicing { func moveImportItem(toAppropriateSubfolder queueItem: ImportQueueItem) async throws @@ -91,7 +92,7 @@ class GameImporterFileService : GameImporterFileServicing { // MARK: - Utility - internal func moveChildImports(forQueueItem queueItem:ImportQueueItem, to destinationFolder:URL) async throws { + internal func moveChildImports(forQueueItem queueItem: ImportQueueItem, to destinationFolder: URL) async throws { guard !queueItem.childQueueItems.isEmpty else { return } diff --git a/PVLibrary/Sources/PVLibrary/Importer/Services/GameImporter/GameImporterSystemsService.swift b/PVLibrary/Sources/PVLibrary/Importer/Services/GameImporter/GameImporterSystemsService.swift index 24cd4861fa..384f0e54bc 100644 --- a/PVLibrary/Sources/PVLibrary/Importer/Services/GameImporter/GameImporterSystemsService.swift +++ b/PVLibrary/Sources/PVLibrary/Importer/Services/GameImporter/GameImporterSystemsService.swift @@ -22,13 +22,21 @@ import Perception import SwiftUI protocol GameImporterSystemsServicing { + // TODO: Make me more generic + // associatedtype GameType: PVGameLibraryEntry + typealias GameType = PVGame // PVGameLibraryEntry + typealias SystemType = PVSystem //AnySystem + func setOpenVGDB(_ vgdb: OpenVGDB) func setExtensionsToSystemMapping(_ mapping: [String: [String]]) - func determineSystems(for queueItem: ImportQueueItem) async throws -> [PVSystem] - func findAnyCurrentGameThatCouldBelongToAnyOfTheseSystems(_ systems: [PVSystem]?, romFilename: String) -> [PVGame]? + func determineSystems(for queueItem: ImportQueueItem) async throws -> [SystemType] + func findAnyCurrentGameThatCouldBelongToAnyOfTheseSystems(_ systems: [AnySystem]?, romFilename: String) -> [GameType]? } -class GameImporterSystemsService : GameImporterSystemsServicing { +class GameImporterSystemsService: GameImporterSystemsServicing { + typealias GameType = PVGame + + var openVGDB: OpenVGDB? /// Map of ROM extensions to their corresponding system identifiers var romExtensionToSystemsMap = [String: [String]]() @@ -46,7 +54,7 @@ class GameImporterSystemsService : GameImporterSystemsServicing { } /// Determines the system for a given candidate file - public func determineSystems(for queueItem: ImportQueueItem) async throws -> [PVSystem] { + public func determineSystems(for queueItem: ImportQueueItem) async throws -> [SystemType] { guard let md5 = queueItem.md5?.uppercased() else { throw GameImporterError.couldNotCalculateMD5 } @@ -80,7 +88,7 @@ class GameImporterSystemsService : GameImporterSystemsServicing { } } - var matchingSystems:[PVSystem] = [] + var matchingSystems:[SystemType] = [] // Try to find system by MD5 using OpenVGDB if let results = try openVGDB?.searchDatabase(usingKey: "romHashMD5", value: md5), @@ -135,13 +143,13 @@ class GameImporterSystemsService : GameImporterSystemsServicing { } /// Determines the system for a given candidate file - private func determineSystemsFromContent(for queueItem: ImportQueueItem, possibleSystems: [PVSystem]) throws -> [PVSystem] { + private func determineSystemsFromContent(for queueItem: ImportQueueItem, possibleSystems: [SystemType]) throws -> [SystemType] { // Implement logic to determine system based on file content or metadata // This could involve checking file headers, parsing content, or using a database of known games let fileName = queueItem.url.deletingPathExtension().lastPathComponent - var matchedSystems:[PVSystem] = [] + var matchedSystems:[SystemType] = [] for system in possibleSystems { do { if let results = try openVGDB?.searchDatabase(usingFilename: fileName, systemID: system.openvgDatabaseID), @@ -234,7 +242,7 @@ class GameImporterSystemsService : GameImporterSystemsServicing { } //TODO: is this called? - internal func determineSystemByMD5(_ queueItem: ImportQueueItem) async throws -> PVSystem? { + internal func determineSystemByMD5(_ queueItem: ImportQueueItem) async throws -> SystemType? { guard let md5 = queueItem.md5?.uppercased() else { throw GameImporterError.couldNotCalculateMD5 } @@ -256,7 +264,7 @@ class GameImporterSystemsService : GameImporterSystemsServicing { //TODO: is this called? /// Determines the systems for a given path - internal func determineSystems(for path: URL, chosenSystem: System?) throws -> [PVSystem] { + internal func determineSystems(for path: URL, chosenSystem: SystemType?) throws -> [SystemType] { if let chosenSystem = chosenSystem { if let system = RomDatabase.systemCache[chosenSystem.identifier] { return [system] @@ -268,7 +276,7 @@ class GameImporterSystemsService : GameImporterSystemsServicing { } /// Finds any current game that could belong to any of the given systems - func findAnyCurrentGameThatCouldBelongToAnyOfTheseSystems(_ systems: [PVSystem]?, romFilename: String) -> [PVGame]? { + func findAnyCurrentGameThatCouldBelongToAnyOfTheseSystems(_ systems: [AnySystem]?, romFilename: String) -> [GameType]? { // Check if existing ROM let allGames = RomDatabase.gamesCache.values.filter ({ @@ -475,7 +483,7 @@ class GameImporterSystemsService : GameImporterSystemsServicing { /// Checks if a file content matches a given system - private func doesFileContentMatch(_ queueItem: ImportQueueItem, forSystem system: PVSystem) -> Bool { + private func doesFileContentMatch(_ queueItem: ImportQueueItem, forSystem system: AnySystem) -> Bool { // Implement system-specific file content matching logic here // This could involve checking file headers, file structure, or other system-specific traits // For now, we'll return false as a placeholder diff --git a/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/PVLibraryEntry.swift b/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/PVLibraryEntry.swift index 42a5efcb09..38ff989af1 100644 --- a/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/PVLibraryEntry.swift +++ b/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/PVLibraryEntry.swift @@ -21,6 +21,7 @@ public protocol PVRecentGameLibraryEntry: PVLibraryEntry { dynamic var core: PVCore? { get } } +public typealias AnyPVGameLibraryEntry = any PVGameLibraryEntry public protocol PVGameLibraryEntry: PVLibraryEntry { dynamic var title: String { get } @@ -30,7 +31,7 @@ public protocol PVGameLibraryEntry: PVLibraryEntry { dynamic var file: PVFile! { get } var relatedFiles: List { get } - dynamic var customArtworkURL: String { get } + dynamic var customArtworkURL: String { get set } dynamic var originalArtworkURL: String { get } dynamic var originalArtworkFile: PVImageFile? { get } diff --git a/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/PVSystem.swift b/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/PVSystem.swift index 6c67ddc473..edb212ab34 100644 --- a/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/PVSystem.swift +++ b/PVLibrary/Sources/PVRealm/RealmPlatform/Entities/PVSystem.swift @@ -140,6 +140,23 @@ public final class PVSystem: Object, Identifiable, SystemProtocol { } } +/// Mock testing +public extension PVSystem { + public convenience init( + identifier: String, + name: String, + shortName: String, + manufacturer: String, + screenType: ScreenType = .crt + ) { + self.init() + self.identifier = id + self.name = name + self.shortName = shortName + self.manufacturer = manufacturer + } +} + public extension PVSystem { var screenType: ScreenType { get { diff --git a/PVPrimitives/Sources/PVPrimitives/Protocols/SystemProtocol.swift b/PVPrimitives/Sources/PVPrimitives/Protocols/SystemProtocol.swift index 8671c22726..cdc52034d1 100644 --- a/PVPrimitives/Sources/PVPrimitives/Protocols/SystemProtocol.swift +++ b/PVPrimitives/Sources/PVPrimitives/Protocols/SystemProtocol.swift @@ -8,7 +8,10 @@ import Foundation -public protocol SystemProtocol { +public typealias _SystemProtocol = SystemProtocol & Identifiable & ObservableObject & Hashable +public typealias AnySystem = any _SystemProtocol + +public protocol SystemProtocol : Hashable { associatedtype BIOSInfoProviderType: BIOSInfoProvider var name: String { get } @@ -43,6 +46,15 @@ public protocol SystemProtocol { } // MARK: Default Implimentations +public extension Identifiable where Self: SystemProtocol { + public var id: String { identifier } +} + +public extension Hashable where Self: SystemProtocol { + public func hash(into hasher: inout Hasher) { + hasher.combine(identifier) + } +} public extension SystemProtocol { var options: SystemOptions { diff --git a/PVUI/Sources/PVSwiftUI/ContinuesManagementView/ContinuesMagementView.swift b/PVUI/Sources/PVSwiftUI/ContinuesManagementView/ContinuesMagementView.swift index fddbe759f3..c82bd571f7 100644 --- a/PVUI/Sources/PVSwiftUI/ContinuesManagementView/ContinuesMagementView.swift +++ b/PVUI/Sources/PVSwiftUI/ContinuesManagementView/ContinuesMagementView.swift @@ -398,6 +398,8 @@ public struct ContinuesMagementView: View { .onAppear { viewModel.subscribeToDriverPublisher() } + .presentationDetents([.medium, .large]) + .presentationDragIndicator(.visible) } } diff --git a/PVUI/Sources/PVSwiftUI/Imports/ImportStatusTesting/MockGameImporter.swift b/PVUI/Sources/PVSwiftUI/Imports/ImportStatusTesting/MockGameImporter.swift new file mode 100644 index 0000000000..bc7581b0e8 --- /dev/null +++ b/PVUI/Sources/PVSwiftUI/Imports/ImportStatusTesting/MockGameImporter.swift @@ -0,0 +1,128 @@ +// +// MockGameImporter.swift +// UITesting +// +// Created by Joseph Mattiello on 11/27/24. +// + +import PVSwiftUI +import SwiftUI +import Combine +import PVPrimitives + +public +class MockGameImporter: GameImporting, ObservableObject { + @Published private(set) public var importStatus: String = "Ready" + @Published public var importQueue: [ImportQueueItem] = [] + @Published private(set) public var processingState: ProcessingState = .idle + + public init(importStatus: String = "", importQueue: [ImportQueueItem] = [], processingState: ProcessingState = .idle, importStartedHandler: GameImporterImportStartedHandler? = nil, completionHandler: GameImporterCompletionHandler? = nil, finishedImportHandler: GameImporterFinishedImportingGameHandler? = nil, finishedArtworkHandler: GameImporterFinishedGettingArtworkHandler? = nil, spotlightCompletionHandler: GameImporterCompletionHandler? = nil, spotlightFinishedImportHandler: GameImporterFinishedImportingGameHandler? = nil) { + self.importStatus = importStatus + self.importQueue = importQueue + self.processingState = processingState + self.importStartedHandler = importStartedHandler + self.completionHandler = completionHandler + self.finishedImportHandler = finishedImportHandler + self.finishedArtworkHandler = finishedArtworkHandler + self.spotlightCompletionHandler = spotlightCompletionHandler + self.spotlightFinishedImportHandler = spotlightFinishedImportHandler + + if self.importQueue.isEmpty { + self.importQueue = [{ + let item = ImportQueueItem(url: .init(fileURLWithPath: "test.bin"), fileType: .unknown) + item.systems = [ + PVSystem(identifier: "com.provenance.jaguar", name: "Jaguar", shortName: "Jag", manufacturer: "Atari", screenType: .crt), + PVSystem(identifier: "com.provenance.jaguarcd", name: "Jaguar CD", shortName: "Jag", manufacturer: "Atari", screenType: .crt) + ] + return item + }(), + ImportQueueItem(url: .init(fileURLWithPath: "test.jpg"), fileType: .artwork), + ImportQueueItem(url: .init(fileURLWithPath: "bios.bin"), fileType: .bios), + ImportQueueItem(url: .init(fileURLWithPath: "test.cue"), fileType: .cdRom), + ImportQueueItem(url: .init(fileURLWithPath: "test.jag"), fileType: .game) + ] + } + } + + public func initSystems() async { + // Mock implementation - no real systems to initialize + importStatus = "Systems initialized" + } + + public func addImport(_ item: ImportQueueItem) { + if !importQueueContainsDuplicate(importQueue, ofItem: item) { + importQueue.append(item) + importStatus = "Added \(item.url.lastPathComponent) to queue" + } + } + + public func addImports(forPaths paths: [URL], targetSystem: AnySystem) { + for path in paths { + let item = ImportQueueItem(url: path, fileType: .unknown) + item.userChosenSystem = targetSystem as? PVSystem // TODO: change when generic system types supported + addImport(item) + } + importStatus = "Added \(paths.count) items to queue" + } + + + public func addImports(forPaths paths: [URL]) { + for path in paths { + let item = ImportQueueItem(url: path, fileType: .unknown) + addImport(item) + } + importStatus = "Added \(paths.count) items to queue" + } + + public func removeImports(at offsets: IndexSet) { + importQueue.remove(atOffsets: offsets) + importStatus = "Removed \(offsets.count) items from queue" + } + + public func startProcessing() { + guard !importQueue.isEmpty else { + importStatus = "No items in queue" + return + } + + processingState = .processing + + // Simulate processing by marking all items as successful after a delay + Task { + importStatus = "Processing \(importQueue.count) items..." + + // Simulate some processing time + try? await Task.sleep(nanoseconds: 2_000_000_000) // 2 seconds + + for index in importQueue.indices { + importQueue[index].status = ImportStatus(rawValue: index % ImportStatus.allCases.count)! + } + + processingState = .idle + importStatus = "Completed processing \(importQueue.count) items" + } + } + + public func sortImportQueueItems(_ importQueueItems: [ImportQueueItem]) -> [ImportQueueItem] { + importQueueItems.sorted(by: { $0.url.lastPathComponent < $1.url.lastPathComponent }) + } + + public func importQueueContainsDuplicate(_ queue: [ImportQueueItem], ofItem queueItem: ImportQueueItem) -> Bool { + queue.contains(where: { $0.url == queueItem.url }) + } + + public var importStartedHandler: GameImporterImportStartedHandler? = nil + /// Closure called when import completes + public var completionHandler: GameImporterCompletionHandler? = nil + /// Closure called when a game finishes importing + public var finishedImportHandler: GameImporterFinishedImportingGameHandler? = nil + /// Closure called when artwork finishes downloading + public var finishedArtworkHandler: GameImporterFinishedGettingArtworkHandler? = nil + + /// Spotlight Handerls + /// Closure called when spotlight completes + public var spotlightCompletionHandler: GameImporterCompletionHandler? = nil + /// Closure called when a game finishes importing + public var spotlightFinishedImportHandler: GameImporterFinishedImportingGameHandler? = nil + +} diff --git a/PVUI/Sources/PVSwiftUI/Imports/ImportStatusTesting/MockImportStatusDelegate.swift b/PVUI/Sources/PVSwiftUI/Imports/ImportStatusTesting/MockImportStatusDelegate.swift new file mode 100644 index 0000000000..19c446416c --- /dev/null +++ b/PVUI/Sources/PVSwiftUI/Imports/ImportStatusTesting/MockImportStatusDelegate.swift @@ -0,0 +1,41 @@ +// +// MockImportStatusDelegate.swift +// UITesting +// +// Created by Joseph Mattiello on 11/27/24. +// + +import PVSwiftUI + +public class MockImportStatusDriverData: ObservableObject { + @MainActor + public let gameImporter = MockGameImporter() //AppState.shared.gameImporter ?? GameImporter.shared + @MainActor + public let pvgamelibraryUpdatesController: PVGameLibraryUpdatesController + + public var isPresent: Bool = false + + @MainActor + public init() { + pvgamelibraryUpdatesController = .init(gameImporter: gameImporter) + } +} + +extension MockImportStatusDriverData: ImportStatusDelegate { + public func dismissAction() { + + } + + public func addImportsAction() { + let importItems: [ImportQueueItem] = [ + .init(url: .init(string: "test.jag")!, fileType: .unknown) + ] + importItems.forEach { + gameImporter.addImport($0) + } + } + + public func forceImportsAction() { + gameImporter.startProcessing() + } +} diff --git a/PVUI/Sources/PVSwiftUI/Imports/ImportStatusView.swift b/PVUI/Sources/PVSwiftUI/Imports/ImportStatusView.swift index f308bab315..18061f76a8 100644 --- a/PVUI/Sources/PVSwiftUI/Imports/ImportStatusView.swift +++ b/PVUI/Sources/PVSwiftUI/Imports/ImportStatusView.swift @@ -7,6 +7,7 @@ // import SwiftUI +import PVUIBase import PVLibrary import PVThemes import Perception @@ -33,22 +34,34 @@ func iconNameForFileType(_ type: FileType) -> String { } } -struct ImportStatusView: View { - @ObservedObject var updatesController: PVGameLibraryUpdatesController - var gameImporter:GameImporter - weak var delegate:ImportStatusDelegate! +public struct ImportStatusView: View { + @ObservedObject + public var updatesController: PVGameLibraryUpdatesController + public var gameImporter:any GameImporting + public weak var delegate:ImportStatusDelegate! + public var dismissAction: (() -> Void)? = nil + + @ObservedObject private var themeManager = ThemeManager.shared + var currentPalette: any UXThemePalette { themeManager.currentPalette } + + public init(updatesController: PVGameLibraryUpdatesController, gameImporter: any GameImporting, delegate: ImportStatusDelegate, dismissAction: (() -> Void)? = nil) { + self.updatesController = updatesController + self.gameImporter = gameImporter + self.delegate = delegate + self.dismissAction = dismissAction + } private func deleteItems(at offsets: IndexSet) { gameImporter.removeImports(at: offsets) } - var body: some View { + public var body: some View { WithPerceptionTracking { NavigationView { List { if gameImporter.importQueue.isEmpty { Text("No items in the import queue") - .foregroundColor(.gray) + .foregroundColor(.secondary) .padding() } else { ForEach(gameImporter.importQueue) { item in @@ -62,7 +75,10 @@ struct ImportStatusView: View { .toolbar { ToolbarItemGroup(placement: .topBarLeading, content: { - Button("Done") { delegate.dismissAction() + if dismissAction != nil { + Button("Done") { delegate.dismissAction() + } + .tint(currentPalette.defaultTintColor?.swiftUIColor) } }) ToolbarItemGroup(placement: .topBarTrailing, @@ -70,18 +86,37 @@ struct ImportStatusView: View { Button("Add Files") { delegate?.addImportsAction() } + .tint(currentPalette.defaultTintColor?.swiftUIColor) Button("Begin") { delegate?.forceImportsAction() } + .tint(currentPalette.defaultTintColor?.swiftUIColor) }) } + .background(currentPalette.gameLibraryBackground.swiftUIColor) } + .background(currentPalette.gameLibraryBackground.swiftUIColor) .presentationDetents([.medium, .large]) .presentationDragIndicator(.visible) } } } -//#Preview { -// -//} +#if DEBUG +#Preview { + @ObservedObject var themeManager = ThemeManager.shared + var currentPalette: any UXThemePalette { themeManager.currentPalette } + + let mockImportStatusDriverData = MockImportStatusDriverData() + + ImportStatusView( + updatesController: mockImportStatusDriverData.pvgamelibraryUpdatesController, + gameImporter: mockImportStatusDriverData.gameImporter, + delegate: mockImportStatusDriverData) { + print("Import Status View Closed") + } + .onAppear { + themeManager.setCurrentPalette(CGAThemes.green.palette) + } +} +#endif diff --git a/PVUI/Sources/PVSwiftUI/Imports/ImportTaskRowView.swift b/PVUI/Sources/PVSwiftUI/Imports/ImportTaskRowView.swift index 11bb685d38..e0c9fa80ff 100644 --- a/PVUI/Sources/PVSwiftUI/Imports/ImportTaskRowView.swift +++ b/PVUI/Sources/PVSwiftUI/Imports/ImportTaskRowView.swift @@ -33,41 +33,54 @@ struct ImportTaskRowView: View { let item: ImportQueueItem @State private var isNavigatingToSystemSelection = false @ObservedObject private var themeManager = ThemeManager.shared - + var currentPalette: any UXThemePalette { themeManager.currentPalette } + var bgColor: Color { themeManager.currentPalette.settingsCellBackground?.swiftUIColor ?? themeManager.currentPalette.menuBackground.swiftUIColor } + + var fgColor: Color { + themeManager.currentPalette.settingsCellText?.swiftUIColor ?? themeManager.currentPalette.menuText.swiftUIColor + } + + var secondaryFgColor: Color { + themeManager.currentPalette.settingsCellTextDetail?.swiftUIColor ?? .secondary + } var body: some View { WithPerceptionTracking { HStack { - //TODO: add icon for fileType +// // TODO: add icon for fileType VStack(alignment: .leading) { Text(item.url.lastPathComponent) .font(.headline) + .foregroundColor(fgColor) + if item.fileType == .bios { Text("BIOS") .font(.subheadline) - .foregroundColor(themeManager.currentPalette.gameLibraryText.swiftUIColor) + .foregroundColor(fgColor) } else if !item.systems.isEmpty { Text("\(item.systems.count) systems") .font(.subheadline) - .foregroundColor(themeManager.currentPalette.gameLibraryText.swiftUIColor) + .foregroundColor(secondaryFgColor) } if item.status == .failure, let errorText = item.errorValue { Text("Error Detail: \(errorText)") .font(.subheadline) - .foregroundColor(themeManager.currentPalette.gameLibraryText.swiftUIColor) + .foregroundColor(secondaryFgColor) } - } Spacer() VStack(alignment: .trailing) { if item.status == .processing { - ProgressView().progressViewStyle(.circular).frame(width: 40, height: 40, alignment: .center) + ProgressView() + .progressViewStyle(.circular) + .frame(width: 40, height: 40, alignment: .center) +// .tint(themeManager.currentPalette.defaultTintColor!) } else { Image(systemName: iconNameForStatus(item.status)) .foregroundColor(item.status.color) @@ -76,7 +89,7 @@ struct ImportTaskRowView: View { if (item.childQueueItems.count > 0) { Text("+\(item.childQueueItems.count) files") .font(.subheadline) - .foregroundColor(themeManager.currentPalette.gameLibraryText.swiftUIColor) + .foregroundColor(fgColor) } } @@ -87,6 +100,7 @@ struct ImportTaskRowView: View { .fill(bgColor) .shadow(radius: 2) ) + //.background(currentPalette.gameLibraryBackground.swiftUIColor) .onTapGesture { if item.status == .conflict { isNavigatingToSystemSelection = true @@ -101,3 +115,18 @@ struct ImportTaskRowView: View { } } } +#if DEBUG +import PVThemes +#Preview { + List { + ImportTaskRowView(item: .init(url: .init(fileURLWithPath: "Test.jpg"), fileType: .artwork)) + ImportTaskRowView(item: .init(url: .init(fileURLWithPath: "Test.bin"), fileType: .bios)) + ImportTaskRowView(item: .init(url: .init(fileURLWithPath: "Test.iso"), fileType: .cdRom)) + ImportTaskRowView(item: .init(url: .init(fileURLWithPath: "Test.jag"), fileType: .game)) + ImportTaskRowView(item: .init(url: .init(fileURLWithPath: "Test.zip"), fileType: .unknown)) + } + .onAppear { + ThemeManager.shared.setCurrentPalette(CGAThemes.green.palette) + } +} +#endif diff --git a/PVUI/Sources/PVSwiftUI/Imports/SystemSelectionView.swift b/PVUI/Sources/PVSwiftUI/Imports/SystemSelectionView.swift index 71074b7b8f..bacfbd6689 100644 --- a/PVUI/Sources/PVSwiftUI/Imports/SystemSelectionView.swift +++ b/PVUI/Sources/PVSwiftUI/Imports/SystemSelectionView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import PVLibrary struct SystemSelectionView: View { @ObservedObject var item: ImportQueueItem @@ -33,6 +34,37 @@ struct SystemSelectionView: View { } } +#if DEBUG +import PVPrimitives #Preview { + + let item: ImportQueueItem = { + let systems: [PVSystem] = [ + .init( + identifier: "com.provenance.jaguar", + name: "Jaguar", + shortName: "Jag", + manufacturer: "Atari", + screenType: .crt + ), + .init( + identifier: "com.provenance.jaguarcd", + name: "Jaguar CD", + shortName: "Jag CD", + manufacturer: "Atari", + screenType: .crt + ) + ] + + let item = ImportQueueItem(url: .init(fileURLWithPath: "Test.bin"), + fileType: .game) + item.systems = systems + return item + }() + + List { + SystemSelectionView(item: item) + } } +#endif diff --git a/PVUI/Sources/PVSwiftUI/Settings/SettingsSwiftUI.swift b/PVUI/Sources/PVSwiftUI/Settings/SettingsSwiftUI.swift index df4fc36ee7..d8dcaab710 100644 --- a/PVUI/Sources/PVSwiftUI/Settings/SettingsSwiftUI.swift +++ b/PVUI/Sources/PVSwiftUI/Settings/SettingsSwiftUI.swift @@ -30,7 +30,7 @@ public struct PVSettingsView: View { @StateObject private var viewModel: PVSettingsViewModel @ObservedObject private var themeManager = ThemeManager.shared - var dismissAction: () -> Void // Add this + var dismissAction: () -> Void weak var menuDelegate: PVMenuDelegate! @ObservedObject var conflictsController: PVGameLibraryUpdatesController diff --git a/PVUI/Sources/PVUIBase/Game Library/PVGameLibraryUpdatesController.swift b/PVUI/Sources/PVUIBase/Game Library/PVGameLibraryUpdatesController.swift index 4e43cad59e..53db68d48b 100644 --- a/PVUI/Sources/PVUIBase/Game Library/PVGameLibraryUpdatesController.swift +++ b/PVUI/Sources/PVUIBase/Game Library/PVGameLibraryUpdatesController.swift @@ -27,7 +27,7 @@ public final class PVGameLibraryUpdatesController: ObservableObject { @Published public var conflicts: [ConflictsController.ConflictItem] = [] - public let gameImporter: GameImporter + public var gameImporter: any GameImporting private let directoryWatcher: DirectoryWatcher private let conflictsWatcher: ConflictsWatcher @@ -39,7 +39,7 @@ public final class PVGameLibraryUpdatesController: ObservableObject { AppState.shared.hudCoordinator } - public init(gameImporter: GameImporter, importPath: URL? = nil) { + public init(gameImporter: any GameImporting, importPath: URL? = nil) { let importPath = importPath ?? Paths.romsImportPath self.gameImporter = gameImporter @@ -90,11 +90,13 @@ public final class PVGameLibraryUpdatesController: ObservableObject { } } + var statusExtractionSateObserver: Task? private func setupExtractionStatusObserver() { let taskID = UUID() DLOG("Starting extraction status observer task: \(taskID)") + statusExtractionSateObserver?.cancel() - Task { + statusExtractionSateObserver = Task { defer { DLOG("Ending extraction status observer task: \(taskID)") } var hideTask: Task? @@ -123,7 +125,7 @@ public final class PVGameLibraryUpdatesController: ObservableObject { isHidingHUD = false } case .idle: - Task { + hideTask = Task { let hudState: HudState = .hidden await self.hudCoordinator.updateHUD(hudState) } diff --git a/Provenance.xcodeproj/project.pbxproj b/Provenance.xcodeproj/project.pbxproj index 2032ce61e6..075164498a 100644 --- a/Provenance.xcodeproj/project.pbxproj +++ b/Provenance.xcodeproj/project.pbxproj @@ -816,16 +816,12 @@ B3E2AA222CB6306600E2636D /* PVYabause.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B3E2AA202CB6306600E2636D /* PVYabause.framework */; platformFilters = (maccatalyst, macos, ); settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; B3E2AA252CB6328100E2636D /* PVVecX.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B3E2AA242CB6328100E2636D /* PVVecX.framework */; }; B3E2AA262CB6328100E2636D /* PVVecX.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B3E2AA242CB6328100E2636D /* PVVecX.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - B3E2AA2C2CB6340F00E2636D /* PVPCSXRearmed.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B3E2AA2B2CB6340F00E2636D /* PVPCSXRearmed.framework */; platformFilter = maccatalyst; }; - B3E2AA2D2CB6341000E2636D /* PVPCSXRearmed.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B3E2AA2B2CB6340F00E2636D /* PVPCSXRearmed.framework */; platformFilter = maccatalyst; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; B3E2AA302CB6346900E2636D /* PVOpera.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B3E2AA2F2CB6346900E2636D /* PVOpera.framework */; }; B3E2AA312CB6346900E2636D /* PVOpera.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B3E2AA2F2CB6346900E2636D /* PVOpera.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; B3E2AA382CB635BB00E2636D /* PVGME.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B3E2AA372CB635BB00E2636D /* PVGME.framework */; }; B3E2AA392CB635BB00E2636D /* PVGME.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B3E2AA372CB635BB00E2636D /* PVGME.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; B3E2AA3E2CB6362C00E2636D /* PVGearcoleco.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B3E2AA3D2CB6362C00E2636D /* PVGearcoleco.framework */; }; B3E2AA3F2CB6362D00E2636D /* PVGearcoleco.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B3E2AA3D2CB6362C00E2636D /* PVGearcoleco.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - B3E2AA412CB6369800E2636D /* PVFlycast.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B3E2AA402CB6369800E2636D /* PVFlycast.framework */; platformFilters = (maccatalyst, macos, ); }; - B3E2AA422CB6369800E2636D /* PVFlycast.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B3E2AA402CB6369800E2636D /* PVFlycast.framework */; platformFilters = (maccatalyst, macos, ); settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; B3E2AA482CB6381400E2636D /* PVEP128Emu.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B3E2AA472CB6381400E2636D /* PVEP128Emu.framework */; }; B3E2AA492CB6381500E2636D /* PVEP128Emu.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B3E2AA472CB6381400E2636D /* PVEP128Emu.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; B3E2AA4E2CB638F400E2636D /* PVDesmume2015.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B3E2AA4D2CB638F400E2636D /* PVDesmume2015.framework */; }; @@ -1555,12 +1551,10 @@ B38FB4A92CBEF6B0006786C6 /* libavcodec.xcframework in Embed Frameworks */, B33BD1C92C6A6261001E933A /* PVStella-Dynamic in Embed Frameworks */, B386DE862C8974DA0021A7BA /* PVFreeDO-Dynamic in Embed Frameworks */, - B3E2AA422CB6369800E2636D /* PVFlycast.framework in Embed Frameworks */, B3E2AA262CB6328100E2636D /* PVVecX.framework in Embed Frameworks */, B3A186862CBCBAC800BB1FFE /* PVMupen64PlusBridge.framework in Embed Frameworks */, B318E28D2CAB512F00D0E599 /* PVMupen64PlusVideoRice.framework in Embed Frameworks */, B38FB4AF2CBEF6B1006786C6 /* libswresample.xcframework in Embed Frameworks */, - B3E2AA2D2CB6341000E2636D /* PVPCSXRearmed.framework in Embed Frameworks */, B33BD1C62C6A622A001E933A /* PVTGBDual-Dynamic in Embed Frameworks */, B33BD1C02C6A6212001E933A /* PVPicoDrive-Dynamic in Embed Frameworks */, B38FB4A22CBEF57C006786C6 /* PVCoreBridgeRetro.framework in Embed Frameworks */, @@ -2942,7 +2936,6 @@ B318E29A2CAB586800D0E599 /* snes9x.framework in Frameworks */, B33BD1BF2C6A6212001E933A /* PVPicoDrive-Dynamic in Frameworks */, B3952F582C698F54000B0308 /* Defaults in Frameworks */, - B3E2AA412CB6369800E2636D /* PVFlycast.framework in Frameworks */, B3952F562C697A02000B0308 /* PVUI in Frameworks */, B318E27D2CAB4C4200D0E599 /* PVSNES.framework in Frameworks */, B3EF26EA2BE7F8BD0066D4B6 /* libz.tbd in Frameworks */, @@ -2975,7 +2968,6 @@ B394CD7C2BDEE53A006B63E8 /* CoreServices.framework in Frameworks */, B394CD872BDEE53A006B63E8 /* AVFoundation.framework in Frameworks */, B394CD892BDEE53A006B63E8 /* CFNetwork.framework in Frameworks */, - B3E2AA2C2CB6340F00E2636D /* PVPCSXRearmed.framework in Frameworks */, B394CD8E2BDEE53A006B63E8 /* Security.framework in Frameworks */, B37228CA2CCDC7A700E6F627 /* FreemiumKit in Frameworks */, B318E2802CAB4C6400D0E599 /* PVFCEU.framework in Frameworks */, diff --git a/UITesting/UITesting.code-workspace b/UITesting/UITesting.code-workspace index 57097327f4..16dc15c8c1 100644 --- a/UITesting/UITesting.code-workspace +++ b/UITesting/UITesting.code-workspace @@ -1,7 +1,7 @@ { "folders": [ { - "path": "." + "path": "UITesting" } ], "settings": {} diff --git a/UITesting/UITesting.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/UITesting/UITesting.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 2eb8e28e29..26f3fbde11 100644 --- a/UITesting/UITesting.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/UITesting/UITesting.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -78,8 +78,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/FlineDev/FreemiumKit.git", "state" : { - "revision" : "68a3c35053afe0c564585da4f16315e1bbcda441", - "version" : "1.14.0" + "revision" : "ff45d8e3fc7af5255f97e059cc95524dfa320196", + "version" : "1.15.0" } }, { @@ -241,7 +241,7 @@ "location" : "https://github.com/pointfreeco/swift-perception.git", "state" : { "branch" : "main", - "revision" : "dccdf5aed1db969afa11d6fbd36b96a4932ebe8c" + "revision" : "8d52279b9809ef27eabe7d5420f03734528f19da" } }, { diff --git a/UITesting/UITesting/UITestingApp.swift b/UITesting/UITesting/UITestingApp.swift index 1c19fea561..de6fff3795 100644 --- a/UITesting/UITesting/UITestingApp.swift +++ b/UITesting/UITesting/UITestingApp.swift @@ -10,46 +10,56 @@ import PVSwiftUI import PVThemes import SwiftUI import UIKit +import Perception + #if canImport(FreemiumKit) import FreemiumKit #endif @main struct UITestingApp: App { - @State private var showingRealmSheet = true + @State private var showingRealmSheet = false @State private var showingMockSheet = false @State private var showingSettings = false - + @State private var showImportStatus = false + + @StateObject + private var mockImportStatusDriverData = MockImportStatusDriverData() + var body: some Scene { WindowGroup { ZStack { - Color.black.ignoresSafeArea() - + Color.secondary.ignoresSafeArea() + VStack(spacing: 20) { Button("Show Realm Driver") { showingRealmSheet = true } .buttonStyle(.borderedProminent) - + Button("Show Mock Driver") { showingMockSheet = true } .buttonStyle(.borderedProminent) - Button("Show Settings") { showingSettings = true } .buttonStyle(.borderedProminent) + + Button("Show Import Queue") { + showImportStatus = true + } + .buttonStyle(.borderedProminent) } } .sheet(isPresented: $showingRealmSheet) { let testRealm = try! RealmSaveStateTestFactory.createInMemoryRealm() let mockDriver = try! RealmSaveStateDriver(realm: testRealm) - + /// Get the first game from realm for the view model let game = testRealm.objects(PVGame.self).first! - + /// Create view model with game data let viewModel = ContinuesMagementViewModel( driver: mockDriver, @@ -57,14 +67,11 @@ struct UITestingApp: App { systemTitle: "Game Boy", numberOfSaves: game.saveStates.count ) - + ContinuesMagementView(viewModel: viewModel) .onAppear { /// Load initial states through the publisher mockDriver.loadSaveStates(forGameId: "1") - - let theme = CGAThemes.purple - ThemeManager.shared.setCurrentPalette(theme.palette) } .presentationBackground(.clear) } @@ -79,11 +86,9 @@ struct UITestingApp: App { numberOfSaves: mockDriver.getAllSaveStates().count, gameUIImage: mockDriver.gameUIImage ) - + ContinuesMagementView(viewModel: viewModel) .onAppear { - let theme = CGAThemes.purple - ThemeManager.shared.setCurrentPalette(theme.palette) } .presentationBackground(.clear) } @@ -91,28 +96,36 @@ struct UITestingApp: App { let gameImporter = GameImporter.shared let pvgamelibraryUpdatesController = PVGameLibraryUpdatesController(gameImporter: gameImporter) let menuDelegate = MockPVMenuDelegate() - + PVSettingsView( conflictsController: pvgamelibraryUpdatesController, menuDelegate: menuDelegate) { - + print("PVSettingsView Closed") + } + } + .sheet(isPresented: $showImportStatus) { + ImportStatusView( + updatesController: mockImportStatusDriverData.pvgamelibraryUpdatesController, + gameImporter: mockImportStatusDriverData.gameImporter, + delegate: mockImportStatusDriverData) { + print("Import Status View Closed") } } .onAppear { - #if canImport(FreemiumKit) - FreemiumKit.shared.overrideForDebug(purchasedTier: 1) - #endif +#if canImport(FreemiumKit) + FreemiumKit.shared.overrideForDebug(purchasedTier: 1) +#endif } } #if canImport(FreemiumKit) - .environmentObject(FreemiumKit.shared) + .environmentObject(FreemiumKit.shared) #endif } } class MockPVMenuDelegate: PVMenuDelegate { func didTapImports() { - + } func didTapSettings() { @@ -141,38 +154,55 @@ class MockPVMenuDelegate: PVMenuDelegate { } struct MainView_Previews: PreviewProvider { - @State private var showingSettings = false - + @State static private var showSaveStatesMock = false + @State static private var showingSettings = false + @State static private var showImportQueue = false + @State static private var mockImportStatusDriverData = MockImportStatusDriverData() + static var previews: some View { ZStack { Color.black.ignoresSafeArea() - + VStack(spacing: 20) { Button("Show Realm Driver") { } .buttonStyle(.borderedProminent) - + Button("Show Mock Driver") { } .buttonStyle(.borderedProminent) Button("Show Settings") { } .buttonStyle(.borderedProminent) + + Button("Show Import Queue") { + showImportQueue = true + } + .buttonStyle(.borderedProminent) } } - .sheet(isPresented: .constant(false)) { - let gameImporter = GameImporter.shared + .sheet(isPresented: $showingSettings) { + let gameImporter = MockGameImporter() let pvgamelibraryUpdatesController = PVGameLibraryUpdatesController(gameImporter: gameImporter) let menuDelegate = MockPVMenuDelegate() - + PVSettingsView( conflictsController: pvgamelibraryUpdatesController, menuDelegate: menuDelegate) { } } + .sheet(isPresented: .init( + get: { mockImportStatusDriverData.isPresent }, + set: { mockImportStatusDriverData.isPresent = $0 } + )) { + ImportStatusView( + updatesController: mockImportStatusDriverData.pvgamelibraryUpdatesController, + gameImporter: mockImportStatusDriverData.gameImporter, + delegate: mockImportStatusDriverData) + } .onAppear { - #if canImport(FreemiumKit) - FreemiumKit.shared.overrideForDebug(purchasedTier: 1) - #endif +#if canImport(FreemiumKit) + FreemiumKit.shared.overrideForDebug(purchasedTier: 1) +#endif } } }