mirror of
https://github.com/Aminark/legado-ios.git
synced 2026-10-24 21:01:22 +08:00
feat: 支持 EPUB 阅读并改善规则兼容与阅读稳定性
支持 EPUB 2/3 导入、目录、封面和章节插图,保留本地书管理与阅读进度。 修复 JSON 模板、动态替换与请求参数兼容,以及 RSS 翻页和连续滚动定位。 更新编辑器工具栏和回归测试,发布版本为 0.1.12(build 18)。
This commit is contained in:
@@ -32,7 +32,7 @@ public struct StagedLocalFile: Sendable {
|
||||
public actor LocalFileService {
|
||||
public let directory: URL
|
||||
|
||||
public init(directory: URL) { self.directory = directory.standardizedFileURL }
|
||||
public init(directory: URL) { self.directory = URL(fileURLWithPath: directory.path, isDirectory: true).standardizedFileURL }
|
||||
|
||||
public func list() throws -> [LocalFileEntry] {
|
||||
guard FileManager.default.fileExists(atPath: directory.path) else { return [] }
|
||||
@@ -41,13 +41,13 @@ public actor LocalFileService {
|
||||
options: [.skipsHiddenFiles]
|
||||
).compactMap { url in
|
||||
let values = try url.resourceValues(forKeys: [.isRegularFileKey, .isSymbolicLinkKey, .fileSizeKey, .contentModificationDateKey])
|
||||
guard values.isRegularFile == true, values.isSymbolicLink != true, url.pathExtension.lowercased() == "txt" else { return nil }
|
||||
guard values.isRegularFile == true, values.isSymbolicLink != true, LocalBookService.supports(url) else { return nil }
|
||||
return .init(url: url, bytes: Int64(values.fileSize ?? 0), modified: values.contentModificationDate)
|
||||
}.sorted { $0.url.lastPathComponent.localizedStandardCompare($1.url.lastPathComponent) == .orderedAscending }
|
||||
}
|
||||
|
||||
public func importFile(from source: URL) throws -> URL {
|
||||
guard source.pathExtension.lowercased() == "txt" else { throw LocalBookError.unsupportedFormat }
|
||||
guard LocalBookService.supports(source) else { throw LocalBookError.unsupportedFormat }
|
||||
let values = try source.resourceValues(forKeys: [.isRegularFileKey])
|
||||
guard values.isRegularFile == true else { throw LocalFileError.missingFile }
|
||||
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
|
||||
@@ -55,7 +55,7 @@ public actor LocalFileService {
|
||||
var target = directory.appendingPathComponent(source.lastPathComponent)
|
||||
var suffix = 2
|
||||
while FileManager.default.fileExists(atPath: target.path) {
|
||||
target = directory.appendingPathComponent("\(stem) (\(suffix)).txt")
|
||||
target = directory.appendingPathComponent("\(stem) (\(suffix)).\(source.pathExtension)")
|
||||
suffix += 1
|
||||
}
|
||||
try FileManager.default.copyItem(at: source, to: target)
|
||||
@@ -69,7 +69,8 @@ public actor LocalFileService {
|
||||
!trimmed.contains(where: { $0 == "/" || $0 == "\\" || $0.unicodeScalars.contains(where: CharacterSet.controlCharacters.contains) }) else {
|
||||
throw LocalFileError.invalidName
|
||||
}
|
||||
let filename = trimmed.lowercased().hasSuffix(".txt") ? trimmed : trimmed + ".txt"
|
||||
let suffix = "." + source.pathExtension
|
||||
let filename = trimmed.lowercased().hasSuffix(suffix.lowercased()) ? trimmed : trimmed + suffix
|
||||
guard filename.utf8.count <= 255 else { throw LocalFileError.invalidName }
|
||||
let target = directory.appendingPathComponent(filename)
|
||||
if source.standardizedFileURL == target.standardizedFileURL { return source }
|
||||
@@ -83,7 +84,7 @@ public actor LocalFileService {
|
||||
guard FileManager.default.fileExists(atPath: source.path) else { return nil }
|
||||
let trash = directory.appendingPathComponent(".Trash", isDirectory: true)
|
||||
try FileManager.default.createDirectory(at: trash, withIntermediateDirectories: true)
|
||||
let staged = trash.appendingPathComponent(UUID().uuidString + ".txt")
|
||||
let staged = trash.appendingPathComponent(UUID().uuidString + "." + source.pathExtension)
|
||||
try FileManager.default.moveItem(at: source, to: staged)
|
||||
return .init(original: source, staged: staged)
|
||||
}
|
||||
@@ -104,7 +105,7 @@ public actor LocalFileService {
|
||||
}
|
||||
|
||||
private func validate(_ url: URL, allowMissing: Bool = false) throws {
|
||||
guard url.isFileURL, url.pathExtension.lowercased() == "txt",
|
||||
guard url.isFileURL, LocalBookService.supports(url),
|
||||
url.standardizedFileURL.deletingLastPathComponent() == directory,
|
||||
url.resolvingSymlinksInPath().deletingLastPathComponent() == directory.resolvingSymlinksInPath() else {
|
||||
throw LocalFileError.outsideLibrary
|
||||
@@ -131,7 +132,7 @@ public actor BookFileExporter {
|
||||
}
|
||||
|
||||
public func originalFile(_ source: URL, filename: String) throws -> BookFileExport {
|
||||
let target = try destination(filename: filename)
|
||||
let target = try destination(filename: filename, extension: source.pathExtension.lowercased())
|
||||
do {
|
||||
try FileManager.default.copyItem(at: source, to: target)
|
||||
return .init(url: target, cachedChapters: 0, totalChapters: 0)
|
||||
@@ -178,11 +179,11 @@ public actor BookFileExporter {
|
||||
try FileManager.default.removeItem(at: export.url.deletingLastPathComponent())
|
||||
}
|
||||
|
||||
private func destination(filename: String) throws -> URL {
|
||||
private func destination(filename: String, extension fileExtension: String = "txt") throws -> URL {
|
||||
let folder = directory.appendingPathComponent(UUID().uuidString, isDirectory: true)
|
||||
try FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true)
|
||||
let safe = filename.components(separatedBy: CharacterSet(charactersIn: "/\\:").union(.controlCharacters)).joined(separator: "_")
|
||||
let stem = String(safe.replacingOccurrences(of: ".txt", with: "", options: [.caseInsensitive, .anchored, .backwards]).prefix(70))
|
||||
return folder.appendingPathComponent((stem.isEmpty ? "书籍" : stem) + ".txt")
|
||||
let stem = String(safe.replacingOccurrences(of: "." + fileExtension, with: "", options: [.caseInsensitive, .anchored, .backwards]).prefix(70))
|
||||
return folder.appendingPathComponent((stem.isEmpty ? "书籍" : stem) + "." + fileExtension)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user