ReadKeep/readeck/Data/Repository/LabelsRepository.swift
Ilyas Hallak a3b3863fa3 Refactor tag management system with improved search and real-time sync
- Add CreateLabelUseCase for consistent label creation across app and extension
- Implement TagRepository for Share Extension to persist new labels to Core Data
- Enhance CoreDataTagManagementView with real-time search functionality
- Add automatic tag synchronization on app startup and resume
- Improve Core Data context configuration for better extension support
- Unify label terminology across UI components (tags -> labels)
- Fix label persistence issues in Share Extension
- Add immediate Core Data persistence for newly created labels
- Bump version to 36
2025-11-10 21:29:38 +01:00

118 lines
4.1 KiB
Swift

import Foundation
import CoreData
class LabelsRepository: PLabelsRepository, @unchecked Sendable {
private let api: PAPI
private let coreDataManager = CoreDataManager.shared
init(api: PAPI) {
self.api = api
}
func getLabels() async throws -> [BookmarkLabel] {
// First, load from Core Data (instant response)
let cachedLabels = try await loadLabelsFromCoreData()
// Then sync with API in background (don't wait)
Task.detached(priority: .background) { [weak self] in
guard let self = self else { return }
do {
let dtos = try await self.api.getBookmarkLabels()
try? await self.saveLabels(dtos)
} catch {
// Silent fail - we already have cached data
}
}
return cachedLabels
}
private func loadLabelsFromCoreData() async throws -> [BookmarkLabel] {
let backgroundContext = coreDataManager.newBackgroundContext()
return try await backgroundContext.perform {
let fetchRequest: NSFetchRequest<TagEntity> = TagEntity.fetchRequest()
fetchRequest.sortDescriptors = [
NSSortDescriptor(key: "count", ascending: false),
NSSortDescriptor(key: "name", ascending: true)
]
let entities = try backgroundContext.fetch(fetchRequest)
return entities.compactMap { entity -> BookmarkLabel? in
guard let name = entity.name, !name.isEmpty else { return nil }
return BookmarkLabel(
name: name,
count: Int(entity.count),
href: name
)
}
}
}
func saveLabels(_ dtos: [BookmarkLabelDto]) async throws {
let backgroundContext = coreDataManager.newBackgroundContext()
try await backgroundContext.perform {
// Batch fetch all existing labels
let fetchRequest: NSFetchRequest<TagEntity> = TagEntity.fetchRequest()
fetchRequest.propertiesToFetch = ["name", "count"]
let existingEntities = try backgroundContext.fetch(fetchRequest)
var existingByName: [String: TagEntity] = [:]
for entity in existingEntities {
if let name = entity.name {
existingByName[name] = entity
}
}
// Insert or update labels
var insertCount = 0
var updateCount = 0
for dto in dtos {
if let existing = existingByName[dto.name] {
// Update count if changed
if existing.count != dto.count {
existing.count = Int32(dto.count)
updateCount += 1
}
} else {
// Insert new label
dto.toEntity(context: backgroundContext)
insertCount += 1
}
}
// Only save if there are changes
if insertCount > 0 || updateCount > 0 {
try backgroundContext.save()
}
}
}
func saveNewLabel(name: String) async throws {
let backgroundContext = coreDataManager.newBackgroundContext()
try await backgroundContext.perform {
let trimmedName = name.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmedName.isEmpty else { return }
// Check if label already exists
let fetchRequest: NSFetchRequest<TagEntity> = TagEntity.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "name == %@", trimmedName)
fetchRequest.fetchLimit = 1
let existingTags = try backgroundContext.fetch(fetchRequest)
// Only create if it doesn't exist
if existingTags.isEmpty {
let newTag = TagEntity(context: backgroundContext)
newTag.name = trimmedName
newTag.count = 1 // New label is being used immediately
try backgroundContext.save()
}
}
}
}