diff --git a/readeck/Data/API/API.swift b/readeck/Data/API/API.swift index 2e6fa09..300a23e 100644 --- a/readeck/Data/API/API.swift +++ b/readeck/Data/API/API.swift @@ -153,21 +153,26 @@ class API: PAPI { } let (data, response) = try await URLSession.shared.data(for: request) - + guard let httpResponse = response as? HTTPURLResponse else { + logger.error("Invalid HTTP response for \(endpoint)") throw APIError.invalidResponse } - + guard 200...299 ~= httpResponse.statusCode else { + logger.error("Server error for \(endpoint): HTTP \(httpResponse.statusCode)") + logger.error("Response data: \(String(data: data, encoding: .utf8) ?? "Unable to decode")") handleUnauthorizedResponse(httpResponse.statusCode) throw APIError.serverError(httpResponse.statusCode) } - + // Als String dekodieren statt als JSON guard let string = String(data: data, encoding: .utf8) else { + logger.error("Unable to decode response as UTF-8 string for \(endpoint)") + logger.error("Data size: \(data.count) bytes") throw APIError.invalidResponse } - + return string } @@ -540,3 +545,16 @@ enum APIError: Error { case invalidResponse case serverError(Int) } + +extension APIError: LocalizedError { + var errorDescription: String? { + switch self { + case .invalidURL: + return "Invalid URL" + case .invalidResponse: + return "Invalid server response" + case .serverError(let statusCode): + return "Server error: HTTP \(statusCode)" + } + } +} diff --git a/readeck/Data/Mappers/DtoMapper.swift b/readeck/Data/Mappers/DtoMapper.swift new file mode 100644 index 0000000..2f959e1 --- /dev/null +++ b/readeck/Data/Mappers/DtoMapper.swift @@ -0,0 +1,12 @@ +// +// DtoMapper.swift +// readeck +// +// Created by Ilyas Hallak on 30.11.25. +// + +extension AnnotationDto { + func toDomain() -> Annotation { + Annotation(id: id, text: text, created: created, startOffset: startOffset, endOffset: endOffset, startSelector: startSelector, endSelector: endSelector) + } +} diff --git a/readeck/Data/Repository/AnnotationsRepository.swift b/readeck/Data/Repository/AnnotationsRepository.swift index 724becb..cd869f2 100644 --- a/readeck/Data/Repository/AnnotationsRepository.swift +++ b/readeck/Data/Repository/AnnotationsRepository.swift @@ -1,11 +1,17 @@ import Foundation class AnnotationsRepository: PAnnotationsRepository { + private let api: PAPI init(api: PAPI) { self.api = api } + + func createAnnotation(bookmarkId: String, color: String, startOffset: Int, endOffset: Int, startSelector: String, endSelector: String) async throws -> Annotation { + try await api.createAnnotation(bookmarkId: bookmarkId, color: color, startOffset: startOffset, endOffset: endOffset, startSelector: startSelector, endSelector: endSelector) + .toDomain() + } func fetchAnnotations(bookmarkId: String) async throws -> [Annotation] { let annotationDtos = try await api.getBookmarkAnnotations(bookmarkId: bookmarkId) @@ -25,4 +31,6 @@ class AnnotationsRepository: PAnnotationsRepository { func deleteAnnotation(bookmarkId: String, annotationId: String) async throws { try await api.deleteAnnotation(bookmarkId: bookmarkId, annotationId: annotationId) } + + } diff --git a/readeck/Data/Repository/SettingsRepository.swift b/readeck/Data/Repository/SettingsRepository.swift index c7a8fa2..63a7217 100644 --- a/readeck/Data/Repository/SettingsRepository.swift +++ b/readeck/Data/Repository/SettingsRepository.swift @@ -1,5 +1,6 @@ import Foundation import CoreData +import Kingfisher class SettingsRepository: PSettingsRepository { private let coreDataManager = CoreDataManager.shared @@ -329,4 +330,47 @@ class SettingsRepository: PSettingsRepository { } } + // MARK: - Cache Settings + + private let maxCacheSizeKey = "KingfisherMaxCacheSize" + + func getCacheSize() async throws -> UInt { + return try await withCheckedThrowingContinuation { continuation in + KingfisherManager.shared.cache.calculateDiskStorageSize { result in + switch result { + case .success(let size): + continuation.resume(returning: size) + case .failure(let error): + continuation.resume(throwing: error) + } + } + } + } + + func getMaxCacheSize() async throws -> UInt { + if let savedSize = userDefault.object(forKey: maxCacheSizeKey) as? UInt { + return savedSize + } else { + // Default: 200 MB + let defaultBytes = UInt(200 * 1024 * 1024) + userDefault.set(defaultBytes, forKey: maxCacheSizeKey) + return defaultBytes + } + } + + func updateMaxCacheSize(_ sizeInBytes: UInt) async throws { + KingfisherManager.shared.cache.diskStorage.config.sizeLimit = sizeInBytes + userDefault.set(sizeInBytes, forKey: maxCacheSizeKey) + logger.info("Updated max cache size to \(sizeInBytes) bytes") + } + + func clearCache() async throws { + return try await withCheckedThrowingContinuation { continuation in + KingfisherManager.shared.cache.clearDiskCache { + KingfisherManager.shared.cache.clearMemoryCache() + self.logger.info("Cache cleared successfully") + continuation.resume() + } + } + } } diff --git a/readeck/Domain/Protocols/PAnnotationsRepository.swift b/readeck/Domain/Protocols/PAnnotationsRepository.swift index 9078c28..bf6d82a 100644 --- a/readeck/Domain/Protocols/PAnnotationsRepository.swift +++ b/readeck/Domain/Protocols/PAnnotationsRepository.swift @@ -1,4 +1,5 @@ protocol PAnnotationsRepository { func fetchAnnotations(bookmarkId: String) async throws -> [Annotation] func deleteAnnotation(bookmarkId: String, annotationId: String) async throws + func createAnnotation(bookmarkId: String, color: String, startOffset: Int, endOffset: Int, startSelector: String, endSelector: String) async throws -> Annotation } diff --git a/readeck/Domain/Protocols/PSettingsRepository.swift b/readeck/Domain/Protocols/PSettingsRepository.swift index 978ef60..53c32ae 100644 --- a/readeck/Domain/Protocols/PSettingsRepository.swift +++ b/readeck/Domain/Protocols/PSettingsRepository.swift @@ -26,4 +26,10 @@ protocol PSettingsRepository { // Offline Settings methods func loadOfflineSettings() async throws -> OfflineSettings func saveOfflineSettings(_ settings: OfflineSettings) async throws + + // Cache Settings methods + func getCacheSize() async throws -> UInt + func getMaxCacheSize() async throws -> UInt + func updateMaxCacheSize(_ sizeInBytes: UInt) async throws + func clearCache() async throws } diff --git a/readeck/Localizations/de.lproj/Localizable.strings b/readeck/Localizations/de.lproj/Localizable.strings index 0b938f9..2b25a1a 100644 --- a/readeck/Localizations/de.lproj/Localizable.strings +++ b/readeck/Localizations/de.lproj/Localizable.strings @@ -82,6 +82,7 @@ "Favorite" = "Favorit"; "Finished reading?" = "Fertig gelesen?"; "Font" = "Schrift"; +"Highlight" = "Markierung"; "Font family" = "Schriftart"; "Font Settings" = "Schrift"; "Font size" = "Schriftgröße"; @@ -148,6 +149,8 @@ "Settings" = "Einstellungen"; "Show Performance Logs" = "Performance-Logs anzeigen"; "Show Timestamps" = "Zeitstempel anzeigen"; +"Synchronization" = "Synchronisation"; +"VPN connections are detected as active internet connections." = "VPN-Verbindungen werden als aktive Internetverbindungen erkannt."; "Speed" = "Geschwindigkeit"; "Syncing with server..." = "Synchronisiere mit Server..."; "Theme" = "Design"; diff --git a/readeck/Localizations/en.lproj/Localizable.strings b/readeck/Localizations/en.lproj/Localizable.strings index 67078d4..d6d9c08 100644 --- a/readeck/Localizations/en.lproj/Localizable.strings +++ b/readeck/Localizations/en.lproj/Localizable.strings @@ -78,6 +78,7 @@ "Favorite" = "Favorite"; "Finished reading?" = "Finished reading?"; "Font" = "Font"; +"Highlight" = "Highlight"; "Font family" = "Font family"; "Font Settings" = "Font Settings"; "Font size" = "Font size"; @@ -144,6 +145,8 @@ "Settings" = "Settings"; "Show Performance Logs" = "Show Performance Logs"; "Show Timestamps" = "Show Timestamps"; +"Synchronization" = "Synchronization"; +"VPN connections are detected as active internet connections." = "VPN connections are detected as active internet connections."; "Speed" = "Speed"; "Syncing with server..." = "Syncing with server..."; "Theme" = "Theme"; diff --git a/readeck/UI/Components/NativeWebView.swift b/readeck/UI/Components/NativeWebView.swift index ca5180b..c8b2f59 100644 --- a/readeck/UI/Components/NativeWebView.swift +++ b/readeck/UI/Components/NativeWebView.swift @@ -402,6 +402,8 @@ struct NativeWebView: View { } private func generateAnnotationOverlayJS(isDarkMode: Bool) -> String { + let highlightLabel = NSLocalizedString("Highlight", comment: "") + return """ // Create annotation color overlay (function() { @@ -456,9 +458,9 @@ struct NativeWebView: View { `; overlay.appendChild(content); - // Add "Markierung" label + // Add localized label const label = document.createElement('span'); - label.textContent = 'Markierung'; + label.textContent = '\(highlightLabel)'; label.style.cssText = ` color: black; font-size: 16px; diff --git a/readeck/UI/Components/WebView.swift b/readeck/UI/Components/WebView.swift index 0be285a..8293d2f 100644 --- a/readeck/UI/Components/WebView.swift +++ b/readeck/UI/Components/WebView.swift @@ -410,6 +410,7 @@ struct WebView: UIViewRepresentable { let greenColor = AnnotationColor.green.cssColor(isDark: isDarkMode) let blueColor = AnnotationColor.blue.cssColor(isDark: isDarkMode) let redColor = AnnotationColor.red.cssColor(isDark: isDarkMode) + let highlightLabel = NSLocalizedString("Highlight", comment: "") return """ // Create annotation color overlay @@ -465,9 +466,9 @@ struct WebView: UIViewRepresentable { `; overlay.appendChild(content); - // Add "Markierung" label + // Add localized label const label = document.createElement('span'); - label.textContent = 'Markierung'; + label.textContent = '\(highlightLabel)'; label.style.cssText = ` color: black; font-size: 16px; diff --git a/readeck/UI/Resources/RELEASE_NOTES.md b/readeck/UI/Resources/RELEASE_NOTES.md index 767653a..25e3830 100644 --- a/readeck/UI/Resources/RELEASE_NOTES.md +++ b/readeck/UI/Resources/RELEASE_NOTES.md @@ -4,6 +4,53 @@ Thanks for using the Readeck iOS app! Below are the release notes for each versi **AppStore:** The App is now in the App Store! [Get it here](https://apps.apple.com/de/app/readeck/id6748764703) for all TestFlight users. If you wish a more stable Version, please download it from there. Or you can continue using TestFlight for the latest features. +## Version 2.0.0 + +### Offline Reading + +- **Read your articles without internet connection** - the feature you've been waiting for! +- Automatic background sync keeps your favorite articles cached +- Choose how many articles to cache (up to 200) +- Cache syncs automatically every 4 hours +- Manual sync button for instant updates +- Smart FIFO cleanup automatically removes old cached articles +- Article images are pre-downloaded for offline viewing +- Cached articles load instantly, even without network + +### Smart Network Monitoring + +- **Automatic offline detection** with reliable network monitoring +- Visual indicator shows when you're offline +- App automatically loads cached articles when offline +- Cache-first loading for instant article access +- Improved VPN handling without false-positives +- Network status checks interface availability for accuracy + +### Offline Settings + +- **New dedicated offline settings screen** +- Enable or disable offline mode +- Adjust number of cached articles with slider +- View last sync timestamp +- Manual sync button +- Toggle settings work instantly + +### Performance & Architecture + +- Clean architecture with dedicated cache repository layer +- Efficient CoreData integration for cached content +- Kingfisher image prefetching for smooth offline experience +- Background sync doesn't block app startup +- Reactive updates with Combine framework + +### Developer Features (DEBUG) + +- Offline mode simulation toggle for testing +- Detailed sync logging for troubleshooting +- Visual debug banner (green=online, red=offline) + +--- + ## Version 1.2.0 ### Annotations & Highlighting