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
This commit is contained in:
parent
1763dd6fa1
commit
624816d914
290
Localizable.xcstrings
Normal file
290
Localizable.xcstrings
Normal file
@ -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"
|
||||||
|
}
|
||||||
49
README.md
49
README.md
@ -10,32 +10,23 @@ https://codeberg.org/readeck/readeck
|
|||||||
|
|
||||||
## Features
|
## 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
|
- Share Extension for adding URLs from Safari and other apps
|
||||||
- Swipe actions for quick bookmark management
|
- Swipe actions for quick bookmark management
|
||||||
- Native iOS design with Dark Mode support
|
- Native iOS design with Dark Mode support
|
||||||
|
- Full iPad Support with Multi-Column Split View
|
||||||
## Requirements
|
- Font Customization
|
||||||
|
- Article View with Reading Time and Word Count
|
||||||
- iOS 17.0+
|
- Search functionality
|
||||||
- Xcode 15.0+
|
|
||||||
- Swift 5.9+
|
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
After installing the app:
|
After installing the app:
|
||||||
|
|
||||||
1. Open the readeck app
|
1. Open the readeck app
|
||||||
2. Go to the **Settings** tab
|
2. Enter your readeck server URL and credentials
|
||||||
3. Enter your readeck server URL and credentials
|
3. The app will automatically load your bookmarks
|
||||||
4. The app will automatically sync your bookmarks
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
- **SwiftUI** for UI
|
|
||||||
- **Core Data** for local storage
|
|
||||||
- **MVVM** architecture pattern
|
|
||||||
- **Repository pattern** for data access
|
|
||||||
|
|
||||||
## Share Extension
|
## 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
|
1. Share any webpage in Safari
|
||||||
2. Select "readeck" from the share sheet
|
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
|
## 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`)
|
4. Push to the branch (`git push origin feature/amazing-feature`)
|
||||||
5. Open a Pull Request
|
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
|
|
||||||
|
|
||||||
|
|||||||
@ -65,7 +65,7 @@ class ShareViewController: UIViewController {
|
|||||||
// URL Container View
|
// URL Container View
|
||||||
let urlContainerView = UIView()
|
let urlContainerView = UIView()
|
||||||
urlContainerView.translatesAutoresizingMaskIntoConstraints = false
|
urlContainerView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
urlContainerView.backgroundColor = UIColor.white
|
urlContainerView.backgroundColor = UIColor.secondarySystemGroupedBackground
|
||||||
urlContainerView.layer.cornerRadius = 12
|
urlContainerView.layer.cornerRadius = 12
|
||||||
urlContainerView.layer.shadowColor = UIColor.black.cgColor
|
urlContainerView.layer.shadowColor = UIColor.black.cgColor
|
||||||
urlContainerView.layer.shadowOffset = CGSize(width: 0, height: 2)
|
urlContainerView.layer.shadowOffset = CGSize(width: 0, height: 2)
|
||||||
@ -86,7 +86,7 @@ class ShareViewController: UIViewController {
|
|||||||
// Title Container View
|
// Title Container View
|
||||||
let titleContainerView = UIView()
|
let titleContainerView = UIView()
|
||||||
titleContainerView.translatesAutoresizingMaskIntoConstraints = false
|
titleContainerView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
titleContainerView.backgroundColor = UIColor.white
|
titleContainerView.backgroundColor = UIColor.secondarySystemGroupedBackground
|
||||||
titleContainerView.layer.cornerRadius = 12
|
titleContainerView.layer.cornerRadius = 12
|
||||||
titleContainerView.layer.shadowColor = UIColor.black.cgColor
|
titleContainerView.layer.shadowColor = UIColor.black.cgColor
|
||||||
titleContainerView.layer.shadowOffset = CGSize(width: 0, height: 2)
|
titleContainerView.layer.shadowOffset = CGSize(width: 0, height: 2)
|
||||||
@ -97,11 +97,10 @@ class ShareViewController: UIViewController {
|
|||||||
// Title TextField
|
// Title TextField
|
||||||
titleTextField = UITextField()
|
titleTextField = UITextField()
|
||||||
titleTextField?.translatesAutoresizingMaskIntoConstraints = false
|
titleTextField?.translatesAutoresizingMaskIntoConstraints = false
|
||||||
titleTextField?.placeholder = "Titel eingeben..."
|
titleTextField?.placeholder = "Optionales Titel eingeben..."
|
||||||
titleTextField?.borderStyle = .none
|
titleTextField?.borderStyle = .none
|
||||||
titleTextField?.font = UIFont.systemFont(ofSize: 16)
|
titleTextField?.font = UIFont.systemFont(ofSize: 16)
|
||||||
titleTextField?.backgroundColor = UIColor.clear
|
titleTextField?.backgroundColor = UIColor.clear
|
||||||
titleTextField?.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged)
|
|
||||||
titleContainerView.addSubview(titleTextField!)
|
titleContainerView.addSubview(titleTextField!)
|
||||||
|
|
||||||
// Status Label
|
// Status Label
|
||||||
@ -120,7 +119,7 @@ class ShareViewController: UIViewController {
|
|||||||
saveButton?.translatesAutoresizingMaskIntoConstraints = false
|
saveButton?.translatesAutoresizingMaskIntoConstraints = false
|
||||||
saveButton?.setTitle("Bookmark speichern", for: .normal)
|
saveButton?.setTitle("Bookmark speichern", for: .normal)
|
||||||
saveButton?.titleLabel?.font = UIFont.systemFont(ofSize: 17, weight: .semibold)
|
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?.setTitleColor(UIColor(named: "green") ?? UIColor.systemGreen, for: .normal)
|
||||||
saveButton?.layer.cornerRadius = 16
|
saveButton?.layer.cornerRadius = 16
|
||||||
saveButton?.layer.shadowColor = UIColor.black.cgColor
|
saveButton?.layer.shadowColor = UIColor.black.cgColor
|
||||||
@ -128,11 +127,8 @@ class ShareViewController: UIViewController {
|
|||||||
saveButton?.layer.shadowRadius = 8
|
saveButton?.layer.shadowRadius = 8
|
||||||
saveButton?.layer.shadowOpacity = 0.2
|
saveButton?.layer.shadowOpacity = 0.2
|
||||||
saveButton?.addTarget(self, action: #selector(saveButtonTapped), for: .touchUpInside)
|
saveButton?.addTarget(self, action: #selector(saveButtonTapped), for: .touchUpInside)
|
||||||
saveButton?.isEnabled = false
|
|
||||||
saveButton?.alpha = 0.6
|
|
||||||
view.addSubview(saveButton!)
|
view.addSubview(saveButton!)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Activity Indicator
|
// Activity Indicator
|
||||||
activityIndicator = UIActivityIndicatorView(style: .medium)
|
activityIndicator = UIActivityIndicatorView(style: .medium)
|
||||||
@ -223,7 +219,6 @@ class ShareViewController: UIViewController {
|
|||||||
if let url = url as? URL {
|
if let url = url as? URL {
|
||||||
self?.extractedURL = url.absoluteString
|
self?.extractedURL = url.absoluteString
|
||||||
self?.urlLabel?.text = 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) {
|
if let text = text as? String, let url = URL(string: text) {
|
||||||
self?.extractedURL = url.absoluteString
|
self?.extractedURL = url.absoluteString
|
||||||
self?.urlLabel?.text = url.absoluteString
|
self?.urlLabel?.text = url.absoluteString
|
||||||
self?.updateSaveButtonState()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -245,15 +239,9 @@ class ShareViewController: UIViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Actions
|
// MARK: - Actions
|
||||||
@objc private func textFieldDidChange() {
|
|
||||||
updateSaveButtonState()
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc private func saveButtonTapped() {
|
@objc private func saveButtonTapped() {
|
||||||
guard let title = titleTextField?.text, !title.isEmpty else {
|
let title = titleTextField?.text ?? ""
|
||||||
showStatus("Bitte geben Sie einen Titel ein.", error: true)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
saveButton?.isEnabled = false
|
saveButton?.isEnabled = false
|
||||||
activityIndicator?.startAnimating()
|
activityIndicator?.startAnimating()
|
||||||
@ -271,12 +259,6 @@ class ShareViewController: UIViewController {
|
|||||||
extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
|
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
|
// MARK: - API Call
|
||||||
private func addBookmarkViaAPI(title: String) async {
|
private func addBookmarkViaAPI(title: String) async {
|
||||||
guard let url = extractedURL, !url.isEmpty else {
|
guard let url = extractedURL, !url.isEmpty else {
|
||||||
@ -346,7 +328,7 @@ class ShareViewController: UIViewController {
|
|||||||
|
|
||||||
if !error {
|
if !error {
|
||||||
// Automatically dismiss after success
|
// Automatically dismiss after success
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||||
self.extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
|
self.extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,10 @@
|
|||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
5D2B7FB92DFA27A400EBDB2B /* URLShare.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 5D2B7FAF2DFA27A400EBDB2B /* URLShare.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
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 */; };
|
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 */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy 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; };
|
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; };
|
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; };
|
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 = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||||
@ -137,6 +142,8 @@
|
|||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
5D348CC32E0C9F4F00D0AF21 /* netfox in Frameworks */,
|
5D348CC32E0C9F4F00D0AF21 /* netfox in Frameworks */,
|
||||||
|
5DA241FB2E17C3B3007531C3 /* RswiftLibrary in Frameworks */,
|
||||||
|
5DA241FD2E17C3B3007531C3 /* rswift in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@ -160,6 +167,7 @@
|
|||||||
5D45F9BF2DF858680048D5B8 = {
|
5D45F9BF2DF858680048D5B8 = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
5DA242122E17D31A007531C3 /* Localizable.xcstrings */,
|
||||||
5D45F9CA2DF858680048D5B8 /* readeck */,
|
5D45F9CA2DF858680048D5B8 /* readeck */,
|
||||||
5D45F9E12DF8586A0048D5B8 /* readeckTests */,
|
5D45F9E12DF8586A0048D5B8 /* readeckTests */,
|
||||||
5D45F9EB2DF8586A0048D5B8 /* readeckUITests */,
|
5D45F9EB2DF8586A0048D5B8 /* readeckUITests */,
|
||||||
@ -217,6 +225,7 @@
|
|||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
dependencies = (
|
dependencies = (
|
||||||
|
5DA241FF2E17C3CE007531C3 /* PBXTargetDependency */,
|
||||||
5D2B7FB82DFA27A400EBDB2B /* PBXTargetDependency */,
|
5D2B7FB82DFA27A400EBDB2B /* PBXTargetDependency */,
|
||||||
);
|
);
|
||||||
fileSystemSynchronizedGroups = (
|
fileSystemSynchronizedGroups = (
|
||||||
@ -225,6 +234,8 @@
|
|||||||
name = readeck;
|
name = readeck;
|
||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
5D348CC22E0C9F4F00D0AF21 /* netfox */,
|
5D348CC22E0C9F4F00D0AF21 /* netfox */,
|
||||||
|
5DA241FA2E17C3B3007531C3 /* RswiftLibrary */,
|
||||||
|
5DA241FC2E17C3B3007531C3 /* rswift */,
|
||||||
);
|
);
|
||||||
productName = readeck;
|
productName = readeck;
|
||||||
productReference = 5D45F9C82DF858680048D5B8 /* readeck.app */;
|
productReference = 5D45F9C82DF858680048D5B8 /* readeck.app */;
|
||||||
@ -308,11 +319,14 @@
|
|||||||
knownRegions = (
|
knownRegions = (
|
||||||
en,
|
en,
|
||||||
Base,
|
Base,
|
||||||
|
"fr-CA",
|
||||||
|
de,
|
||||||
);
|
);
|
||||||
mainGroup = 5D45F9BF2DF858680048D5B8;
|
mainGroup = 5D45F9BF2DF858680048D5B8;
|
||||||
minimizedProjectReferenceProxies = 1;
|
minimizedProjectReferenceProxies = 1;
|
||||||
packageReferences = (
|
packageReferences = (
|
||||||
5D348CC12E0C9F4F00D0AF21 /* XCRemoteSwiftPackageReference "netfox" */,
|
5D348CC12E0C9F4F00D0AF21 /* XCRemoteSwiftPackageReference "netfox" */,
|
||||||
|
5DA241F92E17C3B3007531C3 /* XCRemoteSwiftPackageReference "R.swift" */,
|
||||||
);
|
);
|
||||||
preferredProjectObjectVersion = 77;
|
preferredProjectObjectVersion = 77;
|
||||||
productRefGroup = 5D45F9C92DF858680048D5B8 /* Products */;
|
productRefGroup = 5D45F9C92DF858680048D5B8 /* Products */;
|
||||||
@ -332,6 +346,7 @@
|
|||||||
isa = PBXResourcesBuildPhase;
|
isa = PBXResourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
5DA242132E17D31A007531C3 /* Localizable.xcstrings in Resources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@ -339,6 +354,7 @@
|
|||||||
isa = PBXResourcesBuildPhase;
|
isa = PBXResourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
5DA242142E17D31A007531C3 /* Localizable.xcstrings in Resources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@ -405,6 +421,10 @@
|
|||||||
target = 5D45F9C72DF858680048D5B8 /* readeck */;
|
target = 5D45F9C72DF858680048D5B8 /* readeck */;
|
||||||
targetProxy = 5D45F9E92DF8586A0048D5B8 /* PBXContainerItemProxy */;
|
targetProxy = 5D45F9E92DF8586A0048D5B8 /* PBXContainerItemProxy */;
|
||||||
};
|
};
|
||||||
|
5DA241FF2E17C3CE007531C3 /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
productRef = 5DA241FE2E17C3CE007531C3 /* RswiftGenerateInternalResources */;
|
||||||
|
};
|
||||||
/* End PBXTargetDependency section */
|
/* End PBXTargetDependency section */
|
||||||
|
|
||||||
/* Begin XCBuildConfiguration section */
|
/* Begin XCBuildConfiguration section */
|
||||||
@ -524,6 +544,7 @@
|
|||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
@ -578,6 +599,7 @@
|
|||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
SWIFT_COMPILATION_MODE = wholemodule;
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
@ -596,7 +618,6 @@
|
|||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = readeck/Info.plist;
|
INFOPLIST_FILE = readeck/Info.plist;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "";
|
|
||||||
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
|
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
|
||||||
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
|
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
|
||||||
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
|
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
|
||||||
@ -639,7 +660,6 @@
|
|||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = readeck/Info.plist;
|
INFOPLIST_FILE = readeck/Info.plist;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "";
|
|
||||||
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
|
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
|
||||||
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
|
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
|
||||||
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
|
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
|
||||||
@ -816,6 +836,14 @@
|
|||||||
minimumVersion = 1.21.0;
|
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 */
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|
||||||
/* Begin XCSwiftPackageProductDependency section */
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
@ -824,6 +852,21 @@
|
|||||||
package = 5D348CC12E0C9F4F00D0AF21 /* XCRemoteSwiftPackageReference "netfox" */;
|
package = 5D348CC12E0C9F4F00D0AF21 /* XCRemoteSwiftPackageReference "netfox" */;
|
||||||
productName = 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 */
|
/* End XCSwiftPackageProductDependency section */
|
||||||
};
|
};
|
||||||
rootObject = 5D45F9C02DF858680048D5B8 /* Project object */;
|
rootObject = 5D45F9C02DF858680048D5B8 /* Project object */;
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"originHash" : "7374154e7686de69a9f88fbafb081b646b02140f8d82770f46fa750840581e0e",
|
"originHash" : "ad0d6bd7e4d278f825d201974f944ef5a8c72a7d757d551070c5da6f64b45150",
|
||||||
"pins" : [
|
"pins" : [
|
||||||
{
|
{
|
||||||
"identity" : "netfox",
|
"identity" : "netfox",
|
||||||
@ -9,6 +9,33 @@
|
|||||||
"revision" : "557576032736fd3140422baefb68b8f76c55088f",
|
"revision" : "557576032736fd3140422baefb68b8f76c55088f",
|
||||||
"version" : "1.21.0"
|
"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
|
"version" : 3
|
||||||
|
|||||||
@ -158,28 +158,28 @@ class API: PAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func login(endpoint: String, username: String, password: String) async throws -> UserDto {
|
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 loginRequest = LoginRequestDto(application: "api doc", username: username, password: password)
|
||||||
let requestData = try JSONEncoder().encode(loginRequest)
|
let requestData = try JSONEncoder().encode(loginRequest)
|
||||||
guard let url = URL(string: endpoint + "/api/auth") else {
|
|
||||||
throw APIError.invalidURL
|
|
||||||
}
|
|
||||||
var request = URLRequest(url: url)
|
var request = URLRequest(url: url)
|
||||||
request.httpMethod = "POST"
|
request.httpMethod = "POST"
|
||||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||||
request.httpBody = requestData
|
request.httpBody = requestData
|
||||||
|
|
||||||
let (data, response) = try await URLSession.shared.data(for: request)
|
let (data, response) = try await URLSession.shared.data(for: request)
|
||||||
|
|
||||||
guard let httpResponse = response as? HTTPURLResponse else {
|
guard let httpResponse = response as? HTTPURLResponse else {
|
||||||
throw APIError.invalidResponse
|
throw APIError.invalidResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
guard 200...299 ~= httpResponse.statusCode else {
|
guard 200...299 ~= httpResponse.statusCode else {
|
||||||
throw APIError.serverError(httpResponse.statusCode)
|
throw APIError.serverError(httpResponse.statusCode)
|
||||||
}
|
}
|
||||||
let userDto = try JSONDecoder().decode(UserDto.self, from: data)
|
|
||||||
// Token NICHT automatisch speichern, da Settings noch nicht existieren
|
return try JSONDecoder().decode(UserDto.self, from: data)
|
||||||
return userDto
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 {
|
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 endpoint = "/api/bookmarks"
|
||||||
var queryItems: [URLQueryItem] = []
|
var queryItems: [URLQueryItem] = []
|
||||||
|
|||||||
@ -89,41 +89,3 @@ class BookmarksRepository: PBookmarksRepository {
|
|||||||
return bookmarkDtos.toDomain()
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
21
readeck/Domain/Error/CreateBookmarkError.swift
Normal file
21
readeck/Domain/Error/CreateBookmarkError.swift
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
39
readeck/Domain/Model/BookmarkDetail.swift
Normal file
39
readeck/Domain/Model/BookmarkDetail.swift
Normal file
@ -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: ""
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -20,6 +20,7 @@ struct BookmarkDetailView: View {
|
|||||||
Divider().padding(.horizontal)
|
Divider().padding(.horizontal)
|
||||||
contentSection
|
contentSection
|
||||||
Spacer(minLength: 40)
|
Spacer(minLength: 40)
|
||||||
|
archiveSection
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -190,6 +191,39 @@ struct BookmarkDetailView: View {
|
|||||||
}
|
}
|
||||||
return dateString
|
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 {
|
#Preview {
|
||||||
|
|||||||
@ -5,6 +5,7 @@ class BookmarkDetailViewModel {
|
|||||||
private let getBookmarkUseCase: GetBookmarkUseCase
|
private let getBookmarkUseCase: GetBookmarkUseCase
|
||||||
private let getBookmarkArticleUseCase: GetBookmarkArticleUseCase
|
private let getBookmarkArticleUseCase: GetBookmarkArticleUseCase
|
||||||
private let loadSettingsUseCase: LoadSettingsUseCase
|
private let loadSettingsUseCase: LoadSettingsUseCase
|
||||||
|
private let updateBookmarkUseCase: UpdateBookmarkUseCase
|
||||||
|
|
||||||
var bookmarkDetail: BookmarkDetail = BookmarkDetail.empty
|
var bookmarkDetail: BookmarkDetail = BookmarkDetail.empty
|
||||||
var articleContent: String = ""
|
var articleContent: String = ""
|
||||||
@ -20,6 +21,7 @@ class BookmarkDetailViewModel {
|
|||||||
self.getBookmarkUseCase = factory.makeGetBookmarkUseCase()
|
self.getBookmarkUseCase = factory.makeGetBookmarkUseCase()
|
||||||
self.getBookmarkArticleUseCase = factory.makeGetBookmarkArticleUseCase()
|
self.getBookmarkArticleUseCase = factory.makeGetBookmarkArticleUseCase()
|
||||||
self.loadSettingsUseCase = factory.makeLoadSettingsUseCase()
|
self.loadSettingsUseCase = factory.makeLoadSettingsUseCase()
|
||||||
|
self.updateBookmarkUseCase = factory.makeUpdateBookmarkUseCase()
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
@ -30,11 +32,6 @@ class BookmarkDetailViewModel {
|
|||||||
do {
|
do {
|
||||||
settings = try await loadSettingsUseCase.execute()
|
settings = try await loadSettingsUseCase.execute()
|
||||||
bookmarkDetail = try await getBookmarkUseCase.execute(id: id)
|
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 {
|
} catch {
|
||||||
errorMessage = "Fehler beim Laden des Bookmarks"
|
errorMessage = "Fehler beim Laden des Bookmarks"
|
||||||
}
|
}
|
||||||
@ -57,31 +54,23 @@ class BookmarkDetailViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func processArticleContent() {
|
private func processArticleContent() {
|
||||||
// HTML in Paragraphen aufteilen
|
|
||||||
let paragraphs = articleContent
|
let paragraphs = articleContent
|
||||||
.components(separatedBy: .newlines)
|
.components(separatedBy: .newlines)
|
||||||
.filter { !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }
|
.filter { !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }
|
||||||
|
|
||||||
articleParagraphs = paragraphs
|
articleParagraphs = paragraphs
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@MainActor
|
||||||
extension BookmarkDetail {
|
func archiveBookmark(id: String) async {
|
||||||
static let empty = BookmarkDetail(
|
isLoading = true
|
||||||
id: "",
|
errorMessage = nil
|
||||||
title: "",
|
do {
|
||||||
url: "",
|
try await updateBookmarkUseCase.toggleArchive(bookmarkId: id, isArchived: true)
|
||||||
description: "",
|
bookmarkDetail.isArchived = true
|
||||||
siteName: "",
|
} catch {
|
||||||
authors: [],
|
errorMessage = "Fehler beim Archivieren des Bookmarks"
|
||||||
created: "",
|
}
|
||||||
updated: "",
|
isLoading = false
|
||||||
wordCount: 0,
|
}
|
||||||
readingTime: 0,
|
|
||||||
hasArticle: false,
|
|
||||||
isMarked: false,
|
|
||||||
isArchived: false,
|
|
||||||
thumbnailUrl: "",
|
|
||||||
imageUrl: ""
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,14 +10,14 @@ struct BookmarkCardView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
// Vorschaubild - verwende image oder thumbnail
|
|
||||||
AsyncImage(url: imageURL) { image in
|
AsyncImage(url: imageURL) { image in
|
||||||
image
|
image
|
||||||
.resizable()
|
.resizable()
|
||||||
.aspectRatio(contentMode: .fill)
|
.aspectRatio(contentMode: .fill)
|
||||||
.frame(height: 120)
|
.frame(height: 120)
|
||||||
} placeholder: {
|
} placeholder: {
|
||||||
Image("placeholder")
|
|
||||||
|
Image(R.image.placeholder.name)
|
||||||
.resizable()
|
.resizable()
|
||||||
.aspectRatio(contentMode: .fill)
|
.aspectRatio(contentMode: .fill)
|
||||||
.frame(height: 120)
|
.frame(height: 120)
|
||||||
@ -25,14 +25,12 @@ struct BookmarkCardView: View {
|
|||||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
// Titel
|
|
||||||
Text(bookmark.title)
|
Text(bookmark.title)
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
.fontWeight(.semibold)
|
.fontWeight(.semibold)
|
||||||
.lineLimit(2)
|
.lineLimit(2)
|
||||||
.multilineTextAlignment(.leading)
|
.multilineTextAlignment(.leading)
|
||||||
|
|
||||||
// Meta-Info mit Datum
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
HStack {
|
HStack {
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user