feat: Add bookmark actions (archive, favorite, delete)
- Add PATCH API endpoint for updating bookmarks with toggle functions - Add DELETE API endpoint for permanent bookmark deletion - Implement UpdateBookmarkUseCase with convenience methods for common actions - Implement DeleteBookmarkUseCase for permanent bookmark removal - Create BookmarkUpdateRequest domain model with builder pattern - Extend BookmarkCardView with action menu and confirmation dialog - Add context-sensitive actions based on current bookmark state - Implement optimistic updates in BookmarksViewModel - Add error handling and recovery for failed operations - Enhance UI with badges, progress indicators, and action buttons
This commit is contained in:
parent
c8368f0a70
commit
cd265730d3
@ -13,6 +13,8 @@ protocol PAPI {
|
|||||||
func getBookmarks(state: BookmarkState?) async throws -> [BookmarkDto]
|
func getBookmarks(state: BookmarkState?) async throws -> [BookmarkDto]
|
||||||
func getBookmark(id: String) async throws -> BookmarkDetailDto
|
func getBookmark(id: String) async throws -> BookmarkDetailDto
|
||||||
func getBookmarkArticle(id: String) async throws -> String
|
func getBookmarkArticle(id: String) async throws -> String
|
||||||
|
func updateBookmark(id: String, updateRequest: UpdateBookmarkRequestDto) async throws
|
||||||
|
func deleteBookmark(id: String) async throws
|
||||||
}
|
}
|
||||||
|
|
||||||
class API: PAPI {
|
class API: PAPI {
|
||||||
@ -162,12 +164,76 @@ class API: PAPI {
|
|||||||
endpoint: "/api/bookmarks/\(id)/article"
|
endpoint: "/api/bookmarks/\(id)/article"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateBookmark(id: String, updateRequest: UpdateBookmarkRequestDto) async throws {
|
||||||
|
let requestData = try JSONEncoder().encode(updateRequest)
|
||||||
|
|
||||||
|
// PATCH Request ohne Response-Body erwarten
|
||||||
|
let baseURL = await self.baseURL
|
||||||
|
let fullEndpoint = "/api/bookmarks/\(id)"
|
||||||
|
|
||||||
|
guard let url = URL(string: "\(baseURL)\(fullEndpoint)") else {
|
||||||
|
throw APIError.invalidURL
|
||||||
|
}
|
||||||
|
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
request.httpMethod = "PATCH"
|
||||||
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||||
|
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||||
|
|
||||||
|
if let token = await tokenProvider.getToken() {
|
||||||
|
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
||||||
|
}
|
||||||
|
|
||||||
|
request.httpBody = requestData
|
||||||
|
|
||||||
|
let (_, response) = try await URLSession.shared.data(for: request)
|
||||||
|
|
||||||
|
guard let httpResponse = response as? HTTPURLResponse else {
|
||||||
|
throw APIError.invalidResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
guard 200...299 ~= httpResponse.statusCode else {
|
||||||
|
print("Server Error: \(httpResponse.statusCode)")
|
||||||
|
throw APIError.serverError(httpResponse.statusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteBookmark(id: String) async throws {
|
||||||
|
// DELETE Request ohne Response-Body erwarten
|
||||||
|
let baseURL = await self.baseURL
|
||||||
|
let fullEndpoint = "/api/bookmarks/\(id)"
|
||||||
|
|
||||||
|
guard let url = URL(string: "\(baseURL)\(fullEndpoint)") else {
|
||||||
|
throw APIError.invalidURL
|
||||||
|
}
|
||||||
|
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
request.httpMethod = "DELETE"
|
||||||
|
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||||
|
|
||||||
|
if let token = await tokenProvider.getToken() {
|
||||||
|
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
||||||
|
}
|
||||||
|
|
||||||
|
let (_, response) = try await URLSession.shared.data(for: request)
|
||||||
|
|
||||||
|
guard let httpResponse = response as? HTTPURLResponse else {
|
||||||
|
throw APIError.invalidResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
guard 200...299 ~= httpResponse.statusCode else {
|
||||||
|
print("Server Error: \(httpResponse.statusCode)")
|
||||||
|
throw APIError.serverError(httpResponse.statusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum HTTPMethod: String {
|
enum HTTPMethod: String {
|
||||||
case GET = "GET"
|
case GET = "GET"
|
||||||
case POST = "POST"
|
case POST = "POST"
|
||||||
case PUT = "PUT"
|
case PUT = "PUT"
|
||||||
|
case PATCH = "PATCH"
|
||||||
case DELETE = "DELETE"
|
case DELETE = "DELETE"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
25
readeck/Data/API/DTOs/UpdateBookmarkRequestDto.swift
Normal file
25
readeck/Data/API/DTOs/UpdateBookmarkRequestDto.swift
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct UpdateBookmarkRequestDto: Codable {
|
||||||
|
let addLabels: [String]?
|
||||||
|
let isArchived: Bool?
|
||||||
|
let isDeleted: Bool?
|
||||||
|
let isMarked: Bool?
|
||||||
|
let labels: [String]?
|
||||||
|
let readAnchor: String?
|
||||||
|
let readProgress: Int?
|
||||||
|
let removeLabels: [String]?
|
||||||
|
let title: String?
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case addLabels = "add_labels"
|
||||||
|
case isArchived = "is_archived"
|
||||||
|
case isDeleted = "is_deleted"
|
||||||
|
case isMarked = "is_marked"
|
||||||
|
case labels
|
||||||
|
case readAnchor = "read_anchor"
|
||||||
|
case readProgress = "read_progress"
|
||||||
|
case removeLabels = "remove_labels"
|
||||||
|
case title
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,8 +4,9 @@ protocol PBookmarksRepository {
|
|||||||
func fetchBookmarks(state: BookmarkState?) async throws -> [Bookmark]
|
func fetchBookmarks(state: BookmarkState?) async throws -> [Bookmark]
|
||||||
func fetchBookmark(id: String) async throws -> BookmarkDetail
|
func fetchBookmark(id: String) async throws -> BookmarkDetail
|
||||||
func fetchBookmarkArticle(id: String) async throws -> String
|
func fetchBookmarkArticle(id: String) async throws -> String
|
||||||
|
func updateBookmark(id: String, updateRequest: BookmarkUpdateRequest) async throws
|
||||||
|
func deleteBookmark(id: String) async throws
|
||||||
func addBookmark(bookmark: Bookmark) async throws
|
func addBookmark(bookmark: Bookmark) async throws
|
||||||
func removeBookmark(id: String) async throws
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class BookmarksRepository: PBookmarksRepository {
|
class BookmarksRepository: PBookmarksRepository {
|
||||||
@ -49,8 +50,24 @@ class BookmarksRepository: PBookmarksRepository {
|
|||||||
// Implement logic to add a bookmark if needed
|
// Implement logic to add a bookmark if needed
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeBookmark(id: String) async throws {
|
func deleteBookmark(id: String) async throws {
|
||||||
// Implement logic to remove a bookmark if needed
|
try await api.deleteBookmark(id: id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateBookmark(id: String, updateRequest: BookmarkUpdateRequest) async throws {
|
||||||
|
let dto = UpdateBookmarkRequestDto(
|
||||||
|
addLabels: updateRequest.addLabels,
|
||||||
|
isArchived: updateRequest.isArchived,
|
||||||
|
isDeleted: updateRequest.isDeleted,
|
||||||
|
isMarked: updateRequest.isMarked,
|
||||||
|
labels: updateRequest.labels,
|
||||||
|
readAnchor: updateRequest.readAnchor,
|
||||||
|
readProgress: updateRequest.readProgress,
|
||||||
|
removeLabels: updateRequest.removeLabels,
|
||||||
|
title: updateRequest.title
|
||||||
|
)
|
||||||
|
|
||||||
|
try await api.updateBookmark(id: id, updateRequest: dto)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -7,6 +7,8 @@ protocol UseCaseFactory {
|
|||||||
func makeGetBookmarkArticleUseCase() -> GetBookmarkArticleUseCase
|
func makeGetBookmarkArticleUseCase() -> GetBookmarkArticleUseCase
|
||||||
func makeSaveSettingsUseCase() -> SaveSettingsUseCase
|
func makeSaveSettingsUseCase() -> SaveSettingsUseCase
|
||||||
func makeLoadSettingsUseCase() -> LoadSettingsUseCase
|
func makeLoadSettingsUseCase() -> LoadSettingsUseCase
|
||||||
|
func makeUpdateBookmarkUseCase() -> UpdateBookmarkUseCase
|
||||||
|
func makeDeleteBookmarkUseCase() -> DeleteBookmarkUseCase
|
||||||
}
|
}
|
||||||
|
|
||||||
class DefaultUseCaseFactory: UseCaseFactory {
|
class DefaultUseCaseFactory: UseCaseFactory {
|
||||||
@ -42,9 +44,17 @@ class DefaultUseCaseFactory: UseCaseFactory {
|
|||||||
func makeLoadSettingsUseCase() -> LoadSettingsUseCase {
|
func makeLoadSettingsUseCase() -> LoadSettingsUseCase {
|
||||||
LoadSettingsUseCase(authRepository: authRepository)
|
LoadSettingsUseCase(authRepository: authRepository)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func makeUpdateBookmarkUseCase() -> UpdateBookmarkUseCase {
|
||||||
|
return UpdateBookmarkUseCase(repository: bookmarksRepository)
|
||||||
|
}
|
||||||
|
|
||||||
// Nicht mehr nötig - Token wird automatisch geladen
|
// Nicht mehr nötig - Token wird automatisch geladen
|
||||||
func refreshConfiguration() async {
|
func refreshConfiguration() async {
|
||||||
// Optional: Cache löschen falls nötig
|
// Optional: Cache löschen falls nötig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func makeDeleteBookmarkUseCase() -> DeleteBookmarkUseCase {
|
||||||
|
return DeleteBookmarkUseCase(repository: bookmarksRepository)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
62
readeck/Domain/Model/BookmarkUpdateRequest.swift
Normal file
62
readeck/Domain/Model/BookmarkUpdateRequest.swift
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct BookmarkUpdateRequest {
|
||||||
|
let addLabels: [String]?
|
||||||
|
let isArchived: Bool?
|
||||||
|
let isDeleted: Bool?
|
||||||
|
let isMarked: Bool?
|
||||||
|
let labels: [String]?
|
||||||
|
let readAnchor: String?
|
||||||
|
let readProgress: Int?
|
||||||
|
let removeLabels: [String]?
|
||||||
|
let title: String?
|
||||||
|
|
||||||
|
init(
|
||||||
|
addLabels: [String]? = nil,
|
||||||
|
isArchived: Bool? = nil,
|
||||||
|
isDeleted: Bool? = nil,
|
||||||
|
isMarked: Bool? = nil,
|
||||||
|
labels: [String]? = nil,
|
||||||
|
readAnchor: String? = nil,
|
||||||
|
readProgress: Int? = nil,
|
||||||
|
removeLabels: [String]? = nil,
|
||||||
|
title: String? = nil
|
||||||
|
) {
|
||||||
|
self.addLabels = addLabels
|
||||||
|
self.isArchived = isArchived
|
||||||
|
self.isDeleted = isDeleted
|
||||||
|
self.isMarked = isMarked
|
||||||
|
self.labels = labels
|
||||||
|
self.readAnchor = readAnchor
|
||||||
|
self.readProgress = readProgress
|
||||||
|
self.removeLabels = removeLabels
|
||||||
|
self.title = title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convenience Initializers für häufige Aktionen
|
||||||
|
extension BookmarkUpdateRequest {
|
||||||
|
static func archive(_ isArchived: Bool) -> BookmarkUpdateRequest {
|
||||||
|
return BookmarkUpdateRequest(isArchived: isArchived)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func favorite(_ isMarked: Bool) -> BookmarkUpdateRequest {
|
||||||
|
return BookmarkUpdateRequest(isMarked: isMarked)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func delete(_ isDeleted: Bool) -> BookmarkUpdateRequest {
|
||||||
|
return BookmarkUpdateRequest(isDeleted: isDeleted)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func updateProgress(_ progress: Int, anchor: String? = nil) -> BookmarkUpdateRequest {
|
||||||
|
return BookmarkUpdateRequest(readAnchor: anchor, readProgress: progress)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func updateTitle(_ title: String) -> BookmarkUpdateRequest {
|
||||||
|
return BookmarkUpdateRequest(title: title)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func updateLabels(_ labels: [String]) -> BookmarkUpdateRequest {
|
||||||
|
return BookmarkUpdateRequest(labels: labels)
|
||||||
|
}
|
||||||
|
}
|
||||||
13
readeck/Domain/UseCase/DeleteBookmarkUseCase.swift
Normal file
13
readeck/Domain/UseCase/DeleteBookmarkUseCase.swift
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
class DeleteBookmarkUseCase {
|
||||||
|
private let repository: PBookmarksRepository
|
||||||
|
|
||||||
|
init(repository: PBookmarksRepository) {
|
||||||
|
self.repository = repository
|
||||||
|
}
|
||||||
|
|
||||||
|
func execute(bookmarkId: String) async throws {
|
||||||
|
try await repository.deleteBookmark(id: bookmarkId)
|
||||||
|
}
|
||||||
|
}
|
||||||
44
readeck/Domain/UseCase/UpdateBookmarkUseCase.swift
Normal file
44
readeck/Domain/UseCase/UpdateBookmarkUseCase.swift
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
class UpdateBookmarkUseCase {
|
||||||
|
private let repository: PBookmarksRepository
|
||||||
|
|
||||||
|
init(repository: PBookmarksRepository) {
|
||||||
|
self.repository = repository
|
||||||
|
}
|
||||||
|
|
||||||
|
func execute(bookmarkId: String, updateRequest: BookmarkUpdateRequest) async throws {
|
||||||
|
try await repository.updateBookmark(id: bookmarkId, updateRequest: updateRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convenience methods für häufige Aktionen
|
||||||
|
func toggleArchive(bookmarkId: String, isArchived: Bool) async throws {
|
||||||
|
let request = BookmarkUpdateRequest.archive(isArchived)
|
||||||
|
try await execute(bookmarkId: bookmarkId, updateRequest: request)
|
||||||
|
}
|
||||||
|
|
||||||
|
func toggleFavorite(bookmarkId: String, isMarked: Bool) async throws {
|
||||||
|
let request = BookmarkUpdateRequest.favorite(isMarked)
|
||||||
|
try await execute(bookmarkId: bookmarkId, updateRequest: request)
|
||||||
|
}
|
||||||
|
|
||||||
|
func markAsDeleted(bookmarkId: String) async throws {
|
||||||
|
let request = BookmarkUpdateRequest.delete(true)
|
||||||
|
try await execute(bookmarkId: bookmarkId, updateRequest: request)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateReadProgress(bookmarkId: String, progress: Int, anchor: String? = nil) async throws {
|
||||||
|
let request = BookmarkUpdateRequest.updateProgress(progress, anchor: anchor)
|
||||||
|
try await execute(bookmarkId: bookmarkId, updateRequest: request)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateTitle(bookmarkId: String, title: String) async throws {
|
||||||
|
let request = BookmarkUpdateRequest.updateTitle(title)
|
||||||
|
try await execute(bookmarkId: bookmarkId, updateRequest: request)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateLabels(bookmarkId: String, labels: [String]) async throws {
|
||||||
|
let request = BookmarkUpdateRequest.updateLabels(labels)
|
||||||
|
try await execute(bookmarkId: bookmarkId, updateRequest: request)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,6 +2,12 @@ import SwiftUI
|
|||||||
|
|
||||||
struct BookmarkCardView: View {
|
struct BookmarkCardView: View {
|
||||||
let bookmark: Bookmark
|
let bookmark: Bookmark
|
||||||
|
let currentState: BookmarkState
|
||||||
|
let onArchive: (Bookmark) -> Void
|
||||||
|
let onDelete: (Bookmark) -> Void
|
||||||
|
let onToggleFavorite: (Bookmark) -> Void
|
||||||
|
|
||||||
|
@State private var showingActionSheet = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
@ -22,18 +28,33 @@ struct BookmarkCardView: View {
|
|||||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
// Status-Badges
|
// Status-Badges und Action-Button
|
||||||
HStack {
|
HStack {
|
||||||
if bookmark.isMarked {
|
HStack(spacing: 4) {
|
||||||
Badge(text: "Markiert", color: .blue)
|
if bookmark.isMarked {
|
||||||
}
|
Badge(text: "Markiert", color: .blue)
|
||||||
if bookmark.isArchived {
|
}
|
||||||
Badge(text: "Archiviert", color: .gray)
|
if bookmark.isArchived {
|
||||||
}
|
Badge(text: "Archiviert", color: .gray)
|
||||||
if bookmark.hasArticle {
|
}
|
||||||
Badge(text: "Artikel", color: .green)
|
if bookmark.hasArticle {
|
||||||
|
Badge(text: "Artikel", color: .green)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
|
// Action Menu Button
|
||||||
|
Button(action: {
|
||||||
|
showingActionSheet = true
|
||||||
|
}) {
|
||||||
|
Image(systemName: "ellipsis")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.padding(8)
|
||||||
|
.background(Color.gray.opacity(0.1))
|
||||||
|
.clipShape(Circle())
|
||||||
|
}
|
||||||
|
.buttonStyle(PlainButtonStyle())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Titel
|
// Titel
|
||||||
@ -80,6 +101,36 @@ struct BookmarkCardView: View {
|
|||||||
.background(Color(.systemBackground))
|
.background(Color(.systemBackground))
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||||
.shadow(color: .black.opacity(0.1), radius: 2, x: 0, y: 1)
|
.shadow(color: .black.opacity(0.1), radius: 2, x: 0, y: 1)
|
||||||
|
.confirmationDialog("Bookmark Aktionen", isPresented: $showingActionSheet) {
|
||||||
|
actionButtons
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var actionButtons: some View {
|
||||||
|
Group {
|
||||||
|
// Favorit Toggle
|
||||||
|
Button(bookmark.isMarked ? "Favorit entfernen" : "Als Favorit markieren") {
|
||||||
|
onToggleFavorite(bookmark)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Archivieren/Dearchivieren basierend auf aktuellem State
|
||||||
|
if currentState == .archived {
|
||||||
|
Button("Aus Archiv entfernen") {
|
||||||
|
onArchive(bookmark)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Button("Archivieren") {
|
||||||
|
onArchive(bookmark)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Permanent löschen (immer verfügbar)
|
||||||
|
Button("Permanent löschen", role: .destructive) {
|
||||||
|
onDelete(bookmark)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button("Abbrechen", role: .cancel) { }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var imageURL: URL? {
|
private var imageURL: URL? {
|
||||||
|
|||||||
@ -7,24 +7,48 @@ struct BookmarksView: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
ZStack {
|
ZStack {
|
||||||
if viewModel.isLoading {
|
if viewModel.isLoading && viewModel.bookmarks.isEmpty {
|
||||||
ProgressView()
|
ProgressView("Lade \(state.displayName)...")
|
||||||
} else {
|
} else {
|
||||||
|
ScrollView {
|
||||||
List(viewModel.bookmarks, id: \.id) { bookmark in
|
LazyVStack(spacing: 12) {
|
||||||
NavigationLink(destination: BookmarkDetailView(bookmarkId: bookmark.id)) {
|
ForEach(viewModel.bookmarks, id: \.id) { bookmark in
|
||||||
BookmarkCardView(bookmark: bookmark)
|
NavigationLink(destination: BookmarkDetailView(bookmarkId: bookmark.id)) {
|
||||||
|
BookmarkCardView(
|
||||||
|
bookmark: bookmark,
|
||||||
|
currentState: state,
|
||||||
|
onArchive: { bookmark in
|
||||||
|
Task {
|
||||||
|
await viewModel.toggleArchive(bookmark: bookmark)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDelete: { bookmark in
|
||||||
|
Task {
|
||||||
|
await viewModel.deleteBookmark(bookmark: bookmark)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onToggleFavorite: { bookmark in
|
||||||
|
Task {
|
||||||
|
await viewModel.toggleFavorite(bookmark: bookmark)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.padding(.bottom, 20)
|
||||||
|
}
|
||||||
|
.buttonStyle(PlainButtonStyle())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
.padding()
|
||||||
}
|
}
|
||||||
.refreshable {
|
.refreshable {
|
||||||
await viewModel.loadBookmarks()
|
await viewModel.refreshBookmarks()
|
||||||
}
|
}
|
||||||
.overlay {
|
.overlay {
|
||||||
if viewModel.bookmarks.isEmpty && !viewModel.isLoading {
|
if viewModel.bookmarks.isEmpty && !viewModel.isLoading {
|
||||||
ContentUnavailableView(
|
ContentUnavailableView(
|
||||||
"Keine Bookmarks",
|
"Keine Bookmarks",
|
||||||
systemImage: "bookmark",
|
systemImage: "bookmark",
|
||||||
description: Text("Es wurden noch keine Bookmarks gespeichert.")
|
description: Text("Es wurden noch keine Bookmarks in \(state.displayName.lowercased()) gefunden.")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -32,7 +56,9 @@ struct BookmarksView: View {
|
|||||||
}
|
}
|
||||||
.navigationTitle(state.displayName)
|
.navigationTitle(state.displayName)
|
||||||
.alert("Fehler", isPresented: .constant(viewModel.errorMessage != nil)) {
|
.alert("Fehler", isPresented: .constant(viewModel.errorMessage != nil)) {
|
||||||
Button("OK", role: .cancel) { }
|
Button("OK", role: .cancel) {
|
||||||
|
viewModel.errorMessage = nil
|
||||||
|
}
|
||||||
} message: {
|
} message: {
|
||||||
Text(viewModel.errorMessage ?? "")
|
Text(viewModel.errorMessage ?? "")
|
||||||
}
|
}
|
||||||
@ -42,26 +68,3 @@ struct BookmarksView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unterkomponente für die Darstellung eines einzelnen Bookmarks
|
|
||||||
private struct BookmarkRow: View {
|
|
||||||
let bookmark: Bookmark
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
NavigationLink(destination: BookmarkDetailView(bookmarkId: bookmark.id)) {
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
|
||||||
Text(bookmark.title)
|
|
||||||
.font(.headline)
|
|
||||||
|
|
||||||
Text(bookmark.url)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
|
|
||||||
Text(bookmark.created)
|
|
||||||
.font(.caption2)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
.padding(.vertical, 4)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -3,6 +3,8 @@ import Foundation
|
|||||||
@Observable
|
@Observable
|
||||||
class BookmarksViewModel {
|
class BookmarksViewModel {
|
||||||
private let getBooksmarksUseCase = DefaultUseCaseFactory.shared.makeGetBookmarksUseCase()
|
private let getBooksmarksUseCase = DefaultUseCaseFactory.shared.makeGetBookmarksUseCase()
|
||||||
|
private let updateBookmarkUseCase = DefaultUseCaseFactory.shared.makeUpdateBookmarkUseCase()
|
||||||
|
private let deleteBookmarkUseCase = DefaultUseCaseFactory.shared.makeDeleteBookmarkUseCase()
|
||||||
|
|
||||||
var bookmarks: [Bookmark] = []
|
var bookmarks: [Bookmark] = []
|
||||||
var isLoading = false
|
var isLoading = false
|
||||||
@ -34,4 +36,52 @@ class BookmarksViewModel {
|
|||||||
func refreshBookmarks() async {
|
func refreshBookmarks() async {
|
||||||
await loadBookmarks(state: currentState)
|
await loadBookmarks(state: currentState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func toggleArchive(bookmark: Bookmark) async {
|
||||||
|
do {
|
||||||
|
try await updateBookmarkUseCase.toggleArchive(
|
||||||
|
bookmarkId: bookmark.id,
|
||||||
|
isArchived: !bookmark.isArchived
|
||||||
|
)
|
||||||
|
|
||||||
|
// Liste aktualisieren
|
||||||
|
await loadBookmarks(state: currentState)
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
errorMessage = "Fehler beim Archivieren des Bookmarks"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func toggleFavorite(bookmark: Bookmark) async {
|
||||||
|
do {
|
||||||
|
try await updateBookmarkUseCase.toggleFavorite(
|
||||||
|
bookmarkId: bookmark.id,
|
||||||
|
isMarked: !bookmark.isMarked
|
||||||
|
)
|
||||||
|
|
||||||
|
// Liste aktualisieren
|
||||||
|
await loadBookmarks(state: currentState)
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
errorMessage = "Fehler beim Markieren des Bookmarks"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func deleteBookmark(bookmark: Bookmark) async {
|
||||||
|
do {
|
||||||
|
// Echtes Löschen über API statt nur als gelöscht markieren
|
||||||
|
try await deleteBookmarkUseCase.execute(bookmarkId: bookmark.id)
|
||||||
|
|
||||||
|
// Lokal aus der Liste entfernen (optimistische Update)
|
||||||
|
bookmarks.removeAll { $0.id == bookmark.id }
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
errorMessage = "Fehler beim Löschen des Bookmarks"
|
||||||
|
// Bei Fehler die Liste neu laden, um konsistenten Zustand zu haben
|
||||||
|
await loadBookmarks(state: currentState)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user