ReadKeep/readeck/UI/BookmarkDetail/BookmarkLabelsViewModel.swift
Ilyas Hallak 76bc28ae02 feat: Improve UI components and performance optimizations
- Refactor BookmarksView with better error handling and loading states
- Optimize BookmarkLabelsViewModel with cached properties and reduced recomputation
- Fix Core Data thread safety in LabelsRepository with performAndWait
- Enhance TagManagementView with sorted selected labels display
- Clean up ShareBookmarkViewModel comments
- Update localization strings for error states
- Bump build version to 19

These changes improve overall app performance and user experience across
bookmark management workflows.
2025-08-20 20:38:42 +02:00

179 lines
5.5 KiB
Swift

import Foundation
@Observable
class BookmarkLabelsViewModel {
private let addLabelsUseCase: PAddLabelsToBookmarkUseCase
private let removeLabelsUseCase: PRemoveLabelsFromBookmarkUseCase
private let getLabelsUseCase: PGetLabelsUseCase
var isLoading = false
var isInitialLoading = false
var errorMessage: String?
var showErrorAlert = false
var currentLabels: [String] = [] {
didSet {
if oldValue != currentLabels {
calculatePages()
}
}
}
var newLabelText = ""
var searchText = "" {
didSet {
if oldValue != searchText {
calculatePages()
}
}
}
var allLabels: [BookmarkLabel] = [] {
didSet {
if oldValue != allLabels {
calculatePages()
}
}
}
var labelPages: [[BookmarkLabel]] = []
// Cached properties to avoid recomputation
private var _availableLabels: [BookmarkLabel] = []
private var _filteredLabels: [BookmarkLabel] = []
var availableLabels: [BookmarkLabel] {
return _availableLabels
}
var filteredLabels: [BookmarkLabel] {
return _filteredLabels
}
var availableLabelPages: [[BookmarkLabel]] = []
init(_ factory: UseCaseFactory = DefaultUseCaseFactory.shared, initialLabels: [String] = []) {
self.currentLabels = initialLabels
self.addLabelsUseCase = factory.makeAddLabelsToBookmarkUseCase()
self.removeLabelsUseCase = factory.makeRemoveLabelsFromBookmarkUseCase()
self.getLabelsUseCase = factory.makeGetLabelsUseCase()
}
@MainActor
func loadAllLabels() async {
isInitialLoading = true
defer { isInitialLoading = false }
do {
let labels = try await getLabelsUseCase.execute()
allLabels = labels
} catch {
errorMessage = "failed to load labels"
showErrorAlert = true
}
calculatePages()
}
@MainActor
func addLabels(to bookmarkId: String, labels: [String]) async {
isLoading = true
errorMessage = nil
do {
let uniqueLabels = Set(currentLabels + labels)
currentLabels = currentLabels.filter { uniqueLabels.contains($0) } + labels.filter { !currentLabels.contains($0) }
try await addLabelsUseCase.execute(bookmarkId: bookmarkId, labels: labels)
} catch let error as BookmarkUpdateError {
errorMessage = error.localizedDescription
showErrorAlert = true
} catch {
errorMessage = "Error adding labels"
showErrorAlert = true
}
isLoading = false
}
@MainActor
func addLabel(to bookmarkId: String, label: String) async {
let trimmedLabel = label.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmedLabel.isEmpty else { return }
await addLabels(to: bookmarkId, labels: [trimmedLabel])
newLabelText = ""
searchText = ""
}
@MainActor
func removeLabels(from bookmarkId: String, labels: [String]) async {
isLoading = true
errorMessage = nil
do {
try await removeLabelsUseCase.execute(bookmarkId: bookmarkId, labels: labels)
// Update local labels
currentLabels.removeAll { labels.contains($0) }
} catch let error as BookmarkUpdateError {
errorMessage = error.localizedDescription
showErrorAlert = true
} catch {
errorMessage = "Error removing labels"
showErrorAlert = true
}
isLoading = false
}
@MainActor
func removeLabel(from bookmarkId: String, label: String) async {
await removeLabels(from: bookmarkId, labels: [label])
}
// Convenience method für das Umschalten eines Labels (hinzufügen wenn nicht vorhanden, entfernen wenn vorhanden)
@MainActor
func toggleLabel(for bookmarkId: String, label: String) async {
if currentLabels.contains(label) {
await removeLabel(from: bookmarkId, label: label)
} else {
await addLabel(to: bookmarkId, label: label)
}
}
func updateLabels(_ labels: [String]) {
currentLabels = labels
}
private func calculatePages() {
let pageSize = Constants.Labels.pageSize
// Update cached available labels
_availableLabels = allLabels.filter { !currentLabels.contains($0.name) }
// Update cached filtered labels
if searchText.isEmpty {
_filteredLabels = _availableLabels
} else {
_filteredLabels = _availableLabels.filter { $0.name.localizedCaseInsensitiveContains(searchText) }
}
// Calculate pages for all labels
if allLabels.count <= pageSize {
labelPages = [allLabels]
} else {
labelPages = stride(from: 0, to: allLabels.count, by: pageSize).map {
Array(allLabels[$0..<min($0 + pageSize, allLabels.count)])
}
}
// Calculate pages for filtered labels
if _filteredLabels.count <= pageSize {
availableLabelPages = [_filteredLabels]
} else {
availableLabelPages = stride(from: 0, to: _filteredLabels.count, by: pageSize).map {
Array(_filteredLabels[$0..<min($0 + pageSize, _filteredLabels.count)])
}
}
}
}