diff --git a/readeck.xcodeproj/project.pbxproj b/readeck.xcodeproj/project.pbxproj index d544772..9b5f902 100644 --- a/readeck.xcodeproj/project.pbxproj +++ b/readeck.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ 5D2B7FB92DFA27A400EBDB2B /* URLShare.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 5D2B7FAF2DFA27A400EBDB2B /* URLShare.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 5D348CC32E0C9F4F00D0AF21 /* netfox in Frameworks */ = {isa = PBXBuildFile; productRef = 5D348CC22E0C9F4F00D0AF21 /* netfox */; }; + 5D48E6022EB402F50043F90F /* MarkdownUI in Frameworks */ = {isa = PBXBuildFile; productRef = 5D48E6012EB402F50043F90F /* MarkdownUI */; }; 5D9D95492E623668009AF769 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 5D9D95482E623668009AF769 /* Kingfisher */; }; 5DA241FB2E17C3B3007531C3 /* RswiftLibrary in Frameworks */ = {isa = PBXBuildFile; productRef = 5DA241FA2E17C3B3007531C3 /* RswiftLibrary */; }; /* End PBXBuildFile section */ @@ -86,7 +87,6 @@ Data/Utils/LabelUtils.swift, Domain/Model/Bookmark.swift, Domain/Model/BookmarkLabel.swift, - Logger.swift, readeck.xcdatamodeld, Splash.storyboard, UI/Components/Constants.swift, @@ -94,6 +94,7 @@ UI/Components/TagManagementView.swift, UI/Components/UnifiedLabelChip.swift, UI/Utils/NotificationNames.swift, + Utils/Logger.swift, ); target = 5D2B7FAE2DFA27A400EBDB2B /* URLShare */; }; @@ -151,6 +152,7 @@ 5D9D95492E623668009AF769 /* Kingfisher in Frameworks */, 5D348CC32E0C9F4F00D0AF21 /* netfox in Frameworks */, 5DA241FB2E17C3B3007531C3 /* RswiftLibrary in Frameworks */, + 5D48E6022EB402F50043F90F /* MarkdownUI in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -242,6 +244,7 @@ 5D348CC22E0C9F4F00D0AF21 /* netfox */, 5DA241FA2E17C3B3007531C3 /* RswiftLibrary */, 5D9D95482E623668009AF769 /* Kingfisher */, + 5D48E6012EB402F50043F90F /* MarkdownUI */, ); productName = readeck; productReference = 5D45F9C82DF858680048D5B8 /* readeck.app */; @@ -333,6 +336,7 @@ 5D348CC12E0C9F4F00D0AF21 /* XCRemoteSwiftPackageReference "netfox" */, 5DA241F92E17C3B3007531C3 /* XCRemoteSwiftPackageReference "R.swift" */, 5D9D95472E623668009AF769 /* XCRemoteSwiftPackageReference "Kingfisher" */, + 5D48E6002EB402F50043F90F /* XCRemoteSwiftPackageReference "swift-markdown-ui" */, ); preferredProjectObjectVersion = 77; productRefGroup = 5D45F9C92DF858680048D5B8 /* Products */; @@ -437,7 +441,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = URLShare/URLShare.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 31; + CURRENT_PROJECT_VERSION = 32; DEVELOPMENT_TEAM = 8J69P655GN; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = URLShare/Info.plist; @@ -470,7 +474,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = URLShare/URLShare.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 31; + CURRENT_PROJECT_VERSION = 32; DEVELOPMENT_TEAM = 8J69P655GN; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = URLShare/Info.plist; @@ -625,7 +629,7 @@ CODE_SIGN_ENTITLEMENTS = readeck/readeck.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 31; + CURRENT_PROJECT_VERSION = 32; DEVELOPMENT_ASSET_PATHS = "\"readeck/Preview Content\""; DEVELOPMENT_TEAM = 8J69P655GN; ENABLE_HARDENED_RUNTIME = YES; @@ -669,7 +673,7 @@ CODE_SIGN_ENTITLEMENTS = readeck/readeck.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 31; + CURRENT_PROJECT_VERSION = 32; DEVELOPMENT_ASSET_PATHS = "\"readeck/Preview Content\""; DEVELOPMENT_TEAM = 8J69P655GN; ENABLE_HARDENED_RUNTIME = YES; @@ -854,6 +858,14 @@ minimumVersion = 1.21.0; }; }; + 5D48E6002EB402F50043F90F /* XCRemoteSwiftPackageReference "swift-markdown-ui" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/gonzalezreal/swift-markdown-ui"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.4.1; + }; + }; 5D9D95472E623668009AF769 /* XCRemoteSwiftPackageReference "Kingfisher" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/onevcat/Kingfisher.git"; @@ -878,6 +890,11 @@ package = 5D348CC12E0C9F4F00D0AF21 /* XCRemoteSwiftPackageReference "netfox" */; productName = netfox; }; + 5D48E6012EB402F50043F90F /* MarkdownUI */ = { + isa = XCSwiftPackageProductDependency; + package = 5D48E6002EB402F50043F90F /* XCRemoteSwiftPackageReference "swift-markdown-ui" */; + productName = MarkdownUI; + }; 5D9D95482E623668009AF769 /* Kingfisher */ = { isa = XCSwiftPackageProductDependency; package = 5D9D95472E623668009AF769 /* XCRemoteSwiftPackageReference "Kingfisher" */; diff --git a/readeck.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/readeck.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 671df69..f42a4fd 100644 --- a/readeck.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/readeck.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "3d745f8bc704b9a02b7c5a0c9f0ca6d05865f6fa0a02ec3b2734e9c398279457", + "originHash" : "77d424216eb5411f97bf8ee011ef543bf97f05ec343dfe49b8c22bc78da99635", "pins" : [ { "identity" : "kingfisher", @@ -19,6 +19,15 @@ "version" : "1.21.0" } }, + { + "identity" : "networkimage", + "kind" : "remoteSourceControl", + "location" : "https://github.com/gonzalezreal/NetworkImage", + "state" : { + "revision" : "2849f5323265386e200484b0d0f896e73c3411b9", + "version" : "6.0.1" + } + }, { "identity" : "r.swift", "kind" : "remoteSourceControl", @@ -37,6 +46,24 @@ "version" : "1.6.1" } }, + { + "identity" : "swift-cmark", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-cmark", + "state" : { + "revision" : "5d9bdaa4228b381639fff09403e39a04926e2dbe", + "version" : "0.7.1" + } + }, + { + "identity" : "swift-markdown-ui", + "kind" : "remoteSourceControl", + "location" : "https://github.com/gonzalezreal/swift-markdown-ui", + "state" : { + "revision" : "5f613358148239d0292c0cef674a3c2314737f9e", + "version" : "2.4.1" + } + }, { "identity" : "xcodeedit", "kind" : "remoteSourceControl", diff --git a/readeck/Localizations/de.lproj/Localizable.strings b/readeck/Localizations/de.lproj/Localizable.strings index 7120e61..f2253a1 100644 --- a/readeck/Localizations/de.lproj/Localizable.strings +++ b/readeck/Localizations/de.lproj/Localizable.strings @@ -40,11 +40,11 @@ "Tags" = "Labels"; /* Settings Sections */ -"Font Settings" = "Schriftart-Einstellungen"; +"Font Settings" = "Schriftart"; "Appearance" = "Darstellung"; -"Cache Settings" = "Cache-Einstellungen"; -"General Settings" = "Allgemeine Einstellungen"; -"Server Settings" = "Server-Einstellungen"; +"Cache Settings" = "Cache"; +"General Settings" = "Allgemein"; +"Server Settings" = "Server"; "Server Connection" = "Server-Verbindung"; "Open external links in" = "Öffne externe Links in"; "In App Browser" = "In App Browser"; @@ -67,7 +67,7 @@ "Critical" = "Kritisch"; "Debug" = "Debug"; "DEBUG BUILD" = "DEBUG BUILD"; -"Debug Settings" = "Debug-Einstellungen"; +"Debug Settings" = "Debug"; "Delete" = "Löschen"; "Delete Bookmark" = "Lesezeichen löschen"; "Developer: Ilyas Hallak" = "Entwickler: Ilyas Hallak"; @@ -80,13 +80,13 @@ "Finished reading?" = "Fertig gelesen?"; "Font" = "Schrift"; "Font family" = "Schriftart"; -"Font Settings" = "Schrift-Einstellungen"; +"Font Settings" = "Schrift"; "Font size" = "Schriftgröße"; "From Bremen with 💚" = "Aus Bremen mit 💚"; "General" = "Allgemein"; "Global Level" = "Globales Level"; "Global Minimum Level" = "Globales Minimum-Level"; -"Global Settings" = "Globale Einstellungen"; +"Global Settings" = "Global"; "https://example.com" = "https://example.com"; "https://readeck.example.com" = "https://readeck.example.com"; "Include Source Location" = "Quellort einschließen"; diff --git a/readeck/Logger.swift b/readeck/Logger.swift deleted file mode 100644 index cc8f6c1..0000000 --- a/readeck/Logger.swift +++ /dev/null @@ -1,262 +0,0 @@ -// -// Logger.swift -// readeck -// -// Created by Ilyas Hallak on 16.08.25. -// - -import Foundation -import os - -// MARK: - Log Configuration - -enum LogLevel: Int, CaseIterable { - case debug = 0 - case info = 1 - case notice = 2 - case warning = 3 - case error = 4 - case critical = 5 - - var emoji: String { - switch self { - case .debug: return "🔍" - case .info: return "ℹ️" - case .notice: return "📢" - case .warning: return "⚠️" - case .error: return "❌" - case .critical: return "💥" - } - } -} - -enum LogCategory: String, CaseIterable { - case network = "Network" - case ui = "UI" - case data = "Data" - case auth = "Authentication" - case performance = "Performance" - case general = "General" - case manual = "Manual" - case viewModel = "ViewModel" -} - -class LogConfiguration: ObservableObject { - static let shared = LogConfiguration() - - @Published private var categoryLevels: [LogCategory: LogLevel] = [:] - @Published var globalMinLevel: LogLevel = .debug - @Published var showPerformanceLogs = true - @Published var showTimestamps = true - @Published var includeSourceLocation = true - - private init() { - loadConfiguration() - } - - func setLevel(_ level: LogLevel, for category: LogCategory) { - categoryLevels[category] = level - saveConfiguration() - } - - func getLevel(for category: LogCategory) -> LogLevel { - return categoryLevels[category] ?? globalMinLevel - } - - func shouldLog(_ level: LogLevel, for category: LogCategory) -> Bool { - let categoryLevel = getLevel(for: category) - return level.rawValue >= categoryLevel.rawValue - } - - private func loadConfiguration() { - // Load from UserDefaults - if let data = UserDefaults.standard.data(forKey: "LogConfiguration"), - let config = try? JSONDecoder().decode([String: Int].self, from: data) { - for (categoryString, levelInt) in config { - if let category = LogCategory(rawValue: categoryString), - let level = LogLevel(rawValue: levelInt) { - categoryLevels[category] = level - } - } - } - - globalMinLevel = LogLevel(rawValue: UserDefaults.standard.integer(forKey: "LogGlobalLevel")) ?? .debug - showPerformanceLogs = UserDefaults.standard.bool(forKey: "LogShowPerformance") - showTimestamps = UserDefaults.standard.bool(forKey: "LogShowTimestamps") - includeSourceLocation = UserDefaults.standard.bool(forKey: "LogIncludeSourceLocation") - } - - private func saveConfiguration() { - let config = categoryLevels.mapKeys { $0.rawValue }.mapValues { $0.rawValue } - if let data = try? JSONEncoder().encode(config) { - UserDefaults.standard.set(data, forKey: "LogConfiguration") - } - - UserDefaults.standard.set(globalMinLevel.rawValue, forKey: "LogGlobalLevel") - UserDefaults.standard.set(showPerformanceLogs, forKey: "LogShowPerformance") - UserDefaults.standard.set(showTimestamps, forKey: "LogShowTimestamps") - UserDefaults.standard.set(includeSourceLocation, forKey: "LogIncludeSourceLocation") - } -} - -struct Logger { - private let logger: os.Logger - private let category: LogCategory - private let config = LogConfiguration.shared - - init(subsystem: String = Bundle.main.bundleIdentifier ?? "com.romm.app", category: LogCategory) { - self.logger = os.Logger(subsystem: subsystem, category: category.rawValue) - self.category = category - } - - // MARK: - Log Levels - - func debug(_ message: String, file: String = #file, function: String = #function, line: Int = #line) { - guard config.shouldLog(.debug, for: category) else { return } - let formattedMessage = formatMessage(message, level: .debug, file: file, function: function, line: line) - logger.debug("\(formattedMessage)") - } - - func info(_ message: String, file: String = #file, function: String = #function, line: Int = #line) { - guard config.shouldLog(.info, for: category) else { return } - let formattedMessage = formatMessage(message, level: .info, file: file, function: function, line: line) - logger.info("\(formattedMessage)") - } - - func notice(_ message: String, file: String = #file, function: String = #function, line: Int = #line) { - guard config.shouldLog(.notice, for: category) else { return } - let formattedMessage = formatMessage(message, level: .notice, file: file, function: function, line: line) - logger.notice("\(formattedMessage)") - } - - func warning(_ message: String, file: String = #file, function: String = #function, line: Int = #line) { - guard config.shouldLog(.warning, for: category) else { return } - let formattedMessage = formatMessage(message, level: .warning, file: file, function: function, line: line) - logger.warning("\(formattedMessage)") - } - - func error(_ message: String, file: String = #file, function: String = #function, line: Int = #line) { - guard config.shouldLog(.error, for: category) else { return } - let formattedMessage = formatMessage(message, level: .error, file: file, function: function, line: line) - logger.error("\(formattedMessage)") - } - - func critical(_ message: String, file: String = #file, function: String = #function, line: Int = #line) { - guard config.shouldLog(.critical, for: category) else { return } - let formattedMessage = formatMessage(message, level: .critical, file: file, function: function, line: line) - logger.critical("\(formattedMessage)") - } - - // MARK: - Convenience Methods - - func logNetworkRequest(method: String, url: String, statusCode: Int? = nil) { - guard config.shouldLog(.info, for: category) else { return } - if let statusCode = statusCode { - info("🌐 \(method) \(url) - Status: \(statusCode)") - } else { - info("🌐 \(method) \(url)") - } - } - - func logNetworkError(method: String, url: String, error: Error) { - guard config.shouldLog(.error, for: category) else { return } - self.error("❌ \(method) \(url) - Error: \(error.localizedDescription)") - } - - func logPerformance(_ operation: String, duration: TimeInterval) { - guard config.showPerformanceLogs && config.shouldLog(.info, for: category) else { return } - info("⏱️ \(operation) completed in \(String(format: "%.3f", duration))s") - } - - // MARK: - Private Helpers - - private func formatMessage(_ message: String, level: LogLevel, file: String, function: String, line: Int) -> String { - var components: [String] = [] - - if config.showTimestamps { - let timestamp = DateFormatter.logTimestamp.string(from: Date()) - components.append(timestamp) - } - - components.append(level.emoji) - components.append("[\(category.rawValue)]") - - if config.includeSourceLocation { - components.append("[\(sourceFileName(filePath: file)):\(line)]") - components.append(function) - } - - components.append("-") - components.append(message) - - return components.joined(separator: " ") - } - - private func sourceFileName(filePath: String) -> String { - return URL(fileURLWithPath: filePath).lastPathComponent.replacingOccurrences(of: ".swift", with: "") - } -} - -// MARK: - Category-specific Loggers - -extension Logger { - static let network = Logger(category: .network) - static let ui = Logger(category: .ui) - static let data = Logger(category: .data) - static let auth = Logger(category: .auth) - static let performance = Logger(category: .performance) - static let general = Logger(category: .general) - static let manual = Logger(category: .manual) - static let viewModel = Logger(category: .viewModel) -} - -// MARK: - Performance Measurement Helper - -struct PerformanceMeasurement { - private let startTime = CFAbsoluteTimeGetCurrent() - private let operation: String - private let logger: Logger - - init(operation: String, logger: Logger = .performance) { - self.operation = operation - self.logger = logger - logger.debug("🚀 Starting \(operation)") - } - - func end() { - let duration = CFAbsoluteTimeGetCurrent() - startTime - logger.logPerformance(operation, duration: duration) - } -} - -// MARK: - DateFormatter Extension - -extension DateFormatter { - static let logTimestamp: DateFormatter = { - let formatter = DateFormatter() - formatter.dateFormat = "HH:mm:ss.SSS" - - return formatter - }() -} - -// MARK: - Dictionary Extension - -extension Dictionary { - func mapKeys(_ transform: (Key) throws -> T) rethrows -> [T: Value] { - return try Dictionary(uniqueKeysWithValues: map { (try transform($0.key), $0.value) }) - } -} - -// MARK: - Debug Build Detection - -extension Bundle { - var isDebugBuild: Bool { - #if DEBUG - return true - #else - return false - #endif - } -} - diff --git a/readeck/Resources/RELEASE_NOTES.md b/readeck/Resources/RELEASE_NOTES.md deleted file mode 100644 index bbc938a..0000000 --- a/readeck/Resources/RELEASE_NOTES.md +++ /dev/null @@ -1,120 +0,0 @@ -# Release Notes - -Thanks for using the Readeck iOS app! Below are the release notes for each version. - -**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 1.2.0 - -### Annotations & Highlighting - -- **Highlight important passages** directly in your articles -- Select text to bring up a beautiful color picker overlay -- Choose from four distinct colors: yellow, green, blue, and red -- Your highlights are saved and synced across devices -- Tap on annotations in the list to jump directly to that passage in the article -- Glass morphism design for a modern, elegant look - -### Performance Improvements - -- **Dramatically faster label loading** - especially with 1000+ labels -- Labels now load instantly from local cache, then sync in background -- Optimized label management to prevent crashes and lag -- Share Extension now loads labels without delay -- Reduced memory usage when working with large label collections -- Better offline support - labels always available even without internet - -### Fixes & Improvements - -- Centralized color management for consistent appearance -- Improved annotation creation workflow -- Better text selection handling in article view -- Implemented lazy loading for label lists -- Switched to Core Data as primary source for labels -- Batch operations for faster database queries -- Background sync to keep labels up-to-date without blocking the UI -- Fixed duplicate ID warnings in label lists - ---- - -## Version 1.1.0 - -There is a lot of feature reqeusts and improvements in this release which are based on your feedback. Thank you so much for that! If you like the new features, please consider leaving a review on the App Store to support further development. - -### Modern Reading Experience (iOS 26+) - -- **Completely rebuilt article view** for the latest iOS version -- Smoother scrolling and faster page loading -- Better battery life and memory usage -- Native iOS integration for the best experience - -### Quick Actions - -- **Smart action buttons** appear automatically when you're almost done reading -- Beautiful, modern design that blends with your content -- Quickly favorite or archive articles without scrolling back up -- Buttons fade away elegantly when you scroll back -- Your progress bar now reflects the entire article length - -### Beautiful Article Images - -- **Article header images now display properly** without awkward cropping -- Full images with a subtle blurred background -- Tap to view images in full screen - -### Smoother Performance - -- **Dramatically improved scrolling** - no more stuttering or lag -- Faster article loading times -- Better handling of long articles with many images -- Overall snappier app experience - -### Open Links Your Way - -- **Choose your preferred browser** for opening links -- Open in Safari or in-app browser -- Thanks to christian-putzke for this contribution! - -### Fixes & Improvements - -- Articles no longer overflow the screen width -- Fixed spacing issues in article view -- Improved progress calculation accuracy -- Better handling of article content -- Fixed issues with label names containing spaces - ---- - -## Version 1.0 (Initial Release) - -### Core Features - -- Browse and read saved articles -- Bookmark management with labels -- Full article view with custom fonts -- Text-to-speech support (Beta) -- Archive and favorite functionality -- Choose different Layouts (Compact, Magazine, Natural) - -### Reading Experience - -- Clean, distraction-free reading interface -- Customizable font settings -- Header Image viewer with zoom support -- Progress tracking per article -- Dark mode support - -### Organization - -- Label system for categorization (multi-select) -- Search -- Archive completed articles -- Jump to last read position - -### Share Extension - -- Save articles from other apps -- Quick access to save and label bookmarks -- Save Bookmarks offline if your server is not reachable and sync later - -