// // AppCatalogStore.swift // QuickLocation // import Foundation import UIKit import Kingfisher struct AppCatalogItem: Codable, Equatable { let id: String let name: String let icon: String let keywords: [String] let iconURL: String? init(id: String, name: String, icon: String, keywords: [String], iconURL: String? = nil) { self.id = id self.name = name self.icon = icon self.keywords = keywords self.iconURL = iconURL } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.decode(String.self, forKey: .id) name = try container.decode(String.self, forKey: .name) icon = try container.decodeIfPresent(String.self, forKey: .icon) ?? "" keywords = try container.decodeIfPresent([String].self, forKey: .keywords) ?? [] iconURL = try container.decodeIfPresent(String.self, forKey: .iconURL) } var isRemoteIcon: Bool { guard let iconURL, !iconURL.isEmpty else { return false } return icon.isEmpty } } enum AppCatalogStore { static let sharedItems: [AppCatalogItem] = { guard let url = Bundle.main.url(forResource: "app_catalog", withExtension: "json"), let data = try? Data(contentsOf: url), let items = try? JSONDecoder().decode([AppCatalogItem].self, from: data) else { return [] } return items }() static func item(id: String) -> AppCatalogItem? { sharedItems.first { $0.id == id } } static func resolve(link: AppRestrictLinkRecord) -> AppCatalogItem? { if let local = item(id: link.catalogId) { return local } guard let name = link.displayName, !name.isEmpty else { return nil } return AppCatalogItem( id: link.catalogId, name: name, icon: "", keywords: [], iconURL: link.iconURL ) } static func search(_ query: String) -> [AppCatalogItem] { let q = query.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() guard !q.isEmpty else { return sharedItems } return sharedItems.filter { item in item.name.lowercased().contains(q) || item.keywords.contains(where: { $0.lowercased().contains(q) }) } } static func image(for item: AppCatalogItem) -> UIImage? { guard !item.icon.isEmpty else { return placeholder(for: item.name) } return UIImage(named: item.icon) ?? placeholder(for: item.name) } static func setIcon(for imageView: UIImageView, item: AppCatalogItem, cornerRadius: CGFloat = 10) { imageView.layer.cornerRadius = cornerRadius imageView.clipsToBounds = true imageView.contentMode = .scaleAspectFill if let urlString = item.iconURL, let url = URL(string: urlString) { imageView.kf.setImage(with: url, placeholder: placeholder(for: item.name)) } else { imageView.kf.cancelDownloadTask() imageView.image = image(for: item) } } private static func placeholder(for name: String) -> UIImage? { let size = CGSize(width: 44, height: 44) let renderer = UIGraphicsImageRenderer(size: size) return renderer.image { _ in UIColor(hexStr: "#16B3FF").setFill() UIBezierPath(roundedRect: CGRect(origin: .zero, size: size), cornerRadius: 10).fill() let letter = String(name.prefix(1)) as NSString let attrs: [NSAttributedString.Key: Any] = [ .font: UIFont.systemFont(ofSize: 18, weight: .bold), .foregroundColor: UIColor.white ] let textSize = letter.size(withAttributes: attrs) letter.draw( at: CGPoint(x: (size.width - textSize.width) / 2, y: (size.height - textSize.height) / 2), withAttributes: attrs ) } } }