Skip to content

Commit b8cba28

Browse files
committed
fix(local_book,engine,reader): 統一本地書格式為 TXT,修復正文清理、UTF-16 編碼、章名邊界與 Unicode 切割
1 parent 7e14a00 commit b8cba28

36 files changed

Lines changed: 597 additions & 783 deletions

DEVELOPMENT.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
│ │ ├── di/ # GetIt 依賴注入
6161
│ │ ├── engine/ # 規則引擎(AnalyzeRule/AnalyzeUrl/JS/WebBook/EventBus)
6262
│ │ ├── exception/ # 應用例外
63-
│ │ ├── local_book/ # TXT/EPUB 本地書格式偵測與解析
63+
│ │ ├── local_book/ # TXT 本地書格式偵測與解析
6464
│ │ ├── models/ # 資料契約層(Book/BookSource/Chapter...)
6565
│ │ ├── network/ # Dio 攔截器、StrResponse
6666
│ │ ├── services/ # 業務服務層(書源調度/下載/TTS/備份/校驗/日誌...)
@@ -219,4 +219,4 @@ git push origin vX.Y.Z
219219
- 書源驗證流程涉及 WebView、Cookie 與真實網站互動,容易出現只有真機或真實網站才會發生的問題;優先用 `tool/` 腳本重現。
220220
- 後台任務(`main.dart callbackDispatcher`)在 Isolate 跑,需重新初始化 DI,且不可執行 JS 規則。
221221
- 改 Drift table/DAO 必須跑 `build_runner` 重新生成 `.g.dart`,並處理 schema migration。
222-
- `SettingsProvider``AppConfig``PreferKey` 三方需保持一致,否則 reader/models 會讀到舊值。
222+
- `SettingsProvider``AppConfig``PreferKey` 三方需保持一致,否則 reader/models 會讀到舊值。

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ App 採用自製的排版與渲染引擎,針對中文長篇閱讀最佳化,
4040
| 書籍管理 | 書架、書籍詳情、封面、閱讀進度、書籤、全域替換規則 |
4141
| 離線快取 | 手動預下載、背景下載佇列、快取清理 |
4242
| 資料 | 一鍵備份還原、書架交換、繁簡轉換 |
43-
| 本地書籍 | TXT、EPUB 匯入閱讀 |
43+
| 本地書籍 | TXT 匯入閱讀 |
4444

4545
---
4646

@@ -86,7 +86,7 @@ App 採用自製的排版與渲染引擎,針對中文長篇閱讀最佳化,
8686
- **繁簡轉換**:可選擇不轉換、簡轉繁或繁轉簡。
8787

8888
### 本地電子書
89-
- 支援匯入並閱讀 `TXT` `EPUB` 格式的本地檔案。
89+
- 支援匯入並閱讀 `TXT` 格式的本地檔案。
9090

9191
---
9292

@@ -118,7 +118,7 @@ App 採用自製的排版與渲染引擎,針對中文長篇閱讀最佳化,
118118
夜讀是純工具型閱讀器,不提供也不儲存任何書籍內容。請先匯入書源。
119119

120120
**Q:支援哪些本地電子書格式?**
121-
支援 `TXT``EPUB`
121+
支援 `TXT`
122122

123123
**Q:需要登入或驗證碼的書源能用嗎?**
124124
需要瀏覽器互動驗證(如登入、驗證碼)的書源目前不支援,此類書源會直接回報錯誤,建議改用不需驗證的來源。

docs/changes/completed/2026-07-10/summary.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22

33
- 完成 Reader V2 方案 B 混合架構整合、I1–I6 修正、舊 viewport 清理與全量驗證(678 tests passed)。
44
- 修復 hybrid 重開位置誤差、進度整頁重建與 late admission 卡死,新增精度/熱路徑回歸測試(683 tests passed)。
5+
- 本地書閱讀統一為 TXT,並修復正文缺頁、純文字清理、UTF-16、章名邊界與 Unicode 切割;analyze 與 694 tests 全綠。
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# TXT-only reader content pipeline fixes
2+
3+
## Level
4+
5+
T2 — 跨 services、engine、reader、bookshelf、association 與公開文件的功能收斂及資料正確性修正。
6+
7+
## Decision
8+
9+
本地書入口、匯入服務與閱讀流程統一只接受 TXT。既有不支援格式的書架紀錄不自動刪除,開啟時提供明確錯誤。
10+
11+
## Before
12+
13+
- 本地匯入同時接受 TXT 與另一種封裝格式,但後者把標記內容直接送入純文字閱讀管線。
14+
- 並發多頁正文會略過失敗頁,將殘缺內容視為成功並持久化。
15+
- UTF-16LE/BE 雖可被辨識,實際仍以 UTF-8 解碼。
16+
- 缺少正文規則、圖片標籤與純空白正文可能進入最終文字資料。
17+
- 章名去重沒有尾端邊界,可能刪除正文合法前綴。
18+
- 長段落可在 UTF-16 代理對中間切割。
19+
20+
## After
21+
22+
- 本地書檔案選擇、分享匯入、服務解析與文件只提供 TXT。
23+
- 既有不支援格式在進入閱讀器前得到明確提示,資料保留供使用者自行刪除。
24+
- 任一並發正文頁失敗即讓整章失敗,不保存殘缺正文。
25+
- 正文規則輸出為純文字;缺少正文規則與純空白結果皆視為失敗。
26+
- UTF-16LE/BE TXT 可正確建立目錄並按位元組範圍讀取。
27+
- 章名去重要求合法尾端邊界;文字切割避開代理對。
28+
29+
## Work
30+
31+
1. 先加聚焦測試,覆蓋本地格式、UTF-16、正文清理、章名邊界與 Unicode block 切割。
32+
2. 收斂本地書格式與既有書籍的開啟防線,刪除不再使用的解析/資源恢復程式。
33+
3. 修正文規則、多頁抓取、空內容、編碼、章名與切割邏輯。
34+
4. 增量更新架構地圖、README、開發文件與網站文案。
35+
5. 執行格式化、聚焦測試、書源驗證、`flutter analyze` 與完整 `flutter test`
36+
37+
## Verification Results
38+
39+
- 本次變更的 Dart 檔案已通過 `dart format`
40+
- 59 個聚焦管線測試、既有不支援格式防線測試與 11 個 hybrid reader screen 測試通過。
41+
- 31 個書源驗證支援測試通過。
42+
- `flutter analyze`:No issues found。
43+
- `flutter test`:694 tests passed。
44+
- 依專案規範未執行本地 build。
45+
46+
## Delivery
47+
48+
No commit。

docs/night_reader/association.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## Responsibility
44

5-
- 外部意圖處理:深連結(URI)開書、分享檔案(TXT/EPUB)匯入、意圖對話框。
5+
- 外部意圖處理:深連結(URI)開書、分享 TXT 檔案匯入、意圖對話框。
66
- 未來工作從這裡開始:深連結開書、分享本地書匯入、意圖對話框。
77

88
## Scope
@@ -33,4 +33,4 @@
3333
## Do Not Do
3434

3535
- 不要在 association 直接抓章節或渲染閱讀(層 reader)。
36-
- 不要繞過 `LocalBookService` 自行解析本地書格式。
36+
- 不要繞過 `LocalBookService` 自行解析本地書格式。

docs/night_reader/foundation.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
- `lib/core/constant/``app_const.dart``app_pattern.dart``book_type.dart``source_type.dart``page_anim.dart``prefer_key.dart`
1616
- `lib/core/di/injection.dart``getIt` + `configureDependencies()`
1717
- `lib/core/exception/app_exception.dart`
18-
- `lib/core/local_book/``local_book_formats.dart``kSupportedLocalBookExtensions={'txt','epub'}`)、`txt_parser.dart`
18+
- `lib/core/local_book/``local_book_formats.dart``kSupportedLocalBookExtensions={'txt'}`)、`txt_parser.dart`
1919
- `lib/core/storage/``app_cache.dart``AppCache`,磁碟快取+過期)、`app_storage_paths.dart``file_doc.dart``storage_metrics.dart`
2020
- `lib/core/utils/``utils.dart``url_util.dart``string_utils.dart``html_utils.dart``network_utils.dart``lru_map.dart``logger.dart``ttf_parser.dart` 等。
2121
- `lib/core/widgets/book_cover_widget.dart`
@@ -51,4 +51,4 @@
5151

5252
- 不要在啟動路徑做同步或長耗時操作。
5353
- 不要把新產品線功能塞進 `main_page.dart` 導航(feature freeze)。
54-
- 不要在 `network` 攔截器加入書源規則解析邏輯(屬 engine)。
54+
- 不要在 `network` 攔截器加入書源規則解析邏輯(屬 engine)。

docs/night_reader/services.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
- `lib/core/services/reader_chapter_content_store.dart` + `reader_chapter_content_storage.dart` + `chapter_content_scheduler.dart` + `chapter_content_preparation_pipeline.dart``ReaderChapterContentStore`(章節正文取存封裝)。
2020
- `lib/core/services/book_storage_service.dart``book_cover_storage_service.dart``bookshelf_state_tracker.dart``bookshelf_exchange_service.dart`
2121
- `lib/core/services/backup_service.dart` + `restore_service.dart``BackupService`(zip 匯出 books/sources/rules/bookmarks/download/reader_chapter_content)。
22-
- `lib/core/services/local_book_service.dart``LocalBookImportResult`, TXT/EPUB 匯入)、`epub_service.dart``resource_service.dart``export_book_service.dart`
22+
- `lib/core/services/local_book_service.dart``LocalBookImportResult`TXT 匯入與分段讀取)`resource_service.dart``export_book_service.dart`
2323
- `lib/core/services/app_log_service.dart``AppLog`,全域日誌,**幾乎所有模組引用**)、`crash_handler.dart``app_version.dart``app_permission_service.dart`
2424
- `lib/core/services/update_service.dart` + `update_ignore_store.dart`(版本更新)。
2525
- `lib/core/services/webview_data_service.dart``backstage_webview.dart``rule_big_data_service.dart``default_data.dart``cache_manager.dart``chinese_utils.dart``ChineseUtils.s2t/t2s`)、`event_bus.dart``cookie_store.dart``encoding_detect.dart``rate_limiter.dart`
@@ -62,4 +62,4 @@
6262
- 不要在 service 直接操作 UI(層 feature providers)。
6363
- 不要把 JS 規則執行搬進 Isolate 校驗流程。
6464
- 不要在 `NetworkService` 移除書源併發鎖。
65-
- 不要未測試即改 `BackupService` 的 zip 結構(破壞舊備份還原)。
65+
- 不要未測試即改 `BackupService` 的 zip 結構(破壞舊備份還原)。

docs/night_reader_index.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
| 標題 | 選定選項 | 影響模組 | 理由 |
3131
|---|---|---|---|
3232
| Reader V2 採方案 B 混合架構 | Framework `CustomScrollView.center` 雙 sliver + 自有 block 排版管線 | reader(hybrid / screen / session bridge) | 以精確 extent、連續 admission、零 offset correction 與邏輯錨點取代舊自繪 strip/backward lock,降低補章跳動與 fling 主執行緒排版風險。 |
33+
| 本地書格式統一為 TXT | 檔案選擇、分享匯入、章節索引與 Reader V2 只接受 TXT | foundation / services / bookshelf / association / reader | 維持純文字章節契約,避免封裝格式標記與資源語意進入文字 block 管線。 |
3334

3435
## Module List
3536

@@ -61,4 +62,4 @@
6162
- **reader**:擁有 Reader V2 閱讀器主流程;現行核心為 `features/reader_v2/hybrid`(text → measure → paragraph/pump → center 雙 sliver view,橫切 anchor/overlay/progress/telemetry),外層沿用 screen / session state machine / features bridge。release 重點回歸區。未來工作從這裡開始:精確 extent、admission/領先量、Paragraph cache、章節預載與進度、TTS 逐段高亮、閱讀設定、點擊區、書籤、章內替換、換源 sheet。症狀指向:排版錯位/空白 block、滾動撞邊界、補章跳動、TTS 高亮偏移、進度或設定不保留。
6263
- **settings_about**:擁有設定頁群與關於/更新檢查(`features/settings` + `features/about``SettingsProvider`、各設定子頁、`AboutPage``UpdateCheckRunner`)。未來工作從這裡開始:主題/閱讀/TTS/備份/隱私設定、版本更新檢查、崩潰日誌頁。症狀指向:設定不儲存、主題未套用、更新檢查失敗。`SettingsProvider``AppConfig` 雙向同步。
6364
- **downloads**:擁有背景下載佇列管理頁與快取清理(`features/cache_manager` + `core/services``DownloadService`/scheduler/executor)。未來工作從這裡開始:下載佇列 UI、任務暫停/重試/刪除、快取清理、佇列與 `DownloadService` 互動。症狀指向:佇列不更新、任務卡住、快取未清、背景任務未執行。
64-
- **association**:擁有外部意圖處理(`features/association``AssociationHandlerService`,深連結 URI 與分享檔案)。未來工作從這裡開始:深連結開書、分享 TXT/EPUB 匯入、意圖對話框。症狀指向:從連結/分享開啟失敗、本地書匯入無反應。
65+
- **association**:擁有外部意圖處理(`features/association``AssociationHandlerService`,深連結 URI 與分享檔案)。未來工作從這裡開始:深連結開書、分享 TXT 匯入、意圖對話框。症狀指向:從連結/分享開啟失敗、本地書匯入無反應。

lib/core/constant/app_pattern.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ class AppPattern {
3434

3535
// 本地書籍支援類型
3636
static final RegExp bookFileRegex = RegExp(
37-
r'.*\.(txt|epub|pdf|mobi|azw3|azw)',
37+
r'.*\.txt',
3838
caseSensitive: false,
3939
);
4040
// 壓縮文件支援類型

lib/core/engine/web_book/content_parser.dart

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,18 +36,17 @@ class ContentParser {
3636

3737
try {
3838
final contentRule = source.ruleContent;
39-
if (contentRule == null) return ContentResult(content: body);
39+
if (contentRule == null) return const ContentResult(content: '');
4040

4141
var content = await rule.getStringAsync(
4242
contentRule.content ?? '',
4343
unescape: false,
4444
);
4545

46-
content = HtmlFormatter.formatKeepImg(content, baseUrl: baseUrl);
47-
4846
if (content.contains('&')) {
4947
content = AnalyzeRuleBase.htmlUnescape.convert(content);
5048
}
49+
content = HtmlFormatter.format(content);
5150

5251
final nextUrls = <String>[];
5352
if (contentRule.nextContentUrl != null &&
@@ -85,6 +84,7 @@ class ContentParser {
8584
required String contentStr,
8685
String? baseUrl,
8786
}) async {
87+
if (contentStr.trim().isEmpty) return '';
8888
final replaceRegex = source.ruleContent?.replaceRegex;
8989
if (replaceRegex == null || replaceRegex.isEmpty) return contentStr;
9090

@@ -108,6 +108,8 @@ class ContentParser {
108108
// 若規則解析失敗則保留 trim 後的原文
109109
}
110110

111+
if (str.trim().isEmpty) return '';
112+
111113
// 段落縮排 (對標 Android "  $it" join)
112114
str = str.split(RegExp(r'\r?\n')).map((e) => '  $e').join('\n');
113115
return str;

0 commit comments

Comments
 (0)