fix: 修复书源导入、阅读外观和书签状态

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

Fixes #1
This commit is contained in:
Aminark
2026-10-20 23:30:42 +08:00
parent 0bf332441d
commit 0915a144a7
17 changed files with 903 additions and 101 deletions
@@ -207,6 +207,30 @@ struct ReaderRegressionTests {
#expect(bookmark.resolvedChapterIndex(in: chapters) == nil)
}
@Test func bookmarkSelectionUsesSavedPositionAndCurrentPageAfterReopening() {
let book = Book(bookUrl: "demo://bookmarks", name: "书", author: "作者")
let chapters = (0..<2).map { BookChapter(url: "demo://\($0)", title: "第\($0)章", bookUrl: book.bookUrl, index: $0) }
var saved = Bookmark(time: 1, bookName: book.name, bookAuthor: book.author)
saved.chapterIndex = 0
saved.chapterName = chapters[0].title
saved.chapterPosition = 40
var otherBook = saved
otherBook.bookAuthor = "其他作者"
var otherChapter = saved
otherChapter.chapterIndex = 1
otherChapter.chapterName = chapters[1].title
let bookmarks = [saved, otherBook, otherChapter]
let reopened = ReaderViewModel(demoBook: book, chapters: chapters, initialIndex: 0, content: "正文")
#expect(reopened.bookmarks(at: 40, in: bookmarks) == [saved])
#expect(reopened.bookmarks(at: 0, in: bookmarks, pageRange: NSRange(location: 0, length: 80)) == [saved])
#expect(reopened.bookmarks(at: 80, in: bookmarks, pageRange: NSRange(location: 80, length: 80)).isEmpty)
#expect(reopened.bookmarks(at: 41, in: bookmarks).isEmpty)
#expect(reopened.bookmarks(at: 40, in: []).isEmpty)
reopened.select(index: 1)
#expect(reopened.bookmarks(at: 40, in: bookmarks) == [otherChapter])
}
@Test func continuousReadingExtendsPastThreeChaptersWithoutNavigating() async {
let book = Book(bookUrl: "demo://continuous", name: "书")
let chapters = (0..<20).map { BookChapter(url: "demo://\($0)", title: "第\($0)章", bookUrl: book.bookUrl, index: $0) }
@@ -25,6 +25,51 @@ private actor ImportFixtureTransport: HTTPTransport {
@MainActor
struct RuleLibraryRegressionTests {
@Test func pickedJSONFilesAreReadTogetherAndReviewedBeforeImport() async throws {
let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: directory) }
let inputs = [
("书源.json", #"{"bookSourceUrl":"https://picked-json.test","bookSourceName":"JSON 书源"}"#),
("大写扩展名.JSON", #"[{"bookSourceUrl":"https://picked-uppercase.test","bookSourceName":"大写扩展名"}]"#),
("书源.txt", #"{"bookSourceUrl":"https://picked-text.test","bookSourceName":"文本扩展名"}"#)
]
let files = try inputs.map { name, content in
let url = directory.appendingPathComponent(name)
try Data(content.utf8).write(to: url)
return SourceImportFile(url: url)
}
let model = SourceLibraryModel.demo(sources: [])
await model.previewPickedFiles(files)
#expect(model.notice == nil)
#expect(!model.isWorking)
#expect(model.pendingImport?.validCount == 3)
#expect(model.sources.isEmpty)
let selectedIDs = Set(try #require(model.pendingImport).items.map(\.id))
await model.commitPendingImport(selectedIDs: selectedIDs, keepOriginalName: false, keepOriginalGroup: false,
keepOriginalEnabled: false, customGroup: nil, addCustomGroup: true)
#expect(model.sources.count == 3)
#expect(model.pendingImport == nil)
}
@Test func missingOrDirectoryImportReportsFailureWithoutChangingSources() async throws {
let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: directory) }
let original = BookSource(bookSourceUrl: "https://existing-source.test", bookSourceName: "原有书源")
for url in [directory, directory.appendingPathComponent("missing.json")] {
let model = SourceLibraryModel.demo(sources: [original])
await model.load()
await model.previewFiles([url])
#expect(model.notice?.kind == .error)
#expect(model.pendingImport == nil)
#expect(model.sources == [original])
#expect(!model.isWorking)
}
}
@Test func largeImportKeepsTheMainActorResponsive() async throws {
let comment = String(repeating: "规则说明", count: 1_024)
let entries = (0..<1_000).map { index in