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 Artikel in der Queue" : {
|
||||
"%lld articles in the queue" : {
|
||||
|
||||
},
|
||||
"%lld min" : {
|
||||
|
||||
},
|
||||
"%lld Minuten" : {
|
||||
"%lld minutes" : {
|
||||
|
||||
},
|
||||
"%lld." : {
|
||||
@ -47,6 +47,12 @@
|
||||
},
|
||||
"Abmelden" : {
|
||||
|
||||
},
|
||||
"About the App" : {
|
||||
|
||||
},
|
||||
"Add a new link to your collection" : {
|
||||
|
||||
},
|
||||
"Aktuelle Labels" : {
|
||||
|
||||
@ -61,99 +67,87 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Als Favorit markieren" : {
|
||||
|
||||
},
|
||||
"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" : {
|
||||
|
||||
},
|
||||
"Entfernen" : {
|
||||
|
||||
},
|
||||
"Entwickler: %@" : {
|
||||
"e.g. work, important, later" : {
|
||||
|
||||
},
|
||||
"Erfolgreich angemeldet" : {
|
||||
|
||||
},
|
||||
"Erforderlich" : {
|
||||
|
||||
},
|
||||
"Erneut anmelden & speichern" : {
|
||||
|
||||
},
|
||||
"Es wurden noch keine Bookmarks in %@ gefunden." : {
|
||||
"Error" : {
|
||||
|
||||
},
|
||||
"Externe Links in In-App Safari öffnen" : {
|
||||
"Error: %@" : {
|
||||
|
||||
},
|
||||
"Favorit" : {
|
||||
"Favorite" : {
|
||||
|
||||
},
|
||||
"Fehler" : {
|
||||
|
||||
},
|
||||
"Fehler: %@" : {
|
||||
|
||||
},
|
||||
"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." : {
|
||||
|
||||
},
|
||||
"Geschwindigkeit" : {
|
||||
|
||||
},
|
||||
"https://example.com" : {
|
||||
|
||||
@ -163,12 +157,6 @@
|
||||
},
|
||||
"Ihre aktuelle Server-Verbindung und Anmeldedaten." : {
|
||||
|
||||
},
|
||||
"Keine Artikel in der Queue" : {
|
||||
|
||||
},
|
||||
"Keine Bookmarks" : {
|
||||
|
||||
},
|
||||
"Keine Bookmarks gefunden." : {
|
||||
|
||||
@ -191,68 +179,104 @@
|
||||
"Labels verwalten" : {
|
||||
|
||||
},
|
||||
"Lade %@..." : {
|
||||
"Loading %@..." : {
|
||||
|
||||
},
|
||||
"Lade Artikel..." : {
|
||||
"Loading article..." : {
|
||||
|
||||
},
|
||||
"Lese %lld/%lld: " : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "new",
|
||||
"value" : "Lese %1$lld/%2$lld: "
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Leseeinstellungen" : {
|
||||
|
||||
},
|
||||
"Löschen" : {
|
||||
|
||||
},
|
||||
"Mehr" : {
|
||||
"Mark as favorite" : {
|
||||
|
||||
},
|
||||
"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" : {
|
||||
|
||||
},
|
||||
"New Bookmark" : {
|
||||
|
||||
},
|
||||
"No articles in the queue" : {
|
||||
|
||||
},
|
||||
"No bookmarks" : {
|
||||
|
||||
},
|
||||
"No bookmarks found in %@." : {
|
||||
|
||||
},
|
||||
"OK" : {
|
||||
|
||||
},
|
||||
"Optional: Eigener Titel" : {
|
||||
"Open external links in in-app Safari" : {
|
||||
|
||||
},
|
||||
"Optional: Custom title" : {
|
||||
|
||||
},
|
||||
"Password" : {
|
||||
|
||||
},
|
||||
"Paste" : {
|
||||
|
||||
},
|
||||
"Preview" : {
|
||||
|
||||
},
|
||||
"Progress: %lld%%" : {
|
||||
|
||||
},
|
||||
"Read article aloud" : {
|
||||
|
||||
},
|
||||
"Read-aloud Queue" : {
|
||||
|
||||
},
|
||||
"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" : {
|
||||
@ -260,6 +284,12 @@
|
||||
},
|
||||
"Server-Endpunkt" : {
|
||||
|
||||
},
|
||||
"Settings" : {
|
||||
|
||||
},
|
||||
"Speed" : {
|
||||
|
||||
},
|
||||
"Speichern..." : {
|
||||
|
||||
@ -273,10 +303,10 @@
|
||||
"Suche..." : {
|
||||
|
||||
},
|
||||
"Sync-Einstellungen" : {
|
||||
"Sync interval" : {
|
||||
|
||||
},
|
||||
"Sync-Intervall" : {
|
||||
"Sync Settings" : {
|
||||
|
||||
},
|
||||
"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." : {
|
||||
|
||||
},
|
||||
"Titel" : {
|
||||
|
||||
},
|
||||
"Über die App" : {
|
||||
"Title" : {
|
||||
|
||||
},
|
||||
"URL" : {
|
||||
|
||||
},
|
||||
"URL gefunden:" : {
|
||||
"URL found:" : {
|
||||
|
||||
},
|
||||
"Username" : {
|
||||
@ -302,36 +329,15 @@
|
||||
},
|
||||
"Version %@" : {
|
||||
|
||||
},
|
||||
"Vorlese-Queue" : {
|
||||
|
||||
},
|
||||
"Vorschau" : {
|
||||
|
||||
},
|
||||
"Website" : {
|
||||
|
||||
},
|
||||
"Weiterhören" : {
|
||||
|
||||
},
|
||||
"Wiederherstellen" : {
|
||||
|
||||
},
|
||||
"Wird gespeichert..." : {
|
||||
|
||||
},
|
||||
"Your Password" : {
|
||||
|
||||
},
|
||||
"Your Username" : {
|
||||
|
||||
},
|
||||
"z.B. arbeit, wichtig, später" : {
|
||||
|
||||
},
|
||||
"Zwischenablage" : {
|
||||
|
||||
}
|
||||
},
|
||||
"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 func loadSettingsIfNeeded() async {
|
||||
guard !isLoaded else { return }
|
||||
guard isLoaded == false || cachedSettings == nil else { return }
|
||||
|
||||
do {
|
||||
cachedSettings = try await settingsRepository.loadSettings()
|
||||
|
||||
@ -26,11 +26,11 @@ struct AddBookmarkView: View {
|
||||
.font(.system(size: 48))
|
||||
.foregroundColor(.accentColor)
|
||||
|
||||
Text("Neues Bookmark")
|
||||
Text("New Bookmark")
|
||||
.font(.title2)
|
||||
.fontWeight(.semibold)
|
||||
|
||||
Text("Füge einen neuen Link zu deiner Sammlung hinzu")
|
||||
Text("Add a new link to your collection")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
@ -48,7 +48,7 @@ struct AddBookmarkView: View {
|
||||
|
||||
Spacer()
|
||||
|
||||
Text("Erforderlich")
|
||||
Text("Required")
|
||||
.font(.caption)
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
@ -62,11 +62,11 @@ struct AddBookmarkView: View {
|
||||
|
||||
// Title Field
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Label("Titel", systemImage: "note.text")
|
||||
Label("Title", systemImage: "note.text")
|
||||
.font(.headline)
|
||||
.foregroundColor(.primary)
|
||||
|
||||
TextField("Optional: Eigener Titel", text: $viewModel.title)
|
||||
TextField("Optional: Custom title", text: $viewModel.title)
|
||||
.textFieldStyle(CustomTextFieldStyle())
|
||||
}
|
||||
|
||||
@ -76,7 +76,7 @@ struct AddBookmarkView: View {
|
||||
.font(.headline)
|
||||
.foregroundColor(.primary)
|
||||
|
||||
TextField("z.B. arbeit, wichtig, später", text: $viewModel.labelsText)
|
||||
TextField("e.g. work, important, later", text: $viewModel.labelsText)
|
||||
.textFieldStyle(CustomTextFieldStyle())
|
||||
|
||||
// Labels Preview
|
||||
@ -102,13 +102,13 @@ struct AddBookmarkView: View {
|
||||
// Clipboard Section
|
||||
if viewModel.clipboardURL != nil {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Label("Zwischenablage", systemImage: "doc.on.clipboard")
|
||||
Label("Clipboard", systemImage: "doc.on.clipboard")
|
||||
.font(.headline)
|
||||
.foregroundColor(.primary)
|
||||
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("URL gefunden:")
|
||||
Text("URL found:")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
@ -120,7 +120,7 @@ struct AddBookmarkView: View {
|
||||
|
||||
Spacer()
|
||||
|
||||
Button("Einfügen") {
|
||||
Button("Paste") {
|
||||
viewModel.pasteFromClipboard()
|
||||
}
|
||||
.buttonStyle(SecondaryButtonStyle())
|
||||
@ -133,7 +133,7 @@ struct AddBookmarkView: View {
|
||||
}
|
||||
.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")
|
||||
}
|
||||
|
||||
Text(viewModel.isLoading ? "Wird gespeichert..." : "Bookmark speichern")
|
||||
Text(viewModel.isLoading ? "Saving..." : "Save bookmark")
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
@ -172,7 +172,7 @@ struct AddBookmarkView: View {
|
||||
.disabled(!viewModel.isValid || viewModel.isLoading)
|
||||
|
||||
// Cancel Button
|
||||
Button("Abbrechen") {
|
||||
Button("Cancel") {
|
||||
dismiss()
|
||||
viewModel.clearForm()
|
||||
}
|
||||
@ -186,17 +186,17 @@ struct AddBookmarkView: View {
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("Schließen") {
|
||||
Button("Close") {
|
||||
dismiss()
|
||||
viewModel.clearForm()
|
||||
}
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
.alert("Fehler", isPresented: $viewModel.showErrorAlert) {
|
||||
.alert("Error", isPresented: $viewModel.showErrorAlert) {
|
||||
Button("OK", role: .cancel) { }
|
||||
} message: {
|
||||
Text(viewModel.errorMessage ?? "Unbekannter Fehler")
|
||||
Text(viewModel.errorMessage ?? "Unknown error")
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
|
||||
@ -48,7 +48,7 @@ class AddBookmarkViewModel {
|
||||
|
||||
let message = try await createBookmarkUseCase.execute(createRequest: request)
|
||||
|
||||
// Optional: Zeige die Server-Nachricht an
|
||||
// Optional: Show the server message
|
||||
print("Server response: \(message)")
|
||||
|
||||
clearForm()
|
||||
@ -57,7 +57,7 @@ class AddBookmarkViewModel {
|
||||
errorMessage = error.localizedDescription
|
||||
showErrorAlert = true
|
||||
} catch {
|
||||
errorMessage = "Fehler beim Erstellen des Bookmarks"
|
||||
errorMessage = "Error creating bookmark"
|
||||
showErrorAlert = true
|
||||
}
|
||||
|
||||
|
||||
@ -68,11 +68,11 @@ struct BookmarkDetailView: View {
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.navigationTitle("Schrift-Einstellungen")
|
||||
.navigationTitle("Font Settings")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("Fertig") {
|
||||
Button("Done") {
|
||||
showingFontSettings = false
|
||||
}
|
||||
}
|
||||
@ -173,7 +173,7 @@ struct BookmarkDetailView: View {
|
||||
.padding(.horizontal)
|
||||
.animation(.easeInOut, value: webViewHeight)
|
||||
} else if viewModel.isLoadingArticle {
|
||||
ProgressView("Lade Artikel...")
|
||||
ProgressView("Loading article...")
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.padding()
|
||||
} else {
|
||||
@ -182,7 +182,7 @@ struct BookmarkDetailView: View {
|
||||
}) {
|
||||
HStack {
|
||||
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())
|
||||
.frame(maxWidth: .infinity)
|
||||
@ -196,10 +196,10 @@ struct BookmarkDetailView: View {
|
||||
private var metaInfoSection: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
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: "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
|
||||
if !viewModel.bookmarkDetail.labels.isEmpty {
|
||||
@ -236,7 +236,7 @@ struct BookmarkDetailView: View {
|
||||
Button(action: {
|
||||
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)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
@ -247,7 +247,7 @@ struct BookmarkDetailView: View {
|
||||
viewModel.addBookmarkToSpeechQueue()
|
||||
playerUIState.showPlayer()
|
||||
}) {
|
||||
Text("Artikel vorlesen")
|
||||
Text("Read article aloud")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
@ -296,7 +296,7 @@ struct BookmarkDetailView: View {
|
||||
|
||||
private var archiveSection: some View {
|
||||
VStack(alignment: .center, spacing: 12) {
|
||||
Text("Fertig mit Lesen?")
|
||||
Text("Finished reading?")
|
||||
.font(.headline)
|
||||
.padding(.top, 24)
|
||||
VStack(alignment: .center, spacing: 16) {
|
||||
@ -308,7 +308,7 @@ struct BookmarkDetailView: View {
|
||||
HStack {
|
||||
Image(systemName: viewModel.bookmarkDetail.isMarked ? "star.fill" : "star")
|
||||
.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())
|
||||
.frame(maxHeight: 60)
|
||||
@ -325,7 +325,7 @@ struct BookmarkDetailView: View {
|
||||
}) {
|
||||
HStack {
|
||||
Image(systemName: "archivebox")
|
||||
Text("Bookmark archivieren")
|
||||
Text("Archive bookmark")
|
||||
}
|
||||
.font(.title3.bold())
|
||||
.frame(maxHeight: 60)
|
||||
|
||||
@ -34,7 +34,7 @@ class BookmarkDetailViewModel {
|
||||
settings = try await loadSettingsUseCase.execute()
|
||||
bookmarkDetail = try await getBookmarkUseCase.execute(id: id)
|
||||
} catch {
|
||||
errorMessage = "Fehler beim Laden des Bookmarks"
|
||||
errorMessage = "Error loading bookmark"
|
||||
}
|
||||
|
||||
isLoading = false
|
||||
@ -48,7 +48,7 @@ class BookmarkDetailViewModel {
|
||||
articleContent = try await getBookmarkArticleUseCase.execute(id: id)
|
||||
processArticleContent()
|
||||
} catch {
|
||||
errorMessage = "Fehler beim Laden des Artikels"
|
||||
errorMessage = "Error loading article"
|
||||
}
|
||||
|
||||
isLoadingArticle = false
|
||||
@ -70,7 +70,7 @@ class BookmarkDetailViewModel {
|
||||
try await updateBookmarkUseCase.toggleArchive(bookmarkId: id, isArchived: true)
|
||||
bookmarkDetail.isArchived = true
|
||||
} catch {
|
||||
errorMessage = "Fehler beim Archivieren des Bookmarks"
|
||||
errorMessage = "Error archiving bookmark"
|
||||
}
|
||||
isLoading = false
|
||||
}
|
||||
@ -94,7 +94,7 @@ class BookmarkDetailViewModel {
|
||||
try await updateBookmarkUseCase.toggleFavorite(bookmarkId: id, isMarked: newValue)
|
||||
bookmarkDetail.isMarked = newValue
|
||||
} catch {
|
||||
errorMessage = "Fehler beim Aktualisieren des Favoriten-Status"
|
||||
errorMessage = "Error updating favorite status"
|
||||
}
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
@ -26,37 +26,37 @@ struct BookmarkLabelsView: View {
|
||||
}
|
||||
.padding()
|
||||
.background(Color(.systemGroupedBackground))
|
||||
.navigationTitle("Labels verwalten")
|
||||
.navigationTitle("Manage Labels")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
|
||||
Button("Abbrechen") {
|
||||
Button("Cancel") {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("Fertig") {
|
||||
Button("Done") {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
.alert("Fehler", isPresented: $viewModel.showErrorAlert) {
|
||||
.alert("Error", isPresented: $viewModel.showErrorAlert) {
|
||||
Button("OK") { }
|
||||
} message: {
|
||||
Text(viewModel.errorMessage ?? "Unbekannter Fehler")
|
||||
Text(viewModel.errorMessage ?? "Unknown error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var addLabelSection: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text("Neues Label hinzufügen")
|
||||
Text("Add new label")
|
||||
.font(.headline)
|
||||
.foregroundColor(.primary)
|
||||
|
||||
HStack(spacing: 12) {
|
||||
TextField("Label eingeben...", text: $viewModel.newLabelText)
|
||||
TextField("Enter label...", text: $viewModel.newLabelText)
|
||||
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||
.onSubmit {
|
||||
Task {
|
||||
@ -91,7 +91,7 @@ struct BookmarkLabelsView: View {
|
||||
private var currentLabelsSection: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack {
|
||||
Text("Aktuelle Labels")
|
||||
Text("Current labels")
|
||||
.font(.headline)
|
||||
.foregroundColor(.primary)
|
||||
|
||||
@ -108,7 +108,7 @@ struct BookmarkLabelsView: View {
|
||||
Image(systemName: "tag")
|
||||
.font(.title2)
|
||||
.foregroundColor(.secondary)
|
||||
Text("Keine Labels vorhanden")
|
||||
Text("No labels available")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
@ -29,7 +29,7 @@ class BookmarkLabelsViewModel {
|
||||
errorMessage = error.localizedDescription
|
||||
showErrorAlert = true
|
||||
} catch {
|
||||
errorMessage = "Fehler beim Hinzufügen der Labels"
|
||||
errorMessage = "Error adding labels"
|
||||
showErrorAlert = true
|
||||
}
|
||||
|
||||
@ -58,7 +58,7 @@ class BookmarkLabelsViewModel {
|
||||
errorMessage = error.localizedDescription
|
||||
showErrorAlert = true
|
||||
} catch {
|
||||
errorMessage = "Fehler beim Entfernen der Labels"
|
||||
errorMessage = "Error removing labels"
|
||||
showErrorAlert = true
|
||||
}
|
||||
|
||||
|
||||
@ -37,7 +37,7 @@ struct BookmarkCardView: View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack {
|
||||
|
||||
// Veröffentlichungsdatum
|
||||
// Published date
|
||||
if let publishedDate = formattedPublishedDate {
|
||||
HStack {
|
||||
Label(publishedDate, systemImage: "calendar")
|
||||
@ -59,7 +59,7 @@ struct BookmarkCardView: View {
|
||||
}
|
||||
HStack {
|
||||
|
||||
Label((URLUtil.extractDomain(from: bookmark.url) ?? "Original Seite") + " öffnen", systemImage: "safari")
|
||||
Label((URLUtil.extractDomain(from: bookmark.url) ?? "Original Site") + " open", systemImage: "safari")
|
||||
.onTapGesture {
|
||||
SafariUtil.openInSafari(url: bookmark.url)
|
||||
}
|
||||
@ -68,7 +68,7 @@ struct BookmarkCardView: View {
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
// Progress Bar für Lesefortschritt
|
||||
// Progress Bar for reading progress
|
||||
if bookmark.readProgress > 0 {
|
||||
ProgressView(value: Double(bookmark.readProgress), total: 100)
|
||||
.progressViewStyle(LinearProgressViewStyle())
|
||||
@ -82,20 +82,20 @@ struct BookmarkCardView: View {
|
||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||
.shadow(color: colorScheme == .light ? .black.opacity(0.1) : .white.opacity(0.1), radius: 2, x: 0, y: 1)
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
||||
Button("Löschen", role: .destructive) {
|
||||
Button("Delete", role: .destructive) {
|
||||
onDelete(bookmark)
|
||||
}
|
||||
.tint(.red)
|
||||
}
|
||||
.swipeActions(edge: .leading, allowsFullSwipe: true) {
|
||||
// Archivieren (links)
|
||||
// Archive (left)
|
||||
Button {
|
||||
onArchive(bookmark)
|
||||
} label: {
|
||||
if currentState == .archived {
|
||||
Label("Wiederherstellen", systemImage: "tray.and.arrow.up")
|
||||
Label("Restore", systemImage: "tray.and.arrow.up")
|
||||
} else {
|
||||
Label("Archivieren", systemImage: "archivebox")
|
||||
Label("Archive", systemImage: "archivebox")
|
||||
}
|
||||
}
|
||||
.tint(currentState == .archived ? .blue : .orange)
|
||||
@ -103,7 +103,7 @@ struct BookmarkCardView: View {
|
||||
Button {
|
||||
onToggleFavorite(bookmark)
|
||||
} label: {
|
||||
Label(bookmark.isMarked ? "Entfernen" : "Favorit",
|
||||
Label(bookmark.isMarked ? "Remove" : "Favorite",
|
||||
systemImage: bookmark.isMarked ? "heart.slash" : "heart.fill")
|
||||
}
|
||||
.tint(bookmark.isMarked ? .gray : .pink)
|
||||
@ -127,7 +127,7 @@ struct BookmarkCardView: View {
|
||||
formatter.locale = Locale(identifier: "en_US_POSIX")
|
||||
|
||||
guard let date = formatter.date(from: published) else {
|
||||
// Fallback ohne Millisekunden
|
||||
// Fallback without milliseconds
|
||||
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'"
|
||||
guard let fallbackDate = formatter.date(from: published) else {
|
||||
return nil
|
||||
@ -142,42 +142,42 @@ struct BookmarkCardView: View {
|
||||
let now = Date()
|
||||
let calendar = Calendar.current
|
||||
|
||||
// Heute
|
||||
// Today
|
||||
if calendar.isDateInToday(date) {
|
||||
let formatter = DateFormatter()
|
||||
formatter.timeStyle = .short
|
||||
return "Heute, \(formatter.string(from: date))"
|
||||
return "Today, \(formatter.string(from: date))"
|
||||
}
|
||||
|
||||
// Gestern
|
||||
// Yesterday
|
||||
if calendar.isDateInYesterday(date) {
|
||||
let formatter = DateFormatter()
|
||||
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) {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "EEEE, HH:mm"
|
||||
return formatter.string(from: date)
|
||||
}
|
||||
|
||||
// Dieses Jahr
|
||||
// This year
|
||||
if calendar.isDate(date, equalTo: now, toGranularity: .year) {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "d. MMM, HH:mm"
|
||||
return formatter.string(from: date)
|
||||
}
|
||||
|
||||
// Andere Jahre
|
||||
// Other years
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "d. MMM yyyy"
|
||||
return formatter.string(from: date)
|
||||
}
|
||||
|
||||
private var imageURL: URL? {
|
||||
// Bevorzuge image, dann thumbnail, dann icon
|
||||
// Prioritize image, then thumbnail, then icon
|
||||
if let imageUrl = bookmark.resources.image?.src {
|
||||
return URL(string: imageUrl)
|
||||
} else if let thumbnailUrl = bookmark.resources.thumbnail?.src {
|
||||
|
||||
@ -19,7 +19,14 @@ struct BookmarksView: View {
|
||||
@EnvironmentObject var playerUIState: PlayerUIState
|
||||
let tag: String?
|
||||
|
||||
|
||||
// MARK: Environments
|
||||
|
||||
@Environment(\.horizontalSizeClass) var horizontalSizeClass
|
||||
@Environment(\.verticalSizeClass) var verticalSizeClass
|
||||
|
||||
// MARK: Initializer
|
||||
|
||||
init(viewModel: BookmarksViewModel = .init(), state: BookmarkState, type: [BookmarkType], selectedBookmark: Binding<Bookmark?>, tag: String? = nil) {
|
||||
self.state = state
|
||||
self.type = type
|
||||
@ -27,16 +34,11 @@ struct BookmarksView: View {
|
||||
self.tag = tag
|
||||
self.viewModel = viewModel
|
||||
}
|
||||
|
||||
// MARK: Environments
|
||||
|
||||
@Environment(\.horizontalSizeClass) var horizontalSizeClass
|
||||
@Environment(\.verticalSizeClass) var verticalSizeClass
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
if viewModel.isLoading && viewModel.bookmarks?.bookmarks.isEmpty == true {
|
||||
ProgressView("Lade \(state.displayName)...")
|
||||
ProgressView("Loading \(state.displayName)...")
|
||||
} else {
|
||||
List {
|
||||
ForEach(viewModel.bookmarks?.bookmarks ?? [], id: \.id) { bookmark in
|
||||
@ -96,17 +98,17 @@ struct BookmarksView: View {
|
||||
.overlay {
|
||||
if viewModel.bookmarks?.bookmarks.isEmpty == true && !viewModel.isLoading {
|
||||
ContentUnavailableView(
|
||||
"Keine Bookmarks",
|
||||
"No bookmarks",
|
||||
systemImage: "bookmark",
|
||||
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 {
|
||||
VStack {
|
||||
Spacer()
|
||||
|
||||
@ -61,8 +61,6 @@ class BookmarksViewModel {
|
||||
self.shareTitle = userInfo["title"] as? String ?? ""
|
||||
self.showingAddBookmarkFromShare = true
|
||||
}
|
||||
|
||||
print("Received share notification - URL: \(url)")
|
||||
}
|
||||
|
||||
private func throttleSearch() {
|
||||
@ -87,8 +85,8 @@ class BookmarksViewModel {
|
||||
currentType = type
|
||||
currentTag = tag
|
||||
|
||||
offset = 0 // Offset zurücksetzen
|
||||
hasMoreData = true // Pagination zurücksetzen
|
||||
offset = 0
|
||||
hasMoreData = true
|
||||
|
||||
do {
|
||||
let newBookmarks = try await getBooksmarksUseCase.execute(
|
||||
@ -100,9 +98,9 @@ class BookmarksViewModel {
|
||||
tag: tag
|
||||
)
|
||||
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 {
|
||||
errorMessage = "Fehler beim Laden der Bookmarks"
|
||||
errorMessage = "Error loading bookmarks"
|
||||
bookmarks = nil
|
||||
}
|
||||
|
||||
@ -111,13 +109,13 @@ class BookmarksViewModel {
|
||||
|
||||
@MainActor
|
||||
func loadMoreBookmarks() async {
|
||||
guard !isLoading && hasMoreData else { return } // Verhindern, dass mehrfach geladen wird
|
||||
guard !isLoading && hasMoreData else { return } // prevent multiple loads
|
||||
|
||||
isLoading = true
|
||||
errorMessage = nil
|
||||
|
||||
do {
|
||||
offset += limit // Offset erhöhen
|
||||
offset += limit // inc. offset
|
||||
let newBookmarks = try await getBooksmarksUseCase.execute(
|
||||
state: currentState,
|
||||
limit: limit,
|
||||
@ -128,7 +126,7 @@ class BookmarksViewModel {
|
||||
bookmarks?.bookmarks.append(contentsOf: newBookmarks.bookmarks)
|
||||
hasMoreData = newBookmarks.currentPage != newBookmarks.totalPages
|
||||
} catch {
|
||||
errorMessage = "Fehler beim Nachladen der Bookmarks"
|
||||
errorMessage = "Error loading more bookmarks"
|
||||
}
|
||||
|
||||
isLoading = false
|
||||
@ -147,11 +145,10 @@ class BookmarksViewModel {
|
||||
isArchived: !bookmark.isArchived
|
||||
)
|
||||
|
||||
// Liste aktualisieren
|
||||
await loadBookmarks(state: currentState)
|
||||
|
||||
} catch {
|
||||
errorMessage = "Fehler beim Archivieren des Bookmarks"
|
||||
errorMessage = "Error archiving bookmark"
|
||||
}
|
||||
}
|
||||
|
||||
@ -163,26 +160,21 @@ class BookmarksViewModel {
|
||||
isMarked: !bookmark.isMarked
|
||||
)
|
||||
|
||||
// Liste aktualisieren
|
||||
await loadBookmarks(state: currentState)
|
||||
|
||||
} catch {
|
||||
errorMessage = "Fehler beim Markieren des Bookmarks"
|
||||
errorMessage = "Error marking bookmark"
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func deleteBookmark(bookmark: Bookmark) async {
|
||||
do {
|
||||
// Echtes Löschen über API statt nur als gelöscht markieren
|
||||
try await deleteBookmarkUseCase.execute(bookmarkId: bookmark.id)
|
||||
|
||||
// Lokal aus der Liste entfernen (optimistische Update)
|
||||
bookmarks?.bookmarks.removeAll { $0.id == bookmark.id }
|
||||
|
||||
} catch {
|
||||
errorMessage = "Fehler beim Löschen des Bookmarks"
|
||||
// Bei Fehler die Liste neu laden, um konsistenten Zustand zu haben
|
||||
errorMessage = "Error deleting bookmark"
|
||||
await loadBookmarks(state: currentState)
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@ struct LabelsView: View {
|
||||
if viewModel.isLoading {
|
||||
ProgressView()
|
||||
} else if let errorMessage = viewModel.errorMessage {
|
||||
Text("Fehler: \(errorMessage)")
|
||||
Text("Error: \(errorMessage)")
|
||||
.foregroundColor(.red)
|
||||
} else {
|
||||
List {
|
||||
|
||||
@ -16,7 +16,7 @@ class LabelsViewModel {
|
||||
do {
|
||||
labels = try await getLabelsUseCase.execute()
|
||||
} catch {
|
||||
errorMessage = "Fehler beim Laden der Labels"
|
||||
errorMessage = "Error loading labels"
|
||||
}
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
@ -14,13 +14,13 @@ enum BookmarkState: String, CaseIterable {
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .all:
|
||||
return "Alle"
|
||||
return "All"
|
||||
case .unread:
|
||||
return "Ungelesen"
|
||||
return "Unread"
|
||||
case .favorite:
|
||||
return "Favoriten"
|
||||
return "Favorites"
|
||||
case .archived:
|
||||
return "Archiv"
|
||||
return "Archive"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -44,7 +44,7 @@ struct PhoneTabView: View {
|
||||
}
|
||||
.listRowBackground(Color(R.color.bookmark_list_bg))
|
||||
}
|
||||
.navigationTitle("Mehr")
|
||||
.navigationTitle("More")
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(Color(R.color.bookmark_list_bg))
|
||||
|
||||
@ -52,7 +52,7 @@ struct PhoneTabView: View {
|
||||
.padding(.bottom, 16)
|
||||
}
|
||||
.tabItem {
|
||||
Label("Mehr", systemImage: "ellipsis")
|
||||
Label("More", systemImage: "ellipsis")
|
||||
}
|
||||
.tag(mainTabs.count)
|
||||
.onAppear {
|
||||
|
||||
@ -13,10 +13,10 @@ struct PlayerQueueResumeButton: View {
|
||||
}) {
|
||||
HStack(spacing: 12) {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Vorlese-Queue")
|
||||
Text("Read-aloud Queue")
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
Text("\(queue.queueItems.count) Artikel in der Queue")
|
||||
Text("\(queue.queueItems.count) articles in the queue")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.primary)
|
||||
}
|
||||
@ -25,7 +25,7 @@ struct PlayerQueueResumeButton: View {
|
||||
playerViewModel.resume()
|
||||
playerUIState.showPlayer()
|
||||
}) {
|
||||
Text("Weiterhören")
|
||||
Text("Resume listening")
|
||||
.font(.subheadline)
|
||||
.fontWeight(.semibold)
|
||||
.padding(.horizontal, 14)
|
||||
@ -48,4 +48,4 @@ struct PlayerQueueResumeButton: View {
|
||||
.animation(.spring(), value: queue.hasItems)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,14 +13,14 @@ enum SidebarTab: Hashable, CaseIterable, Identifiable {
|
||||
var label: String {
|
||||
switch self {
|
||||
case .all: return "All"
|
||||
case .unread: return "Ungelesen"
|
||||
case .favorite: return "Favoriten"
|
||||
case .archived: return "Archiv"
|
||||
case .search: return "Suche"
|
||||
case .settings: return "Einstellungen"
|
||||
case .article: return "Artikel"
|
||||
case .unread: return "Unread"
|
||||
case .favorite: return "Favorites"
|
||||
case .archived: return "Archive"
|
||||
case .search: return "Search"
|
||||
case .settings: return "Settings"
|
||||
case .article: return "Articles"
|
||||
case .videos: return "Videos"
|
||||
case .pictures: return "Bilder"
|
||||
case .pictures: return "Pictures"
|
||||
case .tags: return "Tags"
|
||||
}
|
||||
}
|
||||
|
||||
@ -22,16 +22,16 @@ struct FontSettingsView: View {
|
||||
.font(.title2)
|
||||
.foregroundColor(.accentColor)
|
||||
|
||||
Text("Schrift")
|
||||
Text("Font")
|
||||
.font(.title2)
|
||||
.fontWeight(.bold)
|
||||
}
|
||||
|
||||
// Font Family Picker
|
||||
HStack(alignment: .firstTextBaseline, spacing: 16) {
|
||||
Text("Schriftart")
|
||||
Text("Font family")
|
||||
.font(.headline)
|
||||
Picker("Schriftart", selection: $viewModel.selectedFontFamily) {
|
||||
Picker("Font family", selection: $viewModel.selectedFontFamily) {
|
||||
ForEach(FontFamily.allCases, id: \.self) { family in
|
||||
Text(family.displayName).tag(family)
|
||||
}
|
||||
@ -48,9 +48,9 @@ struct FontSettingsView: View {
|
||||
|
||||
// Font Size Picker
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Schriftgröße")
|
||||
Text("Font size")
|
||||
.font(.headline)
|
||||
Picker("Schriftgröße", selection: $viewModel.selectedFontSize) {
|
||||
Picker("Font size", selection: $viewModel.selectedFontSize) {
|
||||
ForEach(FontSize.allCases, id: \.self) { size in
|
||||
Text(size.displayName).tag(size)
|
||||
}
|
||||
@ -65,7 +65,7 @@ struct FontSettingsView: View {
|
||||
|
||||
// Font Preview
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Vorschau")
|
||||
Text("Preview")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
|
||||
@ -76,7 +76,7 @@ class FontSettingsViewModel {
|
||||
selectedFontSize = settings.fontSize ?? .medium
|
||||
}
|
||||
} catch {
|
||||
errorMessage = "Fehler beim Laden der Schrift-Einstellungen"
|
||||
errorMessage = "Error loading font settings"
|
||||
}
|
||||
}
|
||||
|
||||
@ -87,9 +87,9 @@ class FontSettingsViewModel {
|
||||
selectedFontFamily: selectedFontFamily,
|
||||
selectedFontSize: selectedFontSize
|
||||
)
|
||||
successMessage = "Schrift-Einstellungen gespeichert"
|
||||
successMessage = "Font settings saved"
|
||||
} catch {
|
||||
errorMessage = "Fehler beim Speichern der Schrift-Einstellungen"
|
||||
errorMessage = "Error saving font settings"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -23,7 +23,7 @@ struct SettingsContainerView: View {
|
||||
.padding()
|
||||
.background(Color(.systemGroupedBackground))
|
||||
}
|
||||
.navigationTitle("Einstellungen")
|
||||
.navigationTitle("Settings")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,7 +6,6 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
// SectionHeader wird jetzt zentral importiert
|
||||
|
||||
struct SettingsGeneralView: View {
|
||||
@State private var viewModel: SettingsGeneralViewModel
|
||||
@ -17,7 +16,7 @@ struct SettingsGeneralView: View {
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 20) {
|
||||
SectionHeader(title: "Allgemeine Einstellungen", icon: "gear")
|
||||
SectionHeader(title: "General Settings", icon: "gear")
|
||||
.padding(.bottom, 4)
|
||||
|
||||
// Theme
|
||||
@ -34,34 +33,34 @@ struct SettingsGeneralView: View {
|
||||
|
||||
// Sync Settings
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text("Sync-Einstellungen")
|
||||
Text("Sync Settings")
|
||||
.font(.headline)
|
||||
Toggle("Automatischer Sync", isOn: $viewModel.autoSyncEnabled)
|
||||
Toggle("Automatic sync", isOn: $viewModel.autoSyncEnabled)
|
||||
.toggleStyle(SwitchToggleStyle())
|
||||
if viewModel.autoSyncEnabled {
|
||||
HStack {
|
||||
Text("Sync-Intervall")
|
||||
Text("Sync interval")
|
||||
Spacer()
|
||||
Stepper("\(viewModel.syncInterval) Minuten", value: $viewModel.syncInterval, in: 1...60)
|
||||
Stepper("\(viewModel.syncInterval) minutes", value: $viewModel.syncInterval, in: 1...60)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reading Settings
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text("Leseeinstellungen")
|
||||
Text("Reading Settings")
|
||||
.font(.headline)
|
||||
Toggle("Safari Reader Modus", isOn: $viewModel.enableReaderMode)
|
||||
Toggle("Safari Reader Mode", isOn: $viewModel.enableReaderMode)
|
||||
.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())
|
||||
Toggle("Artikel automatisch als gelesen markieren", isOn: $viewModel.autoMarkAsRead)
|
||||
Toggle("Automatically mark articles as read", isOn: $viewModel.autoMarkAsRead)
|
||||
.toggleStyle(SwitchToggleStyle())
|
||||
}
|
||||
|
||||
// Data Management
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text("Datenmanagement")
|
||||
Text("Data Management")
|
||||
.font(.headline)
|
||||
Button(role: .destructive) {
|
||||
Task {
|
||||
@ -71,7 +70,7 @@ struct SettingsGeneralView: View {
|
||||
HStack {
|
||||
Image(systemName: "trash")
|
||||
.foregroundColor(.red)
|
||||
Text("Cache leeren")
|
||||
Text("Clear cache")
|
||||
.foregroundColor(.red)
|
||||
Spacer()
|
||||
}
|
||||
@ -84,7 +83,7 @@ struct SettingsGeneralView: View {
|
||||
HStack {
|
||||
Image(systemName: "arrow.clockwise")
|
||||
.foregroundColor(.red)
|
||||
Text("Einstellungen zurücksetzen")
|
||||
Text("Reset settings")
|
||||
.foregroundColor(.red)
|
||||
Spacer()
|
||||
}
|
||||
@ -93,7 +92,7 @@ struct SettingsGeneralView: View {
|
||||
|
||||
// App Info
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text("Über die App")
|
||||
Text("About the App")
|
||||
.font(.headline)
|
||||
HStack {
|
||||
Image(systemName: "info.circle")
|
||||
@ -104,7 +103,7 @@ struct SettingsGeneralView: View {
|
||||
HStack {
|
||||
Image(systemName: "person.crop.circle")
|
||||
.foregroundColor(.secondary)
|
||||
Text("Entwickler: \(viewModel.developerName)")
|
||||
Text("Developer: \(viewModel.developerName)")
|
||||
Spacer()
|
||||
}
|
||||
HStack {
|
||||
@ -122,7 +121,7 @@ struct SettingsGeneralView: View {
|
||||
}
|
||||
}) {
|
||||
HStack {
|
||||
Text("Einstellungen speichern")
|
||||
Text("Save settings")
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
@ -165,8 +164,8 @@ enum Theme: String, CaseIterable {
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .system: return "System"
|
||||
case .light: return "Hell"
|
||||
case .dark: return "Dunkel"
|
||||
case .light: return "Light"
|
||||
case .dark: return "Dark"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -22,7 +22,9 @@ class SettingsGeneralViewModel {
|
||||
// MARK: - Messages
|
||||
var errorMessage: String?
|
||||
var successMessage: String?
|
||||
// MARK: - Data Management (Platzhalter)
|
||||
|
||||
// MARK: - Data Management (Placeholder)
|
||||
|
||||
// func clearCache() async {}
|
||||
// func resetSettings() async {}
|
||||
|
||||
@ -45,7 +47,7 @@ class SettingsGeneralViewModel {
|
||||
developerName = "Ilyas Hallak"
|
||||
}
|
||||
} catch {
|
||||
errorMessage = "Fehler beim Laden der Einstellungen"
|
||||
errorMessage = "Error loading settings"
|
||||
}
|
||||
}
|
||||
|
||||
@ -63,9 +65,9 @@ class SettingsGeneralViewModel {
|
||||
openExternalLinksInApp: openExternalLinksInApp,
|
||||
autoMarkAsRead: autoMarkAsRead
|
||||
)*/
|
||||
successMessage = "Einstellungen gespeichert"
|
||||
successMessage = "Settings saved"
|
||||
} catch {
|
||||
errorMessage = "Fehler beim Speichern der Einstellungen"
|
||||
errorMessage = "Error saving settings"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -128,7 +128,7 @@ struct SettingsServerView: View {
|
||||
.foregroundColor(.white)
|
||||
.cornerRadius(10)
|
||||
}
|
||||
.disabled(!viewModel.canLogin || viewModel.isLoading)
|
||||
.disabled(!viewModel.canLogin || viewModel.isLoading)
|
||||
}
|
||||
} else {
|
||||
Button(action: {
|
||||
|
||||
@ -48,14 +48,14 @@ class SettingsServerViewModel {
|
||||
isLoggedIn = settings.isLoggedIn
|
||||
}
|
||||
} catch {
|
||||
errorMessage = "Fehler beim Laden der Einstellungen"
|
||||
errorMessage = "Error loading settings"
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func saveServerSettings() async {
|
||||
guard canLogin else {
|
||||
errorMessage = "Bitte füllen Sie alle Felder aus."
|
||||
errorMessage = "Please fill in all fields."
|
||||
return
|
||||
}
|
||||
clearMessages()
|
||||
@ -65,11 +65,11 @@ class SettingsServerViewModel {
|
||||
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)
|
||||
isLoggedIn = true
|
||||
successMessage = "Server-Einstellungen gespeichert und erfolgreich angemeldet."
|
||||
successMessage = "Server settings saved and successfully logged in."
|
||||
try await SettingsRepository().saveHasFinishedSetup(true)
|
||||
NotificationCenter.default.post(name: NSNotification.Name("SetupStatusChanged"), object: nil)
|
||||
} catch {
|
||||
errorMessage = "Verbindung oder Anmeldung fehlgeschlagen: \(error.localizedDescription)"
|
||||
errorMessage = "Connection or login failed: \(error.localizedDescription)"
|
||||
isLoggedIn = false
|
||||
}
|
||||
}
|
||||
@ -79,10 +79,10 @@ class SettingsServerViewModel {
|
||||
do {
|
||||
try await logoutUseCase.execute()
|
||||
isLoggedIn = false
|
||||
successMessage = "Abgemeldet"
|
||||
successMessage = "Logged out"
|
||||
NotificationCenter.default.post(name: NSNotification.Name("SetupStatusChanged"), object: nil)
|
||||
} catch {
|
||||
errorMessage = "Fehler beim Abmelden"
|
||||
errorMessage = "Error logging out"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -59,7 +59,7 @@ private struct CollapsedPlayerBar: View {
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(viewModel.currentText.isEmpty ? "Keine Wiedergabe" : viewModel.currentText)
|
||||
Text(viewModel.currentText.isEmpty ? "No playback" : viewModel.currentText)
|
||||
.font(.subheadline)
|
||||
.fontWeight(.medium)
|
||||
.lineLimit(1)
|
||||
@ -114,7 +114,7 @@ private struct ExpandedPlayerView: View {
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
Text("Vorlese-Queue")
|
||||
Text("Read-aloud Queue")
|
||||
.font(.headline)
|
||||
.fontWeight(.semibold)
|
||||
Spacer()
|
||||
@ -126,13 +126,13 @@ private struct ExpandedPlayerView: View {
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.top, 16)
|
||||
// Fortschrittsbalken für aktuellen Artikel
|
||||
// progress bar for current article
|
||||
if viewModel.articleProgress > 0 && viewModel.articleProgress < 1 {
|
||||
VStack(spacing: 4) {
|
||||
ProgressView(value: viewModel.articleProgress)
|
||||
.progressViewStyle(LinearProgressViewStyle(tint: .accentColor))
|
||||
HStack {
|
||||
Text("Fortschritt: \(Int(viewModel.articleProgress * 100))%")
|
||||
Text("Progress: \(Int(viewModel.articleProgress * 100))%")
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
Spacer()
|
||||
@ -148,7 +148,7 @@ private struct ExpandedPlayerView: View {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "text.line.first.and.arrowtriangle.forward")
|
||||
.foregroundColor(.accentColor)
|
||||
Text("Lese \(viewModel.currentUtteranceIndex + 1)/\(viewModel.queueCount): ")
|
||||
Text("Reading \(viewModel.currentUtteranceIndex + 1)/\(viewModel.queueCount): ")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
Text(viewModel.queueItems[safe: viewModel.currentUtteranceIndex]?.title ?? "")
|
||||
@ -195,7 +195,7 @@ private struct PlayerControls: View {
|
||||
}
|
||||
HStack {
|
||||
Spacer()
|
||||
Picker("Geschwindigkeit", selection: Binding(
|
||||
Picker("Speed", selection: Binding(
|
||||
get: { viewModel.rate },
|
||||
set: { viewModel.setRate($0) }
|
||||
)) {
|
||||
@ -240,7 +240,7 @@ private struct PlayerRate: View {
|
||||
HStack {
|
||||
Image(systemName: "speedometer")
|
||||
.foregroundColor(.accentColor)
|
||||
Picker("Geschwindigkeit", selection: Binding(
|
||||
Picker("Speed", selection: Binding(
|
||||
get: { viewModel.rate },
|
||||
set: { viewModel.setRate($0) }
|
||||
)) {
|
||||
@ -260,7 +260,7 @@ private struct PlayerQueueList: View {
|
||||
@ObservedObject var viewModel: SpeechPlayerViewModel
|
||||
var body: some View {
|
||||
if viewModel.queueCount == 0 {
|
||||
Text("Keine Artikel in der Queue")
|
||||
Text("No articles in the queue")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
.padding()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user