reader-shell 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,14 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(git add:*)",
5
+ "Bash(git commit:*)",
6
+ "Bash(git push:*)",
7
+ "Bash(node:*)",
8
+ "Bash(timeout:*)",
9
+ "Read(//Users/xuejichang/Documents/book/**)"
10
+ ],
11
+ "deny": [],
12
+ "ask": []
13
+ }
14
+ }
package/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # 终端电子书阅读器
2
2
 
3
- 一个基于 Node.js 的终端电子书阅读器,支持导入 epub 文件、记住阅读进度、快捷键翻页和字体颜色切换。
3
+ 一个基于 Node.js 的终端电子书阅读器,支持导入 epub 文件、记住阅读进度、快捷键翻页。
4
+
5
+ ![reader-shell-demo.gif?raw=true](https://cdn.jsdelivr.net/gh/jichangee/gallery@master/imgur/reader-shell-demo.gif?raw=true)
4
6
 
5
7
  ## 功能特性
6
8
  - 支持导入 epub 电子书
@@ -48,4 +50,4 @@ node src/ui.js ./books/三体.epub
48
50
  - [chalk](https://www.npmjs.com/package/chalk) 终端字体颜色
49
51
 
50
52
  ## 许可协议
51
- MIT
53
+ MIT
package/cli.js CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { startReader } from './src/ui.js';
3
3
  import path from 'path';
4
- const epubPath = process.argv[2];
5
- if (!epubPath) {
6
- console.error('用法: reader-shell <epub文件路径>');
4
+ const filePath = process.argv[2];
5
+ if (!filePath) {
6
+ console.error('用法: reader-shell <文件路径>');
7
7
  process.exit(1);
8
8
  }
9
- startReader(path.resolve(epubPath));
9
+ startReader(path.resolve(filePath));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reader-shell",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "main": "cli.js",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -13,7 +13,8 @@
13
13
  "dependencies": {
14
14
  "blessed": "^0.1.81",
15
15
  "chalk": "^5.4.1",
16
- "epub": "^1.3.0"
16
+ "epub": "^1.3.0",
17
+ "iconv-lite": "^0.7.0"
17
18
  },
18
19
  "bin": {
19
20
  "reader-shell": "./cli.js"
package/src/epubReader.js CHANGED
@@ -19,7 +19,7 @@ function parseEpub(epubPath) {
19
19
  chapters[idx] = '';
20
20
  } else {
21
21
  let text = data.toString('utf-8');
22
- chapters[idx] = text.replace(/<[^>]+>/g, '').replace(/\r\n/g, '') + '\n\n 本章完';
22
+ chapters[idx] = text.replace(/<[^>]+>/g, '').replace(/\r\n/g, '');
23
23
  }
24
24
  loaded++;
25
25
  if (loaded === chapterIds.length) {
@@ -0,0 +1,164 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import iconv from 'iconv-lite';
4
+
5
+ /**
6
+ * 检测文件编码
7
+ * @param {Buffer} buffer 文件内容
8
+ * @returns {string} 编码名称
9
+ */
10
+ function detectEncoding(buffer) {
11
+ // 检查BOM标记
12
+ if (buffer.length >= 3 && buffer[0] === 0xEF && buffer[1] === 0xBB && buffer[2] === 0xBF) {
13
+ return 'utf8';
14
+ }
15
+
16
+ if (buffer.length >= 2 && buffer[0] === 0xFF && buffer[1] === 0xFE) {
17
+ return 'utf16le';
18
+ }
19
+
20
+ if (buffer.length >= 2 && buffer[0] === 0xFE && buffer[1] === 0xFF) {
21
+ return 'utf16be';
22
+ }
23
+
24
+ // 简单的编码检测:尝试UTF-8解码
25
+ try {
26
+ const utf8Content = buffer.toString('utf8');
27
+ // 检查是否包含UTF-8无效字符
28
+ if (!utf8Content.includes('�')) {
29
+ return 'utf8';
30
+ }
31
+ } catch (e) {
32
+ // UTF-8解码失败,尝试其他编码
33
+ }
34
+
35
+ // 尝试GBK/GB2312编码(中文Windows常用编码)
36
+ try {
37
+ const gbkContent = iconv.decode(buffer, 'gbk');
38
+ // 检查解码是否成功(没有太多乱码字符)
39
+ if (!gbkContent.includes('�') || gbkContent.indexOf('�') / gbkContent.length < 0.1) {
40
+ return 'gbk';
41
+ }
42
+ } catch (e) {
43
+ // GBK解码失败
44
+ }
45
+
46
+ // 默认使用UTF-8
47
+ return 'utf8';
48
+ }
49
+
50
+ /**
51
+ * 以正确的编码读取文件
52
+ * @param {string} filePath 文件路径
53
+ * @returns {Promise<string>} 文件内容
54
+ */
55
+ function readFileWithCorrectEncoding(filePath) {
56
+ return new Promise((resolve, reject) => {
57
+ fs.readFile(filePath, (err, buffer) => {
58
+ if (err) {
59
+ reject(err);
60
+ return;
61
+ }
62
+
63
+ const encoding = detectEncoding(buffer);
64
+ let content;
65
+
66
+ try {
67
+ if (encoding === 'utf8') {
68
+ content = buffer.toString('utf8');
69
+ } else {
70
+ content = iconv.decode(buffer, encoding);
71
+ }
72
+ resolve(content);
73
+ } catch (e) {
74
+ // 如果指定编码解码失败,尝试UTF-8
75
+ try {
76
+ content = buffer.toString('utf8');
77
+ resolve(content);
78
+ } catch (utf8Err) {
79
+ reject(new Error(`无法解码文件,尝试的编码: ${encoding}`));
80
+ }
81
+ }
82
+ });
83
+ });
84
+ }
85
+
86
+ /**
87
+ * 解析txt文件,返回章节文本数组
88
+ * @param {string} txtPath txt文件路径
89
+ * @returns {Promise<string[]>} 章节文本数组
90
+ */
91
+ function parseTxt(txtPath) {
92
+ return new Promise((resolve, reject) => {
93
+ readFileWithCorrectEncoding(txtPath).then(data => {
94
+ let content = data;
95
+ let chapters = [];
96
+
97
+ // 先尝试按明确的章节标题分割
98
+ const lines = content.split('\n');
99
+ let currentChapter = '';
100
+ let chapterHeaders = [];
101
+
102
+ // 寻找所有章节标题的行号
103
+ lines.forEach((line, index) => {
104
+ if (/^第[一二三四五六七八九十百千万0-9]+[章节]/.test(line.trim())) {
105
+ chapterHeaders.push(index);
106
+ }
107
+ });
108
+
109
+ if (chapterHeaders.length > 1) {
110
+ // 按找到的章节标题分割
111
+ for (let i = 0; i < chapterHeaders.length; i++) {
112
+ const startLine = chapterHeaders[i];
113
+ const endLine = i < chapterHeaders.length - 1 ? chapterHeaders[i + 1] : lines.length;
114
+
115
+ const chapterLines = lines.slice(startLine, endLine);
116
+ const chapterContent = chapterLines.join('\n').trim();
117
+
118
+ if (chapterContent.length > 0) {
119
+ chapters.push(chapterContent);
120
+ }
121
+ }
122
+ }
123
+
124
+ // 如果没有找到章节分割,按段落分割
125
+ if (chapters.length === 0) {
126
+ // 按多个空行分割
127
+ const sections = content.split(/\n\s*\n\s*\n/);
128
+ chapters = sections.filter(section => section.trim().length > 0);
129
+
130
+ // 如果分割后章节太少,按单个空行分割,然后每10个段落合并
131
+ if (chapters.length <= 1) {
132
+ const paragraphs = content.split(/\n\s*\n/);
133
+ chapters = [];
134
+ const chapterSize = 10;
135
+ for (let i = 0; i < paragraphs.length; i += chapterSize) {
136
+ const chapter = paragraphs.slice(i, i + chapterSize).join('\n\n');
137
+ if (chapter.trim().length > 0) {
138
+ chapters.push(chapter);
139
+ }
140
+ }
141
+ }
142
+ }
143
+
144
+ // 如果还是没有合适的章节,整个文件作为一章
145
+ if (chapters.length === 0) {
146
+ chapters = [content];
147
+ }
148
+
149
+ // 清理每章内容
150
+ chapters = chapters.map(chapter => {
151
+ return chapter
152
+ .replace(/\r\n/g, '\n') // 统一换行符
153
+ .replace(/\n{3,}/g, '\n\n') // 去除多余的空行
154
+ .trim();
155
+ }).filter(chapter => chapter.length > 0);
156
+
157
+ resolve(chapters);
158
+ }).catch(err => {
159
+ reject(err);
160
+ });
161
+ });
162
+ }
163
+
164
+ export { parseTxt, detectEncoding, readFileWithCorrectEncoding };
package/src/ui.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import blessed from "blessed";
2
2
  import chalk from "chalk";
3
3
  import { parseEpub } from "./epubReader.js";
4
+ import { parseTxt } from "./txtReader.js";
4
5
  import path from "path";
5
6
  import { getBookProgress, setBookProgress } from "./progressStore.js";
6
7
  import { config } from "./config.js";
@@ -16,9 +17,19 @@ const colorFns = {
16
17
  yellow: chalk.yellow,
17
18
  };
18
19
 
19
- async function startReader(epubPath) {
20
- const chapters = await parseEpub(epubPath);
21
- const progress = getBookProgress(epubPath);
20
+ async function startReader(filePath) {
21
+ const ext = path.extname(filePath).toLowerCase();
22
+ let chapters;
23
+
24
+ if (ext === '.epub') {
25
+ chapters = await parseEpub(filePath);
26
+ } else if (ext === '.txt') {
27
+ chapters = await parseTxt(filePath);
28
+ } else {
29
+ throw new Error(`不支持的文件格式: ${ext}`);
30
+ }
31
+
32
+ const progress = getBookProgress(filePath);
22
33
  let currentChapter = (progress && progress.chapter) || 0;
23
34
  let currentScroll = (progress && progress.scroll) || 0;
24
35
 
@@ -34,7 +45,7 @@ async function startReader(epubPath) {
34
45
  top: 0,
35
46
  left: 0,
36
47
  width: "100%",
37
- height: "80%",
48
+ height: "70%",
38
49
  border: "none",
39
50
  content: config.bossContent
40
51
  });
@@ -72,9 +83,18 @@ async function startReader(epubPath) {
72
83
  screen.append(box);
73
84
  screen.append(status);
74
85
 
75
- function render() {
86
+ // 用于跟踪当前显示的章节,避免重复设置相同内容
87
+ let lastDisplayedChapter = -1;
88
+
89
+ function render(forceContentRefresh = false) {
76
90
  const colorFn = colorFns[fontColor] || chalk.white;
77
- box.setContent(colorFn(chapters[currentChapter] || ""));
91
+
92
+ // 只有当章节改变或强制刷新时才重新设置内容
93
+ if (forceContentRefresh || lastDisplayedChapter !== currentChapter) {
94
+ box.setContent(colorFn(chapters[currentChapter] || ""));
95
+ lastDisplayedChapter = currentChapter;
96
+ }
97
+
78
98
  status.setContent(
79
99
  `progress: ${currentScroll}/${box.getScrollHeight()} | chapter: ${currentChapter + 1}/${
80
100
  chapters.length
@@ -86,14 +106,14 @@ async function startReader(epubPath) {
86
106
  }
87
107
 
88
108
  function saveProgress() {
89
- setBookProgress(epubPath, { chapter: currentChapter, scroll: currentScroll });
109
+ setBookProgress(filePath, { chapter: currentChapter, scroll: currentScroll });
90
110
  }
91
111
 
92
112
  function changeChapter(chapter) {
93
113
  currentChapter += chapter;
94
114
  currentScroll = 0;
95
115
  saveProgress();
96
- render();
116
+ render(true); // 切换章节时强制刷新内容
97
117
  }
98
118
 
99
119
  // 快捷键
@@ -113,7 +133,7 @@ async function startReader(epubPath) {
113
133
  changeChapter(-1)
114
134
  return
115
135
  }
116
- render();
136
+ render(true);
117
137
  });
118
138
  screen.key(["down", "d"], () => {
119
139
  currentScroll++;
@@ -121,7 +141,7 @@ async function startReader(epubPath) {
121
141
  changeChapter(1)
122
142
  return
123
143
  }
124
- render();
144
+ render(true);
125
145
  });
126
146
  screen.key(["a"], () => {
127
147
  // 老板键
@@ -134,7 +154,7 @@ async function startReader(epubPath) {
134
154
  box.height = 0
135
155
  status.height = 0
136
156
  }
137
- render()
157
+ render(false) // 老板键切换时不强制刷新内容
138
158
  });
139
159
  screen.key(["q", "C-c"], () => {
140
160
  saveProgress();
@@ -145,15 +165,15 @@ async function startReader(epubPath) {
145
165
  saveProgress();
146
166
  });
147
167
 
148
- render();
168
+ render(true); // 初始渲染时强制设置内容
149
169
  }
150
170
 
151
171
  // 允许命令行直接运行
152
- const epubPath = process.argv[2];
153
- if (!epubPath) {
154
- console.error("用法: node src/ui.js <epub文件路径>");
172
+ const filePath = process.argv[2];
173
+ if (!filePath) {
174
+ console.error("用法: node src/ui.js <文件路径>");
155
175
  process.exit(1);
156
176
  }
157
- startReader(path.resolve(epubPath));
177
+ startReader(path.resolve(filePath));
158
178
 
159
179
  export { startReader };