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:
parent
85bad35788
commit
05ae421a40
@ -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" */;
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user