From 624816d91425555ded5c04647a668a86ff74e4b8 Mon Sep 17 00:00:00 2001 From: Ilyas Hallak Date: Fri, 4 Jul 2025 22:30:01 +0200 Subject: [PATCH] feat: add German localization and improve share extension UX - Add comprehensive German localization with Localizable.xcstrings - Integrate R.swift for type-safe resource management - Improve share extension UI with better styling and optional title input - Add archive functionality to bookmark detail view - Update README with current features and planned roadmap - Remove title validation requirement from share extension - Optimize share extension auto-dismiss timing - Clean up code structure and remove unused components --- Localizable.xcstrings | 290 ++++++++++++++++++ README.md | 49 ++- URLShare/ShareViewController.swift | 30 +- readeck.xcodeproj/project.pbxproj | 47 ++- .../xcshareddata/swiftpm/Package.resolved | 29 +- readeck/Data/API/API.swift | 14 +- .../Data/Repository/BookmarksRepository.swift | 38 --- .../Domain/Error/CreateBookmarkError.swift | 21 ++ readeck/Domain/Model/BookmarkDetail.swift | 39 +++ .../BookmarkDetail/BookmarkDetailView.swift | 34 ++ .../BookmarkDetailViewModel.swift | 41 +-- readeck/UI/Bookmarks/BookmarkCardView.swift | 6 +- 12 files changed, 505 insertions(+), 133 deletions(-) create mode 100644 Localizable.xcstrings create mode 100644 readeck/Domain/Error/CreateBookmarkError.swift create mode 100644 readeck/Domain/Model/BookmarkDetail.swift diff --git a/Localizable.xcstrings b/Localizable.xcstrings new file mode 100644 index 0000000..f0d25af --- /dev/null +++ b/Localizable.xcstrings @@ -0,0 +1,290 @@ +{ + "sourceLanguage" : "en", + "strings" : { + "" : { + + }, + "%lld min" : { + + }, + "%lld Minuten" : { + + }, + "12 min • Today • example.com" : { + + }, + "Abbrechen" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Abbrechen" + } + } + } + }, + "Abmelden" : { + + }, + "Add Item" : { + + }, + "Anmelden & speichern" : { + + }, + "Archivieren" : { + + }, + "Artikel automatisch als gelesen markieren" : { + + }, + "Automatischer Sync" : { + + }, + "Benutzername" : { + + }, + "Bookmark archivieren" : { + + }, + "Bookmark ist archiviert" : { + + }, + "Bookmark speichern" : { + + }, + "Cache leeren" : { + + }, + "Datenmanagement" : { + + }, + "Debug-Anmeldung" : { + + }, + "done" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fertig" + } + } + } + }, + "Einfügen" : { + + }, + "Einstellungen" : { + + }, + "Einstellungen speichern" : { + + }, + "Einstellungen zurücksetzen" : { + + }, + "Entfernen" : { + + }, + "Entwickler: %@" : { + + }, + "Erfolgreich angemeldet" : { + + }, + "Erforderlich" : { + + }, + "Erneut anmelden & speichern" : { + + }, + "Es wurden noch keine Bookmarks in %@ gefunden." : { + + }, + "Externe Links in In-App Safari öffnen" : { + + }, + "Favorit" : { + + }, + "Fehler" : { + + }, + "Fertig" : { + + }, + "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" : { + + }, + "Geben Sie Ihre Readeck-Server-Details ein, um zu beginnen." : { + + }, + "https://example.com" : { + + }, + "https://readeck.example.com" : { + + }, + "Ihr Benutzername" : { + + }, + "Ihr Passwort" : { + + }, + "Ihre aktuelle Server-Verbindung und Anmeldedaten." : { + + }, + "Item at %@" : { + + }, + "Keine Bookmarks" : { + + }, + "Keine Bookmarks gefunden." : { + + }, + "Keine Ergebnisse" : { + + }, + "Labels" : { + + }, + "Lade %@..." : { + + }, + "Lade Artikel..." : { + + }, + "Leseeinstellungen" : { + + }, + "Löschen" : { + + }, + "Mehr" : { + + }, + "Möchten Sie sich wirklich abmelden? Dies wird alle Ihre Anmeldedaten löschen und Sie zur Einrichtung zurückführen." : { + + }, + "Neues Bookmark" : { + + }, + "OK" : { + + }, + "Optional: Eigener Titel" : { + + }, + "Passwort" : { + + }, + "readeck Bookmark Title" : { + + }, + "Safari Reader Modus" : { + + }, + "Schließen" : { + + }, + "Schrift" : { + + }, + "Schrift-Einstellungen" : { + + }, + "Schriftart" : { + + }, + "Schriftgröße" : { + + }, + "Select a bookmark" : { + + }, + "Select an item" : { + + }, + "Server-Endpunkt" : { + + }, + "Speichern..." : { + + }, + "Suchbegriff eingeben..." : { + + }, + "Suche" : { + + }, + "Suche..." : { + + }, + "Sync-Einstellungen" : { + + }, + "Sync-Intervall" : { + + }, + "Tags" : { + + }, + "Theme" : { + + }, + "This is how your bookmark descriptions and article text will appear in the app. The quick brown fox jumps over the lazy dog." : { + + }, + "Titel" : { + + }, + "Über die App" : { + + }, + "URL" : { + + }, + "URL gefunden:" : { + + }, + "Version %@" : { + + }, + "Vorschau" : { + + }, + "Website" : { + + }, + "Wiederherstellen" : { + + }, + "Wird gespeichert..." : { + + }, + "z.B. arbeit, wichtig, später" : { + + }, + "Zwischenablage" : { + + } + }, + "version" : "1.0" +} \ No newline at end of file diff --git a/README.md b/README.md index 71466c5..e9b7b07 100644 --- a/README.md +++ b/README.md @@ -10,32 +10,23 @@ https://codeberg.org/readeck/readeck ## Features -- Browse and manage bookmarks (Unread, Favorites, Archive) +- Browse and manage bookmarks (All, Unread, Favorites, Archive, Article, Videos, Pictures) - Share Extension for adding URLs from Safari and other apps - Swipe actions for quick bookmark management - Native iOS design with Dark Mode support - -## Requirements - -- iOS 17.0+ -- Xcode 15.0+ -- Swift 5.9+ +- Full iPad Support with Multi-Column Split View +- Font Customization +- Article View with Reading Time and Word Count +- Search functionality ## Configuration After installing the app: 1. Open the readeck app -2. Go to the **Settings** tab -3. Enter your readeck server URL and credentials -4. The app will automatically sync your bookmarks +2. Enter your readeck server URL and credentials +3. The app will automatically load your bookmarks -## Architecture - -- **SwiftUI** for UI -- **Core Data** for local storage -- **MVVM** architecture pattern -- **Repository pattern** for data access ## Share Extension @@ -43,7 +34,17 @@ The app includes a Share Extension that allows adding bookmarks directly from Sa 1. Share any webpage in Safari 2. Select "readeck" from the share sheet -3. The bookmark is automatically added to your collection +3. Enter a title if you want and hit save +4. The bookmark is automatically added to your collection + +## Planned Features +- [ ] Add support for bookmark filtering and sorting options +- [ ] Add support for tags +- [ ] Offline sync with Core Data +- [ ] Add support for collection management +- [ ] Add offline sync capabilities +- [ ] Add support for custom themes + ## Contributing @@ -53,17 +54,3 @@ The app includes a Share Extension that allows adding bookmarks directly from Sa 4. Push to the branch (`git push origin feature/amazing-feature`) 5. Open a Pull Request -## Planned Features -- [ ] Offline sync with Core Data -- [ ] Add support for tags -- [ ] Add support for bookmark filtering and sorting options -- [ ] Implement search functionality -- [ ] Add support for collection management -- [ ] Add support for multiple readeck servers -- [ ] Add offline sync capabilities -- [ ] Add support for custom themes -- [ ] Implement push notifications for new bookmarks -- [ ] Support for iPad multitasking -- [ ] Implement a dark mode toggle in settings -- [ ] Implement a tutorial for first-time users - diff --git a/URLShare/ShareViewController.swift b/URLShare/ShareViewController.swift index 22fac6a..15156c2 100644 --- a/URLShare/ShareViewController.swift +++ b/URLShare/ShareViewController.swift @@ -65,7 +65,7 @@ class ShareViewController: UIViewController { // URL Container View let urlContainerView = UIView() urlContainerView.translatesAutoresizingMaskIntoConstraints = false - urlContainerView.backgroundColor = UIColor.white + urlContainerView.backgroundColor = UIColor.secondarySystemGroupedBackground urlContainerView.layer.cornerRadius = 12 urlContainerView.layer.shadowColor = UIColor.black.cgColor urlContainerView.layer.shadowOffset = CGSize(width: 0, height: 2) @@ -86,7 +86,7 @@ class ShareViewController: UIViewController { // Title Container View let titleContainerView = UIView() titleContainerView.translatesAutoresizingMaskIntoConstraints = false - titleContainerView.backgroundColor = UIColor.white + titleContainerView.backgroundColor = UIColor.secondarySystemGroupedBackground titleContainerView.layer.cornerRadius = 12 titleContainerView.layer.shadowColor = UIColor.black.cgColor titleContainerView.layer.shadowOffset = CGSize(width: 0, height: 2) @@ -97,11 +97,10 @@ class ShareViewController: UIViewController { // Title TextField titleTextField = UITextField() titleTextField?.translatesAutoresizingMaskIntoConstraints = false - titleTextField?.placeholder = "Titel eingeben..." + titleTextField?.placeholder = "Optionales Titel eingeben..." titleTextField?.borderStyle = .none titleTextField?.font = UIFont.systemFont(ofSize: 16) titleTextField?.backgroundColor = UIColor.clear - titleTextField?.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged) titleContainerView.addSubview(titleTextField!) // Status Label @@ -120,7 +119,7 @@ class ShareViewController: UIViewController { saveButton?.translatesAutoresizingMaskIntoConstraints = false saveButton?.setTitle("Bookmark speichern", for: .normal) saveButton?.titleLabel?.font = UIFont.systemFont(ofSize: 17, weight: .semibold) - saveButton?.backgroundColor = UIColor.white + saveButton?.backgroundColor = UIColor.secondarySystemGroupedBackground saveButton?.setTitleColor(UIColor(named: "green") ?? UIColor.systemGreen, for: .normal) saveButton?.layer.cornerRadius = 16 saveButton?.layer.shadowColor = UIColor.black.cgColor @@ -128,11 +127,8 @@ class ShareViewController: UIViewController { saveButton?.layer.shadowRadius = 8 saveButton?.layer.shadowOpacity = 0.2 saveButton?.addTarget(self, action: #selector(saveButtonTapped), for: .touchUpInside) - saveButton?.isEnabled = false - saveButton?.alpha = 0.6 view.addSubview(saveButton!) - // Activity Indicator activityIndicator = UIActivityIndicatorView(style: .medium) @@ -223,7 +219,6 @@ class ShareViewController: UIViewController { if let url = url as? URL { self?.extractedURL = url.absoluteString self?.urlLabel?.text = url.absoluteString - self?.updateSaveButtonState() } } } @@ -235,7 +230,6 @@ class ShareViewController: UIViewController { if let text = text as? String, let url = URL(string: text) { self?.extractedURL = url.absoluteString self?.urlLabel?.text = url.absoluteString - self?.updateSaveButtonState() } } } @@ -245,15 +239,9 @@ class ShareViewController: UIViewController { } // MARK: - Actions - @objc private func textFieldDidChange() { - updateSaveButtonState() - } @objc private func saveButtonTapped() { - guard let title = titleTextField?.text, !title.isEmpty else { - showStatus("Bitte geben Sie einen Titel ein.", error: true) - return - } + let title = titleTextField?.text ?? "" saveButton?.isEnabled = false activityIndicator?.startAnimating() @@ -271,12 +259,6 @@ class ShareViewController: UIViewController { extensionContext?.completeRequest(returningItems: [], completionHandler: nil) } - private func updateSaveButtonState() { - let isValid = !(titleTextField?.text?.isEmpty ?? true) && extractedURL != nil - saveButton?.isEnabled = isValid - saveButton?.alpha = isValid ? 1.0 : 0.6 - } - // MARK: - API Call private func addBookmarkViaAPI(title: String) async { guard let url = extractedURL, !url.isEmpty else { @@ -346,7 +328,7 @@ class ShareViewController: UIViewController { if !error { // Automatically dismiss after success - DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { self.extensionContext?.completeRequest(returningItems: [], completionHandler: nil) } } diff --git a/readeck.xcodeproj/project.pbxproj b/readeck.xcodeproj/project.pbxproj index 96f23c8..731ac69 100644 --- a/readeck.xcodeproj/project.pbxproj +++ b/readeck.xcodeproj/project.pbxproj @@ -9,6 +9,10 @@ /* Begin PBXBuildFile section */ 5D2B7FB92DFA27A400EBDB2B /* URLShare.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 5D2B7FAF2DFA27A400EBDB2B /* URLShare.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 5D348CC32E0C9F4F00D0AF21 /* netfox in Frameworks */ = {isa = PBXBuildFile; productRef = 5D348CC22E0C9F4F00D0AF21 /* netfox */; }; + 5DA241FB2E17C3B3007531C3 /* RswiftLibrary in Frameworks */ = {isa = PBXBuildFile; productRef = 5DA241FA2E17C3B3007531C3 /* RswiftLibrary */; }; + 5DA241FD2E17C3B3007531C3 /* rswift in Frameworks */ = {isa = PBXBuildFile; productRef = 5DA241FC2E17C3B3007531C3 /* rswift */; }; + 5DA242132E17D31A007531C3 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 5DA242122E17D31A007531C3 /* Localizable.xcstrings */; }; + 5DA242142E17D31A007531C3 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 5DA242122E17D31A007531C3 /* Localizable.xcstrings */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -64,6 +68,7 @@ 5D45F9C82DF858680048D5B8 /* readeck.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = readeck.app; sourceTree = BUILT_PRODUCTS_DIR; }; 5D45F9DE2DF8586A0048D5B8 /* readeckTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = readeckTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 5D45F9E82DF8586A0048D5B8 /* readeckUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = readeckUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 5DA242122E17D31A007531C3 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ @@ -137,6 +142,8 @@ buildActionMask = 2147483647; files = ( 5D348CC32E0C9F4F00D0AF21 /* netfox in Frameworks */, + 5DA241FB2E17C3B3007531C3 /* RswiftLibrary in Frameworks */, + 5DA241FD2E17C3B3007531C3 /* rswift in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -160,6 +167,7 @@ 5D45F9BF2DF858680048D5B8 = { isa = PBXGroup; children = ( + 5DA242122E17D31A007531C3 /* Localizable.xcstrings */, 5D45F9CA2DF858680048D5B8 /* readeck */, 5D45F9E12DF8586A0048D5B8 /* readeckTests */, 5D45F9EB2DF8586A0048D5B8 /* readeckUITests */, @@ -217,6 +225,7 @@ buildRules = ( ); dependencies = ( + 5DA241FF2E17C3CE007531C3 /* PBXTargetDependency */, 5D2B7FB82DFA27A400EBDB2B /* PBXTargetDependency */, ); fileSystemSynchronizedGroups = ( @@ -225,6 +234,8 @@ name = readeck; packageProductDependencies = ( 5D348CC22E0C9F4F00D0AF21 /* netfox */, + 5DA241FA2E17C3B3007531C3 /* RswiftLibrary */, + 5DA241FC2E17C3B3007531C3 /* rswift */, ); productName = readeck; productReference = 5D45F9C82DF858680048D5B8 /* readeck.app */; @@ -308,11 +319,14 @@ knownRegions = ( en, Base, + "fr-CA", + de, ); mainGroup = 5D45F9BF2DF858680048D5B8; minimizedProjectReferenceProxies = 1; packageReferences = ( 5D348CC12E0C9F4F00D0AF21 /* XCRemoteSwiftPackageReference "netfox" */, + 5DA241F92E17C3B3007531C3 /* XCRemoteSwiftPackageReference "R.swift" */, ); preferredProjectObjectVersion = 77; productRefGroup = 5D45F9C92DF858680048D5B8 /* Products */; @@ -332,6 +346,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 5DA242132E17D31A007531C3 /* Localizable.xcstrings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -339,6 +354,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 5DA242142E17D31A007531C3 /* Localizable.xcstrings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -405,6 +421,10 @@ target = 5D45F9C72DF858680048D5B8 /* readeck */; targetProxy = 5D45F9E92DF8586A0048D5B8 /* PBXContainerItemProxy */; }; + 5DA241FF2E17C3CE007531C3 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + productRef = 5DA241FE2E17C3CE007531C3 /* RswiftGenerateInternalResources */; + }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ @@ -524,6 +544,7 @@ MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; @@ -578,6 +599,7 @@ MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = YES; }; name = Release; }; @@ -596,7 +618,6 @@ ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = readeck/Info.plist; - INFOPLIST_KEY_LSApplicationCategoryType = ""; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; @@ -639,7 +660,6 @@ ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = readeck/Info.plist; - INFOPLIST_KEY_LSApplicationCategoryType = ""; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; @@ -816,6 +836,14 @@ minimumVersion = 1.21.0; }; }; + 5DA241F92E17C3B3007531C3 /* XCRemoteSwiftPackageReference "R.swift" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/mac-cain13/R.swift.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 7.8.0; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -824,6 +852,21 @@ package = 5D348CC12E0C9F4F00D0AF21 /* XCRemoteSwiftPackageReference "netfox" */; productName = netfox; }; + 5DA241FA2E17C3B3007531C3 /* RswiftLibrary */ = { + isa = XCSwiftPackageProductDependency; + package = 5DA241F92E17C3B3007531C3 /* XCRemoteSwiftPackageReference "R.swift" */; + productName = RswiftLibrary; + }; + 5DA241FC2E17C3B3007531C3 /* rswift */ = { + isa = XCSwiftPackageProductDependency; + package = 5DA241F92E17C3B3007531C3 /* XCRemoteSwiftPackageReference "R.swift" */; + productName = rswift; + }; + 5DA241FE2E17C3CE007531C3 /* RswiftGenerateInternalResources */ = { + isa = XCSwiftPackageProductDependency; + package = 5DA241F92E17C3B3007531C3 /* XCRemoteSwiftPackageReference "R.swift" */; + productName = "plugin:RswiftGenerateInternalResources"; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 5D45F9C02DF858680048D5B8 /* Project object */; diff --git a/readeck.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/readeck.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 1b9f250..1623357 100644 --- a/readeck.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/readeck.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "7374154e7686de69a9f88fbafb081b646b02140f8d82770f46fa750840581e0e", + "originHash" : "ad0d6bd7e4d278f825d201974f944ef5a8c72a7d757d551070c5da6f64b45150", "pins" : [ { "identity" : "netfox", @@ -9,6 +9,33 @@ "revision" : "557576032736fd3140422baefb68b8f76c55088f", "version" : "1.21.0" } + }, + { + "identity" : "r.swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/mac-cain13/R.swift.git", + "state" : { + "revision" : "a9abc6b0afe0fc4a5a71e1d7d2872143dff2d2f1", + "version" : "7.8.0" + } + }, + { + "identity" : "swift-argument-parser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-argument-parser", + "state" : { + "revision" : "309a47b2b1d9b5e991f36961c983ecec72275be3", + "version" : "1.6.1" + } + }, + { + "identity" : "xcodeedit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tomlokhorst/XcodeEdit", + "state" : { + "revision" : "0e550cdee72844b35431afc3a1e176042be6d0f0", + "version" : "2.13.0" + } } ], "version" : 3 diff --git a/readeck/Data/API/API.swift b/readeck/Data/API/API.swift index ba959fc..d77d1b5 100644 --- a/readeck/Data/API/API.swift +++ b/readeck/Data/API/API.swift @@ -158,28 +158,28 @@ class API: PAPI { } func login(endpoint: String, username: String, password: String) async throws -> UserDto { + guard let url = URL(string: endpoint + "/api/auth") else { throw APIError.invalidURL } + let loginRequest = LoginRequestDto(application: "api doc", username: username, password: password) let requestData = try JSONEncoder().encode(loginRequest) - guard let url = URL(string: endpoint + "/api/auth") else { - throw APIError.invalidURL - } var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.httpBody = requestData + let (data, response) = try await URLSession.shared.data(for: request) + guard let httpResponse = response as? HTTPURLResponse else { throw APIError.invalidResponse } + guard 200...299 ~= httpResponse.statusCode else { throw APIError.serverError(httpResponse.statusCode) } - let userDto = try JSONDecoder().decode(UserDto.self, from: data) - // Token NICHT automatisch speichern, da Settings noch nicht existieren - return userDto + + return try JSONDecoder().decode(UserDto.self, from: data) } - // Angepasste getBookmarks-Methode mit Header-Auslesen func getBookmarks(state: BookmarkState? = nil, limit: Int? = nil, offset: Int? = nil, search: String? = nil, type: [BookmarkType]? = nil) async throws -> BookmarksPageDto { var endpoint = "/api/bookmarks" var queryItems: [URLQueryItem] = [] diff --git a/readeck/Data/Repository/BookmarksRepository.swift b/readeck/Data/Repository/BookmarksRepository.swift index 1e5768c..5a63a4f 100644 --- a/readeck/Data/Repository/BookmarksRepository.swift +++ b/readeck/Data/Repository/BookmarksRepository.swift @@ -89,41 +89,3 @@ class BookmarksRepository: PBookmarksRepository { return bookmarkDtos.toDomain() } } - -struct BookmarkDetail { - let id: String - let title: String - let url: String - let description: String - let siteName: String - let authors: [String] - let created: String - let updated: String - let wordCount: Int? - let readingTime: Int? - let hasArticle: Bool - let isMarked: Bool - let isArchived: Bool - 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/Error/CreateBookmarkError.swift b/readeck/Domain/Error/CreateBookmarkError.swift new file mode 100644 index 0000000..e264a85 --- /dev/null +++ b/readeck/Domain/Error/CreateBookmarkError.swift @@ -0,0 +1,21 @@ +import Foundation + +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 + } + } +} \ No newline at end of file diff --git a/readeck/Domain/Model/BookmarkDetail.swift b/readeck/Domain/Model/BookmarkDetail.swift new file mode 100644 index 0000000..9a6ea0e --- /dev/null +++ b/readeck/Domain/Model/BookmarkDetail.swift @@ -0,0 +1,39 @@ +import Foundation + +struct BookmarkDetail { + let id: String + let title: String + let url: String + let description: String + let siteName: String + let authors: [String] + let created: String + let updated: String + let wordCount: Int? + let readingTime: Int? + let hasArticle: Bool + let isMarked: Bool + var isArchived: Bool + let thumbnailUrl: String + let imageUrl: String +} + +extension BookmarkDetail { + static let empty = BookmarkDetail( + id: "", + title: "", + url: "", + description: "", + siteName: "", + authors: [], + created: "", + updated: "", + wordCount: 0, + readingTime: 0, + hasArticle: false, + isMarked: false, + isArchived: false, + thumbnailUrl: "", + imageUrl: "" + ) +} diff --git a/readeck/UI/BookmarkDetail/BookmarkDetailView.swift b/readeck/UI/BookmarkDetail/BookmarkDetailView.swift index c46f109..6c8c9b6 100644 --- a/readeck/UI/BookmarkDetail/BookmarkDetailView.swift +++ b/readeck/UI/BookmarkDetail/BookmarkDetailView.swift @@ -20,6 +20,7 @@ struct BookmarkDetailView: View { Divider().padding(.horizontal) contentSection Spacer(minLength: 40) + archiveSection } } } @@ -190,6 +191,39 @@ struct BookmarkDetailView: View { } return dateString } + + private var archiveSection: some View { + VStack(spacing: 12) { + Text("Fertig mit Lesen?") + .font(.headline) + .padding(.top, 24) + if viewModel.bookmarkDetail.isArchived { + Label("Bookmark ist archiviert", systemImage: "archivebox.fill") + } else { + Button(action: { + Task { + await viewModel.archiveBookmark(id: bookmarkId) + } + }) { + HStack { + Image(systemName: "archivebox") + Text("Bookmark archivieren") + } + .font(.title3.bold()) + .frame(maxWidth: .infinity) + } + .buttonStyle(.borderedProminent) + .disabled(viewModel.isLoading) + } + if let error = viewModel.errorMessage { + Text(error) + .foregroundColor(.red) + .font(.footnote) + } + } + .padding(.horizontal) + .padding(.bottom, 32) + } } #Preview { diff --git a/readeck/UI/BookmarkDetail/BookmarkDetailViewModel.swift b/readeck/UI/BookmarkDetail/BookmarkDetailViewModel.swift index 0a72498..ccc4d73 100644 --- a/readeck/UI/BookmarkDetail/BookmarkDetailViewModel.swift +++ b/readeck/UI/BookmarkDetail/BookmarkDetailViewModel.swift @@ -5,6 +5,7 @@ class BookmarkDetailViewModel { private let getBookmarkUseCase: GetBookmarkUseCase private let getBookmarkArticleUseCase: GetBookmarkArticleUseCase private let loadSettingsUseCase: LoadSettingsUseCase + private let updateBookmarkUseCase: UpdateBookmarkUseCase var bookmarkDetail: BookmarkDetail = BookmarkDetail.empty var articleContent: String = "" @@ -20,6 +21,7 @@ class BookmarkDetailViewModel { self.getBookmarkUseCase = factory.makeGetBookmarkUseCase() self.getBookmarkArticleUseCase = factory.makeGetBookmarkArticleUseCase() self.loadSettingsUseCase = factory.makeLoadSettingsUseCase() + self.updateBookmarkUseCase = factory.makeUpdateBookmarkUseCase() } @MainActor @@ -30,11 +32,6 @@ class BookmarkDetailViewModel { do { settings = try await loadSettingsUseCase.execute() bookmarkDetail = try await getBookmarkUseCase.execute(id: id) - - // Auch das vollständige Bookmark für readProgress laden - // (Falls GetBookmarkUseCase nur BookmarkDetail zurückgibt) - // Du könntest einen separaten UseCase für das vollständige Bookmark erstellen - } catch { errorMessage = "Fehler beim Laden des Bookmarks" } @@ -57,31 +54,23 @@ class BookmarkDetailViewModel { } private func processArticleContent() { - // HTML in Paragraphen aufteilen let paragraphs = articleContent .components(separatedBy: .newlines) .filter { !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty } articleParagraphs = paragraphs } -} - -extension BookmarkDetail { - static let empty = BookmarkDetail( - id: "", - title: "", - url: "", - description: "", - siteName: "", - authors: [], - created: "", - updated: "", - wordCount: 0, - readingTime: 0, - hasArticle: false, - isMarked: false, - isArchived: false, - thumbnailUrl: "", - imageUrl: "" - ) + + @MainActor + func archiveBookmark(id: String) async { + isLoading = true + errorMessage = nil + do { + try await updateBookmarkUseCase.toggleArchive(bookmarkId: id, isArchived: true) + bookmarkDetail.isArchived = true + } catch { + errorMessage = "Fehler beim Archivieren des Bookmarks" + } + isLoading = false + } } diff --git a/readeck/UI/Bookmarks/BookmarkCardView.swift b/readeck/UI/Bookmarks/BookmarkCardView.swift index c708a63..006fc46 100644 --- a/readeck/UI/Bookmarks/BookmarkCardView.swift +++ b/readeck/UI/Bookmarks/BookmarkCardView.swift @@ -10,14 +10,14 @@ struct BookmarkCardView: View { var body: some View { VStack(alignment: .leading, spacing: 8) { - // Vorschaubild - verwende image oder thumbnail AsyncImage(url: imageURL) { image in image .resizable() .aspectRatio(contentMode: .fill) .frame(height: 120) } placeholder: { - Image("placeholder") + + Image(R.image.placeholder.name) .resizable() .aspectRatio(contentMode: .fill) .frame(height: 120) @@ -25,14 +25,12 @@ struct BookmarkCardView: View { .clipShape(RoundedRectangle(cornerRadius: 8)) VStack(alignment: .leading, spacing: 4) { - // Titel Text(bookmark.title) .font(.headline) .fontWeight(.semibold) .lineLimit(2) .multilineTextAlignment(.leading) - // Meta-Info mit Datum VStack(alignment: .leading, spacing: 4) { HStack {