diff --git a/.gitignore b/.gitignore index f63bdfdf..945918ab 100644 --- a/.gitignore +++ b/.gitignore @@ -38,4 +38,7 @@ node_modules # yarn yarn-error.log +# Swift +.build + # End of https://www.gitignore.io/api/macos diff --git a/Package.swift b/Package.swift new file mode 100644 index 00000000..e2a2af94 --- /dev/null +++ b/Package.swift @@ -0,0 +1,31 @@ +// swift-tools-version: 6.0 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "CarLogos", + platforms: [ + .macOS(.v15), + .iOS(.v18), + .tvOS(.v18), + .watchOS(.v11) + ], + products: [ + // Products define the executables and libraries a package produces, making them visible to other packages. + .library( + name: "CarLogos", + targets: ["CarLogos"] + ), + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + .target( + name: "CarLogos", + resources: [ + .copy("logos") + ] + ), + ] +) diff --git a/Sources/CarLogos/Models.swift b/Sources/CarLogos/Models.swift new file mode 100644 index 00000000..2b5f285e --- /dev/null +++ b/Sources/CarLogos/Models.swift @@ -0,0 +1,27 @@ +import Foundation + +public struct CarLogo: Decodable, Sendable, Hashable { + public struct Images: Decodable, Sendable, Hashable { + // Paths are relative to the `logos/` folder (as shipped in the dataset) + public let localThumb: String? + public let localOptimized: String? + public let localOriginal: String? + // Remote fallbacks if you prefer network loading + public let remoteThumb: String? + public let remoteOptimized: String? + public let remoteOriginal: String? + + enum CodingKeys: String, CodingKey { + case localThumb = "localThumb" + case localOptimized = "localOptimized" + case localOriginal = "localOriginal" + case remoteThumb = "thumb" + case remoteOptimized = "optimized" + case remoteOriginal = "original" + } + } + + public let name: String + public let slug: String + public let image: Images +} diff --git a/Sources/CarLogos/Store.swift b/Sources/CarLogos/Store.swift new file mode 100644 index 00000000..a2c86931 --- /dev/null +++ b/Sources/CarLogos/Store.swift @@ -0,0 +1,142 @@ +import Foundation +import SwiftUI + +public enum CarLogosStore { + public enum Variant { + case thumb + case optimized + case original + } + + private static func logosBaseURL() -> URL? { + // Look for the `logos` directory inside bundled resources + if let explicit = Bundle.module.url(forResource: "logos", withExtension: nil) { + return explicit + } + + // Fallback (some SPM setups): resourceURL/logos + if let res = Bundle.module.resourceURL?.appendingPathComponent("logos"), + FileManager.default.fileExists(atPath: res.path) { + return res + } + + return nil + } + + private static func dataJSONURL() -> URL? { + // data.json lives directly under logos/ + if let base = logosBaseURL() { + let url = base.appendingPathComponent("data.json") + if FileManager.default.fileExists(atPath: url.path) { + return url + } + } + // As an extra fallback, try subdirectory lookup + return Bundle.module.url( + forResource: "data", + withExtension: "json", + subdirectory: "logos") + } + + /// Load and decode all logo metadata from the bundled `data.json`. + public static func all() throws -> [CarLogo] { + guard let url = dataJSONURL() else { + throw NSError( + domain: "CarLogos", + code: 1, + userInfo: [ + NSLocalizedDescriptionKey: "data.json not found in bundled resources. Ensure Resources/logos is included." + ]) + } + + let data = try Data(contentsOf: url) + let decoder = JSONDecoder() + + return try decoder.decode([CarLogo].self, from: data) + } + + public static func logoForSlug(_ slug: String) -> CarLogo? { + return try? all().first(where: { $0.slug == slug }) + } + + public static func imageForLogo(_ logo: CarLogo, variant: Variant = .optimized) -> Image? { + guard let url = localURL(for: logo, variant: variant) else { + return nil + } + + #if os(macOS) + if let nsImage = NSImage(contentsOf: url) { + return Image(nsImage: nsImage) + } + #else + if let uiImage = UIImage(contentsOfFile: url.path) { + return Image(uiImage: uiImage) + } + #endif + + return nil + } + + /// Resolve the best local path for a given variant, falling back sensibly. + public static func localPath(for logo: CarLogo, variant: Variant) -> String? { + switch variant { + case .thumb: + return logo.image.localThumb ?? logo.image.localOptimized ?? logo.image.localOriginal + case .optimized: + return logo.image.localOptimized ?? logo.image.localOriginal ?? logo.image.localThumb + case .original: + return logo.image.localOriginal ?? logo.image.localOptimized ?? logo.image.localThumb + } + } + + /// Get a file URL for the selected image variant inside the bundle. + public static func localURL(for logo: CarLogo, variant: Variant) -> URL? { + guard let path = localPath(for: logo, variant: variant) else { + return nil + } + + guard let base = logosBaseURL() else { + return nil + } + + let url = base.appendingPathComponent(path) + if FileManager.default.fileExists(atPath: url.path) { + return url + } + + // Some JSON variants include a leading slash — normalize + let trimmed = path.trimmingCharacters(in: CharacterSet(charactersIn: "/")) + let normalized = base.appendingPathComponent(trimmed) + + return FileManager.default.fileExists(atPath: normalized.path) ? normalized : nil + } + + /// Load platform-native image data for advanced uses. + public static func data(for logo: CarLogo, variant: Variant) -> Data? { + guard let url = localURL(for: logo, variant: variant) else { + return nil + } + + return try? Data(contentsOf: url) + } + + /// Optional: remote URLs if you prefer network loading and a smaller app binary. + public static func remoteURL(for logo: CarLogo, variant: Variant) -> URL? { + let raw: String? + + switch variant { + case .thumb: + raw = logo.image.remoteThumb + case .optimized: + raw = logo.image.remoteOptimized + case .original: + raw = logo.image.remoteOriginal + } + + guard let s = raw, let url = URL(string: s) else { + return nil + } + + return url + } +} diff --git a/Sources/CarLogos/logos b/Sources/CarLogos/logos new file mode 120000 index 00000000..6e6b610e --- /dev/null +++ b/Sources/CarLogos/logos @@ -0,0 +1 @@ +../../logos \ No newline at end of file