Files
links/mobile/Heygo/ViewModels/LinkViewModel.swift
T
2025-08-16 16:51:20 +10:00

308 lines
9.2 KiB
Swift

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<AnyCancellable>()
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> = 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> = 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