Files
Aminark 0915a144a7 fix: 修复书源导入、阅读外观和书签状态
- 修复书源文件选择、访问权限与导入预览流程,支持查看分组内的书源
- 修复背景图片、字体选择与主题切换,限制背景图片解码尺寸
- 根据已保存书签恢复当前位置状态,避免重复添加
- 补充模型、阅读器和文件选择回归测试及测试样本
- 更新应用版本为 0.1.10(build 16)

Fixes #1
2026-10-20 23:30:42 +08:00

140 lines
5.8 KiB
Swift

import CoreText
import ImageIO
import SwiftUI
import UIKit
private struct ReaderPositionPreferenceKey: PreferenceKey {
static let defaultValue: [Int: CGFloat] = [:]
static func reduce(value: inout [Int: CGFloat], nextValue: () -> [Int: CGFloat]) {
value.merge(nextValue(), uniquingKeysWith: { _, new in new })
}
}
struct ReaderStylePreset: Identifiable {
let id: String
let name: String
let background: UIColor
let foreground: UIColor
static let legadoDefaults: [ReaderStylePreset] = [
.init(id: "wechat", name: "微信读书", background: UIColor(hex: 0xC0EDC6), foreground: UIColor(hex: 0x0B0B0B)),
.init(id: "preset1", name: "预设1", background: UIColor(hex: 0xFFFFFF), foreground: UIColor(hex: 0x000000)),
.init(id: "preset2", name: "预设2", background: UIColor(hex: 0xDDC090), foreground: UIColor(hex: 0x3E3422)),
.init(id: "preset3", name: "预设3", background: UIColor(hex: 0xC2D8AA), foreground: UIColor(hex: 0x596C44)),
.init(id: "preset4", name: "预设4", background: UIColor(hex: 0xDBB8E2), foreground: UIColor(hex: 0x68516C)),
.init(id: "preset5", name: "预设5", background: UIColor(hex: 0xABCEE0), foreground: UIColor(hex: 0x3D4C54)),
.init(id: "night", name: "夜间", background: UIColor(hex: 0x000000), foreground: UIColor(hex: 0xADADAD))
]
static let eInk = ReaderStylePreset(
id: "eink",
name: "E-Ink",
background: UIColor(hex: 0xFFFFFF),
foreground: UIColor(hex: 0x000000)
)
static func resolve(_ id: String) -> ReaderStylePreset {
let migratedID = switch id {
case "dark": "night"
case "light": "preset1"
case "paper": "preset2"
default: id
}
return legadoDefaults.first(where: { $0.id == migratedID }) ?? legadoDefaults[0]
}
}
private extension UIColor {
convenience init(hex: UInt32) {
self.init(
red: CGFloat((hex >> 16) & 0xff) / 255,
green: CGFloat((hex >> 8) & 0xff) / 255,
blue: CGFloat(hex & 0xff) / 255,
alpha: 1
)
}
}
enum ReaderBackgroundStore {
static let maximumPixelSize = 2560
static var url: URL {
FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
.appendingPathComponent("Legado/reader-background.jpg")
}
static func load(from source: URL = url) -> UIImage? {
guard let data = try? Data(contentsOf: source, options: .mappedIfSafe) else { return nil }
return try? downsample(data)
}
static func save(_ data: Data, to target: URL = url) throws -> UIImage {
let image = try downsample(data)
guard let encoded = image.jpegData(compressionQuality: 0.9) else {
throw CocoaError(.fileReadCorruptFile)
}
try FileManager.default.createDirectory(at: target.deletingLastPathComponent(), withIntermediateDirectories: true)
try encoded.write(to: target, options: .atomic)
return image
}
private static func downsample(_ data: Data) throws -> UIImage {
// Decode only the size used by the reader, including backgrounds saved
// by older versions. Full-resolution camera photos can exhaust memory.
guard let source = CGImageSourceCreateWithData(data as CFData, [kCGImageSourceShouldCache: false] as CFDictionary),
let image = CGImageSourceCreateThumbnailAtIndex(source, 0, [
kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceCreateThumbnailWithTransform: true,
kCGImageSourceShouldCacheImmediately: true,
kCGImageSourceThumbnailMaxPixelSize: maximumPixelSize
] as CFDictionary) else {
throw CocoaError(.fileReadCorruptFile)
}
return UIImage(cgImage: image)
}
static func remove() { try? FileManager.default.removeItem(at: url) }
}
enum ReaderFontStore {
static var directory: URL {
FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
.appendingPathComponent("Legado/fonts", isDirectory: true)
}
static func install(from source: URL) throws -> String {
let accessing = source.startAccessingSecurityScopedResource()
defer { if accessing { source.stopAccessingSecurityScopedResource() } }
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
let target = directory.appendingPathComponent(source.lastPathComponent)
if target != source {
try? FileManager.default.removeItem(at: target)
try FileManager.default.copyItem(at: source, to: target)
}
guard font(at: target.path, size: 20, weight: 0) != nil else {
throw CocoaError(.fileReadCorruptFile)
}
return target.path
}
static func font(at path: String, size: CGFloat, weight: Int) -> UIFont? {
guard !path.isEmpty,
let provider = CGDataProvider(url: URL(fileURLWithPath: path) as CFURL),
let graphicsFont = CGFont(provider) else { return nil }
CTFontManagerRegisterGraphicsFont(graphicsFont, nil)
guard let postScriptName = graphicsFont.postScriptName else { return nil }
let name = postScriptName as String
guard let base = UIFont(name: name, size: size) else { return nil }
if weight == 1,
let descriptor = base.fontDescriptor.withSymbolicTraits(.traitBold) {
return UIFont(descriptor: descriptor, size: size)
}
if weight == 2 {
let descriptor = base.fontDescriptor.addingAttributes([
.traits: [UIFontDescriptor.TraitKey.weight: UIFont.Weight.light.rawValue]
])
return UIFont(descriptor: descriptor, size: size)
}
return base
}
}