ReadKeep/readeck/UI/BookmarkDetail/BookmarkLabelsView.swift
Ilyas Hallak 3e6db364b5 Add bookmark labels functionality
- Add BookmarkLabel model and DTO
- Create LabelsRepository and PLabelsRepository protocol
- Add GetLabelsUseCase for fetching labels
- Update BookmarkMapper to handle labels
- Add LabelsView and LabelsViewModel for UI
- Update BookmarksView and BookmarkLabelsView to display labels
- Add green2 color asset for labels
- Update API and repository layers to support labels
2025-07-09 22:28:19 +02:00

177 lines
5.8 KiB
Swift

import SwiftUI
struct BookmarkLabelsView: View {
let bookmarkId: String
@State private var viewModel: BookmarkLabelsViewModel
@Environment(\.dismiss) private var dismiss
init(bookmarkId: String, initialLabels: [String]) {
self.bookmarkId = bookmarkId
self._viewModel = State(initialValue: BookmarkLabelsViewModel(initialLabels: initialLabels))
}
var body: some View {
NavigationView {
VStack(spacing: 16) {
// Add new label section
addLabelSection
Divider()
.padding(.horizontal, -16)
// Current labels section
currentLabelsSection
Spacer()
}
.padding()
.background(Color(.systemGroupedBackground))
.navigationTitle("Labels verwalten")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Abbrechen") {
dismiss()
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button("Fertig") {
dismiss()
}
}
}
.alert("Fehler", isPresented: $viewModel.showErrorAlert) {
Button("OK") { }
} message: {
Text(viewModel.errorMessage ?? "Unbekannter Fehler")
}
}
}
private var addLabelSection: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Neues Label hinzufügen")
.font(.headline)
.foregroundColor(.primary)
HStack(spacing: 12) {
TextField("Label eingeben...", text: $viewModel.newLabelText)
.textFieldStyle(RoundedBorderTextFieldStyle())
.onSubmit {
Task {
await viewModel.addLabel(to: bookmarkId, label: viewModel.newLabelText)
}
}
Button(action: {
Task {
await viewModel.addLabel(to: bookmarkId, label: viewModel.newLabelText)
}
}) {
Image(systemName: "plus.circle.fill")
.font(.title2)
.foregroundColor(.white)
.frame(width: 32, height: 32)
.background(
Circle()
.fill(Color.accentColor)
)
}
.disabled(viewModel.newLabelText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || viewModel.isLoading)
}
}
.padding()
.background(
RoundedRectangle(cornerRadius: 12)
.fill(Color(.secondarySystemGroupedBackground))
)
}
private var currentLabelsSection: some View {
VStack(alignment: .leading, spacing: 8) {
HStack {
Text("Aktuelle Labels")
.font(.headline)
.foregroundColor(.primary)
Spacer()
if viewModel.isLoading {
ProgressView()
.scaleEffect(0.8)
}
}
if viewModel.currentLabels.isEmpty {
VStack(spacing: 8) {
Image(systemName: "tag")
.font(.title2)
.foregroundColor(.secondary)
Text("Keine Labels vorhanden")
.font(.subheadline)
.foregroundColor(.secondary)
}
.frame(maxWidth: .infinity)
.padding(.vertical, 32)
} else {
LazyVGrid(columns: [
GridItem(.adaptive(minimum: 80, maximum: 150))
], spacing: 4) {
ForEach(viewModel.currentLabels, id: \.self) { label in
LabelChip(
label: label,
onRemove: {
Task {
await viewModel.removeLabel(from: bookmarkId, label: label)
}
}
)
}
}
}
}
.padding()
.background(
RoundedRectangle(cornerRadius: 12)
.fill(Color(.secondarySystemGroupedBackground))
)
}
}
struct LabelChip: View {
let label: String
let onRemove: () -> Void
var body: some View {
HStack(spacing: 6) {
Text(label)
.font(.caption)
.fontWeight(.medium)
.foregroundColor(.primary)
.lineLimit(1)
.truncationMode(.tail)
Button(action: onRemove) {
Image(systemName: "xmark.circle.fill")
.font(.caption)
.foregroundColor(.secondary)
}
}
.padding(.horizontal, 10)
.padding(.vertical, 6)
.background(
RoundedRectangle(cornerRadius: 16)
.fill(Color.accentColor.opacity(0.15))
.overlay(
RoundedRectangle(cornerRadius: 16)
.stroke(Color.accentColor.opacity(0.4), lineWidth: 1)
)
)
}
}
#Preview {
BookmarkLabelsView(bookmarkId: "test-id", initialLabels: ["wichtig", "arbeit", "persönlich"])
}