From e4121aa0666dcdd714d7a7462dab05fca93b51c3 Mon Sep 17 00:00:00 2001 From: Ilyas Hallak Date: Sat, 1 Nov 2025 21:19:00 +0100 Subject: [PATCH] Add implementation tracking and planning documents - Offline-Konzept.md: Multi-level offline strategy (3 stages) - Offline-Stufe1-Implementierung.md: Detailed implementation plan for stage 1 - Offline-Stufe1-Implementation-Tracking.md: Day-by-day tracking with checkboxes These documents will guide the offline sync implementation over multiple days --- documentation/.DS_Store | Bin 0 -> 6148 bytes documentation/Offline-Konzept.md | 378 +++++ .../Offline-Stufe1-Implementation-Tracking.md | 670 ++++++++ .../Offline-Stufe1-Implementierung.md | 1385 +++++++++++++++++ documentation/claude.md | 304 ++++ documentation/heavy_article.html | 45 + documentation/tabbar2.md | 66 + 7 files changed, 2848 insertions(+) create mode 100644 documentation/.DS_Store create mode 100644 documentation/Offline-Konzept.md create mode 100644 documentation/Offline-Stufe1-Implementation-Tracking.md create mode 100644 documentation/Offline-Stufe1-Implementierung.md create mode 100644 documentation/claude.md create mode 100644 documentation/heavy_article.html create mode 100644 documentation/tabbar2.md diff --git a/documentation/.DS_Store b/documentation/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0 String? + func hasCachedArticle(id: String) -> Bool +} +``` + +### Datenspeicherung +- **CoreData** für Artikel-HTML und Metadaten (Titel, URL, Datum, Reihenfolge) +- **FileManager** optional für große HTML-Dateien (falls CoreData zu groß wird) + +### User Experience +- Bookmark-Liste zeigt Download-Icon für offline verfügbare Artikel +- Beim Öffnen eines gecachten Artikels: Sofortiges Laden ohne Netzwerk-Anfrage +- In Settings: "Offline-Modus" Sektion mit Slider für Anzahl der Artikel +- Cache-Größe wird angezeigt (z.B. "23 Artikel, 12.5 MB") + +--- + +## Stufe 2: Offline-First mit Sync (Erweitert) + +### Beschreibung +Die App funktioniert vollständig offline. Alle Aktionen werden lokal gespeichert und bei Netzwerkverbindung mit dem Server synchronisiert. + +### Features +- **Vollständige Offline-Funktionalität**: Alle Lese-Operationen funktionieren offline +- **Lokale Schreib-Operationen**: + - Bookmarks erstellen, bearbeiten, löschen + - Labels hinzufügen/entfernen + - Artikel archivieren/favorisieren + - Lesefortschritt speichern + - Annotationen/Highlights erstellen +- **Intelligente Synchronisierung**: + - Automatische Sync bei Netzwerkverbindung + - Konfliktauflösung (Server gewinnt vs. Client gewinnt) + - Retry-Mechanismus bei fehlgeschlagenen Syncs +- **Sync-Status**: User sieht jederzeit, ob und was synchronisiert wird +- **Offline-Indikator**: Klarer Status in der UI (Online/Offline/Syncing) + +### Technische Umsetzung + +#### Sync-Manager +```swift +class OfflineSyncManager { + // Bereits vorhanden für Bookmark-Erstellung + // Erweitern für alle Operations + + enum SyncOperation { + case createBookmark(CreateBookmarkRequest) + case updateBookmark(id: String, BookmarkUpdateRequest) + case deleteBookmark(id: String) + case addLabels(bookmarkId: String, labels: [String]) + case removeLabels(bookmarkId: String, labels: [String]) + case updateReadProgress(bookmarkId: String, progress: Int) + case createAnnotation(AnnotationRequest) + case deleteAnnotation(bookmarkId: String, annotationId: String) + } + + func queueOperation(_ operation: SyncOperation) async + func syncAllPendingOperations() async throws + func getPendingOperationsCount() -> Int +} +``` + +#### Lokale Datenbank-Struktur +```swift +// CoreData Entities +entity OfflineBookmark { + id: String + title: String + url: String + content: String? // HTML + metadata: Data // JSON mit allen Bookmark-Daten + labels: [String] + isArchived: Bool + isMarked: Bool + readProgress: Int + annotations: [OfflineAnnotation] + lastModified: Date + syncStatus: String // "synced", "pending", "conflict" +} + +entity PendingSyncOperation { + id: UUID + type: String // operation type + payload: Data // JSON der Operation + createdAt: Date + retryCount: Int + lastError: String? +} +``` + +### Sync-Strategien + +#### Option A: Last-Write-Wins (Einfach) +- Server-Version überschreibt bei Konflikt immer lokale Version +- Einfach zu implementieren +- Potentieller Datenverlust bei Offline-Änderungen + +#### Option B: Timestamp-basiert (Empfohlen) +- Neueste Änderung (basierend auf Timestamp) gewinnt +- Server sendet `updated` Timestamp mit jeder Response +- Client vergleicht mit lokalem Timestamp + +#### Option C: Operational Transformation (Komplex) +- Granulare Merge-Strategien für verschiedene Felder +- Beispiel: Lokale Labels + Server Labels = Union +- Aufwändig, aber maximale Datenerhaltung + +### User Experience +- **Offline-Banner**: "Du bist offline. Änderungen werden synchronisiert, sobald du online bist." +- **Sync-Status Indicator**: + - Grün: Alles synchronisiert + - Gelb: Synchronisierung läuft + - Rot: Sync-Fehler + - Grau: Offline +- **Pending Changes Badge**: Zeigt Anzahl nicht synchronisierter Änderungen +- **Manual Sync Button**: Manuelles Anstoßen der Synchronisierung +- **Sync-Konflikt-Dialog**: Bei Konflikten User entscheiden lassen (Lokal behalten / Server übernehmen / Beide behalten) + +--- + +## Stufe 3: Vollständig Offline-Fähig (Maximum) + +### Beschreibung +Die App kann komplett ohne Server-Verbindung genutzt werden, inklusive lokaler Volltext-Suche und erweiterter Offline-Features. + +### Zusätzliche Features +- **Lokale Volltext-Suche**: + - SQLite FTS5 (Full-Text Search) für schnelle Suche + - Suche in Titeln, URLs, Content, Labels + - Highlighting von Suchbegriffen +- **Intelligente Offline-Strategie**: + - Predictive Caching basierend auf Leseverhalten + - Automatisches Herunterladen von "Ähnlichen Artikeln" + - Background Refresh für häufig gelesene Labels/Tags +- **Erweiterte Export-Funktionen**: + - Kompletten Offline-Cache als ZIP exportieren + - Import von Offline-Daten auf anderem Gerät + - Backup & Restore +- **Reader Mode Optimierungen**: + - Lokale Schriftarten für Offline-Nutzung + - CSS/JS lokal gespeichert + - Keine externen Dependencies +- **Offline-Statistiken**: + - Lesezeit offline vs. online + - Meistgelesene offline Artikel + - Speicherplatz-Statistiken + +### Erweiterte Technische Umsetzung + +#### FTS5 für Suche +```swift +// SQLite Schema +CREATE VIRTUAL TABLE bookmarks_fts USING fts5( + title, + url, + content, + labels, + content='offline_bookmarks', + content_rowid='id' +); + +// Suche +func searchOfflineBookmarks(query: String) -> [Bookmark] { + let sql = """ + SELECT * FROM offline_bookmarks + WHERE id IN ( + SELECT rowid FROM bookmarks_fts + WHERE bookmarks_fts MATCH ? + ) + ORDER BY rank + """ + // Execute and return results +} +``` + +#### Predictive Caching +```swift +class PredictiveCacheManager { + // Analysiere Leseverhalten + func analyzeReadingPatterns() -> ReadingProfile + + // Lade ähnliche Artikel basierend auf: + // - Gleiche Labels/Tags + // - Gleiche Autoren + // - Gleiche Domains + func prefetchRelatedArticles(for bookmark: Bookmark) async + + // Machine Learning (optional) + // CoreML Model für Content-Empfehlungen + func trainRecommendationModel() +} +``` + +#### Background Sync +```swift +// BackgroundTasks Framework +class BackgroundSyncScheduler { + func scheduleBackgroundSync() { + let request = BGAppRefreshTaskRequest(identifier: "de.readeck.sync") + request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60) // 15 min + + try? BGTaskScheduler.shared.submit(request) + } + + func handleBackgroundSync(task: BGTask) async { + // Sync neue Artikel + // Update gecachte Artikel + // Cleanup alte Artikel + } +} +``` + +### Datenspeicherung +- **SQLite + FTS5**: Für Volltextsuche +- **CoreData**: Für strukturierte Daten und Relationships +- **FileManager**: Für HTML-Content, Bilder, Assets +- **NSUbiquitousKeyValueStore**: Optional für iCloud-Sync der Einstellungen + +### User Experience +- **Offline-First Ansatz**: App fühlt sich immer schnell an, auch bei schlechter Verbindung +- **Smart Downloads**: + - "20 neue Artikel verfügbar. Jetzt herunterladen?" + - "Für dich empfohlen: 5 Artikel zum Offline-Lesen" +- **Storage Dashboard**: + - Visualisierung des genutzten Speichers + - Top 10 größte Artikel + - "Speicher optimieren" Funktion (Bilder komprimieren, alte Artikel löschen) +- **Offline-Modus Toggle**: Bewusster "Nur Offline"-Modus aktivierbar +- **Sync-Schedule**: "Täglich um 6:00 Uhr synchronisieren" + +--- + +## Implementierungs-Roadmap + +### Phase 1: Basis (Stufe 1) - ca. 2-3 Wochen + +- [ ] Settings für Offline-Anzahl +- [ ] CoreData Schema für cached Articles +- [ ] Download-Logic in BookmarksRepository +- [ ] UI-Indikatoren für gecachte Artikel +- [ ] Cleanup-Logic (FIFO) + +### Phase 2: Offline-First mit Sync (Stufe 2) - ca. 4-6 Wochen + +- [ ] Erweiterte CoreData Entities +- [ ] OfflineSyncManager erweitern +- [ ] Konfliktauflösung implementieren +- [ ] Sync-Status UI +- [ ] Comprehensive Testing (Edge Cases) + +### Phase 3: Advanced Features (Stufe 3) - ca. 4-6 Wochen + +- [ ] SQLite FTS5 Integration +- [ ] Predictive Caching +- [ ] Background Tasks +- [ ] Export/Import +- [ ] Analytics & Optimizations + +--- + +## Technische Überlegungen + +### Performance +- **Lazy Loading**: Artikel-Content nur laden, wenn benötigt +- **Pagination**: Auch offline große Listen paginieren +- **Image Optimization**: Bilder komprimieren vor dem Speichern (WebP, HEIC) +- **Incremental Sync**: Nur Änderungen synchronisieren, nicht alles neu laden + +### Speicherplatz +- **Quotas**: Maximale Größe für Offline-Cache (z.B. 500MB, 1GB, 2GB) +- **Cleanup-Strategien**: + - Älteste zuerst (FIFO) + - Größte zuerst + - Am wenigsten gelesen zuerst +- **Kompression**: HTML und JSON komprimieren (gzip) + +### Sicherheit +- **Verschlüsselung**: Offline-Daten mit iOS Data Protection verschlüsseln +- **Sensitive Data**: Passwörter niemals lokal speichern (nur Token mit Keychain) +- **Cleanup bei Logout**: Alle Offline-Daten löschen + +### Testing +- **Unit Tests**: Für Sync-Logic, Conflict Resolution +- **Integration Tests**: Offline→Online Szenarien +- **UI Tests**: Offline-Modi, Sync-Status +- **Edge Cases**: + - App-Kill während Sync + - Netzwerk-Loss während Download + - Speicher voll + - Server-Konflikte + +--- + +## Bestehende Features die von Offline profitieren + +### Bereits implementiert +- ✅ **Offline Bookmark Erstellung**: Bookmarks werden lokal gespeichert und bei Verbindung synchronisiert ([OfflineSyncManager.swift](readeck/Data/Repository/OfflineSyncManager.swift)) +- ✅ **Server Reachability Check**: Check ob Server erreichbar ist vor Sync-Operationen +- ✅ **Sync Status UI**: isSyncing und syncStatus werden bereits getrackt + +### Erweiterbar +- **Text-to-Speech**: Geht bereits offline, wenn Artikel gecacht ist +- **Annotations/Highlights**: Können offline erstellt und später synchronisiert werden +- **Lesefortschritt**: Kann lokal getrackt und bei Sync übertragen werden +- **Labels**: Offline hinzufügen/entfernen mit Sync +- **Read Progress**: Bereits vorhanden im BookmarkDetail Model, kann offline getrackt werden + +--- + +## Metriken für Erfolg + +### User-Metriken +- Durchschnittliche Offline-Nutzungszeit +- % der Nutzer, die Offline-Features aktivieren +- Anzahl der offline gelesenen Artikel pro User +- User-Feedback zur Offline-Erfahrung + +### Technische Metriken +- Erfolgsrate der Synchronisierungen +- Durchschnittliche Sync-Dauer +- Anzahl der Sync-Konflikte +- Speicherplatz-Nutzung pro User +- Crash-Rate während Offline-Operationen + +### Performance-Metriken +- App-Start-Zeit mit/ohne Offline-Cache +- Ladezeit für gecachte vs. nicht-gecachte Artikel +- Netzwerk-Traffic Reduktion durch Caching +- Battery Impact durch Background-Sync + +--- + +## Nächste Schritte + +1. **Entscheidung**: Welche Stufe soll zuerst implementiert werden? +2. **Prototyping**: Quick PoC für CoreData Schema und Cache-Logic +3. **UI/UX Design**: Mockups für Offline-Indikatoren und Settings +4. **Implementation**: Schrittweise nach Roadmap +5. **Testing**: Ausgiebiges Testen von Edge Cases +6. **Beta**: TestFlight mit fokussiertem Offline-Testing +7. **Launch**: Schrittweises Rollout mit Feature-Flags + +--- + +*Dokument erstellt: 2025-11-01* +*Version: 1.0* diff --git a/documentation/Offline-Stufe1-Implementation-Tracking.md b/documentation/Offline-Stufe1-Implementation-Tracking.md new file mode 100644 index 0000000..9743015 --- /dev/null +++ b/documentation/Offline-Stufe1-Implementation-Tracking.md @@ -0,0 +1,670 @@ +# Offline Stufe 1 - Implementierungs-Tracking + +**Branch**: `offline-sync` +**Start**: 2025-11-01 +**Geschätzte Dauer**: 5-8 Tage + +--- + +## Phase 1: Foundation & Models (Tag 1) + +### 1.1 Logging-Kategorie erweitern +**Datei**: `readeck/Utils/Logger.swift` + +- [ ] `LogCategory.sync` zur enum hinzufügen +- [ ] `Logger.sync` zur Extension hinzufügen +- [ ] Testen: Logging funktioniert + +**Code-Änderungen**: +```swift +enum LogCategory: String, CaseIterable, Codable { + // ... existing + case sync = "Sync" +} + +extension Logger { + // ... existing + static let sync = Logger(category: .sync) +} +``` + +--- + +### 1.2 Domain Model: OfflineSettings +**Neue Datei**: `readeck/Domain/Model/OfflineSettings.swift` + +- [ ] Struct OfflineSettings erstellen +- [ ] Properties hinzufügen: + - [ ] `enabled: Bool = true` + - [ ] `maxUnreadArticles: Double = 20` + - [ ] `saveImages: Bool = false` + - [ ] `lastSyncDate: Date?` +- [ ] Computed Property `maxUnreadArticlesInt` implementieren +- [ ] Computed Property `shouldSyncOnAppStart` implementieren (4-Stunden-Check) +- [ ] Codable Conformance +- [ ] Testen: shouldSyncOnAppStart Logic + +**Checklist**: +- [ ] File erstellt +- [ ] Alle Properties vorhanden +- [ ] 4-Stunden-Check funktioniert +- [ ] Kompiliert ohne Fehler + +--- + +### 1.3 CoreData Entity: CachedArticle +**Datei**: `readeck.xcdatamodeld` + +- [ ] Neue Model-Version erstellen +- [ ] CachedArticle Entity hinzufügen +- [ ] Attributes definieren: + - [ ] `id` (String, indexed) + - [ ] `bookmarkId` (String, indexed, unique constraint) + - [ ] `bookmarkJSON` (String) + - [ ] `htmlContent` (String) + - [ ] `cachedDate` (Date, indexed) + - [ ] `lastAccessDate` (Date) + - [ ] `size` (Integer 64) + - [ ] `imageURLs` (String, optional) +- [ ] Current Model Version setzen +- [ ] Lightweight Migration in CoreDataManager aktivieren +- [ ] Testen: App startet ohne Crash, keine Migration-Fehler + +**Checklist**: +- [ ] Entity erstellt +- [ ] Alle Attributes vorhanden +- [ ] Indexes gesetzt +- [ ] Migration funktioniert +- [ ] App startet erfolgreich + +--- + +## Phase 2: Data Layer (Tag 1-2) + +### 2.1 Settings Repository Protocol +**Neue Datei**: `readeck/Domain/Protocols/PSettingsRepository.swift` + +- [ ] Protocol `PSettingsRepository` erstellen +- [ ] Methode `loadOfflineSettings()` definieren +- [ ] Methode `saveOfflineSettings(_ settings: OfflineSettings)` definieren + +**Checklist**: +- [ ] File erstellt +- [ ] Protocol definiert +- [ ] Methoden deklariert +- [ ] Kompiliert ohne Fehler + +--- + +### 2.2 Settings Repository Implementation +**Neue Datei**: `readeck/Data/Repository/SettingsRepository.swift` + +- [ ] Class `SettingsRepository` erstellen +- [ ] `PSettingsRepository` implementieren +- [ ] `loadOfflineSettings()` implementieren: + - [ ] UserDefaults laden + - [ ] JSON dekodieren + - [ ] Default-Settings zurückgeben bei Fehler + - [ ] Logger.data für Erfolgsmeldung +- [ ] `saveOfflineSettings()` implementieren: + - [ ] JSON enkodieren + - [ ] UserDefaults speichern + - [ ] Logger.data für Erfolgsmeldung +- [ ] Testen: Save & Load funktioniert + +**Checklist**: +- [ ] File erstellt +- [ ] loadOfflineSettings() implementiert +- [ ] saveOfflineSettings() implementiert +- [ ] Logging integriert +- [ ] Manueller Test erfolgreich + +--- + +### 2.3 BookmarksRepository Protocol erweitern +**Datei**: `readeck/Domain/Protocols/PBookmarksRepository.swift` + +- [ ] Neue Methoden zum Protocol hinzufügen: + - [ ] `cacheBookmarkWithMetadata(bookmark:html:saveImages:) async throws` + - [ ] `getCachedArticle(id:) -> String?` + - [ ] `hasCachedArticle(id:) -> Bool` + - [ ] `getCachedBookmarks() async throws -> [Bookmark]` + - [ ] `getCachedArticlesCount() -> Int` + - [ ] `getCacheSize() -> String` + - [ ] `clearCache() async throws` + - [ ] `cleanupOldestCachedArticles(keepCount:) async throws` + +**Checklist**: +- [ ] Alle Methoden deklariert +- [ ] Async/throws korrekt gesetzt +- [ ] Kompiliert ohne Fehler + +--- + +### 2.4 BookmarksRepository Implementation +**Datei**: `readeck/Data/Repository/BookmarksRepository.swift` + +- [ ] `import Kingfisher` hinzufügen +- [ ] `cacheBookmarkWithMetadata()` implementieren: + - [ ] Prüfen ob bereits gecacht + - [ ] Bookmark als JSON enkodieren + - [ ] CoreData CachedArticle erstellen + - [ ] Speichern in CoreData + - [ ] Logger.sync.info + - [ ] Bei saveImages: extractImageURLsFromHTML() aufrufen + - [ ] Bei saveImages: prefetchImagesWithKingfisher() aufrufen +- [ ] `extractImageURLsFromHTML()` implementieren: + - [ ] Regex für `` Tags + - [ ] URLs extrahieren + - [ ] Nur absolute URLs (http/https) + - [ ] Logger.sync.debug +- [ ] `prefetchImagesWithKingfisher()` implementieren: + - [ ] URLs zu URL-Array konvertieren + - [ ] ImagePrefetcher erstellen + - [ ] Optional: downloadPriority(.low) setzen + - [ ] prefetcher.start() + - [ ] Logger.sync.info +- [ ] `getCachedArticle()` implementieren: + - [ ] NSFetchRequest mit predicate + - [ ] lastAccessDate updaten + - [ ] htmlContent zurückgeben +- [ ] `hasCachedArticle()` implementieren +- [ ] `getCachedBookmarks()` implementieren: + - [ ] Fetch alle CachedArticle + - [ ] Sort by cachedDate descending + - [ ] JSON zu Bookmark dekodieren + - [ ] Array zurückgeben +- [ ] `getCachedArticlesCount()` implementieren +- [ ] `getCacheSize()` implementieren: + - [ ] Alle sizes summieren + - [ ] ByteCountFormatter nutzen +- [ ] `clearCache()` implementieren: + - [ ] NSBatchDeleteRequest + - [ ] Logger.sync.info +- [ ] `cleanupOldestCachedArticles()` implementieren: + - [ ] Sort by cachedDate ascending + - [ ] Älteste löschen wenn > keepCount + - [ ] Logger.sync.info +- [ ] Testen: Alle Methoden funktionieren + +**Checklist**: +- [ ] Kingfisher import +- [ ] cacheBookmarkWithMetadata() fertig +- [ ] extractImageURLsFromHTML() fertig +- [ ] prefetchImagesWithKingfisher() fertig +- [ ] getCachedArticle() fertig +- [ ] hasCachedArticle() fertig +- [ ] getCachedBookmarks() fertig +- [ ] getCachedArticlesCount() fertig +- [ ] getCacheSize() fertig +- [ ] clearCache() fertig +- [ ] cleanupOldestCachedArticles() fertig +- [ ] Logging überall integriert +- [ ] Manuelle Tests erfolgreich + +--- + +## Phase 3: Use Case & Business Logic (Tag 2) + +### 3.1 OfflineCacheSyncUseCase Protocol +**Neue Datei**: `readeck/Domain/UseCase/OfflineCacheSyncUseCase.swift` + +- [ ] Protocol `POfflineCacheSyncUseCase` erstellen +- [ ] Published Properties definieren: + - [ ] `var isSyncing: AnyPublisher` + - [ ] `var syncProgress: AnyPublisher` +- [ ] Methoden deklarieren: + - [ ] `func syncOfflineArticles(settings:) async` + - [ ] `func getCachedArticlesCount() -> Int` + - [ ] `func getCacheSize() -> String` + +**Checklist**: +- [ ] File erstellt +- [ ] Protocol definiert +- [ ] Methoden deklariert +- [ ] Kompiliert ohne Fehler + +--- + +### 3.2 OfflineCacheSyncUseCase Implementation +**Datei**: `readeck/Domain/UseCase/OfflineCacheSyncUseCase.swift` (im selben File) + +- [ ] Class `OfflineCacheSyncUseCase` erstellen +- [ ] Dependencies: + - [ ] `bookmarksRepository: PBookmarksRepository` + - [ ] `settingsRepository: PSettingsRepository` +- [ ] @Published Properties: + - [ ] `_isSyncing = false` + - [ ] `_syncProgress: String?` +- [ ] Publishers als computed properties +- [ ] `syncOfflineArticles()` implementieren: + - [ ] Guard enabled check + - [ ] Logger.sync.info("Starting sync") + - [ ] Set isSyncing = true + - [ ] Fetch bookmarks (state: .unread, limit: settings.maxUnreadArticlesInt) + - [ ] Logger.sync.info("Fetched X bookmarks") + - [ ] Loop durch Bookmarks: + - [ ] Skip wenn bereits gecacht (Logger.sync.debug) + - [ ] syncProgress updaten: "Artikel X/Y..." + - [ ] fetchBookmarkArticle() aufrufen + - [ ] cacheBookmarkWithMetadata() aufrufen + - [ ] successCount++ + - [ ] Bei saveImages: syncProgress "...+ Bilder" + - [ ] Catch: errorCount++, Logger.sync.error + - [ ] cleanupOldestCachedArticles() aufrufen + - [ ] lastSyncDate updaten + - [ ] Logger.sync.info("✅ Synced X, skipped Y, failed Z") + - [ ] Set isSyncing = false + - [ ] syncProgress = Status-Message + - [ ] Sleep 3s, dann syncProgress = nil +- [ ] `getCachedArticlesCount()` implementieren +- [ ] `getCacheSize()` implementieren +- [ ] Error-Handling: + - [ ] Catch block für Haupt-Try + - [ ] Logger.sync.error + - [ ] syncProgress = Error-Message +- [ ] Testen: Sync-Flow komplett durchlaufen + +**Checklist**: +- [ ] Class erstellt +- [ ] Dependencies injiziert +- [ ] syncOfflineArticles() komplett +- [ ] Success/Skip/Error Tracking +- [ ] Logging an allen wichtigen Stellen +- [ ] Progress-Updates +- [ ] Error-Handling +- [ ] getCachedArticlesCount() fertig +- [ ] getCacheSize() fertig +- [ ] Test: Sync läuft durch + +--- + +## Phase 4: Settings UI (Tag 3) + +### 4.1 OfflineSettingsViewModel +**Neue Datei**: `readeck/UI/Settings/OfflineSettingsViewModel.swift` + +- [ ] Class mit @MainActor und @Observable +- [ ] @Published Properties: + - [ ] `offlineSettings: OfflineSettings` + - [ ] `isSyncing = false` + - [ ] `syncProgress: String?` + - [ ] `cachedArticlesCount = 0` + - [ ] `cacheSize = "0 KB"` +- [ ] Dependencies: + - [ ] `settingsRepository: PSettingsRepository` + - [ ] `offlineCacheSyncUseCase: POfflineCacheSyncUseCase` +- [ ] Init mit Dependencies +- [ ] `setupBindings()` implementieren: + - [ ] isSyncing Publisher binden + - [ ] syncProgress Publisher binden + - [ ] Auto-save bei offlineSettings change (debounce 0.5s) +- [ ] `loadSettings()` implementieren +- [ ] `syncNow()` implementieren: + - [ ] Kommentar: Manual sync mit höherer Priority + - [ ] await offlineCacheSyncUseCase.syncOfflineArticles() + - [ ] updateCacheStats() +- [ ] `updateCacheStats()` implementieren +- [ ] Testen: ViewModel funktioniert + +**Checklist**: +- [ ] File erstellt +- [ ] Properties definiert +- [ ] Dependencies injiziert +- [ ] setupBindings() fertig +- [ ] loadSettings() fertig +- [ ] syncNow() fertig +- [ ] updateCacheStats() fertig +- [ ] Test: ViewModel lädt Settings + +--- + +### 4.2 OfflineSettingsView +**Neue Datei**: `readeck/UI/Settings/OfflineSettingsView.swift` + +- [ ] Struct `OfflineSettingsView: View` +- [ ] @StateObject viewModel +- [ ] Body implementieren: + - [ ] Form mit Section + - [ ] Toggle: "Offline-Reading" gebunden an enabled + - [ ] If enabled: + - [ ] VStack: Erklärungstext (caption, secondary) + - [ ] VStack: Slider "Max. Artikel offline" (0-100, step 10) + - [ ] HStack: Anzeige aktueller Wert + - [ ] Toggle: "Bilder speichern" + - [ ] Button: "Jetzt synchronisieren" mit ProgressView + - [ ] If syncProgress: Text anzeigen (caption) + - [ ] If lastSyncDate: Text "Zuletzt synchronisiert: relative" + - [ ] If cachedArticlesCount > 0: HStack mit Stats + - [ ] Section header: "Offline-Reading" +- [ ] navigationTitle("Offline-Reading") +- [ ] onAppear: updateCacheStats() +- [ ] Testen: UI wird korrekt angezeigt + +**Checklist**: +- [ ] File erstellt +- [ ] Form Structure erstellt +- [ ] Toggle für enabled +- [ ] Slider für maxUnreadArticles +- [ ] Toggle für saveImages +- [ ] Sync-Button mit Progress +- [ ] Stats-Anzeige +- [ ] UI-Preview funktioniert +- [ ] Test: Settings werden angezeigt + +--- + +### 4.3 SettingsContainerView Integration +**Datei**: `readeck/UI/Settings/SettingsContainerView.swift` + +- [ ] NavigationLink zu OfflineSettingsView hinzufügen +- [ ] Label mit "Offline-Reading" und Icon "arrow.down.circle" +- [ ] In bestehende Section (z.B. "Allgemein") einfügen +- [ ] Testen: Navigation funktioniert + +**Checklist**: +- [ ] NavigationLink hinzugefügt +- [ ] Icon korrekt +- [ ] Navigation funktioniert + +--- + +### 4.4 Factory erweitern +**Datei**: `readeck/UI/Factory/DefaultUseCaseFactory.swift` + +- [ ] `makeOfflineSettingsViewModel()` implementieren: + - [ ] settingsRepository injecten + - [ ] offlineCacheSyncUseCase injecten + - [ ] OfflineSettingsViewModel instanziieren +- [ ] `makeSettingsRepository()` implementieren (private): + - [ ] SettingsRepository instanziieren +- [ ] `makeOfflineCacheSyncUseCase()` implementieren (private): + - [ ] bookmarksRepository injecten + - [ ] settingsRepository injecten + - [ ] OfflineCacheSyncUseCase instanziieren +- [ ] Testen: Dependencies werden korrekt aufgelöst + +**Checklist**: +- [ ] makeOfflineSettingsViewModel() fertig +- [ ] makeSettingsRepository() fertig +- [ ] makeOfflineCacheSyncUseCase() fertig +- [ ] Test: ViewModel wird erstellt ohne Crash + +--- + +### 4.5 MockUseCaseFactory erweitern (optional) +**Datei**: `readeck/UI/Factory/MockUseCaseFactory.swift` + +- [ ] Mock-Implementierungen für Tests hinzufügen (falls nötig) + +**Checklist**: +- [ ] Mocks erstellt (falls nötig) + +--- + +## Phase 5: App Integration (Tag 3-4) + +### 5.1 AppViewModel erweitern +**Datei**: `readeck/UI/AppViewModel.swift` + +- [ ] `onAppStart()` Methode um Sync erweitern: + - [ ] Nach checkServerReachability() + - [ ] `syncOfflineArticlesIfNeeded()` aufrufen (ohne await!) +- [ ] `syncOfflineArticlesIfNeeded()` implementieren (private): + - [ ] SettingsRepository instanziieren + - [ ] Task.detached(priority: .background) starten + - [ ] Settings laden + - [ ] If shouldSyncOnAppStart: + - [ ] Logger.sync.info("Auto-sync triggered") + - [ ] syncUseCase holen via Factory + - [ ] await syncOfflineArticles() +- [ ] Testen: Auto-Sync bei App-Start (4h-Check) + +**Checklist**: +- [ ] onAppStart() erweitert +- [ ] syncOfflineArticlesIfNeeded() implementiert +- [ ] Task.detached mit .background +- [ ] Kein await vor syncOfflineArticlesIfNeeded() +- [ ] Logger.sync integriert +- [ ] Test: App startet, Sync läuft im Hintergrund + +--- + +### 5.2 BookmarksViewModel erweitern +**Datei**: `readeck/UI/Bookmarks/BookmarksViewModel.swift` + +- [ ] `loadCachedBookmarks()` implementieren (private): + - [ ] bookmarksRepository.getCachedBookmarks() aufrufen + - [ ] If nicht leer: + - [ ] BookmarksPage erstellen mit gecachten Bookmarks + - [ ] bookmarks Property setzen + - [ ] hasMoreData = false + - [ ] errorMessage beibehalten (für Banner) + - [ ] Logger.viewModel.info +- [ ] `loadBookmarks()` erweitern: + - [ ] Im Network-Error catch block: + - [ ] Nach isNetworkError = true + - [ ] await loadCachedBookmarks() aufrufen +- [ ] Testen: Bei Network-Error werden gecachte Bookmarks geladen + +**Checklist**: +- [ ] loadCachedBookmarks() implementiert +- [ ] loadBookmarks() erweitert +- [ ] Logger.viewModel integriert +- [ ] Test: Offline-Modus zeigt gecachte Artikel + +--- + +### 5.3 BookmarksView erweitern +**Datei**: `readeck/UI/Bookmarks/BookmarksView.swift` + +- [ ] `offlineBanner` View hinzufügen (private): + - [ ] HStack mit wifi.slash Icon + - [ ] Text "Offline-Modus – Zeige gespeicherte Artikel" + - [ ] Styling: caption, secondary, padding, background +- [ ] `body` anpassen: + - [ ] ZStack durch VStack(spacing: 0) ersetzen + - [ ] If isNetworkError && bookmarks nicht leer: + - [ ] offlineBanner anzeigen + - [ ] Content darunter + - [ ] FAB als Overlay über VStack +- [ ] `shouldShowCenteredState` anpassen: + - [ ] Kommentar: Nur bei leer UND error + - [ ] return isEmpty && hasError +- [ ] Testen: Offline-Banner erscheint bei Network-Error mit Daten + +**Checklist**: +- [ ] offlineBanner View erstellt +- [ ] body mit VStack umgebaut +- [ ] shouldShowCenteredState angepasst +- [ ] Test: Banner wird angezeigt im Offline-Modus + +--- + +### 5.4 BookmarkDetailViewModel erweitern +**Datei**: `readeck/UI/BookmarkDetail/BookmarkDetailViewModel.swift` + +- [ ] `loadArticle()` erweitern: + - [ ] Vor Server-Request: + - [ ] If let cachedHTML = bookmarksRepository.getCachedArticle(id:) + - [ ] articleHTML = cachedHTML + - [ ] isLoading = false + - [ ] Logger.viewModel.info("Loaded from cache") + - [ ] return + - [ ] Nach Server-Request (im Task.detached): + - [ ] Artikel optional cachen wenn saveImages enabled +- [ ] Testen: Gecachte Artikel laden sofort + +**Checklist**: +- [ ] Cache-Check vor Server-Request +- [ ] Logger.viewModel integriert +- [ ] Optional: Background-Caching nach Load +- [ ] Test: Gecachte Artikel laden instant + +--- + +## Phase 6: Testing & Polish (Tag 4-5) + +### 6.1 Unit Tests +- [ ] OfflineSettings Tests: + - [ ] shouldSyncOnAppStart bei erstem Mal + - [ ] shouldSyncOnAppStart nach 3h (false) + - [ ] shouldSyncOnAppStart nach 5h (true) + - [ ] shouldSyncOnAppStart bei disabled (false) +- [ ] SettingsRepository Tests: + - [ ] Save & Load roundtrip + - [ ] Default values bei leerem UserDefaults +- [ ] BookmarksRepository Cache Tests: + - [ ] cacheBookmarkWithMetadata() + - [ ] getCachedArticle() + - [ ] hasCachedArticle() + - [ ] cleanupOldestCachedArticles() + - [ ] extractImageURLsFromHTML() + +**Checklist**: +- [ ] OfflineSettings Tests geschrieben +- [ ] SettingsRepository Tests geschrieben +- [ ] BookmarksRepository Tests geschrieben +- [ ] Alle Tests grün + +--- + +### 6.2 Integration Tests +- [ ] App-Start Sync: + - [ ] Erste Start: Sync läuft + - [ ] Zweiter Start < 4h: Kein Sync + - [ ] Nach 4h: Sync läuft +- [ ] Manual Sync: + - [ ] Button triggert Sync + - [ ] Progress wird angezeigt + - [ ] Success-Message erscheint +- [ ] Offline-Modus: + - [ ] Flugmodus aktivieren + - [ ] Gecachte Bookmarks werden angezeigt + - [ ] Offline-Banner erscheint + - [ ] Artikel lassen sich öffnen +- [ ] Cache Management: + - [ ] 20 Artikel cachen + - [ ] Stats zeigen 20 Artikel + Größe + - [ ] Cleanup funktioniert bei Limit-Überschreitung + +**Checklist**: +- [ ] App-Start Sync getestet +- [ ] Manual Sync getestet +- [ ] Offline-Modus getestet +- [ ] Cache Management getestet + +--- + +### 6.3 Edge Cases +- [ ] Netzwerk-Verlust während Sync: + - [ ] Partial success wird geloggt + - [ ] Status-Message korrekt +- [ ] Speicher voll: + - [ ] Fehlerbehandlung + - [ ] User-Benachrichtigung +- [ ] 100 Artikel Performance: + - [ ] Sync dauert < 2 Minuten + - [ ] App bleibt responsiv +- [ ] CoreData Migration: + - [ ] Alte App-Version → Neue Version + - [ ] Keine Datenverluste +- [ ] Kingfisher Cache: + - [ ] Bilder werden geladen + - [ ] Cache-Limit wird respektiert + +**Checklist**: +- [ ] Netzwerk-Verlust getestet +- [ ] Speicher voll getestet +- [ ] 100 Artikel Performance OK +- [ ] Migration getestet +- [ ] Kingfisher funktioniert + +--- + +### 6.4 Bug-Fixing & Polish +- [ ] Alle gefundenen Bugs gefixt +- [ ] Code-Review durchgeführt +- [ ] Logging überprüft (nicht zu viel, nicht zu wenig) +- [ ] UI-Polish (Spacing, Colors, etc.) +- [ ] Performance-Optimierungen falls nötig + +**Checklist**: +- [ ] Alle Bugs gefixt +- [ ] Code reviewed +- [ ] Logging optimiert +- [ ] UI poliert +- [ ] Performance OK + +--- + +## Final Checklist + +### Funktionalität +- [ ] Offline-Reading Toggle funktioniert +- [ ] Slider für Artikel-Anzahl funktioniert +- [ ] Bilder-Toggle funktioniert +- [ ] Auto-Sync bei App-Start (4h-Check) +- [ ] Manual-Sync Button funktioniert +- [ ] Offline-Modus zeigt gecachte Artikel +- [ ] Offline-Banner wird angezeigt +- [ ] Cache-Stats werden angezeigt +- [ ] Last-Sync-Date wird angezeigt +- [ ] Background-Sync mit niedriger Priority +- [ ] Kingfisher cached Bilder +- [ ] FIFO Cleanup funktioniert + +### Code-Qualität +- [ ] Alle neuen Files erstellt +- [ ] Alle Protokolle definiert +- [ ] Alle Implementierungen vollständig +- [ ] Logging überall integriert +- [ ] Error-Handling implementiert +- [ ] Keine Compiler-Warnings +- [ ] Keine Force-Unwraps +- [ ] Code dokumentiert (Kommentare wo nötig) + +### Tests +- [ ] Unit Tests geschrieben +- [ ] Integration Tests durchgeführt +- [ ] Edge Cases getestet +- [ ] Performance getestet +- [ ] Alle Tests grün + +### Dokumentation +- [ ] Implementierungsplan vollständig +- [ ] Alle Checkboxen abgehakt +- [ ] Gefundene Issues dokumentiert +- [ ] Nächste Schritte (Stufe 2) überlegt + +--- + +## Commit & PR + +- [ ] Alle Änderungen commited +- [ ] Commit-Messages aussagekräftig +- [ ] Branch gepusht +- [ ] PR erstellt gegen `develop` +- [ ] PR-Beschreibung vollständig: + - [ ] Was wurde implementiert + - [ ] Wie testen + - [ ] Screenshots (Settings-UI) + - [ ] Known Issues (falls vorhanden) + +--- + +## Notes & Issues + +### Gefundene Probleme +_(Hier während der Implementation eintragen)_ + +### Offene Fragen +_(Hier während der Implementation eintragen)_ + +### Verbesserungsideen für Stufe 2 +_(Hier sammeln)_ + +--- + +*Erstellt: 2025-11-01* +*Letztes Update: 2025-11-01* diff --git a/documentation/Offline-Stufe1-Implementierung.md b/documentation/Offline-Stufe1-Implementierung.md new file mode 100644 index 0000000..af3c010 --- /dev/null +++ b/documentation/Offline-Stufe1-Implementierung.md @@ -0,0 +1,1385 @@ +# Offline Stufe 1: Smart Cache für Unread Items - Implementierungsplan + +## Übersicht + +Implementierung eines intelligenten Cache-Systems für ungelesene Artikel mit konfigurierbarer Anzahl (max. 100 Artikel). Die App lädt automatisch Artikel im Hintergrund herunter und macht sie offline verfügbar. + +## Wichtigste Änderungen + +### Offline-Modus Verhalten +- ✅ **Keine gecachten Artikel anzeigen per Icon**: Im Online-Modus gibt es KEINE Indikatoren für gecachte Artikel +- ✅ **Offline-Modus automatisch**: Wenn keine Netzwerkverbindung besteht, werden automatisch nur die gecachten Artikel angezeigt +- ✅ **Unaufdringlicher Banner**: Kleiner Banner über der Liste zeigt "Offline-Modus – Zeige gespeicherte Artikel" +- ✅ **Alle Tabs navigierbar**: User kann weiterhin durch alle Tabs navigieren (kein Full-Screen Error) +- ✅ **Intelligenter Sync**: Nur alle 4 Stunden beim App-Start (verhindert unnötige Syncs) +- ✅ **Background Sync**: Läuft mit niedriger Priorität (`.background`), keine Performance-Einbuße + +### Technische Details +- 🔹 **Standard API-Call**: Nutzt `getBookmarks(state: .unread, limit: X)` für Sync +- 🔹 **CoreData mit JSON**: Speichert komplettes Bookmark-Objekt + HTML-Content +- 🔹 **Kingfisher für Bilder**: Bilder werden via Kingfisher gecacht (bereits im Projekt konfiguriert) +- 🔹 **FIFO Cleanup**: Automatisches Löschen ältester Artikel bei Überschreitung des Limits +- 🔹 **Default: 20 Artikel**: Reduziert initiale Sync-Last (21 API-Calls ohne Bilder) +- 🔹 **Background Priority**: Sync läuft mit `.background` oder `.utility` QoS (Quality of Service) + +--- + +## Features + +### Automatisches Caching +- **Beim App-Start**: Artikel werden automatisch heruntergeladen, wenn mehr als 4 Stunden seit letztem Sync vergangen sind +- **Intelligentes Timing**: Verhindert unnötige Syncs bei häufigem App-Öffnen +- **Background-Task mit niedriger Priorität**: Läuft mit `.background` oder `.utility` Priority +- **Nicht-blockierend**: User kann App normal nutzen während Sync läuft +- **Keine UI-Blockierung**: Sync läuft komplett im Hintergrund ohne Performance-Impact +- **Standard API-Call**: Nutzt den normalen `getBookmarks`-Endpoint mit entsprechenden Parametern + +### Konfigurierbare Einstellungen +- Toggle für Offline-Reading (Ein/Aus, Default: true) +- Slider für Anzahl der zu cachenden Artikel (0-100, Default: 20) +- Toggle für Speichern von Bildern (Default: false) +- Manueller Sync-Button +- Anzeige des letzten Sync-Zeitpunkts + +**Warum Default 20 Artikel?** +- Reduziert Netzwerk-Last beim initialen Sync +- Schnellerer erster Sync für bessere User Experience +- Bei 20 Artikeln: ~20 API-Calls für HTML + ggf. Bild-Downloads +- User kann bei Bedarf auf bis zu 100 erhöhen + +### Offline-Modus Detection +- **Automatische Erkennung**: App erkennt automatisch, wenn keine Netzwerkverbindung besteht +- **Alle Tabs verfügbar**: User kann weiterhin durch alle Tabs navigieren +- **Unaufdringlicher Banner**: Kleiner Banner über den Unread-Artikeln zeigt Offline-Status +- **Gecachte Artikel anzeigen**: Nur gecachte Artikel werden im Offline-Modus angezeigt +- **Bestehende Error-Logik erweitern**: Nutzt vorhandene `isNetworkError` und zeigt gecachte Artikel statt Fehlermeldung + +### Automatische Verwaltung +- FIFO-Prinzip: Älteste Artikel werden automatisch gelöscht, wenn neue hinzukommen +- Cache bleibt innerhalb der konfigurierten Grenzen +- Cleanup bei Logout oder Deaktivierung + +### UI im Offline-Modus +- **Kein Full-Screen Error**: Stattdessen werden gecachte Artikel angezeigt +- **Offline-Banner**: Kleiner, unaufdringlicher Banner über der Liste +- **Alle Tabs navigierbar**: Keine Einschränkung der Navigation +- **Nur gecachte Inhalte**: Nur offline verfügbare Artikel werden angezeigt + +--- + +## UI/UX Design - Settings + +### Neue Settings-Sektion: "Offline-Reading" + +```swift +Section { + // Toggle für Offline-Reading + Toggle("Offline-Reading", isOn: $offlineSettings.enabled) + + if offlineSettings.enabled { + // Erklärungstext + Text("Ungelesene Artikel werden automatisch heruntergeladen und sind offline verfügbar. Änderungen werden synchronisiert, sobald du wieder online bist.") + .font(.caption) + .foregroundColor(.secondary) + .padding(.vertical, 4) + + // Anzahl Max Unread Articles + VStack(alignment: .leading, spacing: 4) { + HStack { + Text("Max. Artikel offline") + Spacer() + Text("\(Int(offlineSettings.maxUnreadArticles))") + .foregroundColor(.secondary) + } + Slider( + value: $offlineSettings.maxUnreadArticles, + in: 0...100, + step: 10 + ) + } + + // Bilder speichern + Toggle("Bilder speichern", isOn: $offlineSettings.saveImages) + + // Manual Sync Button + Button(action: { + Task { + await offlineCacheManager.syncOfflineArticles() + } + }) { + HStack { + Text("Jetzt synchronisieren") + Spacer() + if offlineCacheManager.isSyncing { + ProgressView() + } + } + } + .disabled(offlineCacheManager.isSyncing) + + // Last Sync Date + if let lastSync = offlineSettings.lastSyncDate { + Text("Zuletzt synchronisiert: \(lastSync, formatter: relativeDateFormatter)") + .font(.caption) + .foregroundColor(.secondary) + } + + // Cache-Größe + if offlineCacheManager.cachedArticlesCount > 0 { + HStack { + Text("Gespeicherte Artikel") + Spacer() + Text("\(offlineCacheManager.cachedArticlesCount) Artikel, \(offlineCacheManager.cacheSize)") + .foregroundColor(.secondary) + } + .font(.caption) + } + } +} header: { + Text("Offline-Reading") +} +``` + +--- + +## Netzwerk-Flow beim Sync + +Der Sync-Prozess läuft in mehreren Schritten ab, um Netzwerk-Last zu minimieren und Fehler besser zu behandeln: + +### Schritt 1: Lade Bookmark-Liste +``` +GET /api/bookmarks?is_archived=false&is_marked=false&limit=20 +``` +- Lädt die ersten N ungelesenen Artikel (Default: 20) +- Nutzt den bestehenden `getBookmarks`-Call mit entsprechenden Parametern +- Enthält alle Bookmark-Metadaten (Titel, URL, Datum, etc.) + +### Schritt 2: Lade HTML für jeden Artikel +``` +Für jeden Bookmark: + GET /api/bookmarks/{id}/article +``` +- **Sequenziell** für jeden Artikel (nicht parallel, um Server nicht zu überlasten) +- Lädt den kompletten HTML-Content des Artikels +- Bei Fehler: Artikel wird übersprungen, Sync läuft weiter + +### Schritt 3: Extrahiere Bild-URLs (Optional) +``` +Falls saveImages = true: + - Parse HTML mit Regex nach + - Extrahiere alle absolute URLs + - Speichere URLs in CachedArticle.imageURLs +``` +- Keine zusätzlichen API-Calls nötig +- URLs werden für Schritt 4 vorbereitet + +### Schritt 4: Lade Bilder mit Kingfisher (Optional) +``` +Falls saveImages = true: + - Kingfisher ImagePrefetcher mit allen URLs + - Lädt Bilder im Hintergrund + - Fehler bei einzelnen Bildern stoppen Sync nicht +``` +- **Parallel** durch Kingfisher (effizient) +- Nutzt bestehende Kingfisher-Cache-Konfiguration +- Automatisches Retry bei temporären Fehlern + +### Schritt 5: Speichere in CoreData +``` +Für jeden erfolgreichen Download: + - Speichere Bookmark-JSON + HTML in CachedArticle + - Speichere Metadaten (cachedDate, size, etc.) +``` + +### Schritt 6: Cleanup +``` +- Lösche älteste Artikel wenn Limit überschritten +- Update lastSyncDate in Settings +``` + +### Beispiel-Rechnung für 20 Artikel: + +**Ohne Bilder:** +- 1x API-Call für Bookmark-Liste +- 20x API-Calls für HTML (sequenziell) +- **Total: 21 API-Calls** +- Dauer: ~5-10 Sekunden (je nach Server-Geschwindigkeit) + +**Mit Bildern (Ø 5 Bilder pro Artikel):** +- 1x API-Call für Bookmark-Liste +- 20x API-Calls für HTML (sequenziell) +- ~100x Image-Downloads (parallel durch Kingfisher) +- **Total: 121 Downloads** +- Dauer: ~15-30 Sekunden (je nach Bildgröße und Netzwerk) + +### Fehlerbehandlung: +- **API-Fehler bei einzelnem Artikel**: Überspringen, nächsten versuchen +- **Netzwerk komplett weg**: Sync abbrechen, Fehlermeldung zeigen +- **Speicher voll**: Sync stoppen, User informieren +- **Partial Success**: Zeige "X von Y Artikeln synchronisiert" + +### Background Task Priority + +Um die App-Performance nicht zu beeinträchtigen, läuft der Sync mit niedriger Priorität: + +```swift +// Sync mit Background Priority starten +Task.detached(priority: .background) { + await offlineCacheSyncUseCase.syncOfflineArticles(settings: settings) +} +``` + +**Quality of Service (QoS) Optionen:** +- **`.background`** (Empfohlen): Niedrigste Priorität, läuft nur wenn System idle ist +- **`.utility`**: Niedrige Priorität, für länger laufende Tasks mit Fortschrittsanzeige +- **`.userInitiated`**: Nur für manuellen Sync-Button (User wartet aktiv) + +**Vorteile:** +- ✅ Keine Blockierung der Main-Thread +- ✅ Keine spürbare Performance-Einbuße +- ✅ System kann Task pausieren bei Ressourcen-Knappheit +- ✅ Batterie-schonend durch intelligentes Scheduling + +**Implementation Details:** +- Auto-Sync bei App-Start: `.background` Priority +- Manueller Sync-Button: `.utility` Priority (mit Progress-UI) +- Kingfisher Prefetch: Automatisch mit niedriger Priority + +--- + +## Technische Implementierung + +### 1. Datenmodelle + +#### OfflineSettings Model +**Datei**: `readeck/Domain/Model/OfflineSettings.swift` + +```swift +import Foundation + +struct OfflineSettings: Codable { + var enabled: Bool = true + var maxUnreadArticles: Double = 20 // Double für Slider (Default: 20 Artikel) + var saveImages: Bool = false + var lastSyncDate: Date? + + var maxUnreadArticlesInt: Int { + Int(maxUnreadArticles) + } + + var shouldSyncOnAppStart: Bool { + guard enabled else { return false } + + // Sync if never synced before + guard let lastSync = lastSyncDate else { return true } + + // Sync if more than 4 hours since last sync + let fourHoursAgo = Date().addingTimeInterval(-4 * 60 * 60) + return lastSync < fourHoursAgo + } +} +``` + +#### CachedArticle Entity (CoreData) +**Datei**: `readeck.xcdatamodeld` (CoreData Schema) + +**WICHTIG**: +- Wir speichern sowohl den HTML-Content als auch die kompletten Bookmark-Metadaten als JSON, damit wir im Offline-Modus die Liste vollständig anzeigen können. +- **Bilder werden NICHT in CoreData gespeichert**, sondern über **Kingfisher** gecacht (bereits im Projekt vorhanden und konfiguriert). + +```swift +entity CachedArticle { + id: String (indexed, primary key) + bookmarkId: String (indexed, unique) + bookmarkJSON: String // Komplettes Bookmark-Objekt als JSON + htmlContent: String // Artikel-HTML + cachedDate: Date (indexed) + lastAccessDate: Date + size: Int64 // in Bytes (nur HTML, nicht Bilder) + imageURLs: String? // Komma-separierte Liste der Bild-URLs (für Kingfisher Prefetch) +} +``` + +**Keine separate CachedImage Entity nötig** - Kingfisher verwaltet den Image-Cache automatisch mit den bestehenden Settings in [CacheSettingsView.swift](../readeck/UI/Settings/CacheSettingsView.swift)! + +**Mapping Helpers**: + +```swift +extension CachedArticle { + func toBookmark() throws -> Bookmark { + guard let json = bookmarkJSON, + let data = json.data(using: .utf8) else { + throw NSError(domain: "CachedArticle", code: 1, userInfo: nil) + } + + return try JSONDecoder().decode(Bookmark.self, from: data) + } + + static func from(bookmark: Bookmark, html: String, imageURLs: [String] = []) throws -> CachedArticle { + let cached = CachedArticle() + cached.id = UUID().uuidString + cached.bookmarkId = bookmark.id + + let encoder = JSONEncoder() + let jsonData = try encoder.encode(bookmark) + cached.bookmarkJSON = String(data: jsonData, encoding: .utf8) + + cached.htmlContent = html + cached.cachedDate = Date() + cached.lastAccessDate = Date() + cached.size = Int64(html.utf8.count) + + // Store image URLs for Kingfisher prefetch + if !imageURLs.isEmpty { + cached.imageURLs = imageURLs.joined(separator: ",") + } + + return cached + } + + func getImageURLs() -> [URL] { + guard let imageURLs = imageURLs else { return [] } + return imageURLs.split(separator: ",") + .compactMap { URL(string: String($0)) } + } +} +``` + +--- + +### 2. Repository Layer + +#### PBookmarksRepository erweitern +**Datei**: `readeck/Domain/Protocols/PBookmarksRepository.swift` + +```swift +protocol PBookmarksRepository { + // ... existing methods + + // Offline Cache Methods + func cacheBookmarkWithMetadata(bookmark: Bookmark, html: String, saveImages: Bool) async throws + func getCachedArticle(id: String) -> String? + func hasCachedArticle(id: String) -> Bool + func getCachedArticlesCount() -> Int + func getCacheSize() -> String + func getCachedBookmarks() async throws -> [Bookmark] + func clearCache() async throws + func cleanupOldestCachedArticles(keepCount: Int) async throws +} +``` + +#### BookmarksRepository Implementation +**Datei**: `readeck/Data/Repository/BookmarksRepository.swift` + +**WICHTIG**: Für das Caching der Artikel-Metadaten müssen wir das Bookmark-Objekt mit speichern, damit wir im Offline-Modus die komplette Liste anzeigen können. + +Erweitern mit: + +```swift +func cacheBookmarkWithMetadata(bookmark: Bookmark, html: String, saveImages: Bool) async throws { + // 1. Prüfen ob bereits gecacht + if hasCachedArticle(id: bookmark.id) { + return + } + + // 2. Bookmark + HTML speichern in CoreData + let context = CoreDataManager.shared.context + try await context.perform { + let cachedArticle = CachedArticle(context: context) + + // Bookmark-Metadaten als JSON speichern + let encoder = JSONEncoder() + let bookmarkData = try encoder.encode(bookmark) + let bookmarkJSON = String(data: bookmarkData, encoding: .utf8) + + cachedArticle.id = UUID().uuidString + cachedArticle.bookmarkId = bookmark.id + cachedArticle.bookmarkJSON = bookmarkJSON + cachedArticle.htmlContent = html + cachedArticle.cachedDate = Date() + cachedArticle.lastAccessDate = Date() + cachedArticle.size = Int64(html.utf8.count) + cachedArticle.hasImages = saveImages + + CoreDataManager.shared.save() + } + + // 3. Bilder mit Kingfisher prefetchen (außerhalb CoreData context) + if saveImages { + let imageURLs = extractImageURLsFromHTML(html: html) + cachedArticle.imageURLs = imageURLs.joined(separator: ",") + + // Prefetch images with Kingfisher + Task.detached { + await self.prefetchImagesWithKingfisher(imageURLs: imageURLs) + } + } +} + +private func extractImageURLsFromHTML(html: String) -> [String] { + // Extract all URLs from HTML + var imageURLs: [String] = [] + + // Simple regex pattern for img tags + let pattern = #"]+src=\"([^\"]+)\""# + + if let regex = try? NSRegularExpression(pattern: pattern, options: []) { + let nsString = html as NSString + let results = regex.matches(in: html, options: [], range: NSRange(location: 0, length: nsString.length)) + + for result in results { + if result.numberOfRanges >= 2 { + let urlRange = result.range(at: 1) + if let url = nsString.substring(with: urlRange) as String? { + // Handle relative URLs + if url.hasPrefix("http") { + imageURLs.append(url) + } + } + } + } + } + + return imageURLs +} + +private func prefetchImagesWithKingfisher(imageURLs: [String]) async { + let urls = imageURLs.compactMap { URL(string: $0) } + + guard !urls.isEmpty else { return } + + // Use Kingfisher's prefetcher mit niedriger Priorität + let prefetcher = ImagePrefetcher(urls: urls) { skippedResources, failedResources, completedResources in + print("Prefetch completed: \(completedResources.count)/\(urls.count) images cached") + if !failedResources.isEmpty { + print("Failed to cache \(failedResources.count) images") + } + } + + // Optional: Setze Download-Priority auf .low für Background-Downloads + // prefetcher.options = [.downloadPriority(.low)] + + prefetcher.start() +} + +func getCachedArticle(id: String) -> String? { + let fetchRequest: NSFetchRequest = CachedArticle.fetchRequest() + fetchRequest.predicate = NSPredicate(format: "bookmarkId == %@", id) + fetchRequest.fetchLimit = 1 + + do { + let results = try CoreDataManager.shared.context.fetch(fetchRequest) + if let cached = results.first { + // Update last access date + cached.lastAccessDate = Date() + CoreDataManager.shared.save() + return cached.htmlContent + } + } catch { + print("Error fetching cached article: \(error)") + } + + return nil +} + +func hasCachedArticle(id: String) -> Bool { + return getCachedArticle(id: id) != nil +} + +func getCachedArticlesCount() -> Int { + let fetchRequest: NSFetchRequest = CachedArticle.fetchRequest() + return (try? CoreDataManager.shared.context.count(for: fetchRequest)) ?? 0 +} + +func getCacheSize() -> String { + let fetchRequest: NSFetchRequest = CachedArticle.fetchRequest() + + do { + let articles = try CoreDataManager.shared.context.fetch(fetchRequest) + let totalBytes = articles.reduce(0) { $0 + $1.size } + return ByteCountFormatter.string(fromByteCount: totalBytes, countStyle: .file) + } catch { + return "0 KB" + } +} + +func getCachedBookmarks() async throws -> [Bookmark] { + let fetchRequest: NSFetchRequest = CachedArticle.fetchRequest() + // Sort by cached date, newest first + fetchRequest.sortDescriptors = [NSSortDescriptor(key: "cachedDate", ascending: false)] + + let context = CoreDataManager.shared.context + return try await context.perform { + let cachedArticles = try context.fetch(fetchRequest) + return cachedArticles.compactMap { cached -> Bookmark? in + try? cached.toBookmark() + } + } +} + +func clearCache() async throws { + let fetchRequest: NSFetchRequest = CachedArticle.fetchRequest() + let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) + + try await CoreDataManager.shared.context.perform { + try CoreDataManager.shared.context.execute(deleteRequest) + CoreDataManager.shared.save() + } + + // Optional: Auch Kingfisher-Cache löschen + // KingfisherManager.shared.cache.clearDiskCache() + // KingfisherManager.shared.cache.clearMemoryCache() +} + +func cleanupOldestCachedArticles(keepCount: Int) async throws { + let fetchRequest: NSFetchRequest = CachedArticle.fetchRequest() + fetchRequest.sortDescriptors = [NSSortDescriptor(key: "cachedDate", ascending: true)] + + let context = CoreDataManager.shared.context + try await context.perform { + let allArticles = try context.fetch(fetchRequest) + + // Delete oldest articles if we exceed keepCount + if allArticles.count > keepCount { + let articlesToDelete = allArticles.prefix(allArticles.count - keepCount) + articlesToDelete.forEach { context.delete($0) } + CoreDataManager.shared.save() + } + } +} + +``` + +**Wichtig zu Kingfisher**: +- Import erforderlich: `import Kingfisher` in BookmarksRepository.swift +- Kingfisher ist bereits konfiguriert mit Cache-Limits (siehe [CacheSettingsView.swift](../readeck/UI/Settings/CacheSettingsView.swift)) +- Der User kann die Cache-Größe bereits in den Settings anpassen (50-1200 MB) +- Kingfisher verwaltet automatisch das Löschen alter Bilder basierend auf Speicherplatz-Limits +- Die `ImagePrefetcher` API lädt alle Bilder im Hintergrund herunter und cached sie + +--- + +### 3. Use Cases + +#### OfflineCacheSyncUseCase +**Datei**: `readeck/Domain/UseCase/OfflineCacheSyncUseCase.swift` + +```swift +import Foundation +import Combine + +protocol POfflineCacheSyncUseCase { + var isSyncing: AnyPublisher { get } + var syncProgress: AnyPublisher { get } + + func syncOfflineArticles(settings: OfflineSettings) async + func getCachedArticlesCount() -> Int + func getCacheSize() -> String +} + +// WICHTIG: Der UseCase selbst läuft synchron auf dem aufrufenden Thread. +// Die Background-Priority wird vom Caller gesetzt (z.B. Task.detached(priority: .background)) +// Dadurch ist der UseCase flexibel für verschiedene Prioritäten: +// - Auto-Sync: .background +// - Manual-Sync: .utility oder .userInitiated + +class OfflineCacheSyncUseCase: POfflineCacheSyncUseCase { + private let bookmarksRepository: PBookmarksRepository + private let settingsRepository: PSettingsRepository + + @Published private var _isSyncing = false + @Published private var _syncProgress: String? + + var isSyncing: AnyPublisher { + $_isSyncing.eraseToAnyPublisher() + } + + var syncProgress: AnyPublisher { + $_syncProgress.eraseToAnyPublisher() + } + + init( + bookmarksRepository: PBookmarksRepository, + settingsRepository: PSettingsRepository + ) { + self.bookmarksRepository = bookmarksRepository + self.settingsRepository = settingsRepository + } + + func syncOfflineArticles(settings: OfflineSettings) async { + guard settings.enabled else { return } + + await MainActor.run { + _isSyncing = true + _syncProgress = "Lade ungelesene Artikel..." + } + + do { + // 1. Fetch unread bookmarks (limit by maxUnreadArticles) + let bookmarksPage = try await bookmarksRepository.fetchBookmarks( + state: .unread, + limit: settings.maxUnreadArticlesInt, + offset: nil, + search: nil, + type: nil, + tag: nil + ) + + let bookmarks = bookmarksPage.bookmarks + + await MainActor.run { + _syncProgress = "Laden \(bookmarks.count) Artikel..." + } + + // 2. Download articles with metadata (sequenziell) + var successCount = 0 + var skipCount = 0 + var errorCount = 0 + + for (index, bookmark) in bookmarks.enumerated() { + // Skip if already cached + if bookmarksRepository.hasCachedArticle(id: bookmark.id) { + skipCount += 1 + await MainActor.run { + _syncProgress = "Artikel \(index + 1)/\(bookmarks.count) (bereits gecacht)..." + } + continue + } + + await MainActor.run { + _syncProgress = "Lade Artikel \(index + 1)/\(bookmarks.count)..." + } + + // Download article HTML + do { + let html = try await bookmarksRepository.fetchBookmarkArticle(id: bookmark.id) + + // Cache article with bookmark metadata + try await bookmarksRepository.cacheBookmarkWithMetadata( + bookmark: bookmark, + html: html, + saveImages: settings.saveImages + ) + + successCount += 1 + + // Optional: Show image download progress + if settings.saveImages { + await MainActor.run { + _syncProgress = "Artikel \(index + 1)/\(bookmarks.count) + Bilder..." + } + } + } catch { + print("Failed to cache article \(bookmark.id): \(error)") + errorCount += 1 + continue + } + } + + // 3. Cleanup old articles (keep only maxUnreadArticles) + try await bookmarksRepository.cleanupOldestCachedArticles( + keepCount: settings.maxUnreadArticlesInt + ) + + // 4. Update last sync date + var updatedSettings = settings + updatedSettings.lastSyncDate = Date() + try await settingsRepository.saveOfflineSettings(updatedSettings) + + // Show final status + let statusMessage: String + if errorCount == 0 && successCount > 0 { + statusMessage = "✅ \(successCount) Artikel synchronisiert" + } else if successCount > 0 && errorCount > 0 { + statusMessage = "⚠️ \(successCount) synchronisiert, \(errorCount) fehlgeschlagen" + } else if skipCount == bookmarks.count { + statusMessage = "ℹ️ Alle Artikel bereits gecacht" + } else { + statusMessage = "❌ Synchronisierung fehlgeschlagen" + } + + await MainActor.run { + _isSyncing = false + _syncProgress = statusMessage + } + + // Clear success message after 3 seconds + try? await Task.sleep(nanoseconds: 3_000_000_000) + await MainActor.run { + _syncProgress = nil + } + + } catch { + await MainActor.run { + _isSyncing = false + _syncProgress = "❌ Fehler: \(error.localizedDescription)" + } + + // Clear error message after 5 seconds + try? await Task.sleep(nanoseconds: 5_000_000_000) + await MainActor.run { + _syncProgress = nil + } + } + } + + func getCachedArticlesCount() -> Int { + return bookmarksRepository.getCachedArticlesCount() + } + + func getCacheSize() -> String { + return bookmarksRepository.getCacheSize() + } +} +``` + +--- + +### 4. Settings Repository + +#### PSettingsRepository erweitern +**Datei**: `readeck/Domain/Protocols/PSettingsRepository.swift` (falls vorhanden, sonst neu erstellen) + +```swift +protocol PSettingsRepository { + func loadOfflineSettings() async throws -> OfflineSettings + func saveOfflineSettings(_ settings: OfflineSettings) async throws +} +``` + +#### SettingsRepository Implementation +**Datei**: `readeck/Data/Repository/SettingsRepository.swift` + +```swift +import Foundation + +class SettingsRepository: PSettingsRepository { + private let userDefaults = UserDefaults.standard + private let offlineSettingsKey = "offlineSettings" + + func loadOfflineSettings() async throws -> OfflineSettings { + guard let data = userDefaults.data(forKey: offlineSettingsKey) else { + return OfflineSettings() // Default settings + } + + let decoder = JSONDecoder() + return try decoder.decode(OfflineSettings.self, from: data) + } + + func saveOfflineSettings(_ settings: OfflineSettings) async throws { + let encoder = JSONEncoder() + let data = try encoder.encode(settings) + userDefaults.set(data, forKey: offlineSettingsKey) + } +} +``` + +--- + +### 5. ViewModel für Settings + +#### OfflineSettingsViewModel +**Datei**: `readeck/UI/Settings/OfflineSettingsViewModel.swift` + +```swift +import Foundation +import Combine + +@MainActor +class OfflineSettingsViewModel: ObservableObject { + @Published var offlineSettings: OfflineSettings + @Published var isSyncing = false + @Published var syncProgress: String? + @Published var cachedArticlesCount = 0 + @Published var cacheSize = "0 KB" + + private let settingsRepository: PSettingsRepository + private let offlineCacheSyncUseCase: POfflineCacheSyncUseCase + private var cancellables = Set() + + init( + settingsRepository: PSettingsRepository, + offlineCacheSyncUseCase: POfflineCacheSyncUseCase + ) { + self.settingsRepository = settingsRepository + self.offlineCacheSyncUseCase = offlineCacheSyncUseCase + self.offlineSettings = OfflineSettings() + + setupBindings() + + Task { + await loadSettings() + updateCacheStats() + } + } + + private func setupBindings() { + offlineCacheSyncUseCase.isSyncing + .assign(to: &$isSyncing) + + offlineCacheSyncUseCase.syncProgress + .assign(to: &$syncProgress) + + // Auto-save when settings change + $offlineSettings + .debounce(for: 0.5, scheduler: DispatchQueue.main) + .sink { [weak self] settings in + Task { + try? await self?.settingsRepository.saveOfflineSettings(settings) + } + } + .store(in: &cancellables) + } + + func loadSettings() async { + do { + offlineSettings = try await settingsRepository.loadOfflineSettings() + } catch { + print("Failed to load offline settings: \(error)") + } + } + + func syncNow() async { + // Manueller Sync mit höherer Priorität (.utility statt .background) + // User wartet aktiv auf das Ergebnis und sieht Progress-UI + await offlineCacheSyncUseCase.syncOfflineArticles(settings: offlineSettings) + updateCacheStats() + } + + func updateCacheStats() { + cachedArticlesCount = offlineCacheSyncUseCase.getCachedArticlesCount() + cacheSize = offlineCacheSyncUseCase.getCacheSize() + } +} +``` + +--- + +### 6. Settings UI View + +#### OfflineSettingsView +**Datei**: `readeck/UI/Settings/OfflineSettingsView.swift` + +```swift +import SwiftUI + +struct OfflineSettingsView: View { + @StateObject private var viewModel: OfflineSettingsViewModel + + init(viewModel: OfflineSettingsViewModel) { + _viewModel = StateObject(wrappedValue: viewModel) + } + + var body: some View { + Form { + Section { + Toggle("Offline-Reading", isOn: $viewModel.offlineSettings.enabled) + + if viewModel.offlineSettings.enabled { + VStack(alignment: .leading, spacing: 8) { + Text("Ungelesene Artikel werden automatisch heruntergeladen und sind offline verfügbar. Änderungen werden synchronisiert, sobald du wieder online bist.") + .font(.caption) + .foregroundColor(.secondary) + } + .padding(.vertical, 4) + + VStack(alignment: .leading, spacing: 4) { + HStack { + Text("Max. Artikel offline") + Spacer() + Text("\(viewModel.offlineSettings.maxUnreadArticlesInt)") + .foregroundColor(.secondary) + } + Slider( + value: $viewModel.offlineSettings.maxUnreadArticles, + in: 0...100, + step: 10 + ) + } + + Toggle("Bilder speichern", isOn: $viewModel.offlineSettings.saveImages) + + Button(action: { + Task { + await viewModel.syncNow() + } + }) { + HStack { + Text("Jetzt synchronisieren") + Spacer() + if viewModel.isSyncing { + ProgressView() + } + } + } + .disabled(viewModel.isSyncing) + + if let syncProgress = viewModel.syncProgress { + Text(syncProgress) + .font(.caption) + .foregroundColor(.secondary) + } + + if let lastSync = viewModel.offlineSettings.lastSyncDate { + Text("Zuletzt synchronisiert: \(lastSync, style: .relative)") + .font(.caption) + .foregroundColor(.secondary) + } + + if viewModel.cachedArticlesCount > 0 { + HStack { + Text("Gespeicherte Artikel") + Spacer() + Text("\(viewModel.cachedArticlesCount) Artikel, \(viewModel.cacheSize)") + .foregroundColor(.secondary) + } + .font(.caption) + } + } + } header: { + Text("Offline-Reading") + } + } + .navigationTitle("Offline-Reading") + .onAppear { + viewModel.updateCacheStats() + } + } +} +``` + +--- + +### 7. Integration in SettingsContainerView + +**Datei**: `readeck/UI/Settings/SettingsContainerView.swift` + +In der bestehenden Settings-View einen neuen NavigationLink hinzufügen: + +```swift +Section { + NavigationLink(destination: OfflineSettingsView( + viewModel: DefaultUseCaseFactory.shared.makeOfflineSettingsViewModel() + )) { + Label("Offline-Reading", systemImage: "arrow.down.circle") + } + + // ... existing settings items +} header: { + Text("Allgemein") +} +``` + +--- + +### 8. Factory Erweiterung + +**Datei**: `readeck/UI/Factory/DefaultUseCaseFactory.swift` + +Erweitern mit: + +```swift +// MARK: - Offline Settings + +func makeOfflineSettingsViewModel() -> OfflineSettingsViewModel { + return OfflineSettingsViewModel( + settingsRepository: makeSettingsRepository(), + offlineCacheSyncUseCase: makeOfflineCacheSyncUseCase() + ) +} + +private func makeSettingsRepository() -> PSettingsRepository { + return SettingsRepository() +} + +private func makeOfflineCacheSyncUseCase() -> POfflineCacheSyncUseCase { + return OfflineCacheSyncUseCase( + bookmarksRepository: BookmarksRepository(api: API()), + settingsRepository: makeSettingsRepository() + ) +} +``` + +--- + +### 9. Automatischer Sync mit 4-Stunden-Check + +#### App-Start Sync mit Background Priority +**Datei**: `readeck/UI/AppViewModel.swift` + +Erweitern um: + +```swift +@MainActor +func onAppStart() async { + await checkServerReachability() + syncOfflineArticlesIfNeeded() // Kein await! Läuft im Hintergrund +} + +private func syncOfflineArticlesIfNeeded() { + let settingsRepo = SettingsRepository() + + // Starte Background Task mit niedriger Priorität + Task.detached(priority: .background) { + guard let settings = try? await settingsRepo.loadOfflineSettings() else { + return + } + + // Check if sync is needed (enabled + more than 4 hours) + if settings.shouldSyncOnAppStart { + let syncUseCase = DefaultUseCaseFactory.shared.makeOfflineCacheSyncUseCase() + await syncUseCase.syncOfflineArticles(settings: settings) + } + } +} +``` + +**Wichtig**: Kein `await` vor `syncOfflineArticlesIfNeeded()`, damit der App-Start nicht blockiert wird! + +In `readeckApp.swift`: + +```swift +.task { + await appViewModel.onAppStart() +} +``` + +--- + +### 10. Offline-Modus UI-Anpassungen + +#### BookmarksViewModel erweitern +**Datei**: `readeck/UI/Bookmarks/BookmarksViewModel.swift` + +Erweitern um gecachte Bookmarks zu laden: + +```swift +@MainActor +func loadBookmarks(state: BookmarkState = .unread, type: [BookmarkType] = [.article], tag: String? = nil) async { + guard !isUpdating else { return } + isUpdating = true + defer { isUpdating = false } + + isLoading = true + errorMessage = nil + currentState = state + currentType = type + currentTag = tag + + offset = 0 + hasMoreData = true + + do { + let newBookmarks = try await getBooksmarksUseCase.execute( + state: state, + limit: limit, + offset: offset, + search: searchQuery, + type: type, + tag: tag + ) + bookmarks = newBookmarks + hasMoreData = newBookmarks.currentPage != newBookmarks.totalPages + isNetworkError = false + } catch { + // Check if it's a network error + if let urlError = error as? URLError { + switch urlError.code { + case .notConnectedToInternet, .networkConnectionLost, .timedOut, .cannotConnectToHost, .cannotFindHost: + isNetworkError = true + errorMessage = "No internet connection" + + // NEUE LOGIK: Versuche gecachte Bookmarks zu laden + await loadCachedBookmarks() + default: + isNetworkError = false + errorMessage = "Error loading bookmarks" + } + } else { + isNetworkError = false + errorMessage = "Error loading bookmarks" + } + } + + isLoading = false + isInitialLoading = false +} + +private func loadCachedBookmarks() async { + // Load cached bookmarks from repository + let bookmarksRepository = BookmarksRepository(api: API()) + + do { + let cachedBookmarks = try await bookmarksRepository.getCachedBookmarks() + + if !cachedBookmarks.isEmpty { + // Show cached bookmarks + bookmarks = BookmarksPage( + bookmarks: cachedBookmarks, + currentPage: 1, + totalCount: cachedBookmarks.count, + totalPages: 1, + links: nil + ) + hasMoreData = false + + // Keep error message to show offline banner + // But don't show full-screen error + } + } catch { + print("Failed to load cached bookmarks: \(error)") + } +} +``` + +#### BookmarksView erweitern - Offline-Banner +**Datei**: `readeck/UI/Bookmarks/BookmarksView.swift` + +Anpassen der UI, um im Offline-Modus einen Banner zu zeigen statt Full-Screen-Error: + +```swift +var body: some View { + ZStack { + VStack(spacing: 0) { + // Offline-Banner - nur bei Network-Error und wenn Bookmarks vorhanden + if viewModel.isNetworkError && !(viewModel.bookmarks?.bookmarks.isEmpty ?? true) { + offlineBanner + } + + // Content + if viewModel.isInitialLoading && (viewModel.bookmarks?.bookmarks.isEmpty != false) { + skeletonLoadingView + } else if shouldShowCenteredState { + centeredStateView + } else { + bookmarksList + } + } + + // FAB Button - only show for "Unread" and when not in error/loading state + if (state == .unread || state == .all) && !shouldShowCenteredState && !viewModel.isInitialLoading { + fabButton + } + } + // ... rest of modifiers +} + +// Offline-Banner über der Liste +private var offlineBanner: some View { + HStack(spacing: 8) { + Image(systemName: "wifi.slash") + .font(.caption) + .foregroundColor(.secondary) + + Text("Offline-Modus – Zeige gespeicherte Artikel") + .font(.caption) + .foregroundColor(.secondary) + + Spacer() + } + .padding(.horizontal, 16) + .padding(.vertical, 8) + .background(Color(.systemGray6)) +} + +// Anpassen der Error-Logic +private var shouldShowCenteredState: Bool { + let isEmpty = viewModel.bookmarks?.bookmarks.isEmpty == true + let hasError = viewModel.errorMessage != nil + + // Zeige Full-Screen Error nur wenn leer UND Error (nicht bei gecachten Bookmarks) + return isEmpty && hasError +} +``` + +--- + +### 11. Offline-Artikel Laden + +**Datei**: `readeck/UI/BookmarkDetail/BookmarkDetailViewModel.swift` + +```swift +func loadArticle() async { + isLoading = true + + do { + // 1. Versuche zuerst aus Cache zu laden + if let cachedHTML = bookmarksRepository.getCachedArticle(id: bookmarkId) { + await MainActor.run { + self.articleHTML = cachedHTML + self.isLoading = false + } + return + } + + // 2. Falls nicht gecacht, vom Server laden + let html = try await getBookmarkArticleUseCase.execute(id: bookmarkId) + + await MainActor.run { + self.articleHTML = html + self.isLoading = false + } + + // 3. Optional: Im Hintergrund cachen + Task.detached(priority: .background) { + let settings = try? await SettingsRepository().loadOfflineSettings() + if settings?.enabled == true { + try? await self.bookmarksRepository.cacheBookmarkArticle( + id: self.bookmarkId, + html: html, + saveImages: settings?.saveImages ?? false + ) + } + } + + } catch { + await MainActor.run { + self.error = error + self.isLoading = false + } + } +} +``` + +--- + +## Dateien die erstellt/geändert werden müssen + +### Neu zu erstellen: +1. ✅ `readeck/Domain/Model/OfflineSettings.swift` +2. ✅ `readeck/Domain/UseCase/OfflineCacheSyncUseCase.swift` +3. ✅ `readeck/Domain/Protocols/PSettingsRepository.swift` (falls nicht vorhanden) +4. ✅ `readeck/Data/Repository/SettingsRepository.swift` +5. ✅ `readeck/UI/Settings/OfflineSettingsViewModel.swift` +6. ✅ `readeck/UI/Settings/OfflineSettingsView.swift` +7. ✅ CoreData Entity: `CachedArticle` (mit imageURLs für Kingfisher) + +### Zu erweitern: +1. ✅ `readeck/Domain/Protocols/PBookmarksRepository.swift` - Neue Methoden für Offline-Cache +2. ✅ `readeck/Data/Repository/BookmarksRepository.swift` - Implementation mit **Kingfisher ImagePrefetcher** +3. ✅ `readeck/UI/Settings/SettingsContainerView.swift` - NavigationLink zu Offline-Settings +4. ✅ `readeck/UI/Factory/DefaultUseCaseFactory.swift` - Factory-Methoden für neue ViewModels/UseCases +5. ✅ `readeck/UI/AppViewModel.swift` - Auto-Sync bei App-Start (4-Stunden-Check) +6. ✅ `readeck/UI/Bookmarks/BookmarksViewModel.swift` - Laden gecachter Bookmarks bei Network-Error +7. ✅ `readeck/UI/Bookmarks/BookmarksView.swift` - Offline-Banner statt Full-Screen Error +8. ✅ `readeck/UI/BookmarkDetail/BookmarkDetailViewModel.swift` - Offline-First Artikel-Laden +9. ✅ `readeck.xcdatamodeld` - CoreData Schema + +### Keine Änderung nötig (bereits vorhanden): +- ✅ **Kingfisher** - Bereits im Projekt integriert und konfiguriert +- ✅ **CacheSettingsView** - User kann bereits Image-Cache-Größe anpassen (50-1200 MB) + +--- + +## Testing Checklist + +### Unit Tests +- [ ] OfflineCacheSyncUseCase: Sync-Logic +- [ ] SettingsRepository: Save/Load Settings +- [ ] BookmarksRepository: Cache CRUD Operations +- [ ] Cleanup-Logic: FIFO Prinzip +- [ ] Image URL Extraction aus HTML +- [ ] Kingfisher Prefetch Integration + +### Integration Tests +- [ ] App-Start → Auto-Sync (nur bei >4 Stunden) +- [ ] Settings ändern → Auto-Save +- [ ] Offline-Artikel laden → Cache-Fallback +- [ ] Network-Error → Gecachte Bookmarks anzeigen +- [ ] Bilder werden mit Kingfisher gecacht + +### UI Tests +- [ ] Settings Toggle aktivieren/deaktivieren +- [ ] Slider-Werte ändern +- [ ] Manual Sync Button +- [ ] Offline-Banner wird angezeigt bei Network-Error +- [ ] Cache-Statistiken anzeigen +- [ ] Alle Tabs bleiben navigierbar im Offline-Modus + +### Edge Cases +- [ ] Netzwerk-Loss während Sync +- [ ] App-Kill während Download +- [ ] Speicher voll +- [ ] Cache löschen bei Logout +- [ ] Deaktivieren von Offline-Reading löscht Cache +- [ ] 4-Stunden-Check verhindert unnötige Syncs +- [ ] Kingfisher Cache-Limit wird respektiert + +--- + +## Nächste Schritte + +### Phase 1: Core-Funktionalität (1-2 Tage) +1. CoreData Schema erstellen (CachedArticle Entity mit imageURLs) +2. OfflineSettings Model + Repository implementieren (Default: 20 Artikel) +3. BookmarksRepository um Cache-Methoden erweitern: + - `cacheBookmarkWithMetadata()` mit HTML-Parsing + - Kingfisher ImagePrefetcher Integration + - `getCachedBookmarks()` für Offline-Modus + +### Phase 2: Sync-Logic (1-2 Tage) +4. OfflineCacheSyncUseCase implementieren: + - Sequenzieller Download (21 API-Calls für 20 Artikel) + - Progress-Tracking mit Success/Error/Skip Counts + - Fehlerbehandlung für einzelne Artikel +5. 4-Stunden-Check in `shouldSyncOnAppStart` +6. FIFO Cleanup-Logic + +### Phase 3: UI Integration (1 Tag) +7. OfflineSettingsView erstellen: + - Slider mit Default 20, Max 100 + - Manual Sync Button mit Progress + - Last Sync Date + Cache Stats +8. Integration in SettingsContainerView +9. Factory erweitern für alle neuen Dependencies + +### Phase 4: Offline-Modus (1 Tag) +10. BookmarksViewModel erweitern: + - `loadCachedBookmarks()` bei Network-Error + - Offline-Banner statt Full-Screen Error +11. BookmarkDetailViewModel: Cache-First Loading +12. Auto-Sync bei App-Start + +### Phase 5: Testing & Polish (1-2 Tage) +13. Unit Tests für Sync-Logic & Cache-Operations +14. Integration Tests für Offline-Flow +15. UI Tests für Settings & Offline-Banner +16. Performance Testing mit 100 Artikeln +17. Bug-Fixing & Edge Cases + +**Geschätzte Gesamtdauer: 5-8 Tage** + +--- + +## Zusammenfassung: Kingfisher Integration + +### Warum Kingfisher? +- ✅ **Bereits im Projekt**: Kingfisher ist bereits integriert und konfiguriert +- ✅ **User-konfigurierbar**: Cache-Größe ist in Settings anpassbar (50-1200 MB) +- ✅ **Automatisches Management**: LRU-Cache mit automatischer Größenverwaltung +- ✅ **Performant**: Optimiert für iOS mit Memory & Disk Caching + +### Wie funktioniert es? +1. **HTML Parsen**: Image-URLs werden aus dem HTML extrahiert via Regex +2. **URLs speichern**: In CoreData als komma-separierte Liste (für spätere Verwendung) +3. **Kingfisher Prefetch**: `ImagePrefetcher` lädt alle Bilder im Hintergrund +4. **Automatisches Caching**: Kingfisher speichert Bilder auf Disk +5. **WebView lädt Bilder**: Beim Öffnen des Artikels lädt WebView Bilder aus Kingfisher-Cache + +### Cache-Management +- **Existierende Settings nutzen**: `CacheSettingsView` erlaubt User Cache-Größe zu setzen +- **Automatic Cleanup**: Kingfisher löscht automatisch alte Bilder bei Speicherplatz-Knappheit +- **Separate von Artikel-Cache**: Bilder und HTML werden getrennt verwaltet +- **Optional cleanup**: Bei `clearCache()` kann Kingfisher-Cache mit geleert werden + +### Vorteile gegenüber CoreData für Bilder +- 🚀 **Bessere Performance**: Optimiert für Bilder +- 💾 **Weniger Speicher**: Kompression & Deduplizierung +- 🔄 **Weniger Code**: Keine eigene Image-Download-Logic nötig +- ⚙️ **Konfigurierbar**: User kann Limits selbst setzen + +--- + +*Erstellt: 2025-11-01* +*Aktualisiert: 2025-11-01 (Kingfisher Integration)* +*Basierend auf: Offline-Konzept.md - Stufe 1* diff --git a/documentation/claude.md b/documentation/claude.md new file mode 100644 index 0000000..6dac9fb --- /dev/null +++ b/documentation/claude.md @@ -0,0 +1,304 @@ +# CLAUDE.md - readeck iOS Project Documentation + +## Project Overview + +**readeck iOS** is a native iOS client for [readeck](https://readeck.org) bookmark management. The app provides a clean, native iOS interface for managing bookmarks with features like swipe actions, search, tagging, and reading progress tracking. + +### Key Information +- **Platform:** iOS (iPhone + iPad) +- **Language:** Swift +- **UI Framework:** SwiftUI +- **Architecture:** MVVM + Clean Architecture (3-layer: UI/Domain/Data) +- **Database:** CoreData +- **Dependencies:** Swift Package Manager +- **License:** MIT + +## Architecture Summary + +The project follows Clean Architecture with custom dependency injection: + +``` +UI Layer (SwiftUI Views + ViewModels) + ↓ +Domain Layer (Use Cases + Repository Protocols + Models) + ↓ +Data Layer (Repository Implementations + API + CoreData) +``` + +### Core Components +- **Custom DI:** Protocol-based factory pattern (no external frameworks) +- **MVVM Pattern:** ViewModels handle business logic, Views handle presentation +- **Use Cases:** Single-responsibility business logic encapsulation +- **Repository Pattern:** Data access abstraction with protocols + +## Project Structure + +``` +readeck/ +├── UI/ # SwiftUI Views & ViewModels +│ ├── Bookmarks/ # Main bookmark list +│ ├── BookmarkDetail/ # Article reader +│ ├── AddBookmark/ # Create new bookmarks +│ ├── Search/ # Search functionality +│ ├── Settings/ # App configuration +│ ├── Labels/ # Tag management +│ ├── Menu/ # Navigation & tabs +│ ├── SpeechPlayer/ # Text-to-speech +│ └── Components/ # Reusable UI components +├── Domain/ +│ ├── Model/ # Core business models +│ ├── UseCase/ # Business logic +│ ├── Protocols/ # Repository interfaces +│ └── Error/ # Custom error types +├── Data/ +│ ├── API/ # Network layer & DTOs +│ ├── Repository/ # Data access implementations +│ ├── CoreData/ # Local database +│ └── Utils/ # Helper utilities +└── Localizations/ # i18n strings + ├── Base.lproj/ + ├── en.lproj/ + └── de.lproj/ +``` + +## Key Features + +### Implemented Features +- ✅ Browse bookmarks (All, Unread, Favorites, Archive by type) +- ✅ Share Extension for adding URLs from Safari/other apps +- ✅ Swipe actions for quick bookmark management +- ✅ Native iOS design with Dark Mode support +- ✅ Full iPad Support with Multi-Column Split View +- ✅ Font customization in reader +- ✅ Article view with reading time and word count +- ✅ Search functionality +- ✅ Tag/label management +- ✅ Reading progress tracking +- ✅ Offline support with auto-sync when reconnected +- ✅ Text-to-speech (Read Aloud feature) + +### Planned Features (v1.1.0) +- ⏳ Bookmark filtering and sorting options +- ⏳ Collection management +- ⏳ Custom themes +- ⏳ Text highlighting in articles +- ⏳ Multiple selection for bulk actions + +## Development Setup + +### Requirements +- Xcode 15.0+ +- iOS 17.0+ deployment target +- Swift Package Manager (dependencies auto-resolved) + +### Key Dependencies +- **netfox:** Network debugging (debug builds only) +- **RswiftLibrary:** Resource management + +### Build Configurations +- **Debug:** Includes netfox for network debugging +- **Release:** Production-ready build +- **URLShare Extension:** Share extension target + +## Localization (Weblate Integration) + +### Current Setup +The project has been converted from Apple's String Catalog (.xcstrings) to traditional .strings format for Weblate compatibility: + +``` +readeck/Localizations/ +├── Base.lproj/Localizable.strings # Source language (English) +├── en.lproj/Localizable.strings # English localization +└── de.lproj/Localizable.strings # German localization +``` + +### Weblate Configuration +When setting up Weblate: +- **File mask:** `readeck/Localizations/*.lproj/Localizable.strings` +- **Monolingual base:** `readeck/Localizations/Base.lproj/Localizable.strings` +- **File format:** "iOS Strings (UTF-8)" +- **Repository:** Connect to main Git repository + +### Adding New Languages +1. Create new `.lproj` directory (e.g., `fr.lproj/`) +2. Copy `Base.lproj/Localizable.strings` to new directory +3. Weblate will automatically detect and manage translations + +## App State Management & Navigation + +### Setup Flow & Authentication +The app uses a sophisticated setup and authentication system: + +**Initial Setup Detection:** +- `AppViewModel.hasFinishedSetup` controls the main app flow +- `readeckApp.swift:19` determines whether to show setup or main app +- Setup status is persisted via `SettingsRepository.hasFinishedSetup` + +**Authentication & Keychain Management:** +- `KeychainHelper` (singleton) securely stores sensitive credentials: + - Server endpoint (`readeck_endpoint`) + - Username (`readeck_username`) + - Password (`readeck_password`) + - Authentication token (`readeck_token`) +- Access Group: `8J69P655GN.de.ilyashallak.readeck` for app group sharing +- Automatic logout on 401 responses via `AppViewModel.handleUnauthorizedResponse()` + +**Device-Specific Navigation:** +The app automatically adapts its navigation structure based on device type: + +```swift +// MainTabView.swift determines layout +if UIDevice.isPhone { + PhoneTabView() // Tab-based navigation +} else { + PadSidebarView() // Sidebar + split view navigation +} +``` + +**Navigation Patterns:** +- **iPhone:** `PhoneTabView` - Traditional tab bar with "More" tab for additional features +- **iPad:** `PadSidebarView` - NavigationSplitView with sidebar, content, and detail panes +- Both share the same underlying ViewModels and business logic + +**Key Navigation Components:** +- `SidebarTab` enum defines all available sections +- Main tabs: `.all`, `.unread`, `.favorite`, `.archived` +- More tabs: `.search`, `.article`, `.videos`, `.pictures`, `.tags`, `.settings` +- Consistent routing through `tabView(for:)` methods in both variants + +## Key Architectural Decisions + +### 1. Custom Dependency Injection +- **Why:** Avoid external framework dependencies, full control +- **How:** Protocol-based factory pattern in `DefaultUseCaseFactory` +- **Benefit:** Easy testing with mock implementations + +### 2. Repository Pattern +- **Domain Layer:** Defines protocols (e.g., `PBookmarksRepository`) +- **Data Layer:** Implements protocols (e.g., `BookmarksRepository`) +- **Benefit:** Clean separation between business logic and data access + +### 3. Use Cases +- Single-responsibility classes for each business operation +- Examples: `CreateBookmarkUseCase`, `GetBookmarksUseCase` +- **Benefit:** Testable, reusable business logic + +### 4. SwiftUI + MVVM +- ViewModels as `@ObservableObject` classes +- Views are pure presentation layer +- State management through ViewModels + +## Testing Strategy + +### Current Test Coverage +- **Unit Tests:** `readeckTests/` (basic coverage) +- **UI Tests:** `readeckUITests/` (smoke tests) + +### Testing Philosophy +- Protocol-based DI enables easy mocking +- Use Cases can be tested in isolation +- Repository implementations tested with real/mock data sources + +## Distribution + +### TestFlight Beta +- Public beta available via TestFlight +- Link: `https://testflight.apple.com/join/cV55mKsR` +- Regular updates with new features + +### Release Process +- Uses fastlane for build automation +- Automated screenshot generation +- Version management in Xcode project + +## API Integration + +### readeck Server API +- RESTful API communication +- DTOs in `Data/API/DTOs/` +- Authentication via username/password +- Token management with automatic refresh +- Support for local network addresses (development) + +### Key API Operations +- User authentication +- Bookmark CRUD operations +- Tag/label management +- Article content fetching +- Progress tracking + +## Offline Support + +### Local Storage +- CoreData for offline bookmark storage +- Automatic sync when connection restored +- Queue system for offline operations +- Conflict resolution strategies + +### Sync Strategy +- Background sync when app becomes active +- User-initiated sync option +- Visual indicators for sync status + +## Performance Considerations + +### Memory Management +- Lazy loading of bookmark content +- Image caching for article thumbnails +- Proper SwiftUI view lifecycle management + +### Network Optimization +- Background download of article content +- Request batching where possible +- Retry logic for failed requests + +## Contributing Guidelines + +### Code Style +- Follow Swift API Design Guidelines +- Use SwiftUI best practices +- Maintain separation of concerns between layers + +### New Feature Development +1. Define domain models in `Domain/Model/` +2. Create use case in `Domain/UseCase/` +3. Implement repository in `Data/Repository/` +4. Create ViewModel in appropriate UI folder +5. Build SwiftUI view +6. Update factory for DI wiring + +### Git Workflow +- Main branch: `main` +- Development branch: `develop` +- Feature branches: `feature/feature-name` +- Commit format: `feat:`, `fix:`, `docs:`, etc. + +## Troubleshooting + +### Common Issues +1. **Build Errors:** Ensure Xcode 15.0+ and clean build folder +2. **Network Issues:** Check server URL and credentials +3. **CoreData Migrations:** May need to reset data during development +4. **Localization:** Ensure .strings files are properly formatted + +### Development Tips +- Use netfox in debug builds to monitor API calls +- Check logging configuration in debug settings +- Test both iPhone and iPad layouts +- Verify share extension functionality + +## Security Considerations + +### Data Protection +- Keychain storage for user credentials +- No sensitive data in UserDefaults +- Secure network communication (HTTPS enforced for external domains) + +### Privacy +- No analytics or tracking libraries +- Local data storage only +- User controls all data sync + +--- + +*This documentation is maintained alongside the codebase. Update this file when making architectural changes or adding new features.* \ No newline at end of file diff --git a/documentation/heavy_article.html b/documentation/heavy_article.html new file mode 100644 index 0000000..a274dd1 --- /dev/null +++ b/documentation/heavy_article.html @@ -0,0 +1,45 @@ +
+ + +

Sport vor der Arbeit fördert die Produktivität. (Foto: Jacob Lund / Shutterstock)

Sport vor der Arbeit fördert die Produktivität. (Foto: Jacob Lund / Shutterstock)

+

Zu viele von uns sind zu sehr verplant, übermäßig vernetzt und übermäßig stimuliert von all dem Lärm, den Unterbrechungen und der Komplexität der heutigen Gesellschaft. Der Preis dieses Lebensstils? Du erreichst die letzte Stunden deines letzten Tages und merkst, dass du dein größtes Potenzial für deine sinnlosesten Aktivitäten vergeudet hast.

+

Herausragende Performer, Spitzenleute und Welt-Erschaffer spielen in einer anderen Liga. Die Elon Musks, Mark Zuckerbergs, große Künstler und Top-Wissenschaftler planen ihre Tage alle mit komplett anderer Geisteshaltung und Ritualen als jene, die im Hamsterrad gefangen sind.

+

Als privater Coach von vielen der erfolgreichsten Unternehmer auf diesem Planeten und Gründer von The Titan Summit, das jeden Dezember in Toronto Ultra-Performer von mehr als 43 Nationen vier Tage für ein Elite-Training für Produktivität, Business-Acceleration und Lifestyle-Optimierung zusammenbringt, habe ich mit eigenen Augen beobachtet, wie Menschen, die in einer Woche mehr schaffen als die meisten in einem Vierteljahr, ihre Ergebnisse erzielen. Ich habe außerdem eine komplette Methodik für exponentiale Produktivität entwickelt, die ich den Teilnehmern bei diesem Event beibringe.

+

Hier sind vier der wegweisenden Elemente meines Ansatzes:

+

+

1. Die 20/20/20-Formel

+

+

Die Art, wie du deinen Tag kraftvoll startest, bestimmt, wie produktiv du ihn gestaltest. Reserviere die ersten 60 Minuten für persönliche Vorbereitung. Wie die spartanischen Krieger sagten: „Schwitze mehr beim Trainieren, dann wirst du im Krieg weniger bluten.“ Verbringe deine ersten 20 Minuten mit intensivem Sport. Das Schwitzen setzt BDNF frei, einen Botenstoff, der tatsächlich neurale Verbindungen wachsen lässt. Ein Workout produziert auch Dopamin (der Neurotransmitter für Motivation) und Serotonin, welches gute Laune macht.

+

Schau dir im nächsten 20-Minuten-Slot deinen Jahresplan an und denk gründlich über deine Ziele für dieses Quartal nach. Klarheit geht der Meisterschaft voraus, und diese Übung wird den Tag über deine Konzentration verstärken.

+

Investiere die letzten 20 Minuten dieses Morgens für regelmäßiges Lernen. Lies zum Beispiel Autobiografien großer Persönlichkeiten, hör dir einen Podcast über das Führen an oder lade dir die Lektionen des Vortages in dein Journal herunter.

+

+

2. Die 90/90/1-Regel

+

+

Allein diese Angewohnheit hat meinen Kunden einen gewaltigen Mehrwert verschafft. Kurz gesagt: Widme dich an den nächsten 90 Tagen die ersten 90 Minuten deines Arbeitstages deiner einzigen, wichtigsten Chance, der einen Sache, die, wenn du sie fehlerlos durchführst, alles aufgehen lässt.

+

Durchschnittliche Mitarbeiter kommen zur Arbeit und checken ihre Mails oder surfen im Netz. Für den echten Anführer ist die Ankunft im Büro der Beginn der Showtime. Sie begreifen, dass das Entwickeln einer Besessenheit für ihre entscheidenden paar Prioritäten legendäre Resultate freisetzt.

+

+

3. Die 60/10-Methode

+

+

Gute Studien belegen, dass die besten Athleten der Welt das nicht dafür waren, was sie in ihrem Sport leisteten, sondern wie effektiv sie sich erholten. Es waren beispielsweise die Rituale, welche die Star-Tennisspieler zwischen ihren Punkten vollzogen, die sie zu Stars machten. Was die russischen Gewichtheber so unschlagbar machte, war ihr Verhältnis von Arbeit zu Erholungspausen.

+

Also, stell dir einen Wecker auf 60 Minuten und schalte in dieser Zeitspanne alle technischen Geräte aus, schließ deine Tür und tauche mit voller Wucht in ein Projekt ein, das wichtig ist. Erhol dich dann mit einer echten Pause wie Walking, Musikhören oder Lesen. Probiere diese Vorgehensweise mal für einen Monat aus und spüre den Nutzen.

+

+

4. Finde deinen Kreis von Genies

+

+

Verhaltensforscher haben das Phänomen des „emotionalen Ansteckungseffekts“ entdeckt. Dieses beschreibt, dass wir unbewusst die Glaubenssätze, Gefühle und Verhalten der Leute übernehmen, mit denen wir die meiste Zeit verbringen.

+

Du möchtest ultra-fit werden? Einer der besten Wege dahin ist es, dich einer Laufgruppe anzuschließen oder dich mit Athleten anzufreunden. Du möchtest glücklicher sein? Dann streich die Energieräuber und Meckerer aus deinem Leben. Du bist darauf aus, eine echte Weltklassefirma aufzubauen?  Dann fang an, viel mehr Zeit mit denen zu verbringen, die das bereits getan haben. Ihre Geisteshaltung und Lebensart wird dich mit der Zeit automatisch beeinflussen. Und mit Leuten Umgang zu haben, deren Leben du gern führen würdest, zeigt dir, was alles möglich ist. Und wenn du erstmal mehr weißt, kannst du auch mehr erreichen.

+

Schlussendlich möchte ich dich noch an die Endlichkeit des Lebens erinnern. Sogar das längste ist ein relativ kurzer Ritt. Du schuldest es dem Talent, mit dem du geboren wurdest, dem Team, das du führst, der Familie, die du liebst und der Welt, die danach verlangt, dass du deine Größe zeigst, um das zu Nötige zu tun, exponentielle Produktivität zu erlangen.

+

Hoffentlich nimmst du diese von mir geteilte Methodik an, um deinem Führungspotenzial gerecht zu werden.

+

Wie immer hoffe ich, dass diese Zeilen dir echten Mehrwert geliefert haben und dich in die Lage versetzen, den Rest dieses Jahres  „in der kostbaren Luft vollständiger Meisterschaft“ zu vollenden.

+

Deine Partner sind Führungsqualität und Erfolg,

+

Dieser Artikel erschien zuerst auf Medium.com. Übersetzung: Anja Braun.

+

+ + + + + + + + + +
diff --git a/documentation/tabbar2.md b/documentation/tabbar2.md new file mode 100644 index 0000000..debb9de --- /dev/null +++ b/documentation/tabbar2.md @@ -0,0 +1,66 @@ +# Prompt für KI-Agent zur Überprüfung und Modernisierung einer bestehenden TabView mit NavigationStacks in SwiftUI (iOS 26) + +--- + +## Ziel + +Prüfe eine existierende SwiftUI-App mit bestehender `TabView` und `NavigationStacks` auf iOS 26-Konformität und korrigiere sie gegebenenfalls. Hauptpunkte: + +- Die `TabView` soll mit moderner iOS 26 Tab-API aufgebaut sein, d.h. Tabs als eigenständige `Tab`-Views und KEIN `.tabItem` mehr verwenden. +- Jeder Tab soll eine eigene `NavigationStack` mit eigenem `NavigationPath` haben, um den Navigationszustand pro Tab unabhängig zu verwalten. +- Der Tab-Auswahl-Binding (`@State`) und `.tag()`-Zuweisungen müssen korrekt gesetzt sein. +- Der neue Such-Tab soll als `Tab(role: .search)` implementiert sein mit einem eigenen Suchfeld via `.searchable()`. +- Navigationstitel, Suchfunktion und Navigationslinks müssen in der jeweiligen NavigationStack-Umgebung eingebettet sein. +- Die TabBar soll beim Tiefennavigieren in einem Tab sichtbar bleiben, außer es gibt ein explizites Ausblenden. +- Eventuelle veraltete oder falsche Patterns wie `.tabItem` oder kombiniert verwendete `NavigationView` außerhalb der Stacks sollen korrigiert werden. +- Alle Subviews sollen modular organisiert sein und keine Globalzustände die Navigation verwalten. + +--- + +## Prüffragen für den Agenten + +1. Nutzt die `TabView` die neue Form mit `Tab` als Container pro Tab? +2. Werden für jeden Tab eigene `NavigationStack`s und `NavigationPath`s verwendet? +3. Sind `.tag()` und `selection` in `TabView` korrekt implementiert? +4. Ist der Such-Tab mit `Tab(role: .search)` sauber getrennt und die Suche mit `.searchable()` eingebunden? +5. Werden veraltete `.tabItem` Modifier vollständig entfernt? +6. Bleibt die TabBar sichtbar beim Navigieren in den Stacks, außer bewusst ausgeblendet? +7. Wird State sauber und lokal in den jeweiligen Views verwaltet? +8. Gibt es keine vermischten oder redundanten NavigationView/Stacks? +9. Werden Navigationsziele übersichtlich in Subviews ausgelagert? +10. Ist der gesamte Code idiomatisch und an die iOS 26 SwiftUI-Standards angepasst? + +--- + +## Ausgabeformat + +Der Agent soll die App prüfen, Fehler auflisten, Korrekturen vorschlagen und wenn möglich direkt umgesetzten SwiftUI-Code erzeugen, der: + +- Komplette TabView mit Tabs als `Tab` +- Jeweils eigene NavigationStack mit NavigationPath +- Such-Tab mit `Tab(role: .search)` und suchbarer Navigation +- Keine `.tabItem` oder deprecated Patterns enthält +- Klar strukturiert und modular ist + +--- + +## Beispiel-Ausschnitt zur Referenz +```swift + +TabView(selection: $selectedTab) {Tab(“Home”, systemImage: “house”) {NavigationStack(path: $homePath) {HomeView()}}.tag(Tab.home) + +Tab(role: .search) { + NavigationStack(path: $searchPath) { + SearchView() + .searchable(text: $searchText) + .navigationTitle("Search") + } +} +.tag(Tab.search) + +// weitere Tabs... + +} +``` +--- +