diff --git a/readeck/Data/API/API.swift b/readeck/Data/API/API.swift index 7bddd11..1225feb 100644 --- a/readeck/Data/API/API.swift +++ b/readeck/Data/API/API.swift @@ -13,6 +13,7 @@ protocol PAPI { func getBookmarks(state: BookmarkState?) async throws -> [BookmarkDto] func getBookmark(id: String) async throws -> BookmarkDetailDto func getBookmarkArticle(id: String) async throws -> String + func createBookmark(createRequest: CreateBookmarkRequestDto) async throws -> CreateBookmarkResponseDto func updateBookmark(id: String, updateRequest: UpdateBookmarkRequestDto) async throws func deleteBookmark(id: String) async throws } @@ -165,6 +166,17 @@ class API: PAPI { ) } + func createBookmark(createRequest: CreateBookmarkRequestDto) async throws -> CreateBookmarkResponseDto { + let requestData = try JSONEncoder().encode(createRequest) + + return try await makeJSONRequest( + endpoint: "/api/bookmarks", + method: .POST, + body: requestData, + responseType: CreateBookmarkResponseDto.self + ) + } + func updateBookmark(id: String, updateRequest: UpdateBookmarkRequestDto) async throws { let requestData = try JSONEncoder().encode(updateRequest) diff --git a/readeck/Data/API/DTOs/BookmarkDto.swift b/readeck/Data/API/DTOs/BookmarkDto.swift index 34778c0..0da39ba 100644 --- a/readeck/Data/API/DTOs/BookmarkDto.swift +++ b/readeck/Data/API/DTOs/BookmarkDto.swift @@ -13,7 +13,7 @@ struct BookmarkDto: Codable { let siteName: String let site: String let readingTime: Int? - let wordCount: Int + let wordCount: Int? let hasArticle: Bool let isArchived: Bool let isDeleted: Bool @@ -62,3 +62,5 @@ struct ImageResourceDto: Codable { let height: Int let width: Int } + + diff --git a/readeck/Data/API/DTOs/CreateBookmarkRequestDto.swift b/readeck/Data/API/DTOs/CreateBookmarkRequestDto.swift new file mode 100644 index 0000000..6b226cb --- /dev/null +++ b/readeck/Data/API/DTOs/CreateBookmarkRequestDto.swift @@ -0,0 +1,13 @@ +import Foundation + +struct CreateBookmarkRequestDto: Codable { + let labels: [String]? + let title: String? + let url: String + + init(url: String, title: String? = nil, labels: [String]? = nil) { + self.url = url + self.title = title + self.labels = labels + } +} diff --git a/readeck/Data/DTOs/CreateBookmarkResponseDto.swift b/readeck/Data/DTOs/CreateBookmarkResponseDto.swift new file mode 100644 index 0000000..498f3c2 --- /dev/null +++ b/readeck/Data/DTOs/CreateBookmarkResponseDto.swift @@ -0,0 +1,6 @@ +import Foundation + +struct CreateBookmarkResponseDto: Codable { + let message: String + let status: Int +} diff --git a/readeck/Data/Repository/BookmarksRepository.swift b/readeck/Data/Repository/BookmarksRepository.swift index 5de9e6e..cef39c7 100644 --- a/readeck/Data/Repository/BookmarksRepository.swift +++ b/readeck/Data/Repository/BookmarksRepository.swift @@ -4,9 +4,9 @@ protocol PBookmarksRepository { func fetchBookmarks(state: BookmarkState?) async throws -> [Bookmark] func fetchBookmark(id: String) async throws -> BookmarkDetail func fetchBookmarkArticle(id: String) async throws -> String + func createBookmark(createRequest: CreateBookmarkRequest) async throws -> String func updateBookmark(id: String, updateRequest: BookmarkUpdateRequest) async throws func deleteBookmark(id: String) async throws - func addBookmark(bookmark: Bookmark) async throws } class BookmarksRepository: PBookmarksRepository { @@ -46,8 +46,21 @@ class BookmarksRepository: PBookmarksRepository { return try await api.getBookmarkArticle(id: id) } - func addBookmark(bookmark: Bookmark) async throws { - // Implement logic to add a bookmark if needed + func createBookmark(createRequest: CreateBookmarkRequest) async throws -> String { + let dto = CreateBookmarkRequestDto( + url: createRequest.url, + title: createRequest.title, + labels: createRequest.labels + ) + + let response = try await api.createBookmark(createRequest: dto) + + // Prüfe ob die Erstellung erfolgreich war + guard response.status == 0 else { + throw CreateBookmarkError.serverError(response.message) + } + + return response.message } func deleteBookmark(id: String) async throws { @@ -80,7 +93,7 @@ struct BookmarkDetail { let authors: [String] let created: String let updated: String - let wordCount: Int + let wordCount: Int? let readingTime: Int? let hasArticle: Bool let isMarked: Bool @@ -88,3 +101,23 @@ struct BookmarkDetail { let thumbnailUrl: String let imageUrl: String } + +enum CreateBookmarkError: Error, LocalizedError { + case invalidURL + case duplicateBookmark + case networkError + case serverError(String) + + var errorDescription: String? { + switch self { + case .invalidURL: + return "Die eingegebene URL ist ungültig" + case .duplicateBookmark: + return "Dieser Bookmark existiert bereits" + case .networkError: + return "Netzwerkfehler beim Erstellen des Bookmarks" + case .serverError(let message): + return message // Verwende die Server-Nachricht direkt + } + } +} diff --git a/readeck/Domain/DefaultUseCaseFactory.swift b/readeck/Domain/DefaultUseCaseFactory.swift index 16d99ec..07ec72c 100644 --- a/readeck/Domain/DefaultUseCaseFactory.swift +++ b/readeck/Domain/DefaultUseCaseFactory.swift @@ -9,6 +9,7 @@ protocol UseCaseFactory { func makeLoadSettingsUseCase() -> LoadSettingsUseCase func makeUpdateBookmarkUseCase() -> UpdateBookmarkUseCase func makeDeleteBookmarkUseCase() -> DeleteBookmarkUseCase + func makeCreateBookmarkUseCase() -> CreateBookmarkUseCase } class DefaultUseCaseFactory: UseCaseFactory { @@ -57,4 +58,8 @@ class DefaultUseCaseFactory: UseCaseFactory { func makeDeleteBookmarkUseCase() -> DeleteBookmarkUseCase { return DeleteBookmarkUseCase(repository: bookmarksRepository) } + + func makeCreateBookmarkUseCase() -> CreateBookmarkUseCase { + return CreateBookmarkUseCase(repository: bookmarksRepository) + } } diff --git a/readeck/Domain/Model/Bookmark.swift b/readeck/Domain/Model/Bookmark.swift index c0f5fb3..49088ee 100644 --- a/readeck/Domain/Model/Bookmark.swift +++ b/readeck/Domain/Model/Bookmark.swift @@ -13,7 +13,7 @@ struct Bookmark { let siteName: String let site: String let readingTime: Int? - let wordCount: Int + let wordCount: Int? let hasArticle: Bool let isArchived: Bool let isDeleted: Bool diff --git a/readeck/Domain/Model/CreateBookmarkRequest.swift b/readeck/Domain/Model/CreateBookmarkRequest.swift new file mode 100644 index 0000000..598bebc --- /dev/null +++ b/readeck/Domain/Model/CreateBookmarkRequest.swift @@ -0,0 +1,28 @@ +import Foundation + +struct CreateBookmarkRequest { + let url: String + let title: String? + let labels: [String]? + + init(url: String, title: String? = nil, labels: [String]? = nil) { + self.url = url + self.title = title + self.labels = labels + } +} + +// Convenience Initializers +extension CreateBookmarkRequest { + static func fromURL(_ url: String) -> CreateBookmarkRequest { + return CreateBookmarkRequest(url: url) + } + + static func fromURLWithTitle(_ url: String, title: String) -> CreateBookmarkRequest { + return CreateBookmarkRequest(url: url, title: title) + } + + static func fromURLWithLabels(_ url: String, labels: [String]) -> CreateBookmarkRequest { + return CreateBookmarkRequest(url: url, labels: labels) + } +} \ No newline at end of file diff --git a/readeck/Domain/UseCase/CreateBookmarkUseCase.swift b/readeck/Domain/UseCase/CreateBookmarkUseCase.swift new file mode 100644 index 0000000..4c8662a --- /dev/null +++ b/readeck/Domain/UseCase/CreateBookmarkUseCase.swift @@ -0,0 +1,51 @@ +import Foundation + +class CreateBookmarkUseCase { + private let repository: PBookmarksRepository + + init(repository: PBookmarksRepository) { + self.repository = repository + } + + func execute(createRequest: CreateBookmarkRequest) async throws -> String { + // URL-Validierung + guard URL(string: createRequest.url) != nil else { + throw CreateBookmarkError.invalidURL + } + + return try await repository.createBookmark(createRequest: createRequest) + } + + // Convenience methods für häufige Use Cases + func createFromURL(_ url: String) async throws -> String { + let request = CreateBookmarkRequest.fromURL(url) + return try await execute(createRequest: request) + } + + func createFromURLWithTitle(_ url: String, title: String) async throws -> String { + let request = CreateBookmarkRequest.fromURLWithTitle(url, title: title) + return try await execute(createRequest: request) + } + + func createFromURLWithLabels(_ url: String, labels: [String]) async throws -> String { + let request = CreateBookmarkRequest.fromURLWithLabels(url, labels: labels) + return try await execute(createRequest: request) + } + + func createFromClipboard() async throws -> String? { + return nil + // URL aus Zwischenablage holen (falls verfügbar) + /*#if canImport(UIKit) + import UIKit + guard let clipboardString = UIPasteboard.general.string, + URL(string: clipboardString) != nil else { + return nil + } + + let request = CreateBookmarkRequest.fromURL(clipboardString) + return try await execute(createRequest: request) + #else + return nil + #endif*/ + } +} diff --git a/readeck/UI/AddBookmark/AddBookmarkView.swift b/readeck/UI/AddBookmark/AddBookmarkView.swift new file mode 100644 index 0000000..e371cb8 --- /dev/null +++ b/readeck/UI/AddBookmark/AddBookmarkView.swift @@ -0,0 +1,134 @@ +import SwiftUI + +struct AddBookmarkView: View { + @State private var viewModel = AddBookmarkViewModel() + @Environment(\.dismiss) private var dismiss + + var body: some View { + NavigationView { + Form { + Section(header: Text("Bookmark Details")) { + VStack(alignment: .leading, spacing: 8) { + Text("URL *") + .font(.caption) + .foregroundColor(.secondary) + + TextField("https://example.com", text: $viewModel.url) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .keyboardType(.URL) + .autocapitalization(.none) + .autocorrectionDisabled() + } + + VStack(alignment: .leading, spacing: 8) { + Text("Titel (optional)") + .font(.caption) + .foregroundColor(.secondary) + + TextField("Bookmark Titel", text: $viewModel.title) + .textFieldStyle(RoundedBorderTextFieldStyle()) + } + } + + Section(header: Text("Labels")) { + VStack(alignment: .leading, spacing: 8) { + Text("Labels (durch Komma getrennt)") + .font(.caption) + .foregroundColor(.secondary) + + TextField("work, important, later", text: $viewModel.labelsText) + .textFieldStyle(RoundedBorderTextFieldStyle()) + } + + if !viewModel.parsedLabels.isEmpty { + LazyVGrid(columns: [ + GridItem(.adaptive(minimum: 80)) + ], spacing: 8) { + ForEach(viewModel.parsedLabels, id: \.self) { label in + Text(label) + .font(.caption) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(Color.blue.opacity(0.1)) + .foregroundColor(.blue) + .clipShape(Capsule()) + } + } + } + } + + Section { + Button("Aus Zwischenablage einfügen") { + viewModel.pasteFromClipboard() + } + .disabled(viewModel.clipboardURL == nil) + + if let clipboardURL = viewModel.clipboardURL { + Text("Zwischenablage: \(clipboardURL)") + .font(.caption) + .foregroundColor(.secondary) + .lineLimit(1) + .truncationMode(.middle) + } + } + } + .navigationTitle("Bookmark hinzufügen") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button("Abbrechen") { + dismiss() + } + } + + ToolbarItem(placement: .navigationBarTrailing) { + Button("Speichern") { + Task { + await viewModel.createBookmark() + } + } + .disabled(!viewModel.isValid || viewModel.isLoading) + } + } + .overlay { + if viewModel.isLoading { + ZStack { + Color.black.opacity(0.3) + + VStack(spacing: 16) { + ProgressView() + .scaleEffect(1.2) + + Text("Bookmark wird erstellt...") + .font(.subheadline) + } + .padding(24) + .background(Color(.systemBackground)) + .clipShape(RoundedRectangle(cornerRadius: 12)) + .shadow(radius: 10) + } + .ignoresSafeArea() + } + } + .alert("Erfolgreich", isPresented: $viewModel.showSuccessAlert) { + Button("OK") { + dismiss() + } + } message: { + Text("Bookmark wurde erfolgreich hinzugefügt!") + } + .alert("Fehler", isPresented: $viewModel.showErrorAlert) { + Button("OK", role: .cancel) { } + } message: { + Text(viewModel.errorMessage ?? "Unbekannter Fehler") + } + } + .onAppear { + viewModel.checkClipboard() + } + } +} + +#Preview { + AddBookmarkView() +} \ No newline at end of file diff --git a/readeck/UI/AddBookmark/AddBookmarkViewModel.swift b/readeck/UI/AddBookmark/AddBookmarkViewModel.swift new file mode 100644 index 0000000..460fe9f --- /dev/null +++ b/readeck/UI/AddBookmark/AddBookmarkViewModel.swift @@ -0,0 +1,80 @@ +import Foundation +import UIKit + +@Observable +class AddBookmarkViewModel { + private let createBookmarkUseCase = DefaultUseCaseFactory.shared.makeCreateBookmarkUseCase() + + var url: String = "" + var title: String = "" + var labelsText: String = "" + + var isLoading: Bool = false + var errorMessage: String? + var showErrorAlert: Bool = false + var showSuccessAlert: Bool = false + var clipboardURL: String? + + var isValid: Bool { + !url.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && + URL(string: url.trimmingCharacters(in: .whitespacesAndNewlines)) != nil + } + + var parsedLabels: [String] { + labelsText + .components(separatedBy: ",") + .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } + .filter { !$0.isEmpty } + } + + @MainActor + func createBookmark() async { + guard isValid else { return } + + isLoading = true + errorMessage = nil + + do { + let cleanURL = url.trimmingCharacters(in: .whitespacesAndNewlines) + let cleanTitle = title.trimmingCharacters(in: .whitespacesAndNewlines) + let labels = parsedLabels + + let request = CreateBookmarkRequest( + url: cleanURL, + title: cleanTitle.isEmpty ? nil : cleanTitle, + labels: labels.isEmpty ? nil : labels + ) + + let message = try await createBookmarkUseCase.execute(createRequest: request) + + // Optional: Zeige die Server-Nachricht an + print("Server response: \(message)") + + showSuccessAlert = true + + } catch let error as CreateBookmarkError { + errorMessage = error.localizedDescription + showErrorAlert = true + } catch { + errorMessage = "Fehler beim Erstellen des Bookmarks" + showErrorAlert = true + } + + isLoading = false + } + + func checkClipboard() { + guard let clipboardString = UIPasteboard.general.string, + URL(string: clipboardString) != nil else { + clipboardURL = nil + return + } + + clipboardURL = clipboardString + } + + func pasteFromClipboard() { + guard let clipboardURL = clipboardURL else { return } + url = clipboardURL + } +} \ No newline at end of file diff --git a/readeck/UI/Bookmarks/BookmarkCardView.swift b/readeck/UI/Bookmarks/BookmarkCardView.swift index 1661bfa..7993b60 100644 --- a/readeck/UI/Bookmarks/BookmarkCardView.swift +++ b/readeck/UI/Bookmarks/BookmarkCardView.swift @@ -28,17 +28,17 @@ struct BookmarkCardView: View { .clipShape(RoundedRectangle(cornerRadius: 8)) VStack(alignment: .leading, spacing: 4) { - // Status-Badges und Action-Button + // Status-Icons und Action-Button HStack { - HStack(spacing: 4) { + HStack(spacing: 6) { if bookmark.isMarked { - Badge(text: "Markiert", color: .blue) + IconBadge(systemName: "heart.fill", color: .red) } if bookmark.isArchived { - Badge(text: "Archiviert", color: .gray) + IconBadge(systemName: "archivebox.fill", color: .gray) } if bookmark.hasArticle { - Badge(text: "Artikel", color: .green) + IconBadge(systemName: "doc.text.fill", color: .green) } } @@ -146,18 +146,17 @@ struct BookmarkCardView: View { } } -struct Badge: View { - let text: String +struct IconBadge: View { + let systemName: String let color: Color var body: some View { - Text(text) + Image(systemName: systemName) .font(.caption2) - .padding(.horizontal, 6) - .padding(.vertical, 2) + .padding(6) .background(color.opacity(0.2)) .foregroundColor(color) - .clipShape(Capsule()) + .clipShape(Circle()) } } diff --git a/readeck/UI/Bookmarks/BookmarksView.swift b/readeck/UI/Bookmarks/BookmarksView.swift index 2e3356b..58c15fe 100644 --- a/readeck/UI/Bookmarks/BookmarksView.swift +++ b/readeck/UI/Bookmarks/BookmarksView.swift @@ -2,6 +2,7 @@ import SwiftUI struct BookmarksView: View { @State private var viewModel = BookmarksViewModel() + @State private var showingAddBookmark = false let state: BookmarkState var body: some View { @@ -55,6 +56,18 @@ struct BookmarksView: View { } } .navigationTitle(state.displayName) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button(action: { + showingAddBookmark = true + }) { + Image(systemName: "plus") + } + } + } + .sheet(isPresented: $showingAddBookmark) { + AddBookmarkView() + } .alert("Fehler", isPresented: .constant(viewModel.errorMessage != nil)) { Button("OK", role: .cancel) { viewModel.errorMessage = nil @@ -65,6 +78,14 @@ struct BookmarksView: View { .task { await viewModel.loadBookmarks(state: state) } + .onChange(of: showingAddBookmark) { oldValue, newValue in + // Refresh bookmarks when sheet is dismissed + if oldValue && !newValue { + Task { + await viewModel.refreshBookmarks() + } + } + } } } } diff --git a/readeck/UI/Components/WebView.swift b/readeck/UI/Components/WebView.swift index 2f9afae..b5e490a 100644 --- a/readeck/UI/Components/WebView.swift +++ b/readeck/UI/Components/WebView.swift @@ -4,11 +4,14 @@ import WebKit struct WebView: UIViewRepresentable { let htmlContent: String let onHeightChange: (CGFloat) -> Void + @Environment(\.colorScheme) private var colorScheme func makeUIView(context: Context) -> WKWebView { let webView = WKWebView() webView.navigationDelegate = context.coordinator webView.scrollView.isScrollEnabled = false + webView.isOpaque = false + webView.backgroundColor = UIColor.clear // Message Handler hier einmalig hinzufügen webView.configuration.userContentController.add(context.coordinator, name: "heightUpdate") @@ -21,21 +24,39 @@ struct WebView: UIViewRepresentable { // Nur den HTML-Inhalt laden, keine Handler-Konfiguration context.coordinator.onHeightChange = onHeightChange + let isDarkMode = colorScheme == .dark + let styledHTML = """
+ @@ -95,6 +196,11 @@ struct WebView: UIViewRepresentable { setTimeout(updateHeight, 100); setTimeout(updateHeight, 500); setTimeout(updateHeight, 1000); + + // Höhe bei Bild-Ladevorgängen aktualisieren + document.querySelectorAll('img').forEach(img => { + img.addEventListener('load', updateHeight); + });