mirror of
https://github.com/Aminark/legado-ios.git
synced 2026-10-24 04:41:23 +08:00
feat: 支持 EPUB 阅读并改善规则兼容与阅读稳定性
支持 EPUB 2/3 导入、目录、封面和章节插图,保留本地书管理与阅读进度。 修复 JSON 模板、动态替换与请求参数兼容,以及 RSS 翻页和连续滚动定位。 更新编辑器工具栏和回归测试,发布版本为 0.1.12(build 18)。
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
import ZIPFoundation
|
||||
import LegadoCore
|
||||
@testable import LegadoAppModels
|
||||
|
||||
@MainActor
|
||||
struct EPUBReadingRegressionTests {
|
||||
private func fixture(at root: URL) throws -> URL {
|
||||
let url = root.appendingPathComponent("示例.epub")
|
||||
let archive = try Archive(url: url, accessMode: .create)
|
||||
let files = [
|
||||
("mimetype", "application/epub+zip"),
|
||||
("META-INF/container.xml", #"<container><rootfiles><rootfile full-path="book.opf"/></rootfiles></container>"#),
|
||||
("book.opf", #"<package><metadata xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:title>回归书籍</dc:title><dc:creator>测试作者</dc:creator></metadata><manifest><item id="first" href="first.xhtml" media-type="application/xhtml+xml"/><item id="last" href="last.xhtml" media-type="application/xhtml+xml"/></manifest><spine><itemref idref="first"/><itemref idref="last"/></spine></package>"#),
|
||||
("first.xhtml", "<html><head><title>第一章</title></head><body><h1>第一章</h1><p>第一章测试正文</p></body></html>"),
|
||||
("last.xhtml", "<html><head><title>第二章</title></head><body><h1>第二章</h1><p>第二章测试正文</p></body></html>")
|
||||
]
|
||||
for (path, value) in files {
|
||||
let data = Data(value.utf8)
|
||||
try archive.addEntry(with: path, type: .file, uncompressedSize: Int64(data.count), compressionMethod: .deflate) { position, size in
|
||||
data.subdata(in: Int(position)..<Int(position) + size)
|
||||
}
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
@Test func importedEPUBReadsNavigatesAndResumesAfterRenamingAndReloadingShelf() async throws {
|
||||
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
|
||||
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
|
||||
defer { try? FileManager.default.removeItem(at: root) }
|
||||
let url = try fixture(at: root)
|
||||
let repository = InMemoryBookRepository()
|
||||
let managed = root.appendingPathComponent("managed", isDirectory: true)
|
||||
let library = BookLibraryModel(repository: repository, localDirectory: managed, chapterRepository: InMemoryBookChapterRepository())
|
||||
await library.load()
|
||||
await library.importLocalFiles([url])
|
||||
#expect(library.errorMessage == nil)
|
||||
let book = try #require(library.books.first)
|
||||
let shelfReader = BookshelfReadViewModel()
|
||||
await shelfReader.load(book: book, source: nil)
|
||||
#expect(shelfReader.errorMessage == nil && shelfReader.chapters.count == 2)
|
||||
let reader = ReaderViewModel(book: book, chapters: shelfReader.chapters, initialIndex: 0, cache: .init(directory: root.appendingPathComponent("cache")))
|
||||
#expect(!reader.canEditContent && !reader.usesReplaceRules)
|
||||
await reader.load(rules: [])
|
||||
#expect(reader.content == "第一章测试正文")
|
||||
await reader.prepareContinuousReading(rules: [])
|
||||
#expect(reader.scrollChapters.map(\.content) == ["第一章测试正文", "第二章测试正文"])
|
||||
reader.goNext()
|
||||
await reader.load(rules: [])
|
||||
#expect(reader.content == "第二章测试正文")
|
||||
await library.updateProgress(for: book, chapter: try #require(reader.currentChapter), position: 4)
|
||||
try await library.renameLocalFile(at: URL(string: book.bookUrl)!, to: "改名")
|
||||
let reloaded = BookLibraryModel(repository: repository, localDirectory: managed, chapterRepository: InMemoryBookChapterRepository())
|
||||
await reloaded.load()
|
||||
let saved = try #require(reloaded.books.first)
|
||||
#expect(saved.name == "回归书籍" && saved.originName == "改名.epub")
|
||||
#expect(saved.durChapterIndex == 1 && saved.durChapterPos == 4)
|
||||
let chapters = try LocalBookService().loadChapters(book: saved).chapters
|
||||
let resumed = ReaderViewModel(book: saved, chapters: chapters, initialIndex: saved.durChapterIndex)
|
||||
await resumed.load(rules: [])
|
||||
#expect(resumed.currentIndex == 1 && resumed.requestedPosition == 4)
|
||||
#expect(resumed.content == "第二章测试正文")
|
||||
#expect(try await reloaded.localFiles.list().count == 1)
|
||||
}
|
||||
|
||||
@Test func corruptEPUBImportRollsBackFileAndContinuesOtherImports() async throws {
|
||||
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
|
||||
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
|
||||
defer { try? FileManager.default.removeItem(at: root) }
|
||||
let good = try fixture(at: root)
|
||||
let bad = root.appendingPathComponent("损坏.epub")
|
||||
try Data("bad epub".utf8).write(to: bad)
|
||||
let library = BookLibraryModel(repository: InMemoryBookRepository(), localDirectory: root.appendingPathComponent("managed", isDirectory: true), chapterRepository: InMemoryBookChapterRepository())
|
||||
await library.load()
|
||||
await library.importLocalFiles([bad, good])
|
||||
#expect(library.books.count == 1)
|
||||
#expect(library.errorMessage?.contains("损坏.epub") == true)
|
||||
#expect(try await library.localFiles.list().map { $0.url.lastPathComponent } == ["示例.epub"])
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user