ReadKeep/readeck/Data/Repository/LabelsRepository.swift
Ilyas Hallak f3719fa9d4 Refactor tag management to use Core Data with configurable sorting
This commit introduces a comprehensive refactoring of the tag management
system, replacing the previous API-based approach with a Core Data-first
strategy for improved performance and offline support.

Major Changes:

Tag Management Architecture:
- Add CoreDataTagManagementView using @FetchRequest for reactive updates
- Implement cache-first sync strategy in LabelsRepository
- Create SyncTagsUseCase following Clean Architecture principles
- Add TagSortOrder enum for configurable tag sorting (by count/alphabetically)
- Mark LegacyTagManagementView as deprecated

Share Extension Improvements:
- Replace API-based tag loading with Core Data queries
- Display top 150 tags sorted by usage count
- Remove unnecessary label fetching logic
- Add "Most used tags" localized title
- Improve offline bookmark tag management

Main App Enhancements:
- Add tag sync triggers in AddBookmarkView and BookmarkLabelsView
- Implement user-configurable tag sorting in settings
- Add sort order indicator labels with localization
- Automatic UI updates via SwiftUI @FetchRequest reactivity

Settings & Configuration:
- Add TagSortOrder setting with persistence
- Refactor Settings model structure
- Add FontFamily and FontSize domain models
- Improve settings repository with tag sort order support

Use Case Layer:
- Add SyncTagsUseCase for background tag synchronization
- Update UseCaseFactory with tag sync support
- Add mock implementations for testing

Localization:
- Add German and English translations for:
  - "Most used tags"
  - "Sorted by usage count"
  - "Sorted alphabetically"

Technical Improvements:
- Batch tag updates with conflict detection
- Background sync with silent failure handling
- Reduced server load through local caching
- Better separation of concerns following Clean Architecture
2025-11-08 13:46:40 +01:00

93 lines
3.2 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()
}
}
}
}