import Foundation import CoreData import Combine import CoreSpotlight class LinkViewModel: ObservableObject { @Published var links: [Link] = [] @Published var filteredLinks: [Link] = [] @Published var searchText: String = "" { didSet { filterLinks() } } @Published var selectedSortOption: SortOption = .alias { didSet { sortLinks() } } @Published var selectedTag: Tag? { didSet { filterLinks() } } @Published var isLoading: Bool = false @Published var errorMessage: String? private var cancellables = Set() private let persistenceController = PersistenceController.shared enum SortOption: String, CaseIterable { case alias = "alias" case type = "linkType" case clicks = "clickCount" case dateCreated = "createdAt" case dateUpdated = "updatedAt" var displayName: String { switch self { case .alias: return NSLocalizedString("alias", comment: "Sort by alias") case .type: return NSLocalizedString("type", comment: "Sort by type") case .clicks: return NSLocalizedString("clicks", comment: "Sort by clicks") case .dateCreated: return NSLocalizedString("date_created", comment: "Sort by date created") case .dateUpdated: return NSLocalizedString("date_updated", comment: "Sort by date updated") } } } init() { fetchLinks() initializeSpotlightSearch() } private func initializeSpotlightSearch() { // Initialize Spotlight search indexing when the app starts DispatchQueue.global(qos: .background).async { SpotlightSearchManager.shared.initializeSpotlightIndexing() } } /// Manually refresh Spotlight search index func refreshSpotlightIndex() { DispatchQueue.global(qos: .background).async { SpotlightSearchManager.shared.initializeSpotlightIndexing() } } /// Check Spotlight search status for debugging func checkSpotlightStatus(completion: @escaping (Int) -> Void) { SpotlightSearchManager.shared.getIndexedItemsStatus(completion: completion) } func fetchLinks() { isLoading = true let context = persistenceController.container.viewContext let request: NSFetchRequest = Link.fetchRequest() request.sortDescriptors = [NSSortDescriptor(keyPath: \Link.alias, ascending: true)] do { links = try context.fetch(request) filterLinks() isLoading = false } catch { errorMessage = error.localizedDescription isLoading = false } } func createLink(alias: String, originalURL: String?, text: String?, linkType: LinkType, description: String?, tags: [Tag]) -> Bool { let context = persistenceController.container.viewContext // Check if alias already exists if aliasExists(alias) { errorMessage = NSLocalizedString("alias_already_exists", comment: "Alias already exists") return false } let newLink = Link(context: context) newLink.alias = alias newLink.originalURL = originalURL newLink.text = text newLink.linkType = linkType.rawValue newLink.linkDescription = description newLink.clickCount = 0 newLink.createdAt = Date() newLink.updatedAt = Date() // Add tags for tag in tags { newLink.addToTags(tag) } // Create change log let changeLog = ChangeLog(context: context) changeLog.changeType = "CREATE" changeLog.changeDescription = "Link created" changeLog.changedAt = Date() changeLog.link = newLink do { try context.save() // Index the new link in Spotlight search SpotlightSearchManager.shared.indexLink(newLink) fetchLinks() return true } catch { errorMessage = error.localizedDescription return false } } func updateLink(_ link: Link, alias: String, originalURL: String?, text: String?, linkType: LinkType, description: String?, tags: [Tag]) -> Bool { let context = persistenceController.container.viewContext // Check if alias already exists (excluding current link) if alias != link.alias && aliasExists(alias) { errorMessage = NSLocalizedString("alias_already_exists", comment: "Alias already exists") return false } link.alias = alias link.originalURL = originalURL link.text = text link.linkType = linkType.rawValue link.linkDescription = description link.updatedAt = Date() // Update tags link.removeFromTags(link.tags ?? NSSet()) for tag in tags { link.addToTags(tag) } // Create change log let changeLog = ChangeLog(context: context) changeLog.changeType = "UPDATE" changeLog.changeDescription = "Link updated" changeLog.changedAt = Date() changeLog.link = link do { try context.save() // Re-index the updated link in Spotlight search SpotlightSearchManager.shared.indexLink(link) fetchLinks() return true } catch { errorMessage = error.localizedDescription return false } } func deleteLink(_ link: Link) { let context = persistenceController.container.viewContext let aliasToRemove = link.alias context.delete(link) do { try context.save() // Remove the link from Spotlight search index SpotlightSearchManager.shared.removeLink(withAlias: aliasToRemove) fetchLinks() } catch { errorMessage = error.localizedDescription } } func recordClick(for link: Link) { let context = persistenceController.container.viewContext // Create click log let clickLog = ClickLog(context: context) clickLog.clickedAt = Date() clickLog.link = link // Increment click count link.clickCount += 1 do { try context.save() fetchLinks() } catch { errorMessage = error.localizedDescription } } func aliasExists(_ alias: String) -> Bool { let context = persistenceController.container.viewContext let request: NSFetchRequest = Link.fetchRequest() request.predicate = NSPredicate(format: "alias == %@", alias) request.fetchLimit = 1 do { let count = try context.count(for: request) return count > 0 } catch { return false } } private func filterLinks() { var filtered = links // Filter by search text if !searchText.isEmpty { filtered = filtered.filter { link in link.alias.localizedCaseInsensitiveContains(searchText) || (link.originalURL?.localizedCaseInsensitiveContains(searchText) ?? false) || (link.linkDescription?.localizedCaseInsensitiveContains(searchText) ?? false) } } // Filter by selected tag if let selectedTag = selectedTag { filtered = filtered.filter { link in link.sortedTags.contains(selectedTag) } } filteredLinks = filtered sortLinks() } private func sortLinks() { switch selectedSortOption { case .alias: filteredLinks.sort { $0.alias < $1.alias } case .type: filteredLinks.sort { $0.linkType < $1.linkType } case .clicks: filteredLinks.sort { $0.clickCount > $1.clickCount } case .dateCreated: filteredLinks.sort { $0.createdAt > $1.createdAt } case .dateUpdated: filteredLinks.sort { $0.updatedAt > $1.updatedAt } } } func openLink(_ link: Link, with parameters: [String: String] = [:]) { recordClick(for: link) guard let urlString = link.originalURL else { return } var processedURL = urlString // Process template parameters if link.isTemplate { processedURL = URLTemplateProcessor.processTemplate(urlString, with: parameters) } guard let url = URL(string: processedURL) else { return } if UIApplication.shared.canOpenURL(url) { UIApplication.shared.open(url) } } } // MARK: - Search functionality extension LinkViewModel { func performQuickSearch(_ query: String) { searchText = query // If user presses enter with search text and no results, open as URL if filteredLinks.isEmpty && !query.isEmpty { if let url = URL(string: query.hasPrefix("http") ? query : "https://\(query)") { if UIApplication.shared.canOpenURL(url) { UIApplication.shared.open(url) } } } } } // Import UIKit for UIApplication import UIKit