Translate UI and error messages from German to English
- BookmarkDetail: All user-facing texts and error messages in BookmarkDetailView, BookmarkDetailViewModel, BookmarkLabelsView, and BookmarkLabelsViewModel translated to English. - Bookmarks: All UI strings, swipe actions, and error messages in BookmarkCardView, BookmarksView, BookmarksViewModel, and related enums translated to English. - Labels: All UI and error messages in LabelsView and LabelsViewModel translated to English. - Menu: All sidebar/tab names, navigation titles, and queue texts in BookmarkState, PhoneTabView, PlayerQueueResumeButton, SidebarTab updated to English. - Settings: All section headers, toggle labels, button texts, and error/success messages in FontSettingsView, FontSettingsViewModel, SettingsContainerView, SettingsGeneralView, SettingsGeneralViewModel, SettingsServerView, SettingsServerViewModel translated to English. - SpeechPlayer: All player UI texts, progress, and queue messages in SpeechPlayerView translated to English. This commit unifies the app language to English for all user-facing areas.
This commit is contained in:
parent
c52d974b05
commit
387a026e7d
@ -17,13 +17,13 @@
|
|||||||
"%lld" : {
|
"%lld" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"%lld Artikel in der Queue" : {
|
"%lld articles in the queue" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"%lld min" : {
|
"%lld min" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"%lld Minuten" : {
|
"%lld minutes" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"%lld." : {
|
"%lld." : {
|
||||||
@ -47,6 +47,12 @@
|
|||||||
},
|
},
|
||||||
"Abmelden" : {
|
"Abmelden" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"About the App" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Add a new link to your collection" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Aktuelle Labels" : {
|
"Aktuelle Labels" : {
|
||||||
|
|
||||||
@ -61,99 +67,87 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"Als Favorit markieren" : {
|
|
||||||
|
|
||||||
},
|
},
|
||||||
"Anmelden & speichern" : {
|
"Anmelden & speichern" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Archivieren" : {
|
"Archive" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Artikel automatisch als gelesen markieren" : {
|
"Archive bookmark" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Artikel vorlesen" : {
|
"Automatic sync" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Automatischer Sync" : {
|
"Automatically mark articles as read" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Bookmark archivieren" : {
|
"Cancel" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Bookmark speichern" : {
|
"Clear cache" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Cache leeren" : {
|
"Clipboard" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Datenmanagement" : {
|
"Close" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Debug-Anmeldung" : {
|
"Data Management" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Einfügen" : {
|
"Delete" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Einstellungen" : {
|
"Developer: %@" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Einstellungen speichern" : {
|
"Done" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Einstellungen zurücksetzen" : {
|
"e.g. work, important, later" : {
|
||||||
|
|
||||||
},
|
|
||||||
"Entfernen" : {
|
|
||||||
|
|
||||||
},
|
|
||||||
"Entwickler: %@" : {
|
|
||||||
|
|
||||||
},
|
},
|
||||||
"Erfolgreich angemeldet" : {
|
"Erfolgreich angemeldet" : {
|
||||||
|
|
||||||
},
|
|
||||||
"Erforderlich" : {
|
|
||||||
|
|
||||||
},
|
},
|
||||||
"Erneut anmelden & speichern" : {
|
"Erneut anmelden & speichern" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Es wurden noch keine Bookmarks in %@ gefunden." : {
|
"Error" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Externe Links in In-App Safari öffnen" : {
|
"Error: %@" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Favorit" : {
|
"Favorite" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Fehler" : {
|
"Fehler" : {
|
||||||
|
|
||||||
},
|
|
||||||
"Fehler: %@" : {
|
|
||||||
|
|
||||||
},
|
},
|
||||||
"Fertig" : {
|
"Fertig" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Fertig mit Lesen?" : {
|
"Finished reading?" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Fortschritt: %lld%%" : {
|
"Font" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Füge einen neuen Link zu deiner Sammlung hinzu" : {
|
"Font family" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Font Settings" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Font size" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Geben Sie Ihre Readeck-Server-Details ein, um zu beginnen." : {
|
"Geben Sie Ihre Readeck-Server-Details ein, um zu beginnen." : {
|
||||||
|
|
||||||
},
|
|
||||||
"Geschwindigkeit" : {
|
|
||||||
|
|
||||||
},
|
},
|
||||||
"https://example.com" : {
|
"https://example.com" : {
|
||||||
|
|
||||||
@ -163,12 +157,6 @@
|
|||||||
},
|
},
|
||||||
"Ihre aktuelle Server-Verbindung und Anmeldedaten." : {
|
"Ihre aktuelle Server-Verbindung und Anmeldedaten." : {
|
||||||
|
|
||||||
},
|
|
||||||
"Keine Artikel in der Queue" : {
|
|
||||||
|
|
||||||
},
|
|
||||||
"Keine Bookmarks" : {
|
|
||||||
|
|
||||||
},
|
},
|
||||||
"Keine Bookmarks gefunden." : {
|
"Keine Bookmarks gefunden." : {
|
||||||
|
|
||||||
@ -191,68 +179,104 @@
|
|||||||
"Labels verwalten" : {
|
"Labels verwalten" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Lade %@..." : {
|
"Loading %@..." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Lade Artikel..." : {
|
"Loading article..." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Lese %lld/%lld: " : {
|
"Mark as favorite" : {
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "new",
|
|
||||||
"value" : "Lese %1$lld/%2$lld: "
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Leseeinstellungen" : {
|
|
||||||
|
|
||||||
},
|
|
||||||
"Löschen" : {
|
|
||||||
|
|
||||||
},
|
|
||||||
"Mehr" : {
|
|
||||||
|
|
||||||
},
|
},
|
||||||
"Möchten Sie sich wirklich abmelden? Dies wird alle Ihre Anmeldedaten löschen und Sie zur Einrichtung zurückführen." : {
|
"Möchten Sie sich wirklich abmelden? Dies wird alle Ihre Anmeldedaten löschen und Sie zur Einrichtung zurückführen." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Neues Bookmark" : {
|
"More" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Neues Label hinzufügen" : {
|
"Neues Label hinzufügen" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"New Bookmark" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"No articles in the queue" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"No bookmarks" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"No bookmarks found in %@." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"OK" : {
|
"OK" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Optional: Eigener Titel" : {
|
"Open external links in in-app Safari" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Optional: Custom title" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Password" : {
|
"Password" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Paste" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Preview" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Progress: %lld%%" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Read article aloud" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Read-aloud Queue" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"readeck Bookmark Title" : {
|
"readeck Bookmark Title" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Safari Reader Modus" : {
|
"Reading %lld/%lld: " : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "new",
|
||||||
|
"value" : "Reading %1$lld/%2$lld: "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Reading Settings" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Schließen" : {
|
"Remove" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Schrift" : {
|
"Required" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Schrift-Einstellungen" : {
|
"Reset settings" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Schriftart" : {
|
"Restore" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Schriftgröße" : {
|
"Resume listening" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Safari Reader Mode" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Save bookmark" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Save settings" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Saving..." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Select a bookmark or tag" : {
|
"Select a bookmark or tag" : {
|
||||||
@ -260,6 +284,12 @@
|
|||||||
},
|
},
|
||||||
"Server-Endpunkt" : {
|
"Server-Endpunkt" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Settings" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Speed" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Speichern..." : {
|
"Speichern..." : {
|
||||||
|
|
||||||
@ -273,10 +303,10 @@
|
|||||||
"Suche..." : {
|
"Suche..." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Sync-Einstellungen" : {
|
"Sync interval" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Sync-Intervall" : {
|
"Sync Settings" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Theme" : {
|
"Theme" : {
|
||||||
@ -285,16 +315,13 @@
|
|||||||
"This is how your bookmark descriptions and article text will appear in the app. The quick brown fox jumps over the lazy dog." : {
|
"This is how your bookmark descriptions and article text will appear in the app. The quick brown fox jumps over the lazy dog." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Titel" : {
|
"Title" : {
|
||||||
|
|
||||||
},
|
|
||||||
"Über die App" : {
|
|
||||||
|
|
||||||
},
|
},
|
||||||
"URL" : {
|
"URL" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"URL gefunden:" : {
|
"URL found:" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Username" : {
|
"Username" : {
|
||||||
@ -302,36 +329,15 @@
|
|||||||
},
|
},
|
||||||
"Version %@" : {
|
"Version %@" : {
|
||||||
|
|
||||||
},
|
|
||||||
"Vorlese-Queue" : {
|
|
||||||
|
|
||||||
},
|
|
||||||
"Vorschau" : {
|
|
||||||
|
|
||||||
},
|
},
|
||||||
"Website" : {
|
"Website" : {
|
||||||
|
|
||||||
},
|
|
||||||
"Weiterhören" : {
|
|
||||||
|
|
||||||
},
|
|
||||||
"Wiederherstellen" : {
|
|
||||||
|
|
||||||
},
|
|
||||||
"Wird gespeichert..." : {
|
|
||||||
|
|
||||||
},
|
},
|
||||||
"Your Password" : {
|
"Your Password" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Your Username" : {
|
"Your Username" : {
|
||||||
|
|
||||||
},
|
|
||||||
"z.B. arbeit, wichtig, später" : {
|
|
||||||
|
|
||||||
},
|
|
||||||
"Zwischenablage" : {
|
|
||||||
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"version" : "1.0"
|
"version" : "1.0"
|
||||||
|
|||||||
133
documentation/Architecture.md
Normal file
133
documentation/Architecture.md
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
# Architecture Overview: readeck client
|
||||||
|
|
||||||
|
## 1. Introduction
|
||||||
|
|
||||||
|
**readeck client** is an open-source iOS project for conveniently managing and reading bookmarks. The app uses the MVVM architecture pattern and follows a clear layer structure: **UI**, **Domain**, and **Data**. A key feature is its own dependency injection (DI) based on Swift protocols and the factory pattern—completely without external libraries.
|
||||||
|
|
||||||
|
- **Architecture Pattern:** MVVM (Model-View-ViewModel) + Use Cases
|
||||||
|
- **Layers:** UI, Domain, Data
|
||||||
|
- **Technologies:** Swift, SwiftUI, CoreData, custom DI
|
||||||
|
- **DI:** Protocol-based, factory pattern, no external libraries
|
||||||
|
|
||||||
|
## 2. Architecture Overview
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
UI["UI Layer\n(View, ViewModel)"]
|
||||||
|
Domain["Domain Layer\n(Use Cases, Models, Repository Protocols)"]
|
||||||
|
Data["Data Layer\n(Repository implementations, Database, Entities, API)"]
|
||||||
|
UI --> Domain
|
||||||
|
Domain --> Data
|
||||||
|
```
|
||||||
|
|
||||||
|
**Layer Overview:**
|
||||||
|
|
||||||
|
| Layer | Responsibility |
|
||||||
|
|---------|----------------------|
|
||||||
|
| UI | Presentation, user interaction, ViewModels, bindings |
|
||||||
|
| Domain | Business logic, use cases, models, repository protocols |
|
||||||
|
| Data | Repository implementations, database, entities, API |
|
||||||
|
|
||||||
|
## 3. Dependency Injection (DI)
|
||||||
|
|
||||||
|
**Goal:** Loose coupling, better testability, exchangeability of implementations.
|
||||||
|
|
||||||
|
**Approach:**
|
||||||
|
- Define protocols for dependencies (e.g., repository protocols)
|
||||||
|
- Implement the protocols in concrete classes
|
||||||
|
- Provide dependencies via a central factory
|
||||||
|
- Pass dependencies to ViewModels/use cases via initializers
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```swift
|
||||||
|
// 1. Protocol definition
|
||||||
|
protocol PBookmarksRepository {
|
||||||
|
func getBookmarks() async throws -> [Bookmark]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Implementation
|
||||||
|
class BookmarksRepository: PBookmarksRepository {
|
||||||
|
func getBookmarks() async throws -> [Bookmark] {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Factory
|
||||||
|
class DefaultUseCaseFactory {
|
||||||
|
let bookmarksRepository: PBookmarksRepository = BookmarksRepository()
|
||||||
|
func makeGetBookmarksUseCase() -> GetBookmarksUseCase {
|
||||||
|
GetBookmarksUseCase(bookmarksRepository: bookmarksRepository)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. ViewModel
|
||||||
|
class BookmarksViewModel: ObservableObject {
|
||||||
|
private let getBookmarksUseCase: GetBookmarksUseCase
|
||||||
|
init(factory: DefaultUseCaseFactory) {
|
||||||
|
self.getBookmarksUseCase = factory.makeGetBookmarksUseCase()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Advantages:**
|
||||||
|
- Exchangeability (e.g., for tests)
|
||||||
|
- No dependency on frameworks
|
||||||
|
- Central management of all dependencies
|
||||||
|
|
||||||
|
## 4. Component Description
|
||||||
|
|
||||||
|
| Component | Responsibility |
|
||||||
|
|---------------------|---------------|
|
||||||
|
| View | UI elements, presentation, user interaction |
|
||||||
|
| ViewModel | Bridge between View & Domain, state management |
|
||||||
|
| Use Case | Encapsulates a business logic (e.g., create bookmark) |
|
||||||
|
| Repository Protocol | Interface between Domain & Data layer |
|
||||||
|
| Repository Implementation | Concrete implementation of repository protocols, handles data access |
|
||||||
|
| Data Source / API | Access to external data sources (API, CoreData, Keychain) |
|
||||||
|
| Model/Entity | Represents core data structures |
|
||||||
|
| Dependency Factory | Creates and manages dependencies, central DI point |
|
||||||
|
|
||||||
|
## 5. Data Flow
|
||||||
|
|
||||||
|
1. **User interaction** in the view triggers an action in the ViewModel.
|
||||||
|
2. The **ViewModel** calls a **use case**.
|
||||||
|
3. The **use case** uses a **repository protocol** to load/save data.
|
||||||
|
4. The **repository implementation** accesses a **data source** (e.g., API, CoreData).
|
||||||
|
5. The response flows back up to the view and is displayed.
|
||||||
|
|
||||||
|
## 6. Advantages of this Architecture
|
||||||
|
|
||||||
|
- **Testability:** Protocols and DI allow components to be tested in isolation.
|
||||||
|
- **Maintainability:** Clear separation of concerns, easy extensibility.
|
||||||
|
- **Modularity:** Layers can be developed and adjusted independently.
|
||||||
|
- **Independence:** No dependency on external DI or architecture frameworks.
|
||||||
|
|
||||||
|
## 7. Contributor Tips
|
||||||
|
|
||||||
|
- **New dependencies:** Always define as a protocol and register in the factory.
|
||||||
|
- **Protocols:** Define in the domain layer, implement in the data layer.
|
||||||
|
- **Factory:** Extend the factory for new use cases or repositories.
|
||||||
|
- **No external frameworks:** Intentionally use custom solutions for better control and clarity.
|
||||||
|
|
||||||
|
## 8. Glossary
|
||||||
|
|
||||||
|
| Term | Definition |
|
||||||
|
|---------------------|------------|
|
||||||
|
| Dependency Injection| Technique for providing dependencies from the outside |
|
||||||
|
| Protocol | Swift interface that defines requirements for types |
|
||||||
|
| Factory Pattern | Design pattern for central object creation |
|
||||||
|
| MVVM | Architecture: Model-View-ViewModel |
|
||||||
|
| Use Case | Encapsulates a specific business logic |
|
||||||
|
| Repository Protocol | Interface in the domain layer for data access |
|
||||||
|
| Repository Implementation | Concrete class in the data layer that fulfills a repository protocol |
|
||||||
|
| Data Source | Implementation for data access (API, DB, etc.) |
|
||||||
|
| Model/Entity | Core data structure used in domain or data layer |
|
||||||
|
|
||||||
|
## 9. Recommended Links
|
||||||
|
|
||||||
|
- [Clean Architecture (Uncle Bob)](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)
|
||||||
|
- [Clean Architecture for Swift/iOS (adrian bilescu)](https://adrian-bilescu.medium.com/a-pragmatic-guide-to-clean-architecture-on-ios-e58d19d00559)
|
||||||
|
- [Swift.org: Protocols](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/protocols/)
|
||||||
@ -14,7 +14,7 @@ class CoreDataTokenProvider: TokenProvider {
|
|||||||
private let keychainHelper = KeychainHelper.shared
|
private let keychainHelper = KeychainHelper.shared
|
||||||
|
|
||||||
private func loadSettingsIfNeeded() async {
|
private func loadSettingsIfNeeded() async {
|
||||||
guard !isLoaded else { return }
|
guard isLoaded == false || cachedSettings == nil else { return }
|
||||||
|
|
||||||
do {
|
do {
|
||||||
cachedSettings = try await settingsRepository.loadSettings()
|
cachedSettings = try await settingsRepository.loadSettings()
|
||||||
|
|||||||
@ -26,11 +26,11 @@ struct AddBookmarkView: View {
|
|||||||
.font(.system(size: 48))
|
.font(.system(size: 48))
|
||||||
.foregroundColor(.accentColor)
|
.foregroundColor(.accentColor)
|
||||||
|
|
||||||
Text("Neues Bookmark")
|
Text("New Bookmark")
|
||||||
.font(.title2)
|
.font(.title2)
|
||||||
.fontWeight(.semibold)
|
.fontWeight(.semibold)
|
||||||
|
|
||||||
Text("Füge einen neuen Link zu deiner Sammlung hinzu")
|
Text("Add a new link to your collection")
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
@ -48,7 +48,7 @@ struct AddBookmarkView: View {
|
|||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
Text("Erforderlich")
|
Text("Required")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundColor(.red)
|
.foregroundColor(.red)
|
||||||
}
|
}
|
||||||
@ -62,11 +62,11 @@ struct AddBookmarkView: View {
|
|||||||
|
|
||||||
// Title Field
|
// Title Field
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
Label("Titel", systemImage: "note.text")
|
Label("Title", systemImage: "note.text")
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
|
|
||||||
TextField("Optional: Eigener Titel", text: $viewModel.title)
|
TextField("Optional: Custom title", text: $viewModel.title)
|
||||||
.textFieldStyle(CustomTextFieldStyle())
|
.textFieldStyle(CustomTextFieldStyle())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,7 +76,7 @@ struct AddBookmarkView: View {
|
|||||||
.font(.headline)
|
.font(.headline)
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
|
|
||||||
TextField("z.B. arbeit, wichtig, später", text: $viewModel.labelsText)
|
TextField("e.g. work, important, later", text: $viewModel.labelsText)
|
||||||
.textFieldStyle(CustomTextFieldStyle())
|
.textFieldStyle(CustomTextFieldStyle())
|
||||||
|
|
||||||
// Labels Preview
|
// Labels Preview
|
||||||
@ -102,13 +102,13 @@ struct AddBookmarkView: View {
|
|||||||
// Clipboard Section
|
// Clipboard Section
|
||||||
if viewModel.clipboardURL != nil {
|
if viewModel.clipboardURL != nil {
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
Label("Zwischenablage", systemImage: "doc.on.clipboard")
|
Label("Clipboard", systemImage: "doc.on.clipboard")
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
Text("URL gefunden:")
|
Text("URL found:")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
@ -120,7 +120,7 @@ struct AddBookmarkView: View {
|
|||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
Button("Einfügen") {
|
Button("Paste") {
|
||||||
viewModel.pasteFromClipboard()
|
viewModel.pasteFromClipboard()
|
||||||
}
|
}
|
||||||
.buttonStyle(SecondaryButtonStyle())
|
.buttonStyle(SecondaryButtonStyle())
|
||||||
@ -133,7 +133,7 @@ struct AddBookmarkView: View {
|
|||||||
}
|
}
|
||||||
.padding(.horizontal, 20)
|
.padding(.horizontal, 20)
|
||||||
|
|
||||||
Spacer(minLength: 100) // Platz für Button
|
Spacer(minLength: 100) // Space for button
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,7 +160,7 @@ struct AddBookmarkView: View {
|
|||||||
Image(systemName: "bookmark.fill")
|
Image(systemName: "bookmark.fill")
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(viewModel.isLoading ? "Wird gespeichert..." : "Bookmark speichern")
|
Text(viewModel.isLoading ? "Saving..." : "Save bookmark")
|
||||||
.fontWeight(.semibold)
|
.fontWeight(.semibold)
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
@ -172,7 +172,7 @@ struct AddBookmarkView: View {
|
|||||||
.disabled(!viewModel.isValid || viewModel.isLoading)
|
.disabled(!viewModel.isValid || viewModel.isLoading)
|
||||||
|
|
||||||
// Cancel Button
|
// Cancel Button
|
||||||
Button("Abbrechen") {
|
Button("Cancel") {
|
||||||
dismiss()
|
dismiss()
|
||||||
viewModel.clearForm()
|
viewModel.clearForm()
|
||||||
}
|
}
|
||||||
@ -186,17 +186,17 @@ struct AddBookmarkView: View {
|
|||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
Button("Schließen") {
|
Button("Close") {
|
||||||
dismiss()
|
dismiss()
|
||||||
viewModel.clearForm()
|
viewModel.clearForm()
|
||||||
}
|
}
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.alert("Fehler", isPresented: $viewModel.showErrorAlert) {
|
.alert("Error", isPresented: $viewModel.showErrorAlert) {
|
||||||
Button("OK", role: .cancel) { }
|
Button("OK", role: .cancel) { }
|
||||||
} message: {
|
} message: {
|
||||||
Text(viewModel.errorMessage ?? "Unbekannter Fehler")
|
Text(viewModel.errorMessage ?? "Unknown error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
|
|||||||
@ -48,7 +48,7 @@ class AddBookmarkViewModel {
|
|||||||
|
|
||||||
let message = try await createBookmarkUseCase.execute(createRequest: request)
|
let message = try await createBookmarkUseCase.execute(createRequest: request)
|
||||||
|
|
||||||
// Optional: Zeige die Server-Nachricht an
|
// Optional: Show the server message
|
||||||
print("Server response: \(message)")
|
print("Server response: \(message)")
|
||||||
|
|
||||||
clearForm()
|
clearForm()
|
||||||
@ -57,7 +57,7 @@ class AddBookmarkViewModel {
|
|||||||
errorMessage = error.localizedDescription
|
errorMessage = error.localizedDescription
|
||||||
showErrorAlert = true
|
showErrorAlert = true
|
||||||
} catch {
|
} catch {
|
||||||
errorMessage = "Fehler beim Erstellen des Bookmarks"
|
errorMessage = "Error creating bookmark"
|
||||||
showErrorAlert = true
|
showErrorAlert = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -68,11 +68,11 @@ struct BookmarkDetailView: View {
|
|||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
.navigationTitle("Schrift-Einstellungen")
|
.navigationTitle("Font Settings")
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
Button("Fertig") {
|
Button("Done") {
|
||||||
showingFontSettings = false
|
showingFontSettings = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -173,7 +173,7 @@ struct BookmarkDetailView: View {
|
|||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
.animation(.easeInOut, value: webViewHeight)
|
.animation(.easeInOut, value: webViewHeight)
|
||||||
} else if viewModel.isLoadingArticle {
|
} else if viewModel.isLoadingArticle {
|
||||||
ProgressView("Lade Artikel...")
|
ProgressView("Loading article...")
|
||||||
.frame(maxWidth: .infinity, alignment: .center)
|
.frame(maxWidth: .infinity, alignment: .center)
|
||||||
.padding()
|
.padding()
|
||||||
} else {
|
} else {
|
||||||
@ -182,7 +182,7 @@ struct BookmarkDetailView: View {
|
|||||||
}) {
|
}) {
|
||||||
HStack {
|
HStack {
|
||||||
Image(systemName: "safari")
|
Image(systemName: "safari")
|
||||||
Text((URLUtil.extractDomain(from: viewModel.bookmarkDetail.url) ?? "Original Seite") + " öffnen")
|
Text((URLUtil.extractDomain(from: viewModel.bookmarkDetail.url) ?? "Open original page") + " open")
|
||||||
}
|
}
|
||||||
.font(.title3.bold())
|
.font(.title3.bold())
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
@ -196,10 +196,10 @@ struct BookmarkDetailView: View {
|
|||||||
private var metaInfoSection: some View {
|
private var metaInfoSection: some View {
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
if !viewModel.bookmarkDetail.authors.isEmpty {
|
if !viewModel.bookmarkDetail.authors.isEmpty {
|
||||||
metaRow(icon: "person", text: (viewModel.bookmarkDetail.authors.count > 1 ? "Autor:innen: " : "Autor: ") + viewModel.bookmarkDetail.authors.joined(separator: ", "))
|
metaRow(icon: "person", text: (viewModel.bookmarkDetail.authors.count > 1 ? "Authors: " : "Author: ") + viewModel.bookmarkDetail.authors.joined(separator: ", "))
|
||||||
}
|
}
|
||||||
metaRow(icon: "calendar", text: formatDate(viewModel.bookmarkDetail.created))
|
metaRow(icon: "calendar", text: formatDate(viewModel.bookmarkDetail.created))
|
||||||
metaRow(icon: "textformat", text: "\(viewModel.bookmarkDetail.wordCount ?? 0) Wörter • \(viewModel.bookmarkDetail.readingTime ?? 0) min Lesezeit")
|
metaRow(icon: "textformat", text: "\(viewModel.bookmarkDetail.wordCount ?? 0) words • \(viewModel.bookmarkDetail.readingTime ?? 0) min read")
|
||||||
|
|
||||||
// Labels section
|
// Labels section
|
||||||
if !viewModel.bookmarkDetail.labels.isEmpty {
|
if !viewModel.bookmarkDetail.labels.isEmpty {
|
||||||
@ -236,7 +236,7 @@ struct BookmarkDetailView: View {
|
|||||||
Button(action: {
|
Button(action: {
|
||||||
SafariUtil.openInSafari(url: viewModel.bookmarkDetail.url)
|
SafariUtil.openInSafari(url: viewModel.bookmarkDetail.url)
|
||||||
}) {
|
}) {
|
||||||
Text((URLUtil.extractDomain(from: viewModel.bookmarkDetail.url) ?? "Original Seite") + " öffnen")
|
Text((URLUtil.extractDomain(from: viewModel.bookmarkDetail.url) ?? "Open original page") + " open")
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
@ -247,7 +247,7 @@ struct BookmarkDetailView: View {
|
|||||||
viewModel.addBookmarkToSpeechQueue()
|
viewModel.addBookmarkToSpeechQueue()
|
||||||
playerUIState.showPlayer()
|
playerUIState.showPlayer()
|
||||||
}) {
|
}) {
|
||||||
Text("Artikel vorlesen")
|
Text("Read article aloud")
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
@ -296,7 +296,7 @@ struct BookmarkDetailView: View {
|
|||||||
|
|
||||||
private var archiveSection: some View {
|
private var archiveSection: some View {
|
||||||
VStack(alignment: .center, spacing: 12) {
|
VStack(alignment: .center, spacing: 12) {
|
||||||
Text("Fertig mit Lesen?")
|
Text("Finished reading?")
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
.padding(.top, 24)
|
.padding(.top, 24)
|
||||||
VStack(alignment: .center, spacing: 16) {
|
VStack(alignment: .center, spacing: 16) {
|
||||||
@ -308,7 +308,7 @@ struct BookmarkDetailView: View {
|
|||||||
HStack {
|
HStack {
|
||||||
Image(systemName: viewModel.bookmarkDetail.isMarked ? "star.fill" : "star")
|
Image(systemName: viewModel.bookmarkDetail.isMarked ? "star.fill" : "star")
|
||||||
.foregroundColor(viewModel.bookmarkDetail.isMarked ? .yellow : .gray)
|
.foregroundColor(viewModel.bookmarkDetail.isMarked ? .yellow : .gray)
|
||||||
Text(viewModel.bookmarkDetail.isMarked ? "Favorit" : "Als Favorit markieren")
|
Text(viewModel.bookmarkDetail.isMarked ? "Favorite" : "Mark as favorite")
|
||||||
}
|
}
|
||||||
.font(.title3.bold())
|
.font(.title3.bold())
|
||||||
.frame(maxHeight: 60)
|
.frame(maxHeight: 60)
|
||||||
@ -325,7 +325,7 @@ struct BookmarkDetailView: View {
|
|||||||
}) {
|
}) {
|
||||||
HStack {
|
HStack {
|
||||||
Image(systemName: "archivebox")
|
Image(systemName: "archivebox")
|
||||||
Text("Bookmark archivieren")
|
Text("Archive bookmark")
|
||||||
}
|
}
|
||||||
.font(.title3.bold())
|
.font(.title3.bold())
|
||||||
.frame(maxHeight: 60)
|
.frame(maxHeight: 60)
|
||||||
|
|||||||
@ -34,7 +34,7 @@ class BookmarkDetailViewModel {
|
|||||||
settings = try await loadSettingsUseCase.execute()
|
settings = try await loadSettingsUseCase.execute()
|
||||||
bookmarkDetail = try await getBookmarkUseCase.execute(id: id)
|
bookmarkDetail = try await getBookmarkUseCase.execute(id: id)
|
||||||
} catch {
|
} catch {
|
||||||
errorMessage = "Fehler beim Laden des Bookmarks"
|
errorMessage = "Error loading bookmark"
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoading = false
|
isLoading = false
|
||||||
@ -48,7 +48,7 @@ class BookmarkDetailViewModel {
|
|||||||
articleContent = try await getBookmarkArticleUseCase.execute(id: id)
|
articleContent = try await getBookmarkArticleUseCase.execute(id: id)
|
||||||
processArticleContent()
|
processArticleContent()
|
||||||
} catch {
|
} catch {
|
||||||
errorMessage = "Fehler beim Laden des Artikels"
|
errorMessage = "Error loading article"
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoadingArticle = false
|
isLoadingArticle = false
|
||||||
@ -70,7 +70,7 @@ class BookmarkDetailViewModel {
|
|||||||
try await updateBookmarkUseCase.toggleArchive(bookmarkId: id, isArchived: true)
|
try await updateBookmarkUseCase.toggleArchive(bookmarkId: id, isArchived: true)
|
||||||
bookmarkDetail.isArchived = true
|
bookmarkDetail.isArchived = true
|
||||||
} catch {
|
} catch {
|
||||||
errorMessage = "Fehler beim Archivieren des Bookmarks"
|
errorMessage = "Error archiving bookmark"
|
||||||
}
|
}
|
||||||
isLoading = false
|
isLoading = false
|
||||||
}
|
}
|
||||||
@ -94,7 +94,7 @@ class BookmarkDetailViewModel {
|
|||||||
try await updateBookmarkUseCase.toggleFavorite(bookmarkId: id, isMarked: newValue)
|
try await updateBookmarkUseCase.toggleFavorite(bookmarkId: id, isMarked: newValue)
|
||||||
bookmarkDetail.isMarked = newValue
|
bookmarkDetail.isMarked = newValue
|
||||||
} catch {
|
} catch {
|
||||||
errorMessage = "Fehler beim Aktualisieren des Favoriten-Status"
|
errorMessage = "Error updating favorite status"
|
||||||
}
|
}
|
||||||
isLoading = false
|
isLoading = false
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,37 +26,37 @@ struct BookmarkLabelsView: View {
|
|||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
.background(Color(.systemGroupedBackground))
|
.background(Color(.systemGroupedBackground))
|
||||||
.navigationTitle("Labels verwalten")
|
.navigationTitle("Manage Labels")
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .navigationBarLeading) {
|
ToolbarItem(placement: .navigationBarLeading) {
|
||||||
|
|
||||||
Button("Abbrechen") {
|
Button("Cancel") {
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
Button("Fertig") {
|
Button("Done") {
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.alert("Fehler", isPresented: $viewModel.showErrorAlert) {
|
.alert("Error", isPresented: $viewModel.showErrorAlert) {
|
||||||
Button("OK") { }
|
Button("OK") { }
|
||||||
} message: {
|
} message: {
|
||||||
Text(viewModel.errorMessage ?? "Unbekannter Fehler")
|
Text(viewModel.errorMessage ?? "Unknown error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var addLabelSection: some View {
|
private var addLabelSection: some View {
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
Text("Neues Label hinzufügen")
|
Text("Add new label")
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
|
|
||||||
HStack(spacing: 12) {
|
HStack(spacing: 12) {
|
||||||
TextField("Label eingeben...", text: $viewModel.newLabelText)
|
TextField("Enter label...", text: $viewModel.newLabelText)
|
||||||
.textFieldStyle(RoundedBorderTextFieldStyle())
|
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||||
.onSubmit {
|
.onSubmit {
|
||||||
Task {
|
Task {
|
||||||
@ -91,7 +91,7 @@ struct BookmarkLabelsView: View {
|
|||||||
private var currentLabelsSection: some View {
|
private var currentLabelsSection: some View {
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
HStack {
|
HStack {
|
||||||
Text("Aktuelle Labels")
|
Text("Current labels")
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
|
|
||||||
@ -108,7 +108,7 @@ struct BookmarkLabelsView: View {
|
|||||||
Image(systemName: "tag")
|
Image(systemName: "tag")
|
||||||
.font(.title2)
|
.font(.title2)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
Text("Keine Labels vorhanden")
|
Text("No labels available")
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,7 +29,7 @@ class BookmarkLabelsViewModel {
|
|||||||
errorMessage = error.localizedDescription
|
errorMessage = error.localizedDescription
|
||||||
showErrorAlert = true
|
showErrorAlert = true
|
||||||
} catch {
|
} catch {
|
||||||
errorMessage = "Fehler beim Hinzufügen der Labels"
|
errorMessage = "Error adding labels"
|
||||||
showErrorAlert = true
|
showErrorAlert = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,7 +58,7 @@ class BookmarkLabelsViewModel {
|
|||||||
errorMessage = error.localizedDescription
|
errorMessage = error.localizedDescription
|
||||||
showErrorAlert = true
|
showErrorAlert = true
|
||||||
} catch {
|
} catch {
|
||||||
errorMessage = "Fehler beim Entfernen der Labels"
|
errorMessage = "Error removing labels"
|
||||||
showErrorAlert = true
|
showErrorAlert = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -37,7 +37,7 @@ struct BookmarkCardView: View {
|
|||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
HStack {
|
HStack {
|
||||||
|
|
||||||
// Veröffentlichungsdatum
|
// Published date
|
||||||
if let publishedDate = formattedPublishedDate {
|
if let publishedDate = formattedPublishedDate {
|
||||||
HStack {
|
HStack {
|
||||||
Label(publishedDate, systemImage: "calendar")
|
Label(publishedDate, systemImage: "calendar")
|
||||||
@ -59,7 +59,7 @@ struct BookmarkCardView: View {
|
|||||||
}
|
}
|
||||||
HStack {
|
HStack {
|
||||||
|
|
||||||
Label((URLUtil.extractDomain(from: bookmark.url) ?? "Original Seite") + " öffnen", systemImage: "safari")
|
Label((URLUtil.extractDomain(from: bookmark.url) ?? "Original Site") + " open", systemImage: "safari")
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
SafariUtil.openInSafari(url: bookmark.url)
|
SafariUtil.openInSafari(url: bookmark.url)
|
||||||
}
|
}
|
||||||
@ -68,7 +68,7 @@ struct BookmarkCardView: View {
|
|||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
// Progress Bar für Lesefortschritt
|
// Progress Bar for reading progress
|
||||||
if bookmark.readProgress > 0 {
|
if bookmark.readProgress > 0 {
|
||||||
ProgressView(value: Double(bookmark.readProgress), total: 100)
|
ProgressView(value: Double(bookmark.readProgress), total: 100)
|
||||||
.progressViewStyle(LinearProgressViewStyle())
|
.progressViewStyle(LinearProgressViewStyle())
|
||||||
@ -82,20 +82,20 @@ struct BookmarkCardView: View {
|
|||||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||||
.shadow(color: colorScheme == .light ? .black.opacity(0.1) : .white.opacity(0.1), radius: 2, x: 0, y: 1)
|
.shadow(color: colorScheme == .light ? .black.opacity(0.1) : .white.opacity(0.1), radius: 2, x: 0, y: 1)
|
||||||
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
||||||
Button("Löschen", role: .destructive) {
|
Button("Delete", role: .destructive) {
|
||||||
onDelete(bookmark)
|
onDelete(bookmark)
|
||||||
}
|
}
|
||||||
.tint(.red)
|
.tint(.red)
|
||||||
}
|
}
|
||||||
.swipeActions(edge: .leading, allowsFullSwipe: true) {
|
.swipeActions(edge: .leading, allowsFullSwipe: true) {
|
||||||
// Archivieren (links)
|
// Archive (left)
|
||||||
Button {
|
Button {
|
||||||
onArchive(bookmark)
|
onArchive(bookmark)
|
||||||
} label: {
|
} label: {
|
||||||
if currentState == .archived {
|
if currentState == .archived {
|
||||||
Label("Wiederherstellen", systemImage: "tray.and.arrow.up")
|
Label("Restore", systemImage: "tray.and.arrow.up")
|
||||||
} else {
|
} else {
|
||||||
Label("Archivieren", systemImage: "archivebox")
|
Label("Archive", systemImage: "archivebox")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.tint(currentState == .archived ? .blue : .orange)
|
.tint(currentState == .archived ? .blue : .orange)
|
||||||
@ -103,7 +103,7 @@ struct BookmarkCardView: View {
|
|||||||
Button {
|
Button {
|
||||||
onToggleFavorite(bookmark)
|
onToggleFavorite(bookmark)
|
||||||
} label: {
|
} label: {
|
||||||
Label(bookmark.isMarked ? "Entfernen" : "Favorit",
|
Label(bookmark.isMarked ? "Remove" : "Favorite",
|
||||||
systemImage: bookmark.isMarked ? "heart.slash" : "heart.fill")
|
systemImage: bookmark.isMarked ? "heart.slash" : "heart.fill")
|
||||||
}
|
}
|
||||||
.tint(bookmark.isMarked ? .gray : .pink)
|
.tint(bookmark.isMarked ? .gray : .pink)
|
||||||
@ -127,7 +127,7 @@ struct BookmarkCardView: View {
|
|||||||
formatter.locale = Locale(identifier: "en_US_POSIX")
|
formatter.locale = Locale(identifier: "en_US_POSIX")
|
||||||
|
|
||||||
guard let date = formatter.date(from: published) else {
|
guard let date = formatter.date(from: published) else {
|
||||||
// Fallback ohne Millisekunden
|
// Fallback without milliseconds
|
||||||
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'"
|
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'"
|
||||||
guard let fallbackDate = formatter.date(from: published) else {
|
guard let fallbackDate = formatter.date(from: published) else {
|
||||||
return nil
|
return nil
|
||||||
@ -142,42 +142,42 @@ struct BookmarkCardView: View {
|
|||||||
let now = Date()
|
let now = Date()
|
||||||
let calendar = Calendar.current
|
let calendar = Calendar.current
|
||||||
|
|
||||||
// Heute
|
// Today
|
||||||
if calendar.isDateInToday(date) {
|
if calendar.isDateInToday(date) {
|
||||||
let formatter = DateFormatter()
|
let formatter = DateFormatter()
|
||||||
formatter.timeStyle = .short
|
formatter.timeStyle = .short
|
||||||
return "Heute, \(formatter.string(from: date))"
|
return "Today, \(formatter.string(from: date))"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gestern
|
// Yesterday
|
||||||
if calendar.isDateInYesterday(date) {
|
if calendar.isDateInYesterday(date) {
|
||||||
let formatter = DateFormatter()
|
let formatter = DateFormatter()
|
||||||
formatter.timeStyle = .short
|
formatter.timeStyle = .short
|
||||||
return "Gestern, \(formatter.string(from: date))"
|
return "Yesterday, \(formatter.string(from: date))"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Diese Woche
|
// This week
|
||||||
if calendar.isDate(date, equalTo: now, toGranularity: .weekOfYear) {
|
if calendar.isDate(date, equalTo: now, toGranularity: .weekOfYear) {
|
||||||
let formatter = DateFormatter()
|
let formatter = DateFormatter()
|
||||||
formatter.dateFormat = "EEEE, HH:mm"
|
formatter.dateFormat = "EEEE, HH:mm"
|
||||||
return formatter.string(from: date)
|
return formatter.string(from: date)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dieses Jahr
|
// This year
|
||||||
if calendar.isDate(date, equalTo: now, toGranularity: .year) {
|
if calendar.isDate(date, equalTo: now, toGranularity: .year) {
|
||||||
let formatter = DateFormatter()
|
let formatter = DateFormatter()
|
||||||
formatter.dateFormat = "d. MMM, HH:mm"
|
formatter.dateFormat = "d. MMM, HH:mm"
|
||||||
return formatter.string(from: date)
|
return formatter.string(from: date)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Andere Jahre
|
// Other years
|
||||||
let formatter = DateFormatter()
|
let formatter = DateFormatter()
|
||||||
formatter.dateFormat = "d. MMM yyyy"
|
formatter.dateFormat = "d. MMM yyyy"
|
||||||
return formatter.string(from: date)
|
return formatter.string(from: date)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var imageURL: URL? {
|
private var imageURL: URL? {
|
||||||
// Bevorzuge image, dann thumbnail, dann icon
|
// Prioritize image, then thumbnail, then icon
|
||||||
if let imageUrl = bookmark.resources.image?.src {
|
if let imageUrl = bookmark.resources.image?.src {
|
||||||
return URL(string: imageUrl)
|
return URL(string: imageUrl)
|
||||||
} else if let thumbnailUrl = bookmark.resources.thumbnail?.src {
|
} else if let thumbnailUrl = bookmark.resources.thumbnail?.src {
|
||||||
|
|||||||
@ -19,7 +19,14 @@ struct BookmarksView: View {
|
|||||||
@EnvironmentObject var playerUIState: PlayerUIState
|
@EnvironmentObject var playerUIState: PlayerUIState
|
||||||
let tag: String?
|
let tag: String?
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: Environments
|
||||||
|
|
||||||
|
@Environment(\.horizontalSizeClass) var horizontalSizeClass
|
||||||
|
@Environment(\.verticalSizeClass) var verticalSizeClass
|
||||||
|
|
||||||
// MARK: Initializer
|
// MARK: Initializer
|
||||||
|
|
||||||
init(viewModel: BookmarksViewModel = .init(), state: BookmarkState, type: [BookmarkType], selectedBookmark: Binding<Bookmark?>, tag: String? = nil) {
|
init(viewModel: BookmarksViewModel = .init(), state: BookmarkState, type: [BookmarkType], selectedBookmark: Binding<Bookmark?>, tag: String? = nil) {
|
||||||
self.state = state
|
self.state = state
|
||||||
self.type = type
|
self.type = type
|
||||||
@ -27,16 +34,11 @@ struct BookmarksView: View {
|
|||||||
self.tag = tag
|
self.tag = tag
|
||||||
self.viewModel = viewModel
|
self.viewModel = viewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Environments
|
|
||||||
|
|
||||||
@Environment(\.horizontalSizeClass) var horizontalSizeClass
|
|
||||||
@Environment(\.verticalSizeClass) var verticalSizeClass
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
if viewModel.isLoading && viewModel.bookmarks?.bookmarks.isEmpty == true {
|
if viewModel.isLoading && viewModel.bookmarks?.bookmarks.isEmpty == true {
|
||||||
ProgressView("Lade \(state.displayName)...")
|
ProgressView("Loading \(state.displayName)...")
|
||||||
} else {
|
} else {
|
||||||
List {
|
List {
|
||||||
ForEach(viewModel.bookmarks?.bookmarks ?? [], id: \.id) { bookmark in
|
ForEach(viewModel.bookmarks?.bookmarks ?? [], id: \.id) { bookmark in
|
||||||
@ -96,17 +98,17 @@ struct BookmarksView: View {
|
|||||||
.overlay {
|
.overlay {
|
||||||
if viewModel.bookmarks?.bookmarks.isEmpty == true && !viewModel.isLoading {
|
if viewModel.bookmarks?.bookmarks.isEmpty == true && !viewModel.isLoading {
|
||||||
ContentUnavailableView(
|
ContentUnavailableView(
|
||||||
"Keine Bookmarks",
|
"No bookmarks",
|
||||||
systemImage: "bookmark",
|
systemImage: "bookmark",
|
||||||
description: Text(
|
description: Text(
|
||||||
"Es wurden noch keine Bookmarks in \(state.displayName.lowercased()) gefunden."
|
"No bookmarks found in \(state.displayName.lowercased())."
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FAB Button - nur bei "Ungelesen" anzeigen
|
// FAB Button - only show for "Unread"
|
||||||
if state == .unread || state == .all {
|
if state == .unread || state == .all {
|
||||||
VStack {
|
VStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|||||||
@ -61,8 +61,6 @@ class BookmarksViewModel {
|
|||||||
self.shareTitle = userInfo["title"] as? String ?? ""
|
self.shareTitle = userInfo["title"] as? String ?? ""
|
||||||
self.showingAddBookmarkFromShare = true
|
self.showingAddBookmarkFromShare = true
|
||||||
}
|
}
|
||||||
|
|
||||||
print("Received share notification - URL: \(url)")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func throttleSearch() {
|
private func throttleSearch() {
|
||||||
@ -87,8 +85,8 @@ class BookmarksViewModel {
|
|||||||
currentType = type
|
currentType = type
|
||||||
currentTag = tag
|
currentTag = tag
|
||||||
|
|
||||||
offset = 0 // Offset zurücksetzen
|
offset = 0
|
||||||
hasMoreData = true // Pagination zurücksetzen
|
hasMoreData = true
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let newBookmarks = try await getBooksmarksUseCase.execute(
|
let newBookmarks = try await getBooksmarksUseCase.execute(
|
||||||
@ -100,9 +98,9 @@ class BookmarksViewModel {
|
|||||||
tag: tag
|
tag: tag
|
||||||
)
|
)
|
||||||
bookmarks = newBookmarks
|
bookmarks = newBookmarks
|
||||||
hasMoreData = newBookmarks.currentPage != newBookmarks.totalPages // Prüfen, ob weitere Daten verfügbar sind
|
hasMoreData = newBookmarks.currentPage != newBookmarks.totalPages // check if more data is available
|
||||||
} catch {
|
} catch {
|
||||||
errorMessage = "Fehler beim Laden der Bookmarks"
|
errorMessage = "Error loading bookmarks"
|
||||||
bookmarks = nil
|
bookmarks = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,13 +109,13 @@ class BookmarksViewModel {
|
|||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func loadMoreBookmarks() async {
|
func loadMoreBookmarks() async {
|
||||||
guard !isLoading && hasMoreData else { return } // Verhindern, dass mehrfach geladen wird
|
guard !isLoading && hasMoreData else { return } // prevent multiple loads
|
||||||
|
|
||||||
isLoading = true
|
isLoading = true
|
||||||
errorMessage = nil
|
errorMessage = nil
|
||||||
|
|
||||||
do {
|
do {
|
||||||
offset += limit // Offset erhöhen
|
offset += limit // inc. offset
|
||||||
let newBookmarks = try await getBooksmarksUseCase.execute(
|
let newBookmarks = try await getBooksmarksUseCase.execute(
|
||||||
state: currentState,
|
state: currentState,
|
||||||
limit: limit,
|
limit: limit,
|
||||||
@ -128,7 +126,7 @@ class BookmarksViewModel {
|
|||||||
bookmarks?.bookmarks.append(contentsOf: newBookmarks.bookmarks)
|
bookmarks?.bookmarks.append(contentsOf: newBookmarks.bookmarks)
|
||||||
hasMoreData = newBookmarks.currentPage != newBookmarks.totalPages
|
hasMoreData = newBookmarks.currentPage != newBookmarks.totalPages
|
||||||
} catch {
|
} catch {
|
||||||
errorMessage = "Fehler beim Nachladen der Bookmarks"
|
errorMessage = "Error loading more bookmarks"
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoading = false
|
isLoading = false
|
||||||
@ -147,11 +145,10 @@ class BookmarksViewModel {
|
|||||||
isArchived: !bookmark.isArchived
|
isArchived: !bookmark.isArchived
|
||||||
)
|
)
|
||||||
|
|
||||||
// Liste aktualisieren
|
|
||||||
await loadBookmarks(state: currentState)
|
await loadBookmarks(state: currentState)
|
||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
errorMessage = "Fehler beim Archivieren des Bookmarks"
|
errorMessage = "Error archiving bookmark"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,26 +160,21 @@ class BookmarksViewModel {
|
|||||||
isMarked: !bookmark.isMarked
|
isMarked: !bookmark.isMarked
|
||||||
)
|
)
|
||||||
|
|
||||||
// Liste aktualisieren
|
|
||||||
await loadBookmarks(state: currentState)
|
await loadBookmarks(state: currentState)
|
||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
errorMessage = "Fehler beim Markieren des Bookmarks"
|
errorMessage = "Error marking bookmark"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func deleteBookmark(bookmark: Bookmark) async {
|
func deleteBookmark(bookmark: Bookmark) async {
|
||||||
do {
|
do {
|
||||||
// Echtes Löschen über API statt nur als gelöscht markieren
|
|
||||||
try await deleteBookmarkUseCase.execute(bookmarkId: bookmark.id)
|
try await deleteBookmarkUseCase.execute(bookmarkId: bookmark.id)
|
||||||
|
|
||||||
// Lokal aus der Liste entfernen (optimistische Update)
|
|
||||||
bookmarks?.bookmarks.removeAll { $0.id == bookmark.id }
|
bookmarks?.bookmarks.removeAll { $0.id == bookmark.id }
|
||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
errorMessage = "Fehler beim Löschen des Bookmarks"
|
errorMessage = "Error deleting bookmark"
|
||||||
// Bei Fehler die Liste neu laden, um konsistenten Zustand zu haben
|
|
||||||
await loadBookmarks(state: currentState)
|
await loadBookmarks(state: currentState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,7 @@ struct LabelsView: View {
|
|||||||
if viewModel.isLoading {
|
if viewModel.isLoading {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
} else if let errorMessage = viewModel.errorMessage {
|
} else if let errorMessage = viewModel.errorMessage {
|
||||||
Text("Fehler: \(errorMessage)")
|
Text("Error: \(errorMessage)")
|
||||||
.foregroundColor(.red)
|
.foregroundColor(.red)
|
||||||
} else {
|
} else {
|
||||||
List {
|
List {
|
||||||
|
|||||||
@ -16,7 +16,7 @@ class LabelsViewModel {
|
|||||||
do {
|
do {
|
||||||
labels = try await getLabelsUseCase.execute()
|
labels = try await getLabelsUseCase.execute()
|
||||||
} catch {
|
} catch {
|
||||||
errorMessage = "Fehler beim Laden der Labels"
|
errorMessage = "Error loading labels"
|
||||||
}
|
}
|
||||||
isLoading = false
|
isLoading = false
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,13 +14,13 @@ enum BookmarkState: String, CaseIterable {
|
|||||||
var displayName: String {
|
var displayName: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .all:
|
case .all:
|
||||||
return "Alle"
|
return "All"
|
||||||
case .unread:
|
case .unread:
|
||||||
return "Ungelesen"
|
return "Unread"
|
||||||
case .favorite:
|
case .favorite:
|
||||||
return "Favoriten"
|
return "Favorites"
|
||||||
case .archived:
|
case .archived:
|
||||||
return "Archiv"
|
return "Archive"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -44,7 +44,7 @@ struct PhoneTabView: View {
|
|||||||
}
|
}
|
||||||
.listRowBackground(Color(R.color.bookmark_list_bg))
|
.listRowBackground(Color(R.color.bookmark_list_bg))
|
||||||
}
|
}
|
||||||
.navigationTitle("Mehr")
|
.navigationTitle("More")
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.background(Color(R.color.bookmark_list_bg))
|
.background(Color(R.color.bookmark_list_bg))
|
||||||
|
|
||||||
@ -52,7 +52,7 @@ struct PhoneTabView: View {
|
|||||||
.padding(.bottom, 16)
|
.padding(.bottom, 16)
|
||||||
}
|
}
|
||||||
.tabItem {
|
.tabItem {
|
||||||
Label("Mehr", systemImage: "ellipsis")
|
Label("More", systemImage: "ellipsis")
|
||||||
}
|
}
|
||||||
.tag(mainTabs.count)
|
.tag(mainTabs.count)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
|
|||||||
@ -13,10 +13,10 @@ struct PlayerQueueResumeButton: View {
|
|||||||
}) {
|
}) {
|
||||||
HStack(spacing: 12) {
|
HStack(spacing: 12) {
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
Text("Vorlese-Queue")
|
Text("Read-aloud Queue")
|
||||||
.font(.caption2)
|
.font(.caption2)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
Text("\(queue.queueItems.count) Artikel in der Queue")
|
Text("\(queue.queueItems.count) articles in the queue")
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
}
|
}
|
||||||
@ -25,7 +25,7 @@ struct PlayerQueueResumeButton: View {
|
|||||||
playerViewModel.resume()
|
playerViewModel.resume()
|
||||||
playerUIState.showPlayer()
|
playerUIState.showPlayer()
|
||||||
}) {
|
}) {
|
||||||
Text("Weiterhören")
|
Text("Resume listening")
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.fontWeight(.semibold)
|
.fontWeight(.semibold)
|
||||||
.padding(.horizontal, 14)
|
.padding(.horizontal, 14)
|
||||||
@ -48,4 +48,4 @@ struct PlayerQueueResumeButton: View {
|
|||||||
.animation(.spring(), value: queue.hasItems)
|
.animation(.spring(), value: queue.hasItems)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,14 +13,14 @@ enum SidebarTab: Hashable, CaseIterable, Identifiable {
|
|||||||
var label: String {
|
var label: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .all: return "All"
|
case .all: return "All"
|
||||||
case .unread: return "Ungelesen"
|
case .unread: return "Unread"
|
||||||
case .favorite: return "Favoriten"
|
case .favorite: return "Favorites"
|
||||||
case .archived: return "Archiv"
|
case .archived: return "Archive"
|
||||||
case .search: return "Suche"
|
case .search: return "Search"
|
||||||
case .settings: return "Einstellungen"
|
case .settings: return "Settings"
|
||||||
case .article: return "Artikel"
|
case .article: return "Articles"
|
||||||
case .videos: return "Videos"
|
case .videos: return "Videos"
|
||||||
case .pictures: return "Bilder"
|
case .pictures: return "Pictures"
|
||||||
case .tags: return "Tags"
|
case .tags: return "Tags"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,16 +22,16 @@ struct FontSettingsView: View {
|
|||||||
.font(.title2)
|
.font(.title2)
|
||||||
.foregroundColor(.accentColor)
|
.foregroundColor(.accentColor)
|
||||||
|
|
||||||
Text("Schrift")
|
Text("Font")
|
||||||
.font(.title2)
|
.font(.title2)
|
||||||
.fontWeight(.bold)
|
.fontWeight(.bold)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Font Family Picker
|
// Font Family Picker
|
||||||
HStack(alignment: .firstTextBaseline, spacing: 16) {
|
HStack(alignment: .firstTextBaseline, spacing: 16) {
|
||||||
Text("Schriftart")
|
Text("Font family")
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
Picker("Schriftart", selection: $viewModel.selectedFontFamily) {
|
Picker("Font family", selection: $viewModel.selectedFontFamily) {
|
||||||
ForEach(FontFamily.allCases, id: \.self) { family in
|
ForEach(FontFamily.allCases, id: \.self) { family in
|
||||||
Text(family.displayName).tag(family)
|
Text(family.displayName).tag(family)
|
||||||
}
|
}
|
||||||
@ -48,9 +48,9 @@ struct FontSettingsView: View {
|
|||||||
|
|
||||||
// Font Size Picker
|
// Font Size Picker
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
Text("Schriftgröße")
|
Text("Font size")
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
Picker("Schriftgröße", selection: $viewModel.selectedFontSize) {
|
Picker("Font size", selection: $viewModel.selectedFontSize) {
|
||||||
ForEach(FontSize.allCases, id: \.self) { size in
|
ForEach(FontSize.allCases, id: \.self) { size in
|
||||||
Text(size.displayName).tag(size)
|
Text(size.displayName).tag(size)
|
||||||
}
|
}
|
||||||
@ -65,7 +65,7 @@ struct FontSettingsView: View {
|
|||||||
|
|
||||||
// Font Preview
|
// Font Preview
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
Text("Vorschau")
|
Text("Preview")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
|
|||||||
@ -76,7 +76,7 @@ class FontSettingsViewModel {
|
|||||||
selectedFontSize = settings.fontSize ?? .medium
|
selectedFontSize = settings.fontSize ?? .medium
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
errorMessage = "Fehler beim Laden der Schrift-Einstellungen"
|
errorMessage = "Error loading font settings"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,9 +87,9 @@ class FontSettingsViewModel {
|
|||||||
selectedFontFamily: selectedFontFamily,
|
selectedFontFamily: selectedFontFamily,
|
||||||
selectedFontSize: selectedFontSize
|
selectedFontSize: selectedFontSize
|
||||||
)
|
)
|
||||||
successMessage = "Schrift-Einstellungen gespeichert"
|
successMessage = "Font settings saved"
|
||||||
} catch {
|
} catch {
|
||||||
errorMessage = "Fehler beim Speichern der Schrift-Einstellungen"
|
errorMessage = "Error saving font settings"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -23,7 +23,7 @@ struct SettingsContainerView: View {
|
|||||||
.padding()
|
.padding()
|
||||||
.background(Color(.systemGroupedBackground))
|
.background(Color(.systemGroupedBackground))
|
||||||
}
|
}
|
||||||
.navigationTitle("Einstellungen")
|
.navigationTitle("Settings")
|
||||||
.navigationBarTitleDisplayMode(.large)
|
.navigationBarTitleDisplayMode(.large)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,7 +6,6 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
// SectionHeader wird jetzt zentral importiert
|
|
||||||
|
|
||||||
struct SettingsGeneralView: View {
|
struct SettingsGeneralView: View {
|
||||||
@State private var viewModel: SettingsGeneralViewModel
|
@State private var viewModel: SettingsGeneralViewModel
|
||||||
@ -17,7 +16,7 @@ struct SettingsGeneralView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 20) {
|
VStack(spacing: 20) {
|
||||||
SectionHeader(title: "Allgemeine Einstellungen", icon: "gear")
|
SectionHeader(title: "General Settings", icon: "gear")
|
||||||
.padding(.bottom, 4)
|
.padding(.bottom, 4)
|
||||||
|
|
||||||
// Theme
|
// Theme
|
||||||
@ -34,34 +33,34 @@ struct SettingsGeneralView: View {
|
|||||||
|
|
||||||
// Sync Settings
|
// Sync Settings
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
Text("Sync-Einstellungen")
|
Text("Sync Settings")
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
Toggle("Automatischer Sync", isOn: $viewModel.autoSyncEnabled)
|
Toggle("Automatic sync", isOn: $viewModel.autoSyncEnabled)
|
||||||
.toggleStyle(SwitchToggleStyle())
|
.toggleStyle(SwitchToggleStyle())
|
||||||
if viewModel.autoSyncEnabled {
|
if viewModel.autoSyncEnabled {
|
||||||
HStack {
|
HStack {
|
||||||
Text("Sync-Intervall")
|
Text("Sync interval")
|
||||||
Spacer()
|
Spacer()
|
||||||
Stepper("\(viewModel.syncInterval) Minuten", value: $viewModel.syncInterval, in: 1...60)
|
Stepper("\(viewModel.syncInterval) minutes", value: $viewModel.syncInterval, in: 1...60)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reading Settings
|
// Reading Settings
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
Text("Leseeinstellungen")
|
Text("Reading Settings")
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
Toggle("Safari Reader Modus", isOn: $viewModel.enableReaderMode)
|
Toggle("Safari Reader Mode", isOn: $viewModel.enableReaderMode)
|
||||||
.toggleStyle(SwitchToggleStyle())
|
.toggleStyle(SwitchToggleStyle())
|
||||||
Toggle("Externe Links in In-App Safari öffnen", isOn: $viewModel.openExternalLinksInApp)
|
Toggle("Open external links in in-app Safari", isOn: $viewModel.openExternalLinksInApp)
|
||||||
.toggleStyle(SwitchToggleStyle())
|
.toggleStyle(SwitchToggleStyle())
|
||||||
Toggle("Artikel automatisch als gelesen markieren", isOn: $viewModel.autoMarkAsRead)
|
Toggle("Automatically mark articles as read", isOn: $viewModel.autoMarkAsRead)
|
||||||
.toggleStyle(SwitchToggleStyle())
|
.toggleStyle(SwitchToggleStyle())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Data Management
|
// Data Management
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
Text("Datenmanagement")
|
Text("Data Management")
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
Button(role: .destructive) {
|
Button(role: .destructive) {
|
||||||
Task {
|
Task {
|
||||||
@ -71,7 +70,7 @@ struct SettingsGeneralView: View {
|
|||||||
HStack {
|
HStack {
|
||||||
Image(systemName: "trash")
|
Image(systemName: "trash")
|
||||||
.foregroundColor(.red)
|
.foregroundColor(.red)
|
||||||
Text("Cache leeren")
|
Text("Clear cache")
|
||||||
.foregroundColor(.red)
|
.foregroundColor(.red)
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
@ -84,7 +83,7 @@ struct SettingsGeneralView: View {
|
|||||||
HStack {
|
HStack {
|
||||||
Image(systemName: "arrow.clockwise")
|
Image(systemName: "arrow.clockwise")
|
||||||
.foregroundColor(.red)
|
.foregroundColor(.red)
|
||||||
Text("Einstellungen zurücksetzen")
|
Text("Reset settings")
|
||||||
.foregroundColor(.red)
|
.foregroundColor(.red)
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
@ -93,7 +92,7 @@ struct SettingsGeneralView: View {
|
|||||||
|
|
||||||
// App Info
|
// App Info
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
Text("Über die App")
|
Text("About the App")
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
HStack {
|
HStack {
|
||||||
Image(systemName: "info.circle")
|
Image(systemName: "info.circle")
|
||||||
@ -104,7 +103,7 @@ struct SettingsGeneralView: View {
|
|||||||
HStack {
|
HStack {
|
||||||
Image(systemName: "person.crop.circle")
|
Image(systemName: "person.crop.circle")
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
Text("Entwickler: \(viewModel.developerName)")
|
Text("Developer: \(viewModel.developerName)")
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
HStack {
|
HStack {
|
||||||
@ -122,7 +121,7 @@ struct SettingsGeneralView: View {
|
|||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
HStack {
|
HStack {
|
||||||
Text("Einstellungen speichern")
|
Text("Save settings")
|
||||||
.fontWeight(.semibold)
|
.fontWeight(.semibold)
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
@ -165,8 +164,8 @@ enum Theme: String, CaseIterable {
|
|||||||
var displayName: String {
|
var displayName: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .system: return "System"
|
case .system: return "System"
|
||||||
case .light: return "Hell"
|
case .light: return "Light"
|
||||||
case .dark: return "Dunkel"
|
case .dark: return "Dark"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,7 +22,9 @@ class SettingsGeneralViewModel {
|
|||||||
// MARK: - Messages
|
// MARK: - Messages
|
||||||
var errorMessage: String?
|
var errorMessage: String?
|
||||||
var successMessage: String?
|
var successMessage: String?
|
||||||
// MARK: - Data Management (Platzhalter)
|
|
||||||
|
// MARK: - Data Management (Placeholder)
|
||||||
|
|
||||||
// func clearCache() async {}
|
// func clearCache() async {}
|
||||||
// func resetSettings() async {}
|
// func resetSettings() async {}
|
||||||
|
|
||||||
@ -45,7 +47,7 @@ class SettingsGeneralViewModel {
|
|||||||
developerName = "Ilyas Hallak"
|
developerName = "Ilyas Hallak"
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
errorMessage = "Fehler beim Laden der Einstellungen"
|
errorMessage = "Error loading settings"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,9 +65,9 @@ class SettingsGeneralViewModel {
|
|||||||
openExternalLinksInApp: openExternalLinksInApp,
|
openExternalLinksInApp: openExternalLinksInApp,
|
||||||
autoMarkAsRead: autoMarkAsRead
|
autoMarkAsRead: autoMarkAsRead
|
||||||
)*/
|
)*/
|
||||||
successMessage = "Einstellungen gespeichert"
|
successMessage = "Settings saved"
|
||||||
} catch {
|
} catch {
|
||||||
errorMessage = "Fehler beim Speichern der Einstellungen"
|
errorMessage = "Error saving settings"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -128,7 +128,7 @@ struct SettingsServerView: View {
|
|||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
.cornerRadius(10)
|
.cornerRadius(10)
|
||||||
}
|
}
|
||||||
.disabled(!viewModel.canLogin || viewModel.isLoading)
|
.disabled(!viewModel.canLogin || viewModel.isLoading)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Button(action: {
|
Button(action: {
|
||||||
|
|||||||
@ -48,14 +48,14 @@ class SettingsServerViewModel {
|
|||||||
isLoggedIn = settings.isLoggedIn
|
isLoggedIn = settings.isLoggedIn
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
errorMessage = "Fehler beim Laden der Einstellungen"
|
errorMessage = "Error loading settings"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func saveServerSettings() async {
|
func saveServerSettings() async {
|
||||||
guard canLogin else {
|
guard canLogin else {
|
||||||
errorMessage = "Bitte füllen Sie alle Felder aus."
|
errorMessage = "Please fill in all fields."
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
clearMessages()
|
clearMessages()
|
||||||
@ -65,11 +65,11 @@ class SettingsServerViewModel {
|
|||||||
let user = try await loginUseCase.execute(endpoint: endpoint, username: username.trimmingCharacters(in: .whitespacesAndNewlines), password: password)
|
let user = try await loginUseCase.execute(endpoint: endpoint, username: username.trimmingCharacters(in: .whitespacesAndNewlines), password: password)
|
||||||
try await saveServerSettingsUseCase.execute(endpoint: endpoint, username: username, password: password, token: user.token)
|
try await saveServerSettingsUseCase.execute(endpoint: endpoint, username: username, password: password, token: user.token)
|
||||||
isLoggedIn = true
|
isLoggedIn = true
|
||||||
successMessage = "Server-Einstellungen gespeichert und erfolgreich angemeldet."
|
successMessage = "Server settings saved and successfully logged in."
|
||||||
try await SettingsRepository().saveHasFinishedSetup(true)
|
try await SettingsRepository().saveHasFinishedSetup(true)
|
||||||
NotificationCenter.default.post(name: NSNotification.Name("SetupStatusChanged"), object: nil)
|
NotificationCenter.default.post(name: NSNotification.Name("SetupStatusChanged"), object: nil)
|
||||||
} catch {
|
} catch {
|
||||||
errorMessage = "Verbindung oder Anmeldung fehlgeschlagen: \(error.localizedDescription)"
|
errorMessage = "Connection or login failed: \(error.localizedDescription)"
|
||||||
isLoggedIn = false
|
isLoggedIn = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -79,10 +79,10 @@ class SettingsServerViewModel {
|
|||||||
do {
|
do {
|
||||||
try await logoutUseCase.execute()
|
try await logoutUseCase.execute()
|
||||||
isLoggedIn = false
|
isLoggedIn = false
|
||||||
successMessage = "Abgemeldet"
|
successMessage = "Logged out"
|
||||||
NotificationCenter.default.post(name: NSNotification.Name("SetupStatusChanged"), object: nil)
|
NotificationCenter.default.post(name: NSNotification.Name("SetupStatusChanged"), object: nil)
|
||||||
} catch {
|
} catch {
|
||||||
errorMessage = "Fehler beim Abmelden"
|
errorMessage = "Error logging out"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -59,7 +59,7 @@ private struct CollapsedPlayerBar: View {
|
|||||||
.foregroundColor(.accentColor)
|
.foregroundColor(.accentColor)
|
||||||
}
|
}
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
Text(viewModel.currentText.isEmpty ? "Keine Wiedergabe" : viewModel.currentText)
|
Text(viewModel.currentText.isEmpty ? "No playback" : viewModel.currentText)
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.fontWeight(.medium)
|
.fontWeight(.medium)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
@ -114,7 +114,7 @@ private struct ExpandedPlayerView: View {
|
|||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
Text("Vorlese-Queue")
|
Text("Read-aloud Queue")
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
.fontWeight(.semibold)
|
.fontWeight(.semibold)
|
||||||
Spacer()
|
Spacer()
|
||||||
@ -126,13 +126,13 @@ private struct ExpandedPlayerView: View {
|
|||||||
}
|
}
|
||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
.padding(.top, 16)
|
.padding(.top, 16)
|
||||||
// Fortschrittsbalken für aktuellen Artikel
|
// progress bar for current article
|
||||||
if viewModel.articleProgress > 0 && viewModel.articleProgress < 1 {
|
if viewModel.articleProgress > 0 && viewModel.articleProgress < 1 {
|
||||||
VStack(spacing: 4) {
|
VStack(spacing: 4) {
|
||||||
ProgressView(value: viewModel.articleProgress)
|
ProgressView(value: viewModel.articleProgress)
|
||||||
.progressViewStyle(LinearProgressViewStyle(tint: .accentColor))
|
.progressViewStyle(LinearProgressViewStyle(tint: .accentColor))
|
||||||
HStack {
|
HStack {
|
||||||
Text("Fortschritt: \(Int(viewModel.articleProgress * 100))%")
|
Text("Progress: \(Int(viewModel.articleProgress * 100))%")
|
||||||
.font(.caption2)
|
.font(.caption2)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
Spacer()
|
Spacer()
|
||||||
@ -148,7 +148,7 @@ private struct ExpandedPlayerView: View {
|
|||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
Image(systemName: "text.line.first.and.arrowtriangle.forward")
|
Image(systemName: "text.line.first.and.arrowtriangle.forward")
|
||||||
.foregroundColor(.accentColor)
|
.foregroundColor(.accentColor)
|
||||||
Text("Lese \(viewModel.currentUtteranceIndex + 1)/\(viewModel.queueCount): ")
|
Text("Reading \(viewModel.currentUtteranceIndex + 1)/\(viewModel.queueCount): ")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
Text(viewModel.queueItems[safe: viewModel.currentUtteranceIndex]?.title ?? "")
|
Text(viewModel.queueItems[safe: viewModel.currentUtteranceIndex]?.title ?? "")
|
||||||
@ -195,7 +195,7 @@ private struct PlayerControls: View {
|
|||||||
}
|
}
|
||||||
HStack {
|
HStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
Picker("Geschwindigkeit", selection: Binding(
|
Picker("Speed", selection: Binding(
|
||||||
get: { viewModel.rate },
|
get: { viewModel.rate },
|
||||||
set: { viewModel.setRate($0) }
|
set: { viewModel.setRate($0) }
|
||||||
)) {
|
)) {
|
||||||
@ -240,7 +240,7 @@ private struct PlayerRate: View {
|
|||||||
HStack {
|
HStack {
|
||||||
Image(systemName: "speedometer")
|
Image(systemName: "speedometer")
|
||||||
.foregroundColor(.accentColor)
|
.foregroundColor(.accentColor)
|
||||||
Picker("Geschwindigkeit", selection: Binding(
|
Picker("Speed", selection: Binding(
|
||||||
get: { viewModel.rate },
|
get: { viewModel.rate },
|
||||||
set: { viewModel.setRate($0) }
|
set: { viewModel.setRate($0) }
|
||||||
)) {
|
)) {
|
||||||
@ -260,7 +260,7 @@ private struct PlayerQueueList: View {
|
|||||||
@ObservedObject var viewModel: SpeechPlayerViewModel
|
@ObservedObject var viewModel: SpeechPlayerViewModel
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if viewModel.queueCount == 0 {
|
if viewModel.queueCount == 0 {
|
||||||
Text("Keine Artikel in der Queue")
|
Text("No articles in the queue")
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
.padding()
|
.padding()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user