Add MarkdownUI package and cleanup project structure

- Add swift-markdown-ui package dependency (v2.4.1)
- Remove old Logger.swift (moved to Utils/Logger.swift)
- Remove RELEASE_NOTES.md from Resources (moved to UI/Resources)
- Update German localization strings for settings sections
- Bump build version to 32
This commit is contained in:
Ilyas Hallak 2025-10-30 21:50:33 +01:00
parent 85bad35788
commit 05ae421a40
5 changed files with 57 additions and 395 deletions

View File

@ -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" */;

View File

@ -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",

View File

@ -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";

View File

@ -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<T>(_ transform: (Key) throws -> T) rethrows -> [T: Value] {
return try Dictionary<T, Value>(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
}
}

View File

@ -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