reader-shell 1.0.3 → 1.0.5
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.
- package/.claude/settings.local.json +4 -1
- package/cli.js +4 -4
- package/package.json +3 -2
- package/src/txtReader.js +165 -0
- package/src/ui.js +22 -11
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
|
|
5
|
-
if (!
|
|
6
|
-
console.error('用法: reader-shell
|
|
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(
|
|
9
|
+
startReader(path.resolve(filePath));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reader-shell",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
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/txtReader.js
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
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
|
+
.replace(/\s+/g, '') // 删除所有空格和换行
|
|
155
|
+
.trim();
|
|
156
|
+
}).filter(chapter => chapter.length > 0);
|
|
157
|
+
|
|
158
|
+
resolve(chapters);
|
|
159
|
+
}).catch(err => {
|
|
160
|
+
reject(err);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
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(
|
|
20
|
-
const
|
|
21
|
-
|
|
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: "
|
|
48
|
+
height: "70%",
|
|
38
49
|
border: "none",
|
|
39
50
|
content: config.bossContent
|
|
40
51
|
});
|
|
@@ -95,7 +106,7 @@ async function startReader(epubPath) {
|
|
|
95
106
|
}
|
|
96
107
|
|
|
97
108
|
function saveProgress() {
|
|
98
|
-
setBookProgress(
|
|
109
|
+
setBookProgress(filePath, { chapter: currentChapter, scroll: currentScroll });
|
|
99
110
|
}
|
|
100
111
|
|
|
101
112
|
function changeChapter(chapter) {
|
|
@@ -122,7 +133,7 @@ async function startReader(epubPath) {
|
|
|
122
133
|
changeChapter(-1)
|
|
123
134
|
return
|
|
124
135
|
}
|
|
125
|
-
render();
|
|
136
|
+
render(true);
|
|
126
137
|
});
|
|
127
138
|
screen.key(["down", "d"], () => {
|
|
128
139
|
currentScroll++;
|
|
@@ -130,7 +141,7 @@ async function startReader(epubPath) {
|
|
|
130
141
|
changeChapter(1)
|
|
131
142
|
return
|
|
132
143
|
}
|
|
133
|
-
render();
|
|
144
|
+
render(true);
|
|
134
145
|
});
|
|
135
146
|
screen.key(["a"], () => {
|
|
136
147
|
// 老板键
|
|
@@ -158,11 +169,11 @@ async function startReader(epubPath) {
|
|
|
158
169
|
}
|
|
159
170
|
|
|
160
171
|
// 允许命令行直接运行
|
|
161
|
-
const
|
|
162
|
-
if (!
|
|
163
|
-
console.error("用法: node src/ui.js
|
|
172
|
+
const filePath = process.argv[2];
|
|
173
|
+
if (!filePath) {
|
|
174
|
+
console.error("用法: node src/ui.js <文件路径>");
|
|
164
175
|
process.exit(1);
|
|
165
176
|
}
|
|
166
|
-
startReader(path.resolve(
|
|
177
|
+
startReader(path.resolve(filePath));
|
|
167
178
|
|
|
168
179
|
export { startReader };
|