feat: Add label management to bookmarks and UI improvements
- BookmarkDetail: Add labels property, display and manage labels in detail view - Add AddLabelsToBookmarkUseCase and RemoveLabelsFromBookmarkUseCase - Update UpdateBookmarkUseCase and BookmarkUpdateRequest for label operations - UI: Show labels in BookmarkDetailView, add label management sheet - DefaultUseCaseFactory: Provide use cases for label management - Localizable: Add/adjust label-related strings, minor cleanup - SettingsServerView: Update debug endpoint - SidebarTab: Change 'Alle' to 'All' - Project: Remove unused region from Xcode project
This commit is contained in:
parent
2c5b51ca3a
commit
d2e8228903
@ -14,20 +14,27 @@
|
|||||||
|
|
||||||
},
|
},
|
||||||
"Abbrechen" : {
|
"Abbrechen" : {
|
||||||
"localizations" : {
|
|
||||||
"de" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "needs_review",
|
|
||||||
"value" : "Abbrechen"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"Abmelden" : {
|
"Abmelden" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Add Item" : {
|
"Add Item" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Aktuelle Labels" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"all" : {
|
||||||
|
"extractionState" : "manual",
|
||||||
|
"localizations" : {
|
||||||
|
"de" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Ale"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"Anmelden & speichern" : {
|
"Anmelden & speichern" : {
|
||||||
|
|
||||||
@ -61,17 +68,6 @@
|
|||||||
},
|
},
|
||||||
"Debug-Anmeldung" : {
|
"Debug-Anmeldung" : {
|
||||||
|
|
||||||
},
|
|
||||||
"done" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"de" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Fertig"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"Einfügen" : {
|
"Einfügen" : {
|
||||||
|
|
||||||
@ -117,17 +113,6 @@
|
|||||||
},
|
},
|
||||||
"Fertig mit Lesen?" : {
|
"Fertig mit Lesen?" : {
|
||||||
|
|
||||||
},
|
|
||||||
"font_settings_title" : {
|
|
||||||
"extractionState" : "manual",
|
|
||||||
"localizations" : {
|
|
||||||
"de" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Schrift-Einstellungen"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"Füge einen neuen Link zu deiner Sammlung hinzu" : {
|
"Füge einen neuen Link zu deiner Sammlung hinzu" : {
|
||||||
|
|
||||||
@ -161,9 +146,21 @@
|
|||||||
},
|
},
|
||||||
"Keine Ergebnisse" : {
|
"Keine Ergebnisse" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Keine Labels vorhanden" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Key" : {
|
||||||
|
"extractionState" : "manual"
|
||||||
|
},
|
||||||
|
"Label eingeben..." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Labels" : {
|
"Labels" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Labels verwalten" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Lade %@..." : {
|
"Lade %@..." : {
|
||||||
|
|
||||||
@ -185,6 +182,9 @@
|
|||||||
},
|
},
|
||||||
"Neues Bookmark" : {
|
"Neues Bookmark" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Neues Label hinzufügen" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"OK" : {
|
"OK" : {
|
||||||
|
|
||||||
|
|||||||
@ -319,7 +319,6 @@
|
|||||||
knownRegions = (
|
knownRegions = (
|
||||||
en,
|
en,
|
||||||
Base,
|
Base,
|
||||||
"fr-CA",
|
|
||||||
de,
|
de,
|
||||||
);
|
);
|
||||||
mainGroup = 5D45F9BF2DF858680048D5B8;
|
mainGroup = 5D45F9BF2DF858680048D5B8;
|
||||||
|
|||||||
@ -38,6 +38,7 @@ class BookmarksRepository: PBookmarksRepository {
|
|||||||
hasArticle: bookmarkDetailDto.hasArticle,
|
hasArticle: bookmarkDetailDto.hasArticle,
|
||||||
isMarked: bookmarkDetailDto.isMarked,
|
isMarked: bookmarkDetailDto.isMarked,
|
||||||
isArchived: bookmarkDetailDto.isArchived,
|
isArchived: bookmarkDetailDto.isArchived,
|
||||||
|
labels: bookmarkDetailDto.labels,
|
||||||
thumbnailUrl: bookmarkDetailDto.resources.thumbnail?.src ?? "",
|
thumbnailUrl: bookmarkDetailDto.resources.thumbnail?.src ?? "",
|
||||||
imageUrl: bookmarkDetailDto.resources.image?.src ?? ""
|
imageUrl: bookmarkDetailDto.resources.image?.src ?? ""
|
||||||
)
|
)
|
||||||
|
|||||||
@ -14,6 +14,7 @@ struct BookmarkDetail {
|
|||||||
let hasArticle: Bool
|
let hasArticle: Bool
|
||||||
let isMarked: Bool
|
let isMarked: Bool
|
||||||
var isArchived: Bool
|
var isArchived: Bool
|
||||||
|
let labels: [String]
|
||||||
let thumbnailUrl: String
|
let thumbnailUrl: String
|
||||||
let imageUrl: String
|
let imageUrl: String
|
||||||
}
|
}
|
||||||
@ -33,6 +34,7 @@ extension BookmarkDetail {
|
|||||||
hasArticle: false,
|
hasArticle: false,
|
||||||
isMarked: false,
|
isMarked: false,
|
||||||
isArchived: false,
|
isArchived: false,
|
||||||
|
labels: [],
|
||||||
thumbnailUrl: "",
|
thumbnailUrl: "",
|
||||||
imageUrl: ""
|
imageUrl: ""
|
||||||
)
|
)
|
||||||
|
|||||||
@ -59,4 +59,12 @@ extension BookmarkUpdateRequest {
|
|||||||
static func updateLabels(_ labels: [String]) -> BookmarkUpdateRequest {
|
static func updateLabels(_ labels: [String]) -> BookmarkUpdateRequest {
|
||||||
return BookmarkUpdateRequest(labels: labels)
|
return BookmarkUpdateRequest(labels: labels)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func addLabels(_ labels: [String]) -> BookmarkUpdateRequest {
|
||||||
|
return BookmarkUpdateRequest(addLabels: labels)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func removeLabels(_ labels: [String]) -> BookmarkUpdateRequest {
|
||||||
|
return BookmarkUpdateRequest(removeLabels: labels)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
43
readeck/Domain/UseCase/AddLabelsToBookmarkUseCase.swift
Normal file
43
readeck/Domain/UseCase/AddLabelsToBookmarkUseCase.swift
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
class AddLabelsToBookmarkUseCase {
|
||||||
|
private let repository: PBookmarksRepository
|
||||||
|
|
||||||
|
init(repository: PBookmarksRepository) {
|
||||||
|
self.repository = repository
|
||||||
|
}
|
||||||
|
|
||||||
|
func execute(bookmarkId: String, labels: [String]) async throws {
|
||||||
|
// Validierung der Labels
|
||||||
|
guard !labels.isEmpty else {
|
||||||
|
throw BookmarkUpdateError.emptyLabels
|
||||||
|
}
|
||||||
|
|
||||||
|
// Entferne leere Labels und Duplikate
|
||||||
|
let cleanLabels = Array(Set(labels.filter { !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }))
|
||||||
|
|
||||||
|
guard !cleanLabels.isEmpty else {
|
||||||
|
throw BookmarkUpdateError.emptyLabels
|
||||||
|
}
|
||||||
|
|
||||||
|
let request = BookmarkUpdateRequest.addLabels(cleanLabels)
|
||||||
|
try await repository.updateBookmark(id: bookmarkId, updateRequest: request)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convenience method für einzelne Labels
|
||||||
|
func execute(bookmarkId: String, label: String) async throws {
|
||||||
|
try await execute(bookmarkId: bookmarkId, labels: [label])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom error für Label-Operationen
|
||||||
|
enum BookmarkUpdateError: LocalizedError {
|
||||||
|
case emptyLabels
|
||||||
|
|
||||||
|
var errorDescription: String? {
|
||||||
|
switch self {
|
||||||
|
case .emptyLabels:
|
||||||
|
return "Labels können nicht leer sein"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
31
readeck/Domain/UseCase/RemoveLabelsFromBookmarkUseCase.swift
Normal file
31
readeck/Domain/UseCase/RemoveLabelsFromBookmarkUseCase.swift
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
class RemoveLabelsFromBookmarkUseCase {
|
||||||
|
private let repository: PBookmarksRepository
|
||||||
|
|
||||||
|
init(repository: PBookmarksRepository) {
|
||||||
|
self.repository = repository
|
||||||
|
}
|
||||||
|
|
||||||
|
func execute(bookmarkId: String, labels: [String]) async throws {
|
||||||
|
// Validierung der Labels
|
||||||
|
guard !labels.isEmpty else {
|
||||||
|
throw BookmarkUpdateError.emptyLabels
|
||||||
|
}
|
||||||
|
|
||||||
|
// Entferne leere Labels und Duplikate
|
||||||
|
let cleanLabels = Array(Set(labels.filter { !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }))
|
||||||
|
|
||||||
|
guard !cleanLabels.isEmpty else {
|
||||||
|
throw BookmarkUpdateError.emptyLabels
|
||||||
|
}
|
||||||
|
|
||||||
|
let request = BookmarkUpdateRequest.removeLabels(cleanLabels)
|
||||||
|
try await repository.updateBookmark(id: bookmarkId, updateRequest: request)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convenience method für einzelne Labels
|
||||||
|
func execute(bookmarkId: String, label: String) async throws {
|
||||||
|
try await execute(bookmarkId: bookmarkId, labels: [label])
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -41,4 +41,14 @@ class UpdateBookmarkUseCase {
|
|||||||
let request = BookmarkUpdateRequest.updateLabels(labels)
|
let request = BookmarkUpdateRequest.updateLabels(labels)
|
||||||
try await execute(bookmarkId: bookmarkId, updateRequest: request)
|
try await execute(bookmarkId: bookmarkId, updateRequest: request)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func addLabels(bookmarkId: String, labels: [String]) async throws {
|
||||||
|
let request = BookmarkUpdateRequest.addLabels(labels)
|
||||||
|
try await execute(bookmarkId: bookmarkId, updateRequest: request)
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeLabels(bookmarkId: String, labels: [String]) async throws {
|
||||||
|
let request = BookmarkUpdateRequest.removeLabels(labels)
|
||||||
|
try await execute(bookmarkId: bookmarkId, updateRequest: request)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -6,8 +6,9 @@ struct BookmarkDetailView: View {
|
|||||||
@State private var viewModel = BookmarkDetailViewModel()
|
@State private var viewModel = BookmarkDetailViewModel()
|
||||||
@State private var webViewHeight: CGFloat = 300
|
@State private var webViewHeight: CGFloat = 300
|
||||||
@State private var showingFontSettings = false
|
@State private var showingFontSettings = false
|
||||||
|
@State private var showingLabelsSheet = false
|
||||||
|
|
||||||
private let headerHeight: CGFloat = 260
|
private let headerHeight: CGFloat = 320
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
GeometryReader { geometry in
|
GeometryReader { geometry in
|
||||||
@ -29,10 +30,18 @@ struct BookmarkDetailView: View {
|
|||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
Button(action: {
|
HStack(spacing: 12) {
|
||||||
showingFontSettings = true
|
Button(action: {
|
||||||
}) {
|
showingLabelsSheet = true
|
||||||
Image(systemName: "textformat")
|
}) {
|
||||||
|
Image(systemName: "tag")
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(action: {
|
||||||
|
showingFontSettings = true
|
||||||
|
}) {
|
||||||
|
Image(systemName: "textformat")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -57,6 +66,9 @@ struct BookmarkDetailView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.sheet(isPresented: $showingLabelsSheet) {
|
||||||
|
BookmarkLabelsView(bookmarkId: bookmarkId, initialLabels: viewModel.bookmarkDetail.labels)
|
||||||
|
}
|
||||||
.onChange(of: showingFontSettings) { _, isShowing in
|
.onChange(of: showingFontSettings) { _, isShowing in
|
||||||
if !isShowing {
|
if !isShowing {
|
||||||
// Reload settings when sheet is dismissed
|
// Reload settings when sheet is dismissed
|
||||||
@ -65,6 +77,14 @@ struct BookmarkDetailView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onChange(of: showingLabelsSheet) { _, isShowing in
|
||||||
|
if !isShowing {
|
||||||
|
// Reload bookmark detail when labels sheet is dismissed
|
||||||
|
Task {
|
||||||
|
await viewModel.refreshBookmarkDetail(id: bookmarkId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.task {
|
.task {
|
||||||
await viewModel.loadBookmarkDetail(id: bookmarkId)
|
await viewModel.loadBookmarkDetail(id: bookmarkId)
|
||||||
await viewModel.loadArticleContent(id: bookmarkId)
|
await viewModel.loadArticleContent(id: bookmarkId)
|
||||||
@ -91,13 +111,22 @@ struct BookmarkDetailView: View {
|
|||||||
.fill(Color.gray.opacity(0.4))
|
.fill(Color.gray.opacity(0.4))
|
||||||
.frame(width: geometry.size.width, height: headerHeight)
|
.frame(width: geometry.size.width, height: headerHeight)
|
||||||
}
|
}
|
||||||
|
// Gradient overlay für bessere Button-Sichtbarkeit
|
||||||
LinearGradient(
|
LinearGradient(
|
||||||
gradient: Gradient(colors: [Color.black.opacity(0.6), Color.clear]),
|
gradient: Gradient(colors: [
|
||||||
|
Color.black.opacity(1.0),
|
||||||
|
Color.black.opacity(0.9),
|
||||||
|
Color.black.opacity(0.7),
|
||||||
|
Color.black.opacity(0.4),
|
||||||
|
Color.black.opacity(0.2),
|
||||||
|
Color.clear
|
||||||
|
]),
|
||||||
startPoint: .top,
|
startPoint: .top,
|
||||||
endPoint: .bottom
|
endPoint: .bottom
|
||||||
)
|
)
|
||||||
.frame(height: 120)
|
.frame(height: 240)
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
|
.offset(y: (offset > 0 ? -offset : 0))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(height: headerHeight)
|
.frame(height: headerHeight)
|
||||||
@ -155,6 +184,38 @@ struct BookmarkDetailView: View {
|
|||||||
}
|
}
|
||||||
metaRow(icon: "calendar", text: formatDate(viewModel.bookmarkDetail.created))
|
metaRow(icon: "calendar", text: formatDate(viewModel.bookmarkDetail.created))
|
||||||
metaRow(icon: "textformat", text: "\(viewModel.bookmarkDetail.wordCount ?? 0) Wörter • \(viewModel.bookmarkDetail.readingTime ?? 0) min Lesezeit")
|
metaRow(icon: "textformat", text: "\(viewModel.bookmarkDetail.wordCount ?? 0) Wörter • \(viewModel.bookmarkDetail.readingTime ?? 0) min Lesezeit")
|
||||||
|
|
||||||
|
// Labels section
|
||||||
|
if !viewModel.bookmarkDetail.labels.isEmpty {
|
||||||
|
HStack(alignment: .top, spacing: 8) {
|
||||||
|
Image(systemName: "tag")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.padding(.top, 2)
|
||||||
|
|
||||||
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
|
HStack(spacing: 6) {
|
||||||
|
ForEach(viewModel.bookmarkDetail.labels, id: \.self) { label in
|
||||||
|
Text(label)
|
||||||
|
.font(.caption)
|
||||||
|
.fontWeight(.medium)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
.padding(.horizontal, 8)
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: 12)
|
||||||
|
.fill(Color.accentColor.opacity(0.1))
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 12)
|
||||||
|
.stroke(Color.accentColor.opacity(0.3), lineWidth: 1)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.trailing, 8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
metaRow(icon: "safari") {
|
metaRow(icon: "safari") {
|
||||||
Button(action: {
|
Button(action: {
|
||||||
SafariUtil.openInSafari(url: viewModel.bookmarkDetail.url)
|
SafariUtil.openInSafari(url: viewModel.bookmarkDetail.url)
|
||||||
|
|||||||
@ -73,4 +73,9 @@ class BookmarkDetailViewModel {
|
|||||||
}
|
}
|
||||||
isLoading = false
|
isLoading = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func refreshBookmarkDetail(id: String) async {
|
||||||
|
await loadBookmarkDetail(id: id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
175
readeck/UI/BookmarkDetail/BookmarkLabelsView.swift
Normal file
175
readeck/UI/BookmarkDetail/BookmarkLabelsView.swift
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
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"])
|
||||||
|
}
|
||||||
86
readeck/UI/BookmarkDetail/BookmarkLabelsViewModel.swift
Normal file
86
readeck/UI/BookmarkDetail/BookmarkLabelsViewModel.swift
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
@Observable
|
||||||
|
class BookmarkLabelsViewModel {
|
||||||
|
private let addLabelsUseCase = DefaultUseCaseFactory.shared.makeAddLabelsToBookmarkUseCase()
|
||||||
|
private let removeLabelsUseCase = DefaultUseCaseFactory.shared.makeRemoveLabelsFromBookmarkUseCase()
|
||||||
|
|
||||||
|
var isLoading = false
|
||||||
|
var errorMessage: String?
|
||||||
|
var showErrorAlert = false
|
||||||
|
var currentLabels: [String] = []
|
||||||
|
var newLabelText = ""
|
||||||
|
|
||||||
|
init(initialLabels: [String] = []) {
|
||||||
|
self.currentLabels = initialLabels
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func addLabels(to bookmarkId: String, labels: [String]) async {
|
||||||
|
isLoading = true
|
||||||
|
errorMessage = nil
|
||||||
|
|
||||||
|
do {
|
||||||
|
try await addLabelsUseCase.execute(bookmarkId: bookmarkId, labels: labels)
|
||||||
|
// Update local labels
|
||||||
|
currentLabels.append(contentsOf: labels)
|
||||||
|
currentLabels = Array(Set(currentLabels)) // Remove duplicates
|
||||||
|
} catch let error as BookmarkUpdateError {
|
||||||
|
errorMessage = error.localizedDescription
|
||||||
|
showErrorAlert = true
|
||||||
|
} catch {
|
||||||
|
errorMessage = "Fehler beim Hinzufügen der 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 = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 = "Fehler beim Entfernen der 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -13,6 +13,8 @@ protocol UseCaseFactory {
|
|||||||
func makeLogoutUseCase() -> LogoutUseCase
|
func makeLogoutUseCase() -> LogoutUseCase
|
||||||
func makeSearchBookmarksUseCase() -> SearchBookmarksUseCase
|
func makeSearchBookmarksUseCase() -> SearchBookmarksUseCase
|
||||||
func makeSaveServerSettingsUseCase() -> SaveServerSettingsUseCase
|
func makeSaveServerSettingsUseCase() -> SaveServerSettingsUseCase
|
||||||
|
func makeAddLabelsToBookmarkUseCase() -> AddLabelsToBookmarkUseCase
|
||||||
|
func makeRemoveLabelsFromBookmarkUseCase() -> RemoveLabelsFromBookmarkUseCase
|
||||||
}
|
}
|
||||||
|
|
||||||
class DefaultUseCaseFactory: UseCaseFactory {
|
class DefaultUseCaseFactory: UseCaseFactory {
|
||||||
@ -78,4 +80,12 @@ class DefaultUseCaseFactory: UseCaseFactory {
|
|||||||
func makeSaveServerSettingsUseCase() -> SaveServerSettingsUseCase {
|
func makeSaveServerSettingsUseCase() -> SaveServerSettingsUseCase {
|
||||||
return SaveServerSettingsUseCase(repository: SettingsRepository())
|
return SaveServerSettingsUseCase(repository: SettingsRepository())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func makeAddLabelsToBookmarkUseCase() -> AddLabelsToBookmarkUseCase {
|
||||||
|
return AddLabelsToBookmarkUseCase(repository: bookmarksRepository)
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeRemoveLabelsFromBookmarkUseCase() -> RemoveLabelsFromBookmarkUseCase {
|
||||||
|
return RemoveLabelsFromBookmarkUseCase(repository: bookmarksRepository)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,7 +12,7 @@ enum SidebarTab: Hashable, CaseIterable, Identifiable {
|
|||||||
|
|
||||||
var label: String {
|
var label: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .all: return "Alle"
|
case .all: return "All"
|
||||||
case .unread: return "Ungelesen"
|
case .unread: return "Ungelesen"
|
||||||
case .favorite: return "Favoriten"
|
case .favorite: return "Favoriten"
|
||||||
case .archived: return "Archiv"
|
case .archived: return "Archiv"
|
||||||
|
|||||||
@ -126,7 +126,7 @@ struct SettingsServerView: View {
|
|||||||
Button("Debug-Anmeldung") {
|
Button("Debug-Anmeldung") {
|
||||||
viewModel.username = "admin"
|
viewModel.username = "admin"
|
||||||
viewModel.password = "Diggah123"
|
viewModel.password = "Diggah123"
|
||||||
viewModel.endpoint = "https://readeck.mnk.any64.de"
|
viewModel.endpoint = "https://keep.mnk.any64.de"
|
||||||
}
|
}
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user