ReadKeep/readeck/UI/BookmarkDetail/BookmarkLabelsView.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

98 lines
3.5 KiB
Swift

import SwiftUI
struct BookmarkLabelsView: View {
let bookmarkId: String
@State private var viewModel: BookmarkLabelsViewModel
@Environment(\.dismiss) private var dismiss
@Environment(\.managedObjectContext) private var viewContext
@EnvironmentObject private var appSettings: AppSettings
init(bookmarkId: String, initialLabels: [String], viewModel: BookmarkLabelsViewModel? = nil) {
self.bookmarkId = bookmarkId
self._viewModel = State(initialValue: viewModel ?? BookmarkLabelsViewModel(initialLabels: initialLabels))
UIPageControl.appearance().currentPageIndicatorTintColor = UIColor(Color.primary)
UIPageControl.appearance().pageIndicatorTintColor = UIColor(Color.primary).withAlphaComponent(0.2)
}
var body: some View {
NavigationView {
VStack(spacing: 12) {
availableLabelsSection
Spacer()
}
.padding(.vertical)
.background(Color(.systemGroupedBackground))
.navigationTitle("Manage Labels")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") {
dismiss()
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button("Done") {
dismiss()
}
}
}
.alert("Error", isPresented: $viewModel.showErrorAlert) {
Button("OK") { }
} message: {
Text(viewModel.errorMessage ?? "Unknown error")
}
.ignoresSafeArea(.keyboard)
.onTapGesture {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
.onAppear {
Task {
await viewModel.syncTags()
}
}
}
}
// MARK: - View Components
@ViewBuilder
private var availableLabelsSection: some View {
VStack(alignment: .leading, spacing: 8) {
Text(appSettings.tagSortOrder == .byCount ? "Sorted by usage count".localized : "Sorted alphabetically".localized)
.font(.caption)
.foregroundColor(.secondary)
.padding(.horizontal)
CoreDataTagManagementView(
selectedLabels: Set(viewModel.currentLabels),
searchText: $viewModel.searchText,
fetchLimit: nil,
sortOrder: appSettings.tagSortOrder,
onAddCustomTag: {
Task {
await viewModel.addLabel(to: bookmarkId, label: viewModel.searchText)
}
},
onToggleLabel: { label in
Task {
await viewModel.toggleLabel(for: bookmarkId, label: label)
}
},
onRemoveLabel: { label in
Task {
await viewModel.removeLabel(from: bookmarkId, label: label)
}
}
)
.padding(.horizontal)
}
}
}
#Preview {
BookmarkLabelsView(bookmarkId: "test-id", initialLabels: ["wichtig", "arbeit", "persönlich"], viewModel: .init(MockUseCaseFactory(), initialLabels: ["test"]))
}