readshell-pro 0.3.7 → 0.4.0
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/bin/novel.js +27 -0
- package/package.json +7 -55
- package/LICENSE +0 -17
- package/README.md +0 -142
- package/bin/readshell.js +0 -3
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -2431
- package/dist/index.js.map +0 -1
package/dist/index.js
DELETED
|
@@ -1,2431 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// src/cli/parser.ts
|
|
4
|
-
import yargs from "yargs";
|
|
5
|
-
import { hideBin } from "yargs/helpers";
|
|
6
|
-
|
|
7
|
-
// src/services/BookService.ts
|
|
8
|
-
import { resolve } from "path";
|
|
9
|
-
import { existsSync as existsSync2, statSync } from "fs";
|
|
10
|
-
import { nanoid } from "nanoid";
|
|
11
|
-
|
|
12
|
-
// src/db/client.ts
|
|
13
|
-
import Database from "better-sqlite3";
|
|
14
|
-
|
|
15
|
-
// src/config/paths.ts
|
|
16
|
-
import { join } from "path";
|
|
17
|
-
import { homedir } from "os";
|
|
18
|
-
import { mkdirSync, existsSync } from "fs";
|
|
19
|
-
function getAppDataDir() {
|
|
20
|
-
const platform = process.platform;
|
|
21
|
-
let configDir;
|
|
22
|
-
if (platform === "darwin") {
|
|
23
|
-
configDir = join(homedir(), "Library", "Application Support", "readshell");
|
|
24
|
-
} else if (platform === "win32") {
|
|
25
|
-
configDir = join(process.env["APPDATA"] || join(homedir(), "AppData", "Roaming"), "readshell");
|
|
26
|
-
} else {
|
|
27
|
-
configDir = join(process.env["XDG_CONFIG_HOME"] || join(homedir(), ".config"), "readshell");
|
|
28
|
-
}
|
|
29
|
-
if (!existsSync(configDir)) {
|
|
30
|
-
mkdirSync(configDir, { recursive: true });
|
|
31
|
-
}
|
|
32
|
-
return configDir;
|
|
33
|
-
}
|
|
34
|
-
function getDbPath() {
|
|
35
|
-
return join(getAppDataDir(), "readshell.db");
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// src/utils/logger.ts
|
|
39
|
-
var isDebug = process.env["DEBUG"] === "1" || process.env["DEBUG"] === "true";
|
|
40
|
-
var logger = {
|
|
41
|
-
debug: (...args) => {
|
|
42
|
-
if (isDebug) {
|
|
43
|
-
console.error("[DEBUG]", ...args);
|
|
44
|
-
}
|
|
45
|
-
},
|
|
46
|
-
info: (...args) => {
|
|
47
|
-
console.error("[INFO]", ...args);
|
|
48
|
-
},
|
|
49
|
-
warn: (...args) => {
|
|
50
|
-
console.error("[WARN]", ...args);
|
|
51
|
-
},
|
|
52
|
-
error: (...args) => {
|
|
53
|
-
console.error("[ERROR]", ...args);
|
|
54
|
-
}
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
// src/db/client.ts
|
|
58
|
-
var db = null;
|
|
59
|
-
function getDb() {
|
|
60
|
-
if (!db) {
|
|
61
|
-
const dbPath = getDbPath();
|
|
62
|
-
logger.debug(`\u6570\u636E\u5E93\u8DEF\u5F84: ${dbPath}`);
|
|
63
|
-
db = new Database(dbPath);
|
|
64
|
-
db.pragma("journal_mode = WAL");
|
|
65
|
-
db.pragma("foreign_keys = ON");
|
|
66
|
-
}
|
|
67
|
-
return db;
|
|
68
|
-
}
|
|
69
|
-
function closeDb() {
|
|
70
|
-
if (db) {
|
|
71
|
-
db.close();
|
|
72
|
-
db = null;
|
|
73
|
-
logger.debug("\u6570\u636E\u5E93\u8FDE\u63A5\u5DF2\u5173\u95ED");
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
process.on("exit", () => closeDb());
|
|
77
|
-
process.on("SIGINT", () => {
|
|
78
|
-
closeDb();
|
|
79
|
-
process.exit(0);
|
|
80
|
-
});
|
|
81
|
-
process.on("SIGTERM", () => {
|
|
82
|
-
closeDb();
|
|
83
|
-
process.exit(0);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
// src/db/models/Book.ts
|
|
87
|
-
var BookModel = class {
|
|
88
|
-
/**
|
|
89
|
-
* 插入新书
|
|
90
|
-
*/
|
|
91
|
-
insert(book) {
|
|
92
|
-
const db2 = getDb();
|
|
93
|
-
db2.prepare(`
|
|
94
|
-
INSERT INTO books (id, title, author, file_path, format, file_hash, file_size, created_at)
|
|
95
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
96
|
-
`).run(book.id, book.title, book.author, book.file_path, book.format, book.file_hash, book.file_size, book.created_at);
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* 通过 ID 获取书籍
|
|
100
|
-
*/
|
|
101
|
-
findById(id) {
|
|
102
|
-
const db2 = getDb();
|
|
103
|
-
return db2.prepare("SELECT * FROM books WHERE id = ?").get(id);
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* 通过文件 hash 查找(去重用)
|
|
107
|
-
*/
|
|
108
|
-
findByHash(hash) {
|
|
109
|
-
const db2 = getDb();
|
|
110
|
-
return db2.prepare("SELECT * FROM books WHERE file_hash = ?").get(hash);
|
|
111
|
-
}
|
|
112
|
-
/**
|
|
113
|
-
* 模糊搜索书名
|
|
114
|
-
*/
|
|
115
|
-
searchByTitle(keyword) {
|
|
116
|
-
const db2 = getDb();
|
|
117
|
-
return db2.prepare("SELECT * FROM books WHERE title LIKE ? ORDER BY created_at DESC").all(`%${keyword}%`);
|
|
118
|
-
}
|
|
119
|
-
/**
|
|
120
|
-
* 获取所有书籍
|
|
121
|
-
*/
|
|
122
|
-
findAll() {
|
|
123
|
-
const db2 = getDb();
|
|
124
|
-
return db2.prepare("SELECT * FROM books ORDER BY created_at DESC").all();
|
|
125
|
-
}
|
|
126
|
-
/**
|
|
127
|
-
* 删除书籍
|
|
128
|
-
*/
|
|
129
|
-
delete(id) {
|
|
130
|
-
const db2 = getDb();
|
|
131
|
-
db2.prepare("DELETE FROM books WHERE id = ?").run(id);
|
|
132
|
-
}
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
// src/db/models/Chapter.ts
|
|
136
|
-
var ChapterModel = class {
|
|
137
|
-
/**
|
|
138
|
-
* 批量插入章节索引
|
|
139
|
-
*/
|
|
140
|
-
insertMany(chapters) {
|
|
141
|
-
const db2 = getDb();
|
|
142
|
-
const stmt = db2.prepare(`
|
|
143
|
-
INSERT OR REPLACE INTO chapter_index (book_id, chapter_no, title, byte_offset)
|
|
144
|
-
VALUES (?, ?, ?, ?)
|
|
145
|
-
`);
|
|
146
|
-
db2.transaction(() => {
|
|
147
|
-
for (const chapter of chapters) {
|
|
148
|
-
stmt.run(chapter.book_id, chapter.chapter_no, chapter.title, chapter.byte_offset);
|
|
149
|
-
}
|
|
150
|
-
})();
|
|
151
|
-
}
|
|
152
|
-
/**
|
|
153
|
-
* 获取指定书籍的所有章节
|
|
154
|
-
*/
|
|
155
|
-
findByBookId(bookId) {
|
|
156
|
-
const db2 = getDb();
|
|
157
|
-
return db2.prepare("SELECT * FROM chapter_index WHERE book_id = ? ORDER BY chapter_no").all(bookId);
|
|
158
|
-
}
|
|
159
|
-
/**
|
|
160
|
-
* 获取指定章节
|
|
161
|
-
*/
|
|
162
|
-
findChapter(bookId, chapterNo) {
|
|
163
|
-
const db2 = getDb();
|
|
164
|
-
return db2.prepare("SELECT * FROM chapter_index WHERE book_id = ? AND chapter_no = ?").get(bookId, chapterNo);
|
|
165
|
-
}
|
|
166
|
-
/**
|
|
167
|
-
* 获取书籍章节总数
|
|
168
|
-
*/
|
|
169
|
-
getChapterCount(bookId) {
|
|
170
|
-
const db2 = getDb();
|
|
171
|
-
const result = db2.prepare("SELECT COUNT(*) as count FROM chapter_index WHERE book_id = ?").get(bookId);
|
|
172
|
-
return result.count;
|
|
173
|
-
}
|
|
174
|
-
/**
|
|
175
|
-
* 删除指定书籍的章节索引
|
|
176
|
-
*/
|
|
177
|
-
deleteByBookId(bookId) {
|
|
178
|
-
const db2 = getDb();
|
|
179
|
-
db2.prepare("DELETE FROM chapter_index WHERE book_id = ?").run(bookId);
|
|
180
|
-
}
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
// src/db/models/Recent.ts
|
|
184
|
-
var RecentModel = class {
|
|
185
|
-
/**
|
|
186
|
-
* 记录打开(插入或更新计数)
|
|
187
|
-
*/
|
|
188
|
-
recordOpen(bookId) {
|
|
189
|
-
const db2 = getDb();
|
|
190
|
-
const now = Date.now();
|
|
191
|
-
db2.prepare(`
|
|
192
|
-
INSERT INTO recent_reads (book_id, opened_at, open_count)
|
|
193
|
-
VALUES (?, ?, 1)
|
|
194
|
-
ON CONFLICT(book_id) DO UPDATE SET
|
|
195
|
-
opened_at = excluded.opened_at,
|
|
196
|
-
open_count = open_count + 1
|
|
197
|
-
`).run(bookId, now);
|
|
198
|
-
}
|
|
199
|
-
/**
|
|
200
|
-
* 获取最近阅读列表(按时间倒序)
|
|
201
|
-
*/
|
|
202
|
-
getRecent(limit = 20) {
|
|
203
|
-
const db2 = getDb();
|
|
204
|
-
return db2.prepare("SELECT * FROM recent_reads ORDER BY opened_at DESC LIMIT ?").all(limit);
|
|
205
|
-
}
|
|
206
|
-
/**
|
|
207
|
-
* 删除记录
|
|
208
|
-
*/
|
|
209
|
-
delete(bookId) {
|
|
210
|
-
const db2 = getDb();
|
|
211
|
-
db2.prepare("DELETE FROM recent_reads WHERE book_id = ?").run(bookId);
|
|
212
|
-
}
|
|
213
|
-
};
|
|
214
|
-
|
|
215
|
-
// src/db/models/Progress.ts
|
|
216
|
-
var ProgressModel = class {
|
|
217
|
-
/**
|
|
218
|
-
* 保存或更新阅读进度
|
|
219
|
-
*/
|
|
220
|
-
upsert(progress) {
|
|
221
|
-
const db2 = getDb();
|
|
222
|
-
db2.prepare(`
|
|
223
|
-
INSERT INTO reading_progress (book_id, chapter_no, byte_offset, percent, updated_at, opened_at)
|
|
224
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
225
|
-
ON CONFLICT(book_id) DO UPDATE SET
|
|
226
|
-
chapter_no = excluded.chapter_no,
|
|
227
|
-
byte_offset = excluded.byte_offset,
|
|
228
|
-
percent = excluded.percent,
|
|
229
|
-
updated_at = excluded.updated_at,
|
|
230
|
-
opened_at = excluded.opened_at
|
|
231
|
-
`).run(progress.book_id, progress.chapter_no, progress.byte_offset, progress.percent, progress.updated_at, progress.opened_at);
|
|
232
|
-
}
|
|
233
|
-
/**
|
|
234
|
-
* 获取指定书籍的阅读进度
|
|
235
|
-
*/
|
|
236
|
-
findByBookId(bookId) {
|
|
237
|
-
const db2 = getDb();
|
|
238
|
-
return db2.prepare("SELECT * FROM reading_progress WHERE book_id = ?").get(bookId);
|
|
239
|
-
}
|
|
240
|
-
/**
|
|
241
|
-
* 获取最近打开的书籍进度(用于 resume)
|
|
242
|
-
*/
|
|
243
|
-
getLastOpened() {
|
|
244
|
-
const db2 = getDb();
|
|
245
|
-
return db2.prepare("SELECT * FROM reading_progress ORDER BY opened_at DESC LIMIT 1").get();
|
|
246
|
-
}
|
|
247
|
-
/**
|
|
248
|
-
* 删除指定书籍的进度
|
|
249
|
-
*/
|
|
250
|
-
delete(bookId) {
|
|
251
|
-
const db2 = getDb();
|
|
252
|
-
db2.prepare("DELETE FROM reading_progress WHERE book_id = ?").run(bookId);
|
|
253
|
-
}
|
|
254
|
-
};
|
|
255
|
-
|
|
256
|
-
// src/parsers/TxtParser.ts
|
|
257
|
-
import { readFileSync } from "fs";
|
|
258
|
-
import { detect } from "chardet";
|
|
259
|
-
import iconv from "iconv-lite";
|
|
260
|
-
var CHAPTER_PATTERNS = [
|
|
261
|
-
/^第[零一二三四五六七八九十百千万\d]+[章节回卷集部篇]/m,
|
|
262
|
-
/^Chapter\s+\d+/im,
|
|
263
|
-
/^CHAPTER\s+\d+/m,
|
|
264
|
-
/^第\s*\d+\s*[章节回]/m
|
|
265
|
-
];
|
|
266
|
-
async function parseTxt(filePath) {
|
|
267
|
-
const buffer = readFileSync(filePath);
|
|
268
|
-
const encoding = detect(buffer) || "utf-8";
|
|
269
|
-
logger.debug(`\u68C0\u6D4B\u5230\u7F16\u7801: ${encoding}`);
|
|
270
|
-
const content = encoding.toLowerCase() === "utf-8" ? buffer.toString("utf-8") : iconv.decode(buffer, encoding);
|
|
271
|
-
const title = extractTitle(filePath);
|
|
272
|
-
const chapters = extractChapters(content);
|
|
273
|
-
return {
|
|
274
|
-
title,
|
|
275
|
-
author: null,
|
|
276
|
-
content,
|
|
277
|
-
chapters
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
function extractTitle(filePath) {
|
|
281
|
-
const basename = filePath.split("/").pop() || filePath;
|
|
282
|
-
return basename.replace(/\.txt$/i, "").trim() || "\u672A\u547D\u540D";
|
|
283
|
-
}
|
|
284
|
-
function extractChapters(content) {
|
|
285
|
-
const chapters = [];
|
|
286
|
-
const lines = content.split("\n");
|
|
287
|
-
let byteOffset = 0;
|
|
288
|
-
for (const line of lines) {
|
|
289
|
-
const trimmed = line.trim();
|
|
290
|
-
for (const pattern of CHAPTER_PATTERNS) {
|
|
291
|
-
if (pattern.test(trimmed)) {
|
|
292
|
-
chapters.push({
|
|
293
|
-
title: trimmed,
|
|
294
|
-
byteOffset
|
|
295
|
-
});
|
|
296
|
-
break;
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
byteOffset += Buffer.byteLength(line + "\n", "utf-8");
|
|
300
|
-
}
|
|
301
|
-
logger.debug(`\u68C0\u6D4B\u5230 ${chapters.length} \u4E2A\u7AE0\u8282`);
|
|
302
|
-
return chapters;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// src/parsers/EpubParser.ts
|
|
306
|
-
import { EPub } from "epub2";
|
|
307
|
-
import { convert } from "html-to-text";
|
|
308
|
-
import { detect as detect2 } from "chardet";
|
|
309
|
-
import iconv2 from "iconv-lite";
|
|
310
|
-
async function parseEpub(filePath) {
|
|
311
|
-
logger.debug(`\u5F00\u59CB\u89E3\u6790 EPUB: ${filePath}`);
|
|
312
|
-
const epub = await openEpub(filePath);
|
|
313
|
-
const title = epub.metadata.title || "\u672A\u77E5\u4E66\u540D";
|
|
314
|
-
const author = epub.metadata.creator || null;
|
|
315
|
-
const chapters = [];
|
|
316
|
-
let fullContent = "";
|
|
317
|
-
let currentByteOffset = 0;
|
|
318
|
-
for (const chapterRef of epub.flow) {
|
|
319
|
-
if (!chapterRef.id) continue;
|
|
320
|
-
try {
|
|
321
|
-
const htmlText = await getChapterHtml(epub, chapterRef.id);
|
|
322
|
-
const plainText = convert(htmlText, {
|
|
323
|
-
wordwrap: false,
|
|
324
|
-
selectors: [
|
|
325
|
-
// 忽略图片和链接
|
|
326
|
-
{ selector: "img", format: "skip" },
|
|
327
|
-
{ selector: "a", options: { ignoreHref: true } }
|
|
328
|
-
]
|
|
329
|
-
});
|
|
330
|
-
if (!plainText.trim()) continue;
|
|
331
|
-
const chapterTitle = chapterRef.title || "\u65E0\u6807\u9898\u7AE0\u8282";
|
|
332
|
-
chapters.push({
|
|
333
|
-
title: chapterTitle,
|
|
334
|
-
byteOffset: currentByteOffset
|
|
335
|
-
});
|
|
336
|
-
const chapterContent = `
|
|
337
|
-
|
|
338
|
-
${chapterTitle}
|
|
339
|
-
|
|
340
|
-
${plainText}
|
|
341
|
-
`;
|
|
342
|
-
fullContent += chapterContent;
|
|
343
|
-
currentByteOffset += Buffer.byteLength(chapterContent, "utf-8");
|
|
344
|
-
} catch (err) {
|
|
345
|
-
logger.debug(`\u8B66\u544A: \u8BFB\u53D6\u7AE0\u8282 ${chapterRef.id} \u5931\u8D25`, err);
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
return {
|
|
349
|
-
title,
|
|
350
|
-
author,
|
|
351
|
-
content: fullContent,
|
|
352
|
-
chapters
|
|
353
|
-
};
|
|
354
|
-
}
|
|
355
|
-
function openEpub(filePath) {
|
|
356
|
-
return new Promise((resolve3, reject) => {
|
|
357
|
-
const epub = new EPub(filePath);
|
|
358
|
-
epub.on("error", (err) => reject(err));
|
|
359
|
-
epub.on("end", () => resolve3(epub));
|
|
360
|
-
epub.parse();
|
|
361
|
-
});
|
|
362
|
-
}
|
|
363
|
-
function detectHtmlEncoding(buffer) {
|
|
364
|
-
const head = buffer.slice(0, 1024).toString("latin1");
|
|
365
|
-
const xmlDecl = head.match(/<\?xml[^>]*encoding=["']([^"']+)["']/i);
|
|
366
|
-
if (xmlDecl?.[1]) return xmlDecl[1];
|
|
367
|
-
const metaCharset = head.match(/charset=["']?([A-Za-z0-9_-]+)["']?/i);
|
|
368
|
-
if (metaCharset?.[1] && metaCharset[1].toLowerCase() !== "utf-8") {
|
|
369
|
-
return metaCharset[1];
|
|
370
|
-
}
|
|
371
|
-
const chardetResult = detect2(buffer);
|
|
372
|
-
return chardetResult || "utf-8";
|
|
373
|
-
}
|
|
374
|
-
function getChapterHtml(epub, chapterId) {
|
|
375
|
-
return new Promise((resolve3, reject) => {
|
|
376
|
-
epub.getFile(chapterId, (err, data) => {
|
|
377
|
-
if (err) return reject(err);
|
|
378
|
-
const buffer = data;
|
|
379
|
-
const encoding = detectHtmlEncoding(buffer);
|
|
380
|
-
const htmlText = encoding.toLowerCase().replace("-", "") === "utf8" || encoding.toLowerCase() === "utf-8" ? buffer.toString("utf-8") : iconv2.decode(buffer, encoding);
|
|
381
|
-
resolve3(htmlText);
|
|
382
|
-
});
|
|
383
|
-
});
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
// src/parsers/index.ts
|
|
387
|
-
async function parseFile(filePath, format) {
|
|
388
|
-
switch (format) {
|
|
389
|
-
case "txt":
|
|
390
|
-
return parseTxt(filePath);
|
|
391
|
-
case "epub":
|
|
392
|
-
return parseEpub(filePath);
|
|
393
|
-
default:
|
|
394
|
-
throw new Error(`\u4E0D\u652F\u6301\u7684\u6587\u4EF6\u683C\u5F0F: ${format}`);
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
// src/utils/hash.ts
|
|
399
|
-
import { createHash } from "crypto";
|
|
400
|
-
import { readFileSync as readFileSync2 } from "fs";
|
|
401
|
-
async function computeFileHash(filePath) {
|
|
402
|
-
const buffer = readFileSync2(filePath);
|
|
403
|
-
const hash = createHash("sha256");
|
|
404
|
-
hash.update(buffer);
|
|
405
|
-
return hash.digest("hex");
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
// src/services/BookService.ts
|
|
409
|
-
var BookService = class {
|
|
410
|
-
bookModel = new BookModel();
|
|
411
|
-
chapterModel = new ChapterModel();
|
|
412
|
-
recentModel = new RecentModel();
|
|
413
|
-
progressModel = new ProgressModel();
|
|
414
|
-
/**
|
|
415
|
-
* 导入书籍文件
|
|
416
|
-
*/
|
|
417
|
-
async importBook(filePath) {
|
|
418
|
-
const absPath = resolve(filePath);
|
|
419
|
-
if (!existsSync2(absPath)) {
|
|
420
|
-
throw new Error(`\u6587\u4EF6\u4E0D\u5B58\u5728: ${absPath}`);
|
|
421
|
-
}
|
|
422
|
-
const format = this.detectFormat(absPath);
|
|
423
|
-
if (!format) {
|
|
424
|
-
throw new Error("\u4E0D\u652F\u6301\u7684\u6587\u4EF6\u683C\u5F0F\u3002\u76EE\u524D\u652F\u6301: .txt, .epub");
|
|
425
|
-
}
|
|
426
|
-
const fileHash = await computeFileHash(absPath);
|
|
427
|
-
const existing = this.bookModel.findByHash(fileHash);
|
|
428
|
-
if (existing) {
|
|
429
|
-
logger.debug(`\u6587\u4EF6\u5DF2\u5B58\u5728: ${existing.title} (${existing.id})`);
|
|
430
|
-
return existing;
|
|
431
|
-
}
|
|
432
|
-
const parsed = await parseFile(absPath, format);
|
|
433
|
-
const stats = statSync(absPath);
|
|
434
|
-
const book = {
|
|
435
|
-
id: nanoid(),
|
|
436
|
-
title: parsed.title,
|
|
437
|
-
author: parsed.author || null,
|
|
438
|
-
file_path: absPath,
|
|
439
|
-
format,
|
|
440
|
-
file_hash: fileHash,
|
|
441
|
-
file_size: stats.size,
|
|
442
|
-
created_at: Date.now()
|
|
443
|
-
};
|
|
444
|
-
this.bookModel.insert(book);
|
|
445
|
-
if (parsed.chapters.length > 0) {
|
|
446
|
-
this.chapterModel.insertMany(
|
|
447
|
-
parsed.chapters.map((ch, idx) => ({
|
|
448
|
-
book_id: book.id,
|
|
449
|
-
chapter_no: idx,
|
|
450
|
-
title: ch.title,
|
|
451
|
-
byte_offset: ch.byteOffset
|
|
452
|
-
}))
|
|
453
|
-
);
|
|
454
|
-
}
|
|
455
|
-
logger.debug(`\u5BFC\u5165\u6210\u529F: ${book.title}`);
|
|
456
|
-
return book;
|
|
457
|
-
}
|
|
458
|
-
/**
|
|
459
|
-
* 查找书籍(ID 或模糊匹配书名)
|
|
460
|
-
*/
|
|
461
|
-
findBook(target) {
|
|
462
|
-
const byId = this.bookModel.findById(target);
|
|
463
|
-
if (byId) return byId;
|
|
464
|
-
const results = this.bookModel.searchByTitle(target);
|
|
465
|
-
return results[0];
|
|
466
|
-
}
|
|
467
|
-
/**
|
|
468
|
-
* 搜索书籍
|
|
469
|
-
*/
|
|
470
|
-
searchBooks(keyword) {
|
|
471
|
-
return this.bookModel.searchByTitle(keyword);
|
|
472
|
-
}
|
|
473
|
-
/**
|
|
474
|
-
* 获取所有书籍
|
|
475
|
-
*/
|
|
476
|
-
getAllBooks() {
|
|
477
|
-
return this.bookModel.findAll();
|
|
478
|
-
}
|
|
479
|
-
/**
|
|
480
|
-
* 删除书籍及相关数据
|
|
481
|
-
*/
|
|
482
|
-
deleteBook(id) {
|
|
483
|
-
this.chapterModel.deleteByBookId(id);
|
|
484
|
-
this.progressModel.delete(id);
|
|
485
|
-
this.recentModel.delete(id);
|
|
486
|
-
this.bookModel.delete(id);
|
|
487
|
-
}
|
|
488
|
-
/**
|
|
489
|
-
* 检测文件格式
|
|
490
|
-
*/
|
|
491
|
-
detectFormat(filePath) {
|
|
492
|
-
const ext = filePath.toLowerCase().split(".").pop();
|
|
493
|
-
if (ext === "txt") return "txt";
|
|
494
|
-
if (ext === "epub") return "epub";
|
|
495
|
-
return null;
|
|
496
|
-
}
|
|
497
|
-
};
|
|
498
|
-
|
|
499
|
-
// src/locales/zh.ts
|
|
500
|
-
var zh_default = {
|
|
501
|
-
// Common
|
|
502
|
-
"common.yes": "\u662F",
|
|
503
|
-
"common.no": "\u5426",
|
|
504
|
-
"common.confirm": "\u786E\u8BA4",
|
|
505
|
-
"common.cancel": "\u53D6\u6D88",
|
|
506
|
-
"common.quit": "\u6309 q \u9000\u51FA",
|
|
507
|
-
// CLI
|
|
508
|
-
"cli.import.desc": "\u5BFC\u5165\u672C\u5730\u6587\u4EF6\u6216\u76EE\u5F55\u5230\u4E66\u67B6",
|
|
509
|
-
"cli.import.help": "\u6587\u4EF6\u6216\u76EE\u5F55\u8DEF\u5F84\uFF08\u652F\u6301 .txt / .epub\uFF09",
|
|
510
|
-
"cli.import.success": "\u2713 \u5DF2\u5BFC\u5165:",
|
|
511
|
-
"cli.import.fail": "\u5BFC\u5165\u5931\u8D25:",
|
|
512
|
-
"cli.import.not_found": "\u8DEF\u5F84\u4E0D\u5B58\u5728:",
|
|
513
|
-
"cli.import.unsupported": "\u4E0D\u652F\u6301\u7684\u6587\u4EF6\u683C\u5F0F\u3002\u76EE\u524D\u652F\u6301: .txt, .epub",
|
|
514
|
-
"cli.import.scan_dir": "\u626B\u63CF\u76EE\u5F55",
|
|
515
|
-
"cli.import.found_files": "\u627E\u5230\u4EE5\u4E0B\u4E66\u7C4D\uFF1A",
|
|
516
|
-
"cli.import.confirm_batch": "\u662F\u5426\u786E\u8BA4\u5BFC\u5165\u4E0A\u8FF0 {0} \u672C\u4E66\uFF1F(y/N)",
|
|
517
|
-
"cli.import.canceled": "\u2713 \u5BFC\u5165\u5DF2\u53D6\u6D88",
|
|
518
|
-
"cli.resume.desc": "\u6062\u590D\u4E0A\u6B21\u9605\u8BFB",
|
|
519
|
-
"cli.resume.none": "\u2717 \u6CA1\u6709\u627E\u5230\u6700\u8FD1\u9605\u8BFB\u8BB0\u5F55\uFF0C\u8BF7\u5148\u4F7F\u7528 novel import <file> \u6216 novel open <book-id> \u6253\u5F00\u4E00\u672C\u4E66\u3002",
|
|
520
|
-
"cli.open.desc": "\u6253\u5F00\u6307\u5B9A\u4E66\u7C4D",
|
|
521
|
-
"cli.open.help": "\u4E66\u7C4D ID \u6216\u4E66\u540D\uFF08\u652F\u6301\u6A21\u7CCA\u5339\u914D\uFF09",
|
|
522
|
-
"cli.open.not_found": "\u2717 \u672A\u627E\u5230\u5339\u914D\u4E66\u7C4D:",
|
|
523
|
-
"cli.library.desc": "\u67E5\u770B\u4E66\u67B6\u4E66\u7C4D\u5217\u8868",
|
|
524
|
-
"cli.library.help": "\u53EF\u9009\u5173\u952E\u5B57\u641C\u7D22",
|
|
525
|
-
"cli.library.none": "\u2717 \u4E66\u67B6\u4E3A\u7A7A\u3002\u4F7F\u7528 novel import <file> \u5BFC\u5165\u4F60\u7684\u7B2C\u4E00\u672C\u4E66\u3002",
|
|
526
|
-
"cli.library.search_none": "\u{1F4DA} \u672A\u627E\u5230\u5339\u914D\u300C{0}\u300D\u7684\u4E66\u7C4D\u3002",
|
|
527
|
-
"cli.library.search_result": "\u{1F4DA} \u641C\u7D22\u7ED3\u679C ({0} \u672C):\n",
|
|
528
|
-
"cli.remove.desc": "\u4ECE\u4E66\u67B6\u79FB\u9664\u4E66\u7C4D\uFF08\u4EC5\u5220\u9664\u8BB0\u5F55\uFF0C\u4E0D\u5220\u6E90\u6587\u4EF6\uFF09",
|
|
529
|
-
"cli.remove.help": "\u4E66\u7C4D ID \u6216\u4E66\u540D\uFF08\u652F\u6301\u6A21\u7CCA\u5339\u914D\uFF09",
|
|
530
|
-
"cli.remove.not_found": "\u2717 \u672A\u627E\u5230\u5339\u914D\u4E66\u7C4D:",
|
|
531
|
-
"cli.remove.success": "\u2713 \u5DF2\u79FB\u9664\u4E66\u7C4D:",
|
|
532
|
-
"cli.remove.fail": "\u79FB\u9664\u4E66\u7C4D\u5931\u8D25:",
|
|
533
|
-
"cli.lang.desc": "\u5207\u6362\u754C\u9762\u8BED\u8A00",
|
|
534
|
-
"cli.lang.help": "\u76EE\u6807\u8BED\u8A00\uFF08zh \u4E2D\u6587 | en \u82F1\u6587\uFF09",
|
|
535
|
-
"cli.lang.success": "\u2713 \u8BED\u8A00\u5DF2\u5207\u6362\u4E3A: {0}",
|
|
536
|
-
"cli.lang.unsupported": "\u2717 \u4E0D\u652F\u6301\u7684\u8BED\u8A00: {0}",
|
|
537
|
-
"cli.config.desc": "\u4FEE\u6539\u5E94\u7528\u914D\u7F6E",
|
|
538
|
-
"cli.config.line_spacing.desc": "\u884C\u95F4\u8DDD: 0 (\u7D27\u51D1) | 1 (\u9002\u4E2D) | 2 (\u5BBD\u677E)",
|
|
539
|
-
"cli.config.line_spacing.success": "\u2713 \u884C\u95F4\u8DDD\u5DF2\u8BBE\u7F6E\u4E3A: {0}",
|
|
540
|
-
"cli.config.reading_mode.desc": "\u9605\u8BFB\u6A21\u5F0F: page (\u7FFB\u9875) | scroll (\u6EDA\u52A8)",
|
|
541
|
-
"cli.update.desc": "\u68C0\u67E5\u6700\u65B0\u7248\u672C\u5E76\u81EA\u52A8\u66F4\u65B0",
|
|
542
|
-
"cli.update.checking": "\u6B63\u5728\u68C0\u67E5\u66F4\u65B0...",
|
|
543
|
-
"cli.update.latest": "\u2713 \u5F53\u524D\u5DF2\u662F\u6700\u65B0\u7248\u672C (v{0})",
|
|
544
|
-
"cli.update.updating": "\u53D1\u73B0\u65B0\u7248\u672C v{0} (\u5F53\u524D v{1})\uFF0C\u6B63\u5728\u66F4\u65B0...",
|
|
545
|
-
"cli.update.success": "\u2713 \u5347\u7EA7\u6210\u529F\uFF01\u8BF7\u91CD\u65B0\u8FD0\u884C novel \u547D\u4EE4\u3002",
|
|
546
|
-
"cli.update.fail": "\u2717 \u5347\u7EA7\u5931\u8D25: {0}",
|
|
547
|
-
// TUI - Library
|
|
548
|
-
"tui.lib.loading": "\u{1F4DA} \u52A0\u8F7D\u4E66\u67B6...",
|
|
549
|
-
"tui.lib.empty.title": "\u{1F4DA} \u4E66\u67B6",
|
|
550
|
-
"tui.lib.empty.desc": "\u4E66\u67B6\u4E3A\u7A7A\u3002\u4F7F\u7528 novel import <file> \u5BFC\u5165\u4F60\u7684\u7B2C\u4E00\u672C\u4E66\u3002",
|
|
551
|
-
"tui.lib.title": "\u{1F4DA} \u4E66\u67B6 ({0} \u672C)",
|
|
552
|
-
"tui.lib.tips": " \u2191\u2193/jk \u9009\u62E9 \xB7 Enter \u6253\u5F00 \xB7 d/x \u5220\u9664 \xB7 q \u9000\u51FA",
|
|
553
|
-
// TUI - Reader
|
|
554
|
-
"tui.reader.loading": "\u8BFB\u53D6\u4E2D...",
|
|
555
|
-
"tui.reader.bookmark_add": "\u2713 \u589E\u52A0\u4E66\u7B7E: {0}",
|
|
556
|
-
"tui.reader.status.remaining": "\u9884\u8BA1\u5269\u4F59 {0}",
|
|
557
|
-
"tui.nav.tab.chapters": "[\u5168\u90E8\u7AE0\u8282]",
|
|
558
|
-
"tui.nav.tab.bookmarks": "[\u6211\u7684\u4E66\u7B7E]",
|
|
559
|
-
"tui.nav.tips": "Enter \u8DF3\u8F6C \xB7 Tab \u5207\u6362 \xB7 Esc/q \u5173\u95ED",
|
|
560
|
-
"tui.nav.hint": "\u6309 C \u952E\u5F39\u51FA\u6B64\u6E05\u5355\uFF0C\u6309 Tab \u5207\u6362\u4E66\u7B7E",
|
|
561
|
-
"tui.nav.empty": "\u6CA1\u6709\u8BB0\u5F55",
|
|
562
|
-
"tui.nav.page": "\u7B2C {0} / {1} \u9875",
|
|
563
|
-
// Login / Logout / Sync
|
|
564
|
-
"cli.login.already": "\u5DF2\u767B\u5F55\uFF08{0}\uFF09\uFF0C\u5982\u9700\u5207\u6362\u8BF7\u5148 novel logout",
|
|
565
|
-
"cli.login.open_browser": "\u8BF7\u5728\u6D4F\u89C8\u5668\u6253\u5F00\uFF1A{0}",
|
|
566
|
-
"cli.login.user_code": "\u6388\u6743\u7801\uFF1A{0}\uFF08\u5728 Dashboard \u786E\u8BA4\u65F6\u9700\u6838\u5BF9\u6B64\u7801\uFF09",
|
|
567
|
-
"cli.login.waiting": "\u7B49\u5F85\u6388\u6743\u4E2D...\uFF085 \u5206\u949F\u8D85\u65F6\uFF09",
|
|
568
|
-
"cli.login.success": "\u767B\u5F55\u6210\u529F\uFF01\u8D26\u6237\u5C42\u7EA7\uFF1A{0}",
|
|
569
|
-
"cli.login.timeout": "\u6388\u6743\u8D85\u65F6\uFF0C\u8BF7\u91CD\u65B0\u8FD0\u884C novel login",
|
|
570
|
-
"cli.login.migrate_hint": "\u63D0\u793A\uFF1A\u5982\u4ECE\u5F00\u6E90\u7248\u8FC1\u79FB\uFF0C\u8BF7\u8FD0\u884C\uFF1Anpm uninstall -g readshell && npm install -g readshell-pro",
|
|
571
|
-
"cli.logout.success": "\u5DF2\u9000\u51FA\u767B\u5F55\uFF0C\u5207\u6362\u4E3A\u672C\u5730\u6A21\u5F0F",
|
|
572
|
-
"cli.sync.not_logged_in": "\u8BF7\u5148\u8FD0\u884C novel login",
|
|
573
|
-
"cli.sync.need_pro": "\u4E91\u540C\u6B65\u9700\u8981 Pro \u8BA2\u9605\uFF0C\u8BF7\u8BBF\u95EE app.readshell.com \u5347\u7EA7",
|
|
574
|
-
"cli.sync.running": "\u540C\u6B65\u4E2D...",
|
|
575
|
-
"cli.sync.success": "\u540C\u6B65\u5B8C\u6210\uFF08\u4E0A\u4F20 {0} \u6761\uFF0C\u4E0B\u8F7D {1} \u6761\uFF09",
|
|
576
|
-
"cli.sync.failed": "\u540C\u6B65\u5931\u8D25\uFF1A{0}"
|
|
577
|
-
};
|
|
578
|
-
|
|
579
|
-
// src/locales/en.ts
|
|
580
|
-
var en = {
|
|
581
|
-
// Common
|
|
582
|
-
"common.yes": "Yes",
|
|
583
|
-
"common.no": "No",
|
|
584
|
-
"common.confirm": "Confirm",
|
|
585
|
-
"common.cancel": "Cancel",
|
|
586
|
-
"common.quit": "Press q to quit",
|
|
587
|
-
// CLI
|
|
588
|
-
"cli.import.desc": "Import local file or directory to library",
|
|
589
|
-
"cli.import.help": "File or directory path (supports .txt / .epub)",
|
|
590
|
-
"cli.import.success": "\u2713 Imported:",
|
|
591
|
-
"cli.import.fail": "Import failed:",
|
|
592
|
-
"cli.import.not_found": "Path not found:",
|
|
593
|
-
"cli.import.unsupported": "Unsupported file format. Currently supports: .txt, .epub",
|
|
594
|
-
"cli.import.scan_dir": "Scanning directory",
|
|
595
|
-
"cli.import.found_files": "Found following books:",
|
|
596
|
-
"cli.import.confirm_batch": "Confirm importing these {0} books? (y/N)",
|
|
597
|
-
"cli.import.canceled": "\u2713 Import canceled",
|
|
598
|
-
"cli.resume.desc": "Resume last reading",
|
|
599
|
-
"cli.resume.none": "\u2717 No recent reading record found. Please use `novel import <file>` or `novel open <book-id>` first.",
|
|
600
|
-
"cli.open.desc": "Open specific book",
|
|
601
|
-
"cli.open.help": "Book ID or title (fuzzy match)",
|
|
602
|
-
"cli.open.not_found": "\u2717 Book not found:",
|
|
603
|
-
"cli.library.desc": "View book list / library",
|
|
604
|
-
"cli.library.help": "Optional keyword search",
|
|
605
|
-
"cli.library.none": "\u2717 Library is empty. Use `novel import <file>` to import your first book.",
|
|
606
|
-
"cli.library.search_none": '\u{1F4DA} No books found matching "{0}".',
|
|
607
|
-
"cli.library.search_result": "\u{1F4DA} Search results ({0} books):\n",
|
|
608
|
-
"cli.remove.desc": "Remove book from library (only deletes records, not source file)",
|
|
609
|
-
"cli.remove.help": "Book ID or title (fuzzy match)",
|
|
610
|
-
"cli.remove.not_found": "\u2717 Book not found:",
|
|
611
|
-
"cli.remove.success": "\u2713 Book removed:",
|
|
612
|
-
"cli.remove.fail": "Failed to remove book:",
|
|
613
|
-
"cli.lang.desc": "Switch interface language",
|
|
614
|
-
"cli.lang.help": "Target language (zh Chinese | en English)",
|
|
615
|
-
"cli.lang.success": "\u2713 Language switched to: {0}",
|
|
616
|
-
"cli.lang.unsupported": "\u2717 Unsupported language: {0}",
|
|
617
|
-
"cli.config.desc": "Modify application configuration",
|
|
618
|
-
"cli.config.line_spacing.desc": "Line spacing: 0 (tight) | 1 (medium) | 2 (loose)",
|
|
619
|
-
"cli.config.line_spacing.success": "\u2713 Line spacing set to: {0}",
|
|
620
|
-
"cli.config.reading_mode.desc": "Reading mode: page (paging) | scroll (scrolling)",
|
|
621
|
-
"cli.update.desc": "Check for the latest version and update automatically",
|
|
622
|
-
"cli.update.checking": "Checking for updates...",
|
|
623
|
-
"cli.update.latest": "\u2713 Already up to date (v{0})",
|
|
624
|
-
"cli.update.updating": "New version v{0} found (current v{1}), updating...",
|
|
625
|
-
"cli.update.success": "\u2713 Successfully updated! Please restart novel command.",
|
|
626
|
-
"cli.update.fail": "\u2717 Update failed: {0}",
|
|
627
|
-
// TUI - Library
|
|
628
|
-
"tui.lib.loading": "\u{1F4DA} Loading library...",
|
|
629
|
-
"tui.lib.empty.title": "\u{1F4DA} Library",
|
|
630
|
-
"tui.lib.empty.desc": "Library is empty. Use `novel import <file>` to import your first book.",
|
|
631
|
-
"tui.lib.title": "\u{1F4DA} Library ({0} books)",
|
|
632
|
-
"tui.lib.tips": " \u2191\u2193/jk select \xB7 Enter open \xB7 d/x delete \xB7 q quit",
|
|
633
|
-
// TUI - Reader
|
|
634
|
-
"tui.reader.loading": "Loading...",
|
|
635
|
-
"tui.reader.bookmark_add": "\u2713 Bookmark added: {0}",
|
|
636
|
-
"tui.reader.status.remaining": "Est. remaining {0}",
|
|
637
|
-
// TUI - ChapterNav
|
|
638
|
-
"tui.nav.tab.chapters": "[All Chapters]",
|
|
639
|
-
"tui.nav.tab.bookmarks": "[My Bookmarks]",
|
|
640
|
-
"tui.nav.tips": "Enter jump \xB7 Tab switch \xB7 Esc/q close",
|
|
641
|
-
"tui.nav.hint": "Press C to open this list, press Tab to switch to bookmarks",
|
|
642
|
-
"tui.nav.empty": "No records",
|
|
643
|
-
"tui.nav.page": "Page {0} / {1}",
|
|
644
|
-
// Login / Logout / Sync
|
|
645
|
-
"cli.login.already": "Already logged in ({0}). Run novel logout to switch accounts.",
|
|
646
|
-
"cli.login.open_browser": "Open in browser: {0}",
|
|
647
|
-
"cli.login.user_code": "Authorization code: {0} (verify this in the Dashboard)",
|
|
648
|
-
"cli.login.waiting": "Waiting for authorization... (5-minute timeout)",
|
|
649
|
-
"cli.login.success": "Login successful! Account tier: {0}",
|
|
650
|
-
"cli.login.timeout": "Authorization timed out. Please run novel login again.",
|
|
651
|
-
"cli.login.migrate_hint": "Tip: migrating from open-source? Run: npm uninstall -g readshell && npm install -g readshell-pro",
|
|
652
|
-
"cli.logout.success": "Logged out. Switched to local-only mode.",
|
|
653
|
-
"cli.sync.not_logged_in": "Please run novel login first.",
|
|
654
|
-
"cli.sync.need_pro": "Cloud sync requires a Pro subscription. Visit app.readshell.com to upgrade.",
|
|
655
|
-
"cli.sync.running": "Syncing...",
|
|
656
|
-
"cli.sync.success": "Sync complete (uploaded {0}, downloaded {1})",
|
|
657
|
-
"cli.sync.failed": "Sync failed: {0}"
|
|
658
|
-
};
|
|
659
|
-
var en_default = en;
|
|
660
|
-
|
|
661
|
-
// src/config/AppConfig.ts
|
|
662
|
-
import Conf from "conf";
|
|
663
|
-
var defaults = {
|
|
664
|
-
linesPerPage: 0,
|
|
665
|
-
showStatusBar: true,
|
|
666
|
-
readingMode: "page",
|
|
667
|
-
language: "zh",
|
|
668
|
-
lineSpacing: 0,
|
|
669
|
-
bossKeyLang: "nodejs"
|
|
670
|
-
};
|
|
671
|
-
var config = new Conf({
|
|
672
|
-
projectName: "readshell",
|
|
673
|
-
defaults
|
|
674
|
-
});
|
|
675
|
-
function getConfig() {
|
|
676
|
-
return {
|
|
677
|
-
linesPerPage: config.get("linesPerPage"),
|
|
678
|
-
showStatusBar: config.get("showStatusBar"),
|
|
679
|
-
readingMode: config.get("readingMode"),
|
|
680
|
-
language: config.get("language"),
|
|
681
|
-
lineSpacing: config.get("lineSpacing"),
|
|
682
|
-
bossKeyLang: config.get("bossKeyLang"),
|
|
683
|
-
authToken: config.get("authToken"),
|
|
684
|
-
authUserId: config.get("authUserId"),
|
|
685
|
-
authDeviceId: config.get("authDeviceId"),
|
|
686
|
-
authTier: config.get("authTier"),
|
|
687
|
-
authExpiresAt: config.get("authExpiresAt")
|
|
688
|
-
};
|
|
689
|
-
}
|
|
690
|
-
function setConfig(key, value) {
|
|
691
|
-
config.set(key, value);
|
|
692
|
-
}
|
|
693
|
-
function isLoggedIn() {
|
|
694
|
-
const token = config.get("authToken");
|
|
695
|
-
const expiresAt = config.get("authExpiresAt");
|
|
696
|
-
if (!token) return false;
|
|
697
|
-
if (!expiresAt) return false;
|
|
698
|
-
return new Date(expiresAt) > /* @__PURE__ */ new Date();
|
|
699
|
-
}
|
|
700
|
-
function getAuthToken() {
|
|
701
|
-
return config.get("authToken") ?? null;
|
|
702
|
-
}
|
|
703
|
-
function getAuthDeviceId() {
|
|
704
|
-
return config.get("authDeviceId") ?? null;
|
|
705
|
-
}
|
|
706
|
-
function getAuthTier() {
|
|
707
|
-
return config.get("authTier") ?? null;
|
|
708
|
-
}
|
|
709
|
-
function clearAuth() {
|
|
710
|
-
config.delete("authToken");
|
|
711
|
-
config.delete("authUserId");
|
|
712
|
-
config.delete("authDeviceId");
|
|
713
|
-
config.delete("authTier");
|
|
714
|
-
config.delete("authExpiresAt");
|
|
715
|
-
}
|
|
716
|
-
function saveAuth(data) {
|
|
717
|
-
config.set("authToken", data.authToken);
|
|
718
|
-
config.set("authUserId", data.authUserId);
|
|
719
|
-
config.set("authDeviceId", data.authDeviceId);
|
|
720
|
-
config.set("authTier", data.authTier);
|
|
721
|
-
config.set("authExpiresAt", data.authExpiresAt);
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
// src/locales/index.ts
|
|
725
|
-
var dictionaries = {
|
|
726
|
-
zh: zh_default,
|
|
727
|
-
en: en_default
|
|
728
|
-
};
|
|
729
|
-
var currentLang = "zh";
|
|
730
|
-
var currentDict = dictionaries.zh;
|
|
731
|
-
function initI18n() {
|
|
732
|
-
const config2 = getConfig();
|
|
733
|
-
currentLang = config2.language || "zh";
|
|
734
|
-
currentDict = dictionaries[currentLang] || dictionaries.zh;
|
|
735
|
-
}
|
|
736
|
-
function setLanguage(lang) {
|
|
737
|
-
currentLang = lang;
|
|
738
|
-
currentDict = dictionaries[lang] || dictionaries.zh;
|
|
739
|
-
}
|
|
740
|
-
function t(key, ...args) {
|
|
741
|
-
let template = currentDict[key];
|
|
742
|
-
if (!template) {
|
|
743
|
-
return key;
|
|
744
|
-
}
|
|
745
|
-
if (args.length > 0) {
|
|
746
|
-
args.forEach((arg, index) => {
|
|
747
|
-
template = template.replace(`{${index}}`, String(arg));
|
|
748
|
-
});
|
|
749
|
-
}
|
|
750
|
-
return template;
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
// src/cli/commands/import.ts
|
|
754
|
-
import { statSync as statSync2, readdirSync } from "fs";
|
|
755
|
-
import { resolve as resolve2, join as join2, extname } from "path";
|
|
756
|
-
import * as readline from "readline/promises";
|
|
757
|
-
function scanDirectory(dir) {
|
|
758
|
-
let results = [];
|
|
759
|
-
try {
|
|
760
|
-
const list = readdirSync(dir);
|
|
761
|
-
for (const file of list) {
|
|
762
|
-
const fullPath = join2(dir, file);
|
|
763
|
-
const stat = statSync2(fullPath);
|
|
764
|
-
if (stat.isDirectory()) {
|
|
765
|
-
results = results.concat(scanDirectory(fullPath));
|
|
766
|
-
} else {
|
|
767
|
-
const ext = extname(fullPath).toLowerCase();
|
|
768
|
-
if (ext === ".txt" || ext === ".epub") {
|
|
769
|
-
results.push(fullPath);
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
} catch (err) {
|
|
774
|
-
logger.error("Scan error:", err);
|
|
775
|
-
}
|
|
776
|
-
return results;
|
|
777
|
-
}
|
|
778
|
-
var importCommand = {
|
|
779
|
-
command: "import <file>",
|
|
780
|
-
describe: t("cli.import.desc"),
|
|
781
|
-
builder: (yargs2) => {
|
|
782
|
-
return yargs2.positional("file", {
|
|
783
|
-
describe: t("cli.import.help"),
|
|
784
|
-
type: "string",
|
|
785
|
-
demandOption: true
|
|
786
|
-
});
|
|
787
|
-
},
|
|
788
|
-
handler: async (argv) => {
|
|
789
|
-
try {
|
|
790
|
-
const targetPath = resolve2(argv.file);
|
|
791
|
-
let stat;
|
|
792
|
-
try {
|
|
793
|
-
stat = statSync2(targetPath);
|
|
794
|
-
} catch (e) {
|
|
795
|
-
console.log(`${t("cli.import.not_found")} ${targetPath}`);
|
|
796
|
-
process.exit(1);
|
|
797
|
-
}
|
|
798
|
-
const bookService = new BookService();
|
|
799
|
-
if (stat.isDirectory()) {
|
|
800
|
-
console.log(`${t("cli.import.scan_dir")} ${targetPath}...`);
|
|
801
|
-
const files = scanDirectory(targetPath);
|
|
802
|
-
if (files.length === 0) {
|
|
803
|
-
console.log(`\u2717 ` + t("cli.import.unsupported"));
|
|
804
|
-
return;
|
|
805
|
-
}
|
|
806
|
-
console.log(t("cli.import.found_files"));
|
|
807
|
-
files.forEach((f, i) => console.log(` ${i + 1}. ${f}`));
|
|
808
|
-
const rl = readline.createInterface({
|
|
809
|
-
input: process.stdin,
|
|
810
|
-
output: process.stdout
|
|
811
|
-
});
|
|
812
|
-
const answer = await rl.question(t("cli.import.confirm_batch", files.length) + " ");
|
|
813
|
-
rl.close();
|
|
814
|
-
if (answer.toLowerCase() === "y") {
|
|
815
|
-
for (const file of files) {
|
|
816
|
-
try {
|
|
817
|
-
const book = await bookService.importBook(file);
|
|
818
|
-
console.log(`${t("cli.import.success")} ${book.title} (${book.id})`);
|
|
819
|
-
} catch (err) {
|
|
820
|
-
console.log(`${t("cli.import.fail")} ${file} - ${err}`);
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
} else {
|
|
824
|
-
console.log(t("cli.import.canceled"));
|
|
825
|
-
}
|
|
826
|
-
} else {
|
|
827
|
-
const book = await bookService.importBook(argv.file);
|
|
828
|
-
console.log(`${t("cli.import.success")} ${book.title} (${book.id})`);
|
|
829
|
-
}
|
|
830
|
-
} catch (error) {
|
|
831
|
-
console.log(`${t("cli.import.fail")} ${error}`);
|
|
832
|
-
process.exit(1);
|
|
833
|
-
}
|
|
834
|
-
}
|
|
835
|
-
};
|
|
836
|
-
|
|
837
|
-
// src/services/ProgressService.ts
|
|
838
|
-
var ProgressService = class {
|
|
839
|
-
progressModel = new ProgressModel();
|
|
840
|
-
recentModel = new RecentModel();
|
|
841
|
-
/**
|
|
842
|
-
* 保存阅读进度(退出时调用)
|
|
843
|
-
*/
|
|
844
|
-
saveProgress(bookId, chapterNo, byteOffset, percent) {
|
|
845
|
-
const now = Date.now();
|
|
846
|
-
this.progressModel.upsert({
|
|
847
|
-
book_id: bookId,
|
|
848
|
-
chapter_no: chapterNo,
|
|
849
|
-
byte_offset: byteOffset,
|
|
850
|
-
percent,
|
|
851
|
-
updated_at: now,
|
|
852
|
-
opened_at: now
|
|
853
|
-
});
|
|
854
|
-
this.recentModel.recordOpen(bookId);
|
|
855
|
-
logger.debug(`\u8FDB\u5EA6\u5DF2\u4FDD\u5B58: book=${bookId}, chapter=${chapterNo}, offset=${byteOffset}, ${(percent * 100).toFixed(1)}%`);
|
|
856
|
-
}
|
|
857
|
-
/**
|
|
858
|
-
* 获取指定书籍的阅读进度(resume 时调用)
|
|
859
|
-
*/
|
|
860
|
-
getProgress(bookId) {
|
|
861
|
-
return this.progressModel.findByBookId(bookId);
|
|
862
|
-
}
|
|
863
|
-
/**
|
|
864
|
-
* 获取最近打开的书籍进度(启动时调用,决定 resume 哪本书)
|
|
865
|
-
*/
|
|
866
|
-
getLastOpenedBook() {
|
|
867
|
-
return this.progressModel.getLastOpened();
|
|
868
|
-
}
|
|
869
|
-
};
|
|
870
|
-
|
|
871
|
-
// src/ui/renderApp.ts
|
|
872
|
-
import React6 from "react";
|
|
873
|
-
import { render } from "ink";
|
|
874
|
-
|
|
875
|
-
// src/ui/App.tsx
|
|
876
|
-
import { useState as useState6 } from "react";
|
|
877
|
-
import { Box as Box7, Text as Text7 } from "ink";
|
|
878
|
-
|
|
879
|
-
// src/ui/pages/ResumePage.tsx
|
|
880
|
-
import { useEffect, useState } from "react";
|
|
881
|
-
import { Box, Text, useApp } from "ink";
|
|
882
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
883
|
-
function ResumePage({ onNavigate }) {
|
|
884
|
-
const { exit } = useApp();
|
|
885
|
-
const [checking, setChecking] = useState(true);
|
|
886
|
-
useEffect(() => {
|
|
887
|
-
const progressService = new ProgressService();
|
|
888
|
-
const lastProgress = progressService.getLastOpenedBook();
|
|
889
|
-
if (!lastProgress) {
|
|
890
|
-
setChecking(false);
|
|
891
|
-
return;
|
|
892
|
-
}
|
|
893
|
-
const bookModel = new BookModel();
|
|
894
|
-
const book = bookModel.findById(lastProgress.book_id);
|
|
895
|
-
if (!book) {
|
|
896
|
-
setChecking(false);
|
|
897
|
-
return;
|
|
898
|
-
}
|
|
899
|
-
onNavigate("reader", book.id, lastProgress.byte_offset);
|
|
900
|
-
}, [onNavigate]);
|
|
901
|
-
if (checking) {
|
|
902
|
-
return /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { color: "cyan", children: "\u{1F4D6} \u68C0\u67E5\u9605\u8BFB\u8BB0\u5F55..." }) });
|
|
903
|
-
}
|
|
904
|
-
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", padding: 1, children: [
|
|
905
|
-
/* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: "\u{1F4D6} ReadShell \u2014 \u7EC8\u7AEF\u5185\u8F7B\u9605\u8BFB" }),
|
|
906
|
-
/* @__PURE__ */ jsxs(Box, { marginTop: 1, flexDirection: "column", children: [
|
|
907
|
-
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u6682\u65E0\u9605\u8BFB\u8BB0\u5F55\u3002" }),
|
|
908
|
-
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u4F7F\u7528 novel import <file> \u5BFC\u5165\u4E00\u672C\u4E66\u5F00\u59CB\u9605\u8BFB\u3002" }),
|
|
909
|
-
/* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u6309 q \u9000\u51FA" }) })
|
|
910
|
-
] })
|
|
911
|
-
] });
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
// src/ui/pages/LibraryPage.tsx
|
|
915
|
-
import { useState as useState2, useEffect as useEffect2 } from "react";
|
|
916
|
-
import { Box as Box2, Text as Text2, useApp as useApp2, useInput } from "ink";
|
|
917
|
-
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
918
|
-
function LibraryPage({ onNavigate }) {
|
|
919
|
-
const { exit } = useApp2();
|
|
920
|
-
const [books, setBooks] = useState2([]);
|
|
921
|
-
const [selectedIndex, setSelectedIndex] = useState2(0);
|
|
922
|
-
const [loading, setLoading] = useState2(true);
|
|
923
|
-
const isRawModeSupported = process.stdin.isTTY ?? false;
|
|
924
|
-
useEffect2(() => {
|
|
925
|
-
const bookService = new BookService();
|
|
926
|
-
setBooks(bookService.getAllBooks());
|
|
927
|
-
setLoading(false);
|
|
928
|
-
}, []);
|
|
929
|
-
useInput((input, key) => {
|
|
930
|
-
if (input === "q") {
|
|
931
|
-
exit();
|
|
932
|
-
return;
|
|
933
|
-
}
|
|
934
|
-
if (books.length === 0) return;
|
|
935
|
-
if (key.upArrow || input === "k") {
|
|
936
|
-
setSelectedIndex((prev) => Math.max(prev - 1, 0));
|
|
937
|
-
}
|
|
938
|
-
if (key.downArrow || input === "j") {
|
|
939
|
-
setSelectedIndex((prev) => Math.min(prev + 1, books.length - 1));
|
|
940
|
-
}
|
|
941
|
-
if (key.backspace || key.delete || input === "d" || input === "x") {
|
|
942
|
-
const selected = books[selectedIndex];
|
|
943
|
-
if (selected) {
|
|
944
|
-
const bookService = new BookService();
|
|
945
|
-
bookService.deleteBook(selected.id);
|
|
946
|
-
setBooks((prev) => {
|
|
947
|
-
const next = prev.filter((b) => b.id !== selected.id);
|
|
948
|
-
if (selectedIndex >= next.length) {
|
|
949
|
-
setSelectedIndex(Math.max(0, next.length - 1));
|
|
950
|
-
}
|
|
951
|
-
return next;
|
|
952
|
-
});
|
|
953
|
-
}
|
|
954
|
-
return;
|
|
955
|
-
}
|
|
956
|
-
if (key.return) {
|
|
957
|
-
const selected = books[selectedIndex];
|
|
958
|
-
if (selected) {
|
|
959
|
-
const progressService = new ProgressService();
|
|
960
|
-
const progress = progressService.getProgress(selected.id);
|
|
961
|
-
onNavigate("reader", selected.id, progress?.byte_offset ?? 0);
|
|
962
|
-
}
|
|
963
|
-
}
|
|
964
|
-
}, { isActive: isRawModeSupported });
|
|
965
|
-
if (loading) {
|
|
966
|
-
return /* @__PURE__ */ jsx2(Box2, { padding: 1, children: /* @__PURE__ */ jsx2(Text2, { color: "cyan", children: t("tui.lib.loading") }) });
|
|
967
|
-
}
|
|
968
|
-
if (books.length === 0) {
|
|
969
|
-
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", padding: 1, children: [
|
|
970
|
-
/* @__PURE__ */ jsx2(Text2, { bold: true, color: "cyan", children: t("tui.lib.empty.title") }),
|
|
971
|
-
/* @__PURE__ */ jsxs2(Box2, { marginTop: 1, flexDirection: "column", children: [
|
|
972
|
-
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: t("tui.lib.empty.desc") }),
|
|
973
|
-
/* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: t("common.quit") }) })
|
|
974
|
-
] })
|
|
975
|
-
] });
|
|
976
|
-
}
|
|
977
|
-
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", padding: 1, children: [
|
|
978
|
-
/* @__PURE__ */ jsx2(Text2, { bold: true, color: "cyan", children: t("tui.lib.title", books.length) }),
|
|
979
|
-
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: t("tui.lib.tips") }),
|
|
980
|
-
/* @__PURE__ */ jsx2(Box2, { flexDirection: "column", marginTop: 1, children: books.map((book, index) => {
|
|
981
|
-
const isSelected = index === selectedIndex;
|
|
982
|
-
return /* @__PURE__ */ jsx2(Box2, { paddingX: 1, justifyContent: "space-between", children: /* @__PURE__ */ jsxs2(Box2, { children: [
|
|
983
|
-
/* @__PURE__ */ jsxs2(
|
|
984
|
-
Text2,
|
|
985
|
-
{
|
|
986
|
-
color: isSelected ? "cyan" : void 0,
|
|
987
|
-
bold: isSelected,
|
|
988
|
-
children: [
|
|
989
|
-
isSelected ? "\u25B8 " : " ",
|
|
990
|
-
book.title
|
|
991
|
-
]
|
|
992
|
-
}
|
|
993
|
-
),
|
|
994
|
-
/* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
995
|
-
" (",
|
|
996
|
-
book.format,
|
|
997
|
-
")"
|
|
998
|
-
] })
|
|
999
|
-
] }) }, book.id);
|
|
1000
|
-
}) })
|
|
1001
|
-
] });
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
// src/ui/pages/ReaderPage.tsx
|
|
1005
|
-
import { useState as useState5, useEffect as useEffect4, useRef } from "react";
|
|
1006
|
-
import { Box as Box6, Text as Text6, useApp as useApp3, useStdout } from "ink";
|
|
1007
|
-
|
|
1008
|
-
// src/ui/components/TextRenderer.tsx
|
|
1009
|
-
import { Box as Box3, Text as Text3 } from "ink";
|
|
1010
|
-
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
1011
|
-
function renderLineWithHighlight(line) {
|
|
1012
|
-
if (!line) return /* @__PURE__ */ jsx3(Text3, { children: " " });
|
|
1013
|
-
const regex = /(「.*?」|“.*?”|『.*?』|《.*?》)/g;
|
|
1014
|
-
const parts = line.split(regex);
|
|
1015
|
-
return /* @__PURE__ */ jsx3(Text3, { children: parts.map((part, index) => {
|
|
1016
|
-
if (regex.test(part)) {
|
|
1017
|
-
}
|
|
1018
|
-
const isHighlight = index % 2 === 1;
|
|
1019
|
-
if (isHighlight) {
|
|
1020
|
-
return /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: part }, index);
|
|
1021
|
-
}
|
|
1022
|
-
return /* @__PURE__ */ jsx3(Text3, { children: part }, index);
|
|
1023
|
-
}) });
|
|
1024
|
-
}
|
|
1025
|
-
function TextRenderer({ lines, height, lineSpacing = 0 }) {
|
|
1026
|
-
const displayLines = [...lines];
|
|
1027
|
-
if (height && displayLines.length < height) {
|
|
1028
|
-
const padding = height - displayLines.length;
|
|
1029
|
-
for (let i = 0; i < padding; i++) {
|
|
1030
|
-
displayLines.push("");
|
|
1031
|
-
}
|
|
1032
|
-
}
|
|
1033
|
-
return /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", children: displayLines.map((line, index) => /* @__PURE__ */ jsx3(Box3, { marginBottom: lineSpacing, children: renderLineWithHighlight(line) }, index)) });
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
// src/ui/components/StatusBar.tsx
|
|
1037
|
-
import { Box as Box4, Text as Text4 } from "ink";
|
|
1038
|
-
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1039
|
-
function StatusBar({
|
|
1040
|
-
bookTitle,
|
|
1041
|
-
chapterTitle,
|
|
1042
|
-
percent,
|
|
1043
|
-
currentPage,
|
|
1044
|
-
totalPages,
|
|
1045
|
-
remainingTime
|
|
1046
|
-
}) {
|
|
1047
|
-
const displayPercent = (percent * 100).toFixed(1);
|
|
1048
|
-
const titleDisplay = chapterTitle ? `${bookTitle} \xB7 ${chapterTitle}` : bookTitle;
|
|
1049
|
-
return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "row", justifyContent: "space-between", borderStyle: "single", borderTop: false, borderLeft: false, borderRight: false, paddingX: 1, children: [
|
|
1050
|
-
/* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsxs3(Text4, { color: "gray", children: [
|
|
1051
|
-
"\u{1F4D6} ",
|
|
1052
|
-
titleDisplay
|
|
1053
|
-
] }) }),
|
|
1054
|
-
/* @__PURE__ */ jsxs3(Box4, { children: [
|
|
1055
|
-
remainingTime && /* @__PURE__ */ jsxs3(Text4, { dimColor: true, children: [
|
|
1056
|
-
t("tui.reader.status.remaining", remainingTime),
|
|
1057
|
-
" "
|
|
1058
|
-
] }),
|
|
1059
|
-
/* @__PURE__ */ jsxs3(Text4, { color: "gray", children: [
|
|
1060
|
-
currentPage,
|
|
1061
|
-
"/",
|
|
1062
|
-
totalPages
|
|
1063
|
-
] }),
|
|
1064
|
-
/* @__PURE__ */ jsxs3(Text4, { color: "gray", children: [
|
|
1065
|
-
displayPercent,
|
|
1066
|
-
"%"
|
|
1067
|
-
] }),
|
|
1068
|
-
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: t("common.quit") })
|
|
1069
|
-
] })
|
|
1070
|
-
] });
|
|
1071
|
-
}
|
|
1072
|
-
|
|
1073
|
-
// src/ui/components/ChapterNav.tsx
|
|
1074
|
-
import { useState as useState3, useEffect as useEffect3 } from "react";
|
|
1075
|
-
import { Box as Box5, Text as Text5, useInput as useInput2 } from "ink";
|
|
1076
|
-
import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1077
|
-
function ChapterNav({
|
|
1078
|
-
chapters,
|
|
1079
|
-
bookmarks,
|
|
1080
|
-
currentChapterId,
|
|
1081
|
-
termHeight,
|
|
1082
|
-
onSelect,
|
|
1083
|
-
onClose
|
|
1084
|
-
}) {
|
|
1085
|
-
const [activeTab, setActiveTab] = useState3("chapters");
|
|
1086
|
-
const isBookmarks = activeTab === "bookmarks";
|
|
1087
|
-
const currentList = isBookmarks ? bookmarks : chapters;
|
|
1088
|
-
const initialIndex = currentChapterId && !isBookmarks ? Math.max(
|
|
1089
|
-
0,
|
|
1090
|
-
chapters.findIndex((c) => c.id === currentChapterId)
|
|
1091
|
-
) : 0;
|
|
1092
|
-
const [selectedIndex, setSelectedIndex] = useState3(initialIndex);
|
|
1093
|
-
useEffect3(() => {
|
|
1094
|
-
setSelectedIndex(isBookmarks ? 0 : initialIndex);
|
|
1095
|
-
}, [activeTab, initialIndex, isBookmarks]);
|
|
1096
|
-
const pageSize = Math.max(5, termHeight - 6);
|
|
1097
|
-
const windowStart = Math.max(0, Math.floor(selectedIndex / pageSize) * pageSize);
|
|
1098
|
-
const visibleItems = currentList.slice(windowStart, windowStart + pageSize);
|
|
1099
|
-
const isRawModeSupported = process.stdin.isTTY ?? false;
|
|
1100
|
-
useInput2(
|
|
1101
|
-
(input, key) => {
|
|
1102
|
-
if (key.escape || input === "q") {
|
|
1103
|
-
onClose();
|
|
1104
|
-
return;
|
|
1105
|
-
}
|
|
1106
|
-
if (key.tab) {
|
|
1107
|
-
setActiveTab((prev) => prev === "chapters" ? "bookmarks" : "chapters");
|
|
1108
|
-
return;
|
|
1109
|
-
}
|
|
1110
|
-
if (key.return) {
|
|
1111
|
-
if (currentList[selectedIndex]) {
|
|
1112
|
-
onSelect(currentList[selectedIndex].byte_offset);
|
|
1113
|
-
}
|
|
1114
|
-
return;
|
|
1115
|
-
}
|
|
1116
|
-
if (key.upArrow || input === "k") {
|
|
1117
|
-
setSelectedIndex((prev) => Math.max(0, prev - 1));
|
|
1118
|
-
}
|
|
1119
|
-
if (key.downArrow || input === "j") {
|
|
1120
|
-
setSelectedIndex((prev) => Math.min(currentList.length - 1, prev + 1));
|
|
1121
|
-
}
|
|
1122
|
-
},
|
|
1123
|
-
{ isActive: isRawModeSupported }
|
|
1124
|
-
);
|
|
1125
|
-
return /* @__PURE__ */ jsxs4(
|
|
1126
|
-
Box5,
|
|
1127
|
-
{
|
|
1128
|
-
flexDirection: "column",
|
|
1129
|
-
borderStyle: "round",
|
|
1130
|
-
borderColor: "green",
|
|
1131
|
-
paddingX: 2,
|
|
1132
|
-
paddingY: 1,
|
|
1133
|
-
width: "80%",
|
|
1134
|
-
alignSelf: "center",
|
|
1135
|
-
marginTop: 2,
|
|
1136
|
-
children: [
|
|
1137
|
-
/* @__PURE__ */ jsxs4(Box5, { justifyContent: "space-between", marginBottom: 1, children: [
|
|
1138
|
-
/* @__PURE__ */ jsxs4(Box5, { children: [
|
|
1139
|
-
/* @__PURE__ */ jsxs4(Text5, { bold: true, color: activeTab === "chapters" ? "green" : "gray", children: [
|
|
1140
|
-
t("tui.nav.tab.chapters"),
|
|
1141
|
-
" "
|
|
1142
|
-
] }),
|
|
1143
|
-
/* @__PURE__ */ jsx5(Text5, { bold: true, color: activeTab === "bookmarks" ? "green" : "gray", children: t("tui.nav.tab.bookmarks") })
|
|
1144
|
-
] }),
|
|
1145
|
-
/* @__PURE__ */ jsx5(Text5, { dimColor: true, children: t("tui.nav.tips") })
|
|
1146
|
-
] }),
|
|
1147
|
-
visibleItems.length === 0 ? /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: t("tui.nav.empty") }) : visibleItems.map((item, idx) => {
|
|
1148
|
-
const actualIndex = windowStart + idx;
|
|
1149
|
-
const isSelected = actualIndex === selectedIndex;
|
|
1150
|
-
return /* @__PURE__ */ jsxs4(
|
|
1151
|
-
Text5,
|
|
1152
|
-
{
|
|
1153
|
-
color: isSelected ? "green" : void 0,
|
|
1154
|
-
bold: isSelected,
|
|
1155
|
-
children: [
|
|
1156
|
-
isSelected ? "\u25B6 " : " ",
|
|
1157
|
-
item.title
|
|
1158
|
-
]
|
|
1159
|
-
},
|
|
1160
|
-
item.id
|
|
1161
|
-
);
|
|
1162
|
-
}),
|
|
1163
|
-
/* @__PURE__ */ jsx5(Box5, { marginTop: 1, justifyContent: "flex-end", children: /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: t("tui.nav.page", Math.floor(selectedIndex / pageSize) + 1, Math.ceil(currentList.length / pageSize) || 1) }) })
|
|
1164
|
-
]
|
|
1165
|
-
}
|
|
1166
|
-
);
|
|
1167
|
-
}
|
|
1168
|
-
|
|
1169
|
-
// src/ui/hooks/useReader.ts
|
|
1170
|
-
import { useState as useState4, useCallback, useMemo } from "react";
|
|
1171
|
-
function useReader(pages, initialByteOffset) {
|
|
1172
|
-
const initialPage = useMemo(() => {
|
|
1173
|
-
if (!initialByteOffset || pages.length === 0) return 0;
|
|
1174
|
-
let targetPage = 0;
|
|
1175
|
-
for (let i = 0; i < pages.length; i++) {
|
|
1176
|
-
if (pages[i].byteOffset <= initialByteOffset) {
|
|
1177
|
-
targetPage = i;
|
|
1178
|
-
} else {
|
|
1179
|
-
break;
|
|
1180
|
-
}
|
|
1181
|
-
}
|
|
1182
|
-
return targetPage;
|
|
1183
|
-
}, [pages, initialByteOffset]);
|
|
1184
|
-
const [state, setState] = useState4({
|
|
1185
|
-
currentPage: initialPage,
|
|
1186
|
-
totalPages: pages.length
|
|
1187
|
-
});
|
|
1188
|
-
const nextPage = useCallback(() => {
|
|
1189
|
-
setState((prev) => ({
|
|
1190
|
-
...prev,
|
|
1191
|
-
currentPage: Math.min(prev.currentPage + 1, prev.totalPages - 1)
|
|
1192
|
-
}));
|
|
1193
|
-
}, []);
|
|
1194
|
-
const prevPage = useCallback(() => {
|
|
1195
|
-
setState((prev) => ({
|
|
1196
|
-
...prev,
|
|
1197
|
-
currentPage: Math.max(prev.currentPage - 1, 0)
|
|
1198
|
-
}));
|
|
1199
|
-
}, []);
|
|
1200
|
-
const goToPage = useCallback((pageNum) => {
|
|
1201
|
-
setState((prev) => ({
|
|
1202
|
-
...prev,
|
|
1203
|
-
currentPage: Math.max(0, Math.min(pageNum, prev.totalPages - 1))
|
|
1204
|
-
}));
|
|
1205
|
-
}, []);
|
|
1206
|
-
const goToOffset = useCallback((byteOffset) => {
|
|
1207
|
-
let targetPage = 0;
|
|
1208
|
-
for (let i = 0; i < pages.length; i++) {
|
|
1209
|
-
if (pages[i].byteOffset <= byteOffset) {
|
|
1210
|
-
targetPage = i;
|
|
1211
|
-
} else {
|
|
1212
|
-
break;
|
|
1213
|
-
}
|
|
1214
|
-
}
|
|
1215
|
-
setState((prev) => ({
|
|
1216
|
-
...prev,
|
|
1217
|
-
currentPage: targetPage
|
|
1218
|
-
}));
|
|
1219
|
-
}, [pages]);
|
|
1220
|
-
const getCurrentPage = useCallback(() => {
|
|
1221
|
-
return pages[state.currentPage];
|
|
1222
|
-
}, [state.currentPage, pages]);
|
|
1223
|
-
const getCurrentOffset = useCallback(() => {
|
|
1224
|
-
return pages[state.currentPage]?.byteOffset ?? 0;
|
|
1225
|
-
}, [state.currentPage, pages]);
|
|
1226
|
-
const getPercent = useCallback(() => {
|
|
1227
|
-
if (state.totalPages === 0) return 0;
|
|
1228
|
-
return (state.currentPage + 1) / state.totalPages;
|
|
1229
|
-
}, [state.currentPage, state.totalPages]);
|
|
1230
|
-
const isFirstPage = state.currentPage === 0;
|
|
1231
|
-
const isLastPage = state.currentPage === state.totalPages - 1;
|
|
1232
|
-
return {
|
|
1233
|
-
...state,
|
|
1234
|
-
nextPage,
|
|
1235
|
-
prevPage,
|
|
1236
|
-
goToPage,
|
|
1237
|
-
goToOffset,
|
|
1238
|
-
getCurrentPage,
|
|
1239
|
-
getCurrentOffset,
|
|
1240
|
-
getPercent,
|
|
1241
|
-
isFirstPage,
|
|
1242
|
-
isLastPage
|
|
1243
|
-
};
|
|
1244
|
-
}
|
|
1245
|
-
|
|
1246
|
-
// src/ui/hooks/useKeyboard.ts
|
|
1247
|
-
import { useInput as useInput3 } from "ink";
|
|
1248
|
-
function useKeyboard(handlers, isActive = true) {
|
|
1249
|
-
const isRawModeSupported = process.stdin.isTTY ?? false;
|
|
1250
|
-
const shouldListen = isRawModeSupported && isActive;
|
|
1251
|
-
useInput3((input, key) => {
|
|
1252
|
-
if (input === " " || input === "j" || key.downArrow || input === "f") {
|
|
1253
|
-
handlers.onNext?.();
|
|
1254
|
-
}
|
|
1255
|
-
if (input === "k" || key.upArrow || input === "b") {
|
|
1256
|
-
handlers.onPrev?.();
|
|
1257
|
-
}
|
|
1258
|
-
if (input === "q") {
|
|
1259
|
-
handlers.onQuit?.();
|
|
1260
|
-
}
|
|
1261
|
-
if (input === "c") {
|
|
1262
|
-
handlers.onChapterList?.();
|
|
1263
|
-
}
|
|
1264
|
-
if (input === "?") {
|
|
1265
|
-
handlers.onHelp?.();
|
|
1266
|
-
}
|
|
1267
|
-
if (handlers.onBossKey && (key.escape || input === "esc" || input === "b" || input === "B")) {
|
|
1268
|
-
handlers.onBossKey?.();
|
|
1269
|
-
}
|
|
1270
|
-
if (handlers.onBookmarkAdd && (input === "m" || input === "M")) {
|
|
1271
|
-
handlers.onBookmarkAdd?.();
|
|
1272
|
-
}
|
|
1273
|
-
}, { isActive: shouldListen });
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
|
-
// src/utils/stringWidth.ts
|
|
1277
|
-
function getStringWidth(str) {
|
|
1278
|
-
let width = 0;
|
|
1279
|
-
for (const char of str) {
|
|
1280
|
-
width += isFullWidth(char) ? 2 : 1;
|
|
1281
|
-
}
|
|
1282
|
-
return width;
|
|
1283
|
-
}
|
|
1284
|
-
function isFullWidth(char) {
|
|
1285
|
-
const code = char.codePointAt(0);
|
|
1286
|
-
if (code === void 0) return false;
|
|
1287
|
-
return (
|
|
1288
|
-
// CJK 统一表意字符
|
|
1289
|
-
code >= 19968 && code <= 40959 || // CJK 统一表意字符扩展 A
|
|
1290
|
-
code >= 13312 && code <= 19903 || // CJK 统一表意字符扩展 B
|
|
1291
|
-
code >= 131072 && code <= 173791 || // CJK 兼容表意字符
|
|
1292
|
-
code >= 63744 && code <= 64255 || // 全角 ASCII、全角标点
|
|
1293
|
-
code >= 65281 && code <= 65376 || code >= 65504 && code <= 65510 || // CJK 标点符号
|
|
1294
|
-
code >= 12288 && code <= 12351 || // 日文平假名/片假名
|
|
1295
|
-
code >= 12352 && code <= 12543 || // 韩文音节
|
|
1296
|
-
code >= 44032 && code <= 55215
|
|
1297
|
-
);
|
|
1298
|
-
}
|
|
1299
|
-
|
|
1300
|
-
// src/utils/paginate.ts
|
|
1301
|
-
function paginate(text, width, height, step) {
|
|
1302
|
-
const pages = [];
|
|
1303
|
-
const rawLines = text.split("\n");
|
|
1304
|
-
const actualStep = step || height;
|
|
1305
|
-
const wrappedLines = [];
|
|
1306
|
-
let currentOffset = 0;
|
|
1307
|
-
for (const rawLine of rawLines) {
|
|
1308
|
-
const wrapped = wrapLine(rawLine, width);
|
|
1309
|
-
for (const line of wrapped) {
|
|
1310
|
-
wrappedLines.push({ text: line, byteOffset: currentOffset });
|
|
1311
|
-
}
|
|
1312
|
-
currentOffset += Buffer.byteLength(rawLine + "\n", "utf-8");
|
|
1313
|
-
}
|
|
1314
|
-
for (let i = 0; i < wrappedLines.length; i += actualStep) {
|
|
1315
|
-
const pageLines = wrappedLines.slice(i, i + height);
|
|
1316
|
-
if (pageLines.length === 0) break;
|
|
1317
|
-
pages.push({
|
|
1318
|
-
lines: pageLines.map((l) => l.text),
|
|
1319
|
-
byteOffset: pageLines[0]?.byteOffset ?? 0
|
|
1320
|
-
});
|
|
1321
|
-
if (i + actualStep >= wrappedLines.length) break;
|
|
1322
|
-
}
|
|
1323
|
-
return pages;
|
|
1324
|
-
}
|
|
1325
|
-
function wrapLine(line, width) {
|
|
1326
|
-
if (line.length === 0) return [""];
|
|
1327
|
-
const result = [];
|
|
1328
|
-
let currentLine = "";
|
|
1329
|
-
let currentWidth = 0;
|
|
1330
|
-
for (const char of line) {
|
|
1331
|
-
const charWidth = getStringWidth(char);
|
|
1332
|
-
if (currentWidth + charWidth > width) {
|
|
1333
|
-
result.push(currentLine);
|
|
1334
|
-
currentLine = char;
|
|
1335
|
-
currentWidth = charWidth;
|
|
1336
|
-
} else {
|
|
1337
|
-
currentLine += char;
|
|
1338
|
-
currentWidth += charWidth;
|
|
1339
|
-
}
|
|
1340
|
-
}
|
|
1341
|
-
if (currentLine.length > 0) {
|
|
1342
|
-
result.push(currentLine);
|
|
1343
|
-
}
|
|
1344
|
-
return result.length > 0 ? result : [""];
|
|
1345
|
-
}
|
|
1346
|
-
|
|
1347
|
-
// src/services/ChapterService.ts
|
|
1348
|
-
var ChapterService = class {
|
|
1349
|
-
chapterModel = new ChapterModel();
|
|
1350
|
-
/**
|
|
1351
|
-
* 获取指定书籍的所有章节
|
|
1352
|
-
*/
|
|
1353
|
-
getChapters(bookId) {
|
|
1354
|
-
return this.chapterModel.findByBookId(bookId);
|
|
1355
|
-
}
|
|
1356
|
-
/**
|
|
1357
|
-
* 获取指定章节信息
|
|
1358
|
-
*/
|
|
1359
|
-
getChapter(bookId, chapterNo) {
|
|
1360
|
-
return this.chapterModel.findChapter(bookId, chapterNo);
|
|
1361
|
-
}
|
|
1362
|
-
/**
|
|
1363
|
-
* 获取章节总数
|
|
1364
|
-
*/
|
|
1365
|
-
getChapterCount(bookId) {
|
|
1366
|
-
return this.chapterModel.getChapterCount(bookId);
|
|
1367
|
-
}
|
|
1368
|
-
/**
|
|
1369
|
-
* 获取指定书籍下的所有章节
|
|
1370
|
-
*/
|
|
1371
|
-
getChaptersByBookId(bookId) {
|
|
1372
|
-
return this.chapterModel.findByBookId(bookId);
|
|
1373
|
-
}
|
|
1374
|
-
/**
|
|
1375
|
-
* 根据 offset 查询当前所属章节(用于高亮当前所在章)
|
|
1376
|
-
*/
|
|
1377
|
-
getChapterByOffset(bookId, byteOffset) {
|
|
1378
|
-
const chapters = this.chapterModel.findByBookId(bookId);
|
|
1379
|
-
if (chapters.length === 0) return void 0;
|
|
1380
|
-
let current;
|
|
1381
|
-
for (const chapter of chapters) {
|
|
1382
|
-
if (chapter.byte_offset <= byteOffset) {
|
|
1383
|
-
current = chapter;
|
|
1384
|
-
} else {
|
|
1385
|
-
break;
|
|
1386
|
-
}
|
|
1387
|
-
}
|
|
1388
|
-
return current;
|
|
1389
|
-
}
|
|
1390
|
-
};
|
|
1391
|
-
|
|
1392
|
-
// src/services/RecentService.ts
|
|
1393
|
-
var RecentService = class {
|
|
1394
|
-
recentModel = new RecentModel();
|
|
1395
|
-
bookModel = new BookModel();
|
|
1396
|
-
/**
|
|
1397
|
-
* 获取最近阅读的书籍列表(包含书籍详情)
|
|
1398
|
-
*/
|
|
1399
|
-
getRecentBooks(limit = 20) {
|
|
1400
|
-
const recentRecords = this.recentModel.getRecent(limit);
|
|
1401
|
-
return recentRecords.map((record) => this.bookModel.findById(record.book_id)).filter((book) => book !== void 0);
|
|
1402
|
-
}
|
|
1403
|
-
/**
|
|
1404
|
-
* 记录打开事件
|
|
1405
|
-
*/
|
|
1406
|
-
recordOpen(bookId) {
|
|
1407
|
-
this.recentModel.recordOpen(bookId);
|
|
1408
|
-
}
|
|
1409
|
-
};
|
|
1410
|
-
|
|
1411
|
-
// src/db/models/Bookmark.ts
|
|
1412
|
-
var BookmarkModel = class {
|
|
1413
|
-
/**
|
|
1414
|
-
* 插入书签
|
|
1415
|
-
*/
|
|
1416
|
-
insert(bookmark) {
|
|
1417
|
-
const db2 = getDb();
|
|
1418
|
-
db2.prepare(`
|
|
1419
|
-
INSERT INTO bookmarks (book_id, title, byte_offset, created_at)
|
|
1420
|
-
VALUES (?, ?, ?, ?)
|
|
1421
|
-
`).run(bookmark.book_id, bookmark.title, bookmark.byte_offset, bookmark.created_at);
|
|
1422
|
-
}
|
|
1423
|
-
/**
|
|
1424
|
-
* 获取指定书籍的所有书签
|
|
1425
|
-
*/
|
|
1426
|
-
findByBookId(bookId) {
|
|
1427
|
-
const db2 = getDb();
|
|
1428
|
-
return db2.prepare("SELECT * FROM bookmarks WHERE book_id = ? ORDER BY created_at DESC").all(bookId);
|
|
1429
|
-
}
|
|
1430
|
-
/**
|
|
1431
|
-
* 获取指定书签
|
|
1432
|
-
*/
|
|
1433
|
-
findById(id) {
|
|
1434
|
-
const db2 = getDb();
|
|
1435
|
-
return db2.prepare("SELECT * FROM bookmarks WHERE id = ?").get(id);
|
|
1436
|
-
}
|
|
1437
|
-
/**
|
|
1438
|
-
* 获取书籍书签总数
|
|
1439
|
-
*/
|
|
1440
|
-
getCount(bookId) {
|
|
1441
|
-
const db2 = getDb();
|
|
1442
|
-
const result = db2.prepare("SELECT COUNT(*) as count FROM bookmarks WHERE book_id = ?").get(bookId);
|
|
1443
|
-
return result.count;
|
|
1444
|
-
}
|
|
1445
|
-
/**
|
|
1446
|
-
* 删除书签
|
|
1447
|
-
*/
|
|
1448
|
-
delete(id) {
|
|
1449
|
-
const db2 = getDb();
|
|
1450
|
-
db2.prepare("DELETE FROM bookmarks WHERE id = ?").run(id);
|
|
1451
|
-
}
|
|
1452
|
-
/**
|
|
1453
|
-
* 移除整本书的书签 (配合彻底清理书籍使用)
|
|
1454
|
-
*/
|
|
1455
|
-
deleteByBookId(bookId) {
|
|
1456
|
-
const db2 = getDb();
|
|
1457
|
-
db2.prepare("DELETE FROM bookmarks WHERE book_id = ?").run(bookId);
|
|
1458
|
-
}
|
|
1459
|
-
};
|
|
1460
|
-
|
|
1461
|
-
// src/services/BookmarkService.ts
|
|
1462
|
-
var BookmarkService = class {
|
|
1463
|
-
bookmarkModel;
|
|
1464
|
-
constructor() {
|
|
1465
|
-
this.bookmarkModel = new BookmarkModel();
|
|
1466
|
-
}
|
|
1467
|
-
/**
|
|
1468
|
-
* 增加一条书签
|
|
1469
|
-
* @param title 该书签展现给用户的文案(一句话大纲)
|
|
1470
|
-
*/
|
|
1471
|
-
addBookmark(bookId, title, byteOffset) {
|
|
1472
|
-
this.bookmarkModel.insert({
|
|
1473
|
-
book_id: bookId,
|
|
1474
|
-
title,
|
|
1475
|
-
byte_offset: byteOffset,
|
|
1476
|
-
created_at: Date.now()
|
|
1477
|
-
});
|
|
1478
|
-
}
|
|
1479
|
-
/**
|
|
1480
|
-
* 罗列该书全部的书签
|
|
1481
|
-
*/
|
|
1482
|
-
getBookmarksByBookId(bookId) {
|
|
1483
|
-
return this.bookmarkModel.findByBookId(bookId);
|
|
1484
|
-
}
|
|
1485
|
-
/**
|
|
1486
|
-
* 删掉对应书签
|
|
1487
|
-
*/
|
|
1488
|
-
removeBookmark(id) {
|
|
1489
|
-
this.bookmarkModel.delete(id);
|
|
1490
|
-
}
|
|
1491
|
-
};
|
|
1492
|
-
|
|
1493
|
-
// src/utils/bossKey.ts
|
|
1494
|
-
var isBossKeyEnabled = false;
|
|
1495
|
-
function triggerBossKey() {
|
|
1496
|
-
isBossKeyEnabled = true;
|
|
1497
|
-
}
|
|
1498
|
-
function isBossKeyActive() {
|
|
1499
|
-
return isBossKeyEnabled;
|
|
1500
|
-
}
|
|
1501
|
-
var fakeLogs = {
|
|
1502
|
-
nodejs: `
|
|
1503
|
-
file:///home/user/project/node_modules/vite/dist/node/chunks/dep-BbV93i69.js:43916
|
|
1504
|
-
throw new Error(\`[vite] Failed to resolve module import "./App.vue". Check if the file exists.\`);
|
|
1505
|
-
^
|
|
1506
|
-
|
|
1507
|
-
Error: [vite] Failed to resolve module import "./App.vue". Check if the file exists.
|
|
1508
|
-
at Object.run (file:///home/user/project/node_modules/vite/dist/node/chunks/dep-BbV93i69.js:43916:13)
|
|
1509
|
-
at async file:///home/user/project/node_modules/vite/dist/node/cli.js:722:7
|
|
1510
|
-
at async startVite (file:///home/user/project/node_modules/vite/dist/node/cli.js:700:5)
|
|
1511
|
-
at async Object.handler (file:///home/user/project/node_modules/vite/dist/node/cli.js:650:1)
|
|
1512
|
-
|
|
1513
|
-
Node.js v20.11.0
|
|
1514
|
-
`,
|
|
1515
|
-
python: `
|
|
1516
|
-
Traceback (most recent call last):
|
|
1517
|
-
File "/home/user/project/main.py", line 42, in <module>
|
|
1518
|
-
result = process_data(df)
|
|
1519
|
-
File "/home/user/project/utils/pipeline.py", line 118, in process_data
|
|
1520
|
-
return df.groupby("user_id").apply(transform)
|
|
1521
|
-
File "/home/user/project/utils/pipeline.py", line 97, in transform
|
|
1522
|
-
raise ValueError(f"Missing required column: '{col}'")
|
|
1523
|
-
ValueError: Missing required column: 'timestamp'
|
|
1524
|
-
|
|
1525
|
-
During handling of the above exception, another exception occurred:
|
|
1526
|
-
|
|
1527
|
-
Traceback (most recent call last):
|
|
1528
|
-
File "/home/user/project/main.py", line 47, in <module>
|
|
1529
|
-
raise RuntimeError("Pipeline failed. Check logs for details.")
|
|
1530
|
-
RuntimeError: Pipeline failed. Check logs for details.
|
|
1531
|
-
`,
|
|
1532
|
-
java: `
|
|
1533
|
-
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "str" is null
|
|
1534
|
-
at com.example.app.StringUtils.process(StringUtils.java:34)
|
|
1535
|
-
at com.example.app.DataProcessor.run(DataProcessor.java:112)
|
|
1536
|
-
at com.example.app.Main.main(Main.java:21)
|
|
1537
|
-
|
|
1538
|
-
BUILD FAILURE
|
|
1539
|
-
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.11.0:compile
|
|
1540
|
-
[ERROR] -> [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoFailureException
|
|
1541
|
-
`,
|
|
1542
|
-
c: `
|
|
1543
|
-
make[2]: *** [CMakeFiles/app.dir/src/main.c.o] Error 1
|
|
1544
|
-
make[1]: *** [CMakeFiles/app.dir/all] Error 2
|
|
1545
|
-
make: *** [all] Error 2
|
|
1546
|
-
|
|
1547
|
-
/home/user/project/src/main.c:87:5: error: use of undeclared identifier 'ctx'
|
|
1548
|
-
ctx->buffer = malloc(sizeof(Buffer));
|
|
1549
|
-
^
|
|
1550
|
-
/home/user/project/src/main.c:102:12: warning: implicit declaration of function 'init_buffer' [-Wimplicit-function-declaration]
|
|
1551
|
-
result = init_buffer(ctx, DEFAULT_SIZE);
|
|
1552
|
-
^
|
|
1553
|
-
2 errors, 1 warning generated.
|
|
1554
|
-
`,
|
|
1555
|
-
go: `
|
|
1556
|
-
# command-line-arguments
|
|
1557
|
-
./main.go:58:13: undefined: parseConfig
|
|
1558
|
-
./main.go:74:9: cannot use result (variable of type *Response) as type Handler
|
|
1559
|
-
./main.go:91:2: declared and not used: errCh
|
|
1560
|
-
|
|
1561
|
-
go: github.com/example/app@v1.3.2: reading github.com/example/app/go.mod at revision v1.3.2: unknown revision v1.3.2
|
|
1562
|
-
exit status 1
|
|
1563
|
-
`
|
|
1564
|
-
};
|
|
1565
|
-
function performBossKeyAction() {
|
|
1566
|
-
process.stdout.write("\x1B[3J\x1B[2J\x1B[1;1H");
|
|
1567
|
-
const lang = getConfig().bossKeyLang ?? "nodejs";
|
|
1568
|
-
const fakeLog = fakeLogs[lang];
|
|
1569
|
-
process.stdout.write(fakeLog + "\n");
|
|
1570
|
-
process.exit(0);
|
|
1571
|
-
}
|
|
1572
|
-
|
|
1573
|
-
// src/utils/time.ts
|
|
1574
|
-
function estimateReadingTime(charCount, isChinese = true) {
|
|
1575
|
-
const charsPerMinute = isChinese ? 500 : 1250;
|
|
1576
|
-
return Math.ceil(charCount / charsPerMinute);
|
|
1577
|
-
}
|
|
1578
|
-
function formatReadingTime(minutes) {
|
|
1579
|
-
if (minutes < 60) {
|
|
1580
|
-
return `${minutes} \u5206\u949F`;
|
|
1581
|
-
}
|
|
1582
|
-
const hours = Math.floor(minutes / 60);
|
|
1583
|
-
const mins = minutes % 60;
|
|
1584
|
-
return mins > 0 ? `${hours} \u5C0F\u65F6 ${mins} \u5206\u949F` : `${hours} \u5C0F\u65F6`;
|
|
1585
|
-
}
|
|
1586
|
-
|
|
1587
|
-
// src/services/CloudSyncService.ts
|
|
1588
|
-
var API_BASE = "https://api.readshell.com";
|
|
1589
|
-
var CloudSyncService = class {
|
|
1590
|
-
constructor(token, deviceId, tier) {
|
|
1591
|
-
this.token = token;
|
|
1592
|
-
this.deviceId = deviceId;
|
|
1593
|
-
this.tier = tier;
|
|
1594
|
-
}
|
|
1595
|
-
async apiFetch(path, init = {}) {
|
|
1596
|
-
return fetch(`${API_BASE}${path}`, {
|
|
1597
|
-
...init,
|
|
1598
|
-
headers: {
|
|
1599
|
-
"Content-Type": "application/json",
|
|
1600
|
-
Authorization: `Bearer ${this.token}`,
|
|
1601
|
-
...init.headers
|
|
1602
|
-
}
|
|
1603
|
-
});
|
|
1604
|
-
}
|
|
1605
|
-
/**
|
|
1606
|
-
* 推送单本书的进度到云端
|
|
1607
|
-
*/
|
|
1608
|
-
async pushProgress(bookId, progress) {
|
|
1609
|
-
const payload = {
|
|
1610
|
-
deviceId: this.deviceId,
|
|
1611
|
-
books: [],
|
|
1612
|
-
progress: [{
|
|
1613
|
-
bookId,
|
|
1614
|
-
byteOffset: progress.byteOffset,
|
|
1615
|
-
percentage: progress.percent,
|
|
1616
|
-
clientUpdatedAt: new Date(progress.updatedAt).toISOString()
|
|
1617
|
-
}],
|
|
1618
|
-
bookmarks: []
|
|
1619
|
-
};
|
|
1620
|
-
await this.apiFetch("/sync/push", {
|
|
1621
|
-
method: "POST",
|
|
1622
|
-
body: JSON.stringify(payload)
|
|
1623
|
-
}).catch(() => {
|
|
1624
|
-
});
|
|
1625
|
-
}
|
|
1626
|
-
/**
|
|
1627
|
-
* 推送单本书的书签到云端
|
|
1628
|
-
*/
|
|
1629
|
-
async pushBookmarks(bookId, bookmarks) {
|
|
1630
|
-
const syncBookmarks = bookmarks.map((b) => ({
|
|
1631
|
-
id: String(b.id ?? `${b.book_id}-${b.byte_offset}`),
|
|
1632
|
-
bookId: b.book_id,
|
|
1633
|
-
byteOffset: b.byte_offset,
|
|
1634
|
-
label: b.title,
|
|
1635
|
-
deleted: false,
|
|
1636
|
-
deletedAt: null,
|
|
1637
|
-
createdAt: new Date(b.created_at).toISOString()
|
|
1638
|
-
}));
|
|
1639
|
-
const payload = {
|
|
1640
|
-
deviceId: this.deviceId,
|
|
1641
|
-
books: [],
|
|
1642
|
-
progress: [],
|
|
1643
|
-
bookmarks: syncBookmarks
|
|
1644
|
-
};
|
|
1645
|
-
await this.apiFetch("/sync/push", {
|
|
1646
|
-
method: "POST",
|
|
1647
|
-
body: JSON.stringify(payload)
|
|
1648
|
-
}).catch(() => {
|
|
1649
|
-
});
|
|
1650
|
-
}
|
|
1651
|
-
/**
|
|
1652
|
-
* 拉取云端数据并合并到本地 SQLite
|
|
1653
|
-
*/
|
|
1654
|
-
async pull() {
|
|
1655
|
-
try {
|
|
1656
|
-
const res = await this.apiFetch("/sync/pull");
|
|
1657
|
-
if (!res.ok) return 0;
|
|
1658
|
-
const json = await res.json();
|
|
1659
|
-
if (!json.success || !json.data) return 0;
|
|
1660
|
-
const progressModel = new ProgressModel();
|
|
1661
|
-
let count = 0;
|
|
1662
|
-
for (const p of json.data.progress) {
|
|
1663
|
-
progressModel.upsert({
|
|
1664
|
-
book_id: p.bookId,
|
|
1665
|
-
chapter_no: 0,
|
|
1666
|
-
byte_offset: p.byteOffset,
|
|
1667
|
-
percent: p.percentage,
|
|
1668
|
-
updated_at: new Date(p.clientUpdatedAt).getTime(),
|
|
1669
|
-
opened_at: new Date(p.clientUpdatedAt).getTime()
|
|
1670
|
-
});
|
|
1671
|
-
count++;
|
|
1672
|
-
}
|
|
1673
|
-
return count;
|
|
1674
|
-
} catch {
|
|
1675
|
-
return 0;
|
|
1676
|
-
}
|
|
1677
|
-
}
|
|
1678
|
-
/**
|
|
1679
|
-
* 完整双向同步:推送本地所有进度和书签,然后拉取云端数据
|
|
1680
|
-
*/
|
|
1681
|
-
async fullSync() {
|
|
1682
|
-
const progressModel = new ProgressModel();
|
|
1683
|
-
const bookmarkModel = new BookmarkModel();
|
|
1684
|
-
const bookModel = new BookModel();
|
|
1685
|
-
const books = bookModel.findAll();
|
|
1686
|
-
const bookPayloads = [];
|
|
1687
|
-
const progressPayloads = [];
|
|
1688
|
-
const bookmarkPayloads = [];
|
|
1689
|
-
for (const book of books) {
|
|
1690
|
-
bookPayloads.push({
|
|
1691
|
-
id: book.id,
|
|
1692
|
-
title: book.title,
|
|
1693
|
-
author: book.author,
|
|
1694
|
-
format: book.format,
|
|
1695
|
-
fileHash: book.file_hash
|
|
1696
|
-
});
|
|
1697
|
-
const progress = progressModel.findByBookId(book.id);
|
|
1698
|
-
if (progress) {
|
|
1699
|
-
progressPayloads.push({
|
|
1700
|
-
bookId: book.id,
|
|
1701
|
-
byteOffset: progress.byte_offset,
|
|
1702
|
-
percentage: progress.percent,
|
|
1703
|
-
clientUpdatedAt: new Date(progress.updated_at).toISOString()
|
|
1704
|
-
});
|
|
1705
|
-
}
|
|
1706
|
-
const bookmarks = bookmarkModel.findByBookId(book.id);
|
|
1707
|
-
for (const b of bookmarks) {
|
|
1708
|
-
bookmarkPayloads.push({
|
|
1709
|
-
id: String(b.id ?? `${b.book_id}-${b.byte_offset}`),
|
|
1710
|
-
bookId: b.book_id,
|
|
1711
|
-
byteOffset: b.byte_offset,
|
|
1712
|
-
label: b.title,
|
|
1713
|
-
deleted: false,
|
|
1714
|
-
deletedAt: null,
|
|
1715
|
-
createdAt: new Date(b.created_at).toISOString()
|
|
1716
|
-
});
|
|
1717
|
-
}
|
|
1718
|
-
}
|
|
1719
|
-
const pushed = progressPayloads.length + bookmarkPayloads.length;
|
|
1720
|
-
try {
|
|
1721
|
-
await this.apiFetch("/sync/push", {
|
|
1722
|
-
method: "POST",
|
|
1723
|
-
body: JSON.stringify({
|
|
1724
|
-
deviceId: this.deviceId,
|
|
1725
|
-
books: bookPayloads,
|
|
1726
|
-
progress: progressPayloads,
|
|
1727
|
-
bookmarks: bookmarkPayloads
|
|
1728
|
-
})
|
|
1729
|
-
});
|
|
1730
|
-
} catch {
|
|
1731
|
-
}
|
|
1732
|
-
const pulled = await this.pull();
|
|
1733
|
-
return { pushed, pulled };
|
|
1734
|
-
}
|
|
1735
|
-
};
|
|
1736
|
-
function createSyncService() {
|
|
1737
|
-
if (!isLoggedIn()) return null;
|
|
1738
|
-
const token = getAuthToken();
|
|
1739
|
-
const deviceId = getAuthDeviceId();
|
|
1740
|
-
const tier = getAuthTier();
|
|
1741
|
-
if (!token || !deviceId || !tier || tier === "free") return null;
|
|
1742
|
-
return new CloudSyncService(token, deviceId, tier);
|
|
1743
|
-
}
|
|
1744
|
-
|
|
1745
|
-
// src/ui/pages/ReaderPage.tsx
|
|
1746
|
-
import { Fragment, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1747
|
-
function ReaderContent({
|
|
1748
|
-
book,
|
|
1749
|
-
bookId,
|
|
1750
|
-
pages,
|
|
1751
|
-
initialByteOffset,
|
|
1752
|
-
termHeight,
|
|
1753
|
-
contentHeight,
|
|
1754
|
-
lineSpacing
|
|
1755
|
-
}) {
|
|
1756
|
-
const { exit } = useApp3();
|
|
1757
|
-
const [chapterTitle, setChapterTitle] = useState5();
|
|
1758
|
-
const [currentChapter, setCurrentChapter] = useState5();
|
|
1759
|
-
const [showChapterNav, setShowChapterNav] = useState5(false);
|
|
1760
|
-
const [allChapters, setAllChapters] = useState5([]);
|
|
1761
|
-
const [allBookmarks, setAllBookmarks] = useState5([]);
|
|
1762
|
-
const [toastMessage, setToastMessage] = useState5(null);
|
|
1763
|
-
const progressServiceRef = useRef(new ProgressService());
|
|
1764
|
-
const chapterServiceRef = useRef(new ChapterService());
|
|
1765
|
-
const bookmarkServiceRef = useRef(new BookmarkService());
|
|
1766
|
-
const reader = useReader(pages, initialByteOffset);
|
|
1767
|
-
const saveReadingProgress = () => {
|
|
1768
|
-
const offset = reader.getCurrentOffset();
|
|
1769
|
-
const percent = reader.getPercent();
|
|
1770
|
-
const chapter = chapterServiceRef.current.getChapterByOffset(bookId, offset);
|
|
1771
|
-
const chapterNo = chapter?.chapter_no ?? 0;
|
|
1772
|
-
progressServiceRef.current.saveProgress(bookId, chapterNo, offset, percent);
|
|
1773
|
-
logger.debug(`\u8FDB\u5EA6\u5DF2\u4FDD\u5B58: offset=${offset}, ${(percent * 100).toFixed(1)}%`);
|
|
1774
|
-
};
|
|
1775
|
-
useEffect4(() => {
|
|
1776
|
-
const currentOffset = reader.getCurrentOffset();
|
|
1777
|
-
const chapter = chapterServiceRef.current.getChapterByOffset(bookId, currentOffset);
|
|
1778
|
-
setCurrentChapter(chapter ?? void 0);
|
|
1779
|
-
setChapterTitle(chapter?.title ?? void 0);
|
|
1780
|
-
}, [reader.currentPage, bookId]);
|
|
1781
|
-
useEffect4(() => {
|
|
1782
|
-
const chaptersList = chapterServiceRef.current.getChaptersByBookId(bookId);
|
|
1783
|
-
setAllChapters(chaptersList);
|
|
1784
|
-
if (showChapterNav) {
|
|
1785
|
-
setAllBookmarks(bookmarkServiceRef.current.getBookmarksByBookId(bookId));
|
|
1786
|
-
}
|
|
1787
|
-
}, [bookId, showChapterNav]);
|
|
1788
|
-
const handleAddBookmark = () => {
|
|
1789
|
-
const currentPageInfo = reader.getCurrentPage();
|
|
1790
|
-
if (!currentPageInfo) return;
|
|
1791
|
-
let markTitle = "\u65E0\u6807\u9898\u4E66\u7B7E";
|
|
1792
|
-
for (const line of currentPageInfo.lines) {
|
|
1793
|
-
const stripped = line.trim();
|
|
1794
|
-
if (stripped.length > 0) {
|
|
1795
|
-
markTitle = stripped.slice(0, 15) + (stripped.length > 15 ? "..." : "");
|
|
1796
|
-
break;
|
|
1797
|
-
}
|
|
1798
|
-
}
|
|
1799
|
-
const currentOffset = reader.getCurrentOffset();
|
|
1800
|
-
bookmarkServiceRef.current.addBookmark(bookId, markTitle, currentOffset);
|
|
1801
|
-
setToastMessage(t("tui.reader.bookmark_add", markTitle));
|
|
1802
|
-
setTimeout(() => setToastMessage(null), 2e3);
|
|
1803
|
-
};
|
|
1804
|
-
useEffect4(() => {
|
|
1805
|
-
return () => {
|
|
1806
|
-
saveReadingProgress();
|
|
1807
|
-
const syncService = createSyncService();
|
|
1808
|
-
if (syncService) {
|
|
1809
|
-
const offset = reader.getCurrentOffset();
|
|
1810
|
-
const percent = reader.getPercent();
|
|
1811
|
-
syncService.pushProgress(bookId, {
|
|
1812
|
-
byteOffset: offset,
|
|
1813
|
-
percent,
|
|
1814
|
-
updatedAt: Date.now()
|
|
1815
|
-
}).catch(() => {
|
|
1816
|
-
});
|
|
1817
|
-
}
|
|
1818
|
-
};
|
|
1819
|
-
}, [bookId, reader]);
|
|
1820
|
-
useKeyboard(
|
|
1821
|
-
{
|
|
1822
|
-
onNext: () => reader.nextPage(),
|
|
1823
|
-
onPrev: () => reader.prevPage(),
|
|
1824
|
-
onQuit: () => exit(),
|
|
1825
|
-
onChapterList: () => setShowChapterNav(true),
|
|
1826
|
-
onBossKey: () => {
|
|
1827
|
-
saveReadingProgress();
|
|
1828
|
-
triggerBossKey();
|
|
1829
|
-
exit();
|
|
1830
|
-
},
|
|
1831
|
-
onBookmarkAdd: handleAddBookmark
|
|
1832
|
-
},
|
|
1833
|
-
!showChapterNav
|
|
1834
|
-
// 如果浮层显示,则停止普通的阅读快捷键
|
|
1835
|
-
);
|
|
1836
|
-
const currentPage = reader.getCurrentPage();
|
|
1837
|
-
const currentLines = currentPage?.lines ?? [];
|
|
1838
|
-
const calculatedContentHeight = Math.max(1, termHeight - 2);
|
|
1839
|
-
const totalChars = (book.file_size ?? 0) / 3;
|
|
1840
|
-
const remainingChars = Math.max(0, totalChars * (1 - reader.getPercent()));
|
|
1841
|
-
const remainingMinutes = estimateReadingTime(remainingChars, true);
|
|
1842
|
-
const remainingTimeStr = formatReadingTime(remainingMinutes);
|
|
1843
|
-
return /* @__PURE__ */ jsx6(Box6, { flexDirection: "column", height: termHeight, children: !showChapterNav ? /* @__PURE__ */ jsxs5(Fragment, { children: [
|
|
1844
|
-
/* @__PURE__ */ jsx6(Box6, { flexDirection: "column", flexGrow: 1, paddingX: 1, children: /* @__PURE__ */ jsx6(TextRenderer, { lines: currentLines, height: calculatedContentHeight, lineSpacing }) }),
|
|
1845
|
-
/* @__PURE__ */ jsx6(
|
|
1846
|
-
StatusBar,
|
|
1847
|
-
{
|
|
1848
|
-
bookTitle: book.title,
|
|
1849
|
-
percent: reader.getPercent(),
|
|
1850
|
-
chapterTitle,
|
|
1851
|
-
currentPage: reader.currentPage + 1,
|
|
1852
|
-
totalPages: reader.totalPages,
|
|
1853
|
-
remainingTime: remainingTimeStr
|
|
1854
|
-
}
|
|
1855
|
-
),
|
|
1856
|
-
toastMessage && /* @__PURE__ */ jsx6(Box6, { alignSelf: "flex-end", marginTop: -2, marginRight: 1, borderStyle: "round", borderColor: "green", paddingX: 1, children: /* @__PURE__ */ jsx6(Text6, { color: "green", children: toastMessage }) })
|
|
1857
|
-
] }) : /* @__PURE__ */ jsx6(
|
|
1858
|
-
ChapterNav,
|
|
1859
|
-
{
|
|
1860
|
-
chapters: allChapters,
|
|
1861
|
-
bookmarks: allBookmarks,
|
|
1862
|
-
currentChapterId: currentChapter?.id,
|
|
1863
|
-
termHeight,
|
|
1864
|
-
onSelect: (offset) => {
|
|
1865
|
-
reader.goToOffset(offset);
|
|
1866
|
-
setShowChapterNav(false);
|
|
1867
|
-
},
|
|
1868
|
-
onClose: () => setShowChapterNav(false)
|
|
1869
|
-
}
|
|
1870
|
-
) });
|
|
1871
|
-
}
|
|
1872
|
-
function ReaderPage({ bookId, initialByteOffset, onNavigate: _onNavigate }) {
|
|
1873
|
-
const { exit } = useApp3();
|
|
1874
|
-
const { stdout } = useStdout();
|
|
1875
|
-
const [book, setBook] = useState5(null);
|
|
1876
|
-
const [pages, setPages] = useState5(null);
|
|
1877
|
-
const [error, setError] = useState5(null);
|
|
1878
|
-
const termWidth = stdout?.columns ?? 80;
|
|
1879
|
-
const termHeight = stdout?.rows ?? 24;
|
|
1880
|
-
const appConfig = getConfig();
|
|
1881
|
-
const lineSpacing = appConfig.lineSpacing || 0;
|
|
1882
|
-
const contentHeight = Math.max(Math.floor((termHeight - 3) / (1 + lineSpacing)), 2);
|
|
1883
|
-
useEffect4(() => {
|
|
1884
|
-
try {
|
|
1885
|
-
const bookModel = new BookModel();
|
|
1886
|
-
const bookRecord = bookModel.findById(bookId);
|
|
1887
|
-
if (!bookRecord) {
|
|
1888
|
-
setError(`\u4E66\u7C4D\u4E0D\u5B58\u5728: ${bookId}`);
|
|
1889
|
-
return;
|
|
1890
|
-
}
|
|
1891
|
-
setBook(bookRecord);
|
|
1892
|
-
const recentService = new RecentService();
|
|
1893
|
-
recentService.recordOpen(bookId);
|
|
1894
|
-
parseFile(bookRecord.file_path, bookRecord.format).then((parsed) => {
|
|
1895
|
-
const stepSize = appConfig.readingMode === "scroll" ? Math.max(1, Math.floor(contentHeight / 2)) : contentHeight;
|
|
1896
|
-
const paginatedPages = paginate(parsed.content, termWidth - 2, contentHeight, stepSize);
|
|
1897
|
-
setPages(paginatedPages);
|
|
1898
|
-
logger.debug(`\u52A0\u8F7D\u5B8C\u6210: ${bookRecord.title}, ${paginatedPages.length} \u9875, \u6A21\u5F0F: ${appConfig.readingMode}`);
|
|
1899
|
-
}).catch((err) => {
|
|
1900
|
-
setError(`\u5185\u5BB9\u89E3\u6790\u5931\u8D25: ${err instanceof Error ? err.message : String(err)}`);
|
|
1901
|
-
});
|
|
1902
|
-
} catch (err) {
|
|
1903
|
-
setError(`\u52A0\u8F7D\u8FC7\u7A0B\u51FA\u9519: ${err instanceof Error ? err.message : String(err)}`);
|
|
1904
|
-
}
|
|
1905
|
-
}, [bookId, termWidth, contentHeight]);
|
|
1906
|
-
useKeyboard({
|
|
1907
|
-
onQuit: () => exit()
|
|
1908
|
-
});
|
|
1909
|
-
if (error) {
|
|
1910
|
-
return /* @__PURE__ */ jsxs5(Box6, { padding: 1, flexDirection: "column", children: [
|
|
1911
|
-
/* @__PURE__ */ jsxs5(Text6, { color: "red", children: [
|
|
1912
|
-
"\u2717 ",
|
|
1913
|
-
error
|
|
1914
|
-
] }),
|
|
1915
|
-
/* @__PURE__ */ jsx6(Text6, { dimColor: true, children: t("common.quit") })
|
|
1916
|
-
] });
|
|
1917
|
-
}
|
|
1918
|
-
if (!book || !pages) {
|
|
1919
|
-
return /* @__PURE__ */ jsx6(Box6, { padding: 1, children: /* @__PURE__ */ jsxs5(Text6, { color: "cyan", children: [
|
|
1920
|
-
"\u{1F4D6} ",
|
|
1921
|
-
t("tui.reader.loading")
|
|
1922
|
-
] }) });
|
|
1923
|
-
}
|
|
1924
|
-
return /* @__PURE__ */ jsx6(
|
|
1925
|
-
ReaderContent,
|
|
1926
|
-
{
|
|
1927
|
-
book,
|
|
1928
|
-
bookId,
|
|
1929
|
-
pages,
|
|
1930
|
-
initialByteOffset,
|
|
1931
|
-
termHeight,
|
|
1932
|
-
contentHeight,
|
|
1933
|
-
lineSpacing
|
|
1934
|
-
}
|
|
1935
|
-
);
|
|
1936
|
-
}
|
|
1937
|
-
|
|
1938
|
-
// src/ui/App.tsx
|
|
1939
|
-
import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1940
|
-
function App({ initialPage = "resume", bookId, initialByteOffset }) {
|
|
1941
|
-
const [currentPage, setCurrentPage] = useState6(initialPage);
|
|
1942
|
-
const [currentBookId, setCurrentBookId] = useState6(bookId);
|
|
1943
|
-
const [currentByteOffset, setCurrentByteOffset] = useState6(initialByteOffset);
|
|
1944
|
-
const navigateTo = (page, targetBookId, byteOffset) => {
|
|
1945
|
-
setCurrentPage(page);
|
|
1946
|
-
if (targetBookId) setCurrentBookId(targetBookId);
|
|
1947
|
-
if (byteOffset !== void 0) setCurrentByteOffset(byteOffset);
|
|
1948
|
-
};
|
|
1949
|
-
return /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", width: "100%", children: [
|
|
1950
|
-
currentPage === "resume" && /* @__PURE__ */ jsx7(ResumePage, { onNavigate: navigateTo }),
|
|
1951
|
-
currentPage === "library" && /* @__PURE__ */ jsx7(LibraryPage, { onNavigate: navigateTo }),
|
|
1952
|
-
currentPage === "reader" && currentBookId && /* @__PURE__ */ jsx7(
|
|
1953
|
-
ReaderPage,
|
|
1954
|
-
{
|
|
1955
|
-
bookId: currentBookId,
|
|
1956
|
-
initialByteOffset: currentByteOffset,
|
|
1957
|
-
onNavigate: navigateTo
|
|
1958
|
-
}
|
|
1959
|
-
),
|
|
1960
|
-
currentPage === "reader" && !currentBookId && /* @__PURE__ */ jsx7(Box7, { children: /* @__PURE__ */ jsx7(Text7, { color: "red", children: "\u9519\u8BEF: \u672A\u6307\u5B9A\u4E66\u7C4D" }) })
|
|
1961
|
-
] });
|
|
1962
|
-
}
|
|
1963
|
-
|
|
1964
|
-
// src/ui/renderApp.ts
|
|
1965
|
-
function renderApp(options = {}) {
|
|
1966
|
-
const { initialPage = "resume", bookId, initialByteOffset } = options;
|
|
1967
|
-
const { waitUntilExit } = render(
|
|
1968
|
-
React6.createElement(App, {
|
|
1969
|
-
initialPage,
|
|
1970
|
-
bookId,
|
|
1971
|
-
initialByteOffset
|
|
1972
|
-
})
|
|
1973
|
-
);
|
|
1974
|
-
waitUntilExit().then(() => {
|
|
1975
|
-
if (isBossKeyActive()) {
|
|
1976
|
-
performBossKeyAction();
|
|
1977
|
-
}
|
|
1978
|
-
process.exit(0);
|
|
1979
|
-
}).catch(() => {
|
|
1980
|
-
process.exit(1);
|
|
1981
|
-
});
|
|
1982
|
-
}
|
|
1983
|
-
|
|
1984
|
-
// src/cli/commands/resume.ts
|
|
1985
|
-
var resumeCommand = {
|
|
1986
|
-
command: "resume",
|
|
1987
|
-
describe: t("cli.resume.desc"),
|
|
1988
|
-
handler: async () => {
|
|
1989
|
-
try {
|
|
1990
|
-
const progressService = new ProgressService();
|
|
1991
|
-
const lastProgress = progressService.getLastOpenedBook();
|
|
1992
|
-
if (!lastProgress) {
|
|
1993
|
-
console.log(t("cli.resume.none"));
|
|
1994
|
-
return;
|
|
1995
|
-
}
|
|
1996
|
-
const bookService = new BookService();
|
|
1997
|
-
const book = bookService.findBook(lastProgress.book_id);
|
|
1998
|
-
if (!book) {
|
|
1999
|
-
console.log(t("cli.resume.none"));
|
|
2000
|
-
return;
|
|
2001
|
-
}
|
|
2002
|
-
logger.debug(`\u6062\u590D\u9605\u8BFB: ${book.title}, offset=${lastProgress.byte_offset}`);
|
|
2003
|
-
renderApp({
|
|
2004
|
-
initialPage: "reader",
|
|
2005
|
-
bookId: lastProgress.book_id,
|
|
2006
|
-
initialByteOffset: lastProgress.byte_offset
|
|
2007
|
-
});
|
|
2008
|
-
} catch (error) {
|
|
2009
|
-
logger.error("\u6062\u590D\u9605\u8BFB\u5931\u8D25:", error);
|
|
2010
|
-
process.exit(1);
|
|
2011
|
-
}
|
|
2012
|
-
}
|
|
2013
|
-
};
|
|
2014
|
-
|
|
2015
|
-
// src/cli/commands/open.ts
|
|
2016
|
-
var openCommand = {
|
|
2017
|
-
command: "open <target>",
|
|
2018
|
-
describe: t("cli.open.desc"),
|
|
2019
|
-
builder: (yargs2) => {
|
|
2020
|
-
return yargs2.positional("target", {
|
|
2021
|
-
describe: t("cli.open.help"),
|
|
2022
|
-
type: "string",
|
|
2023
|
-
demandOption: true
|
|
2024
|
-
});
|
|
2025
|
-
},
|
|
2026
|
-
handler: async (argv) => {
|
|
2027
|
-
try {
|
|
2028
|
-
const bookService = new BookService();
|
|
2029
|
-
const book = bookService.findBook(argv.target);
|
|
2030
|
-
if (!book) {
|
|
2031
|
-
console.log(`${t("cli.open.not_found")} ${argv.target}`);
|
|
2032
|
-
process.exit(1);
|
|
2033
|
-
}
|
|
2034
|
-
const progressService = new ProgressService();
|
|
2035
|
-
const progress = progressService.getProgress(book.id);
|
|
2036
|
-
const byteOffset = progress?.byte_offset ?? 0;
|
|
2037
|
-
logger.debug(`\u6253\u5F00: ${book.title}, offset=${byteOffset}`);
|
|
2038
|
-
renderApp({
|
|
2039
|
-
initialPage: "reader",
|
|
2040
|
-
bookId: book.id,
|
|
2041
|
-
initialByteOffset: byteOffset
|
|
2042
|
-
});
|
|
2043
|
-
} catch (error) {
|
|
2044
|
-
logger.error("\u6253\u5F00\u5931\u8D25:", error);
|
|
2045
|
-
process.exit(1);
|
|
2046
|
-
}
|
|
2047
|
-
}
|
|
2048
|
-
};
|
|
2049
|
-
|
|
2050
|
-
// src/cli/commands/library.ts
|
|
2051
|
-
var libraryCommand = {
|
|
2052
|
-
command: "list",
|
|
2053
|
-
aliases: ["library"],
|
|
2054
|
-
describe: t("cli.library.desc"),
|
|
2055
|
-
builder: (yargs2) => {
|
|
2056
|
-
return yargs2.option("search", {
|
|
2057
|
-
alias: "s",
|
|
2058
|
-
describe: t("cli.library.help"),
|
|
2059
|
-
type: "string"
|
|
2060
|
-
});
|
|
2061
|
-
},
|
|
2062
|
-
handler: async (argv) => {
|
|
2063
|
-
try {
|
|
2064
|
-
if (argv.search) {
|
|
2065
|
-
const bookService = new BookService();
|
|
2066
|
-
const books = bookService.searchBooks(argv.search);
|
|
2067
|
-
if (books.length === 0) {
|
|
2068
|
-
console.log(t("cli.library.search_none", argv.search));
|
|
2069
|
-
return;
|
|
2070
|
-
}
|
|
2071
|
-
console.log(t("cli.library.search_result", books.length));
|
|
2072
|
-
books.forEach((book, index) => {
|
|
2073
|
-
console.log(` ${index + 1}. ${book.title} [${book.id}] (${book.format})`);
|
|
2074
|
-
});
|
|
2075
|
-
return;
|
|
2076
|
-
}
|
|
2077
|
-
renderApp({ initialPage: "library" });
|
|
2078
|
-
} catch (error) {
|
|
2079
|
-
logger.error("\u83B7\u53D6\u4E66\u67B6\u5931\u8D25:", error);
|
|
2080
|
-
process.exit(1);
|
|
2081
|
-
}
|
|
2082
|
-
}
|
|
2083
|
-
};
|
|
2084
|
-
|
|
2085
|
-
// src/cli/commands/remove.ts
|
|
2086
|
-
var removeCommand = {
|
|
2087
|
-
command: "remove <target>",
|
|
2088
|
-
describe: t("cli.remove.desc"),
|
|
2089
|
-
builder: (yargs2) => {
|
|
2090
|
-
return yargs2.positional("target", {
|
|
2091
|
-
describe: t("cli.remove.help"),
|
|
2092
|
-
type: "string",
|
|
2093
|
-
demandOption: true
|
|
2094
|
-
});
|
|
2095
|
-
},
|
|
2096
|
-
handler: async (argv) => {
|
|
2097
|
-
try {
|
|
2098
|
-
const bookService = new BookService();
|
|
2099
|
-
const book = bookService.findBook(argv.target);
|
|
2100
|
-
if (!book) {
|
|
2101
|
-
console.log(`${t("cli.remove.not_found")} ${argv.target}`);
|
|
2102
|
-
process.exit(1);
|
|
2103
|
-
}
|
|
2104
|
-
bookService.deleteBook(book.id);
|
|
2105
|
-
console.log(`${t("cli.remove.success")} ${book.title}`);
|
|
2106
|
-
} catch (error) {
|
|
2107
|
-
logger.error("\u79FB\u9664\u4E66\u7C4D\u5931\u8D25:", error);
|
|
2108
|
-
process.exit(1);
|
|
2109
|
-
}
|
|
2110
|
-
}
|
|
2111
|
-
};
|
|
2112
|
-
|
|
2113
|
-
// src/cli/commands/lang.ts
|
|
2114
|
-
var langCommand = {
|
|
2115
|
-
command: "lang <target>",
|
|
2116
|
-
describe: t("cli.lang.desc"),
|
|
2117
|
-
builder: (yargs2) => {
|
|
2118
|
-
return yargs2.positional("target", {
|
|
2119
|
-
describe: t("cli.lang.help"),
|
|
2120
|
-
type: "string",
|
|
2121
|
-
choices: ["zh", "en"],
|
|
2122
|
-
demandOption: true
|
|
2123
|
-
});
|
|
2124
|
-
},
|
|
2125
|
-
handler: (argv) => {
|
|
2126
|
-
const lang = argv.target;
|
|
2127
|
-
if (lang === "zh" || lang === "en") {
|
|
2128
|
-
setConfig("language", lang);
|
|
2129
|
-
setLanguage(lang);
|
|
2130
|
-
console.log(t("cli.lang.success", lang));
|
|
2131
|
-
} else {
|
|
2132
|
-
console.log(t("cli.lang.unsupported", lang));
|
|
2133
|
-
process.exit(1);
|
|
2134
|
-
}
|
|
2135
|
-
}
|
|
2136
|
-
};
|
|
2137
|
-
|
|
2138
|
-
// src/cli/commands/update.ts
|
|
2139
|
-
import { execSync } from "child_process";
|
|
2140
|
-
var updateCommand = {
|
|
2141
|
-
command: "update",
|
|
2142
|
-
describe: t("cli.update.desc"),
|
|
2143
|
-
handler: async () => {
|
|
2144
|
-
try {
|
|
2145
|
-
console.log(t("cli.update.checking"));
|
|
2146
|
-
const localVersion = true ? "0.3.7" : "0.2.2";
|
|
2147
|
-
const npmOutput = execSync("npm view readshell-pro version", { encoding: "utf-8" });
|
|
2148
|
-
const latestVersion = npmOutput.trim();
|
|
2149
|
-
if (!latestVersion) {
|
|
2150
|
-
throw new Error("Could not fetch npm version");
|
|
2151
|
-
}
|
|
2152
|
-
if (latestVersion === localVersion) {
|
|
2153
|
-
console.log(t("cli.update.latest", localVersion));
|
|
2154
|
-
return;
|
|
2155
|
-
}
|
|
2156
|
-
console.log(t("cli.update.updating", latestVersion, localVersion));
|
|
2157
|
-
execSync("npm install -g readshell-pro@latest", { stdio: "inherit" });
|
|
2158
|
-
console.log(t("cli.update.success"));
|
|
2159
|
-
} catch (error) {
|
|
2160
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
2161
|
-
console.log(t("cli.update.fail", msg));
|
|
2162
|
-
logger.error("\u66F4\u65B0\u5931\u8D25:", error);
|
|
2163
|
-
process.exit(1);
|
|
2164
|
-
}
|
|
2165
|
-
}
|
|
2166
|
-
};
|
|
2167
|
-
|
|
2168
|
-
// src/cli/commands/config.ts
|
|
2169
|
-
var BOSS_KEY_LANGS = ["nodejs", "python", "java", "c", "go"];
|
|
2170
|
-
var configCommand = {
|
|
2171
|
-
command: "config <key> <value>",
|
|
2172
|
-
describe: t("cli.config.desc"),
|
|
2173
|
-
builder: (yargs2) => {
|
|
2174
|
-
return yargs2.positional("key", {
|
|
2175
|
-
describe: "\u914D\u7F6E\u9879\u540D\u79F0 (language|line-spacing|reading-mode|boss-key-lang)",
|
|
2176
|
-
type: "string",
|
|
2177
|
-
choices: ["language", "line-spacing", "reading-mode", "boss-key-lang"],
|
|
2178
|
-
demandOption: true
|
|
2179
|
-
}).positional("value", {
|
|
2180
|
-
describe: "\u914D\u7F6E\u9879\u5185\u5BB9",
|
|
2181
|
-
type: "string",
|
|
2182
|
-
demandOption: true
|
|
2183
|
-
});
|
|
2184
|
-
},
|
|
2185
|
-
handler: (argv) => {
|
|
2186
|
-
const { key, value } = argv;
|
|
2187
|
-
try {
|
|
2188
|
-
if (key === "language") {
|
|
2189
|
-
if (value === "zh" || value === "en") {
|
|
2190
|
-
setConfig("language", value);
|
|
2191
|
-
console.log(t("cli.lang.success", value));
|
|
2192
|
-
} else {
|
|
2193
|
-
console.log(t("cli.lang.unsupported", value));
|
|
2194
|
-
}
|
|
2195
|
-
} else if (key === "line-spacing") {
|
|
2196
|
-
const spacing = parseInt(value, 10);
|
|
2197
|
-
if (!isNaN(spacing) && spacing >= 0 && spacing <= 2) {
|
|
2198
|
-
setConfig("lineSpacing", spacing);
|
|
2199
|
-
console.log(t("cli.config.line_spacing.success", spacing));
|
|
2200
|
-
} else {
|
|
2201
|
-
console.log(t("cli.config.line_spacing.desc"));
|
|
2202
|
-
}
|
|
2203
|
-
} else if (key === "reading-mode") {
|
|
2204
|
-
if (value === "page" || value === "scroll") {
|
|
2205
|
-
setConfig("readingMode", value);
|
|
2206
|
-
console.log(`\u2713 Reading mode set to: ${value}`);
|
|
2207
|
-
} else {
|
|
2208
|
-
console.log(t("cli.config.reading_mode.desc"));
|
|
2209
|
-
}
|
|
2210
|
-
} else if (key === "boss-key-lang") {
|
|
2211
|
-
if (BOSS_KEY_LANGS.includes(value)) {
|
|
2212
|
-
setConfig("bossKeyLang", value);
|
|
2213
|
-
console.log(`\u2713 Boss key language set to: ${value}`);
|
|
2214
|
-
} else {
|
|
2215
|
-
console.log(`\u2717 Unsupported language. Choose from: ${BOSS_KEY_LANGS.join(", ")}`);
|
|
2216
|
-
}
|
|
2217
|
-
}
|
|
2218
|
-
} catch (err) {
|
|
2219
|
-
console.error("\u2717 Failed to update config:", err);
|
|
2220
|
-
process.exit(1);
|
|
2221
|
-
}
|
|
2222
|
-
}
|
|
2223
|
-
};
|
|
2224
|
-
|
|
2225
|
-
// src/cli/commands/login.ts
|
|
2226
|
-
var API_BASE2 = "https://api.readshell.com";
|
|
2227
|
-
var POLL_INTERVAL_MS = 5e3;
|
|
2228
|
-
var TIMEOUT_MS = 5 * 60 * 1e3;
|
|
2229
|
-
var loginCommand = {
|
|
2230
|
-
command: "login",
|
|
2231
|
-
describe: "Login to ReadShell Pro (cloud sync)",
|
|
2232
|
-
handler: async () => {
|
|
2233
|
-
if (isLoggedIn()) {
|
|
2234
|
-
const tier = getAuthTier() ?? "unknown";
|
|
2235
|
-
console.log(t("cli.login.already", tier));
|
|
2236
|
-
return;
|
|
2237
|
-
}
|
|
2238
|
-
const initRes = await fetch(`${API_BASE2}/auth/device/init`, { method: "POST" });
|
|
2239
|
-
if (!initRes.ok) {
|
|
2240
|
-
console.error("Failed to start login flow. Please try again.");
|
|
2241
|
-
process.exit(1);
|
|
2242
|
-
}
|
|
2243
|
-
const initJson = await initRes.json();
|
|
2244
|
-
if (!initJson.success || !initJson.data) {
|
|
2245
|
-
console.error("Failed to start login flow. Please try again.");
|
|
2246
|
-
process.exit(1);
|
|
2247
|
-
}
|
|
2248
|
-
const { deviceCode, userCode, verificationUrl } = initJson.data;
|
|
2249
|
-
console.log(t("cli.login.open_browser", verificationUrl));
|
|
2250
|
-
console.log(t("cli.login.user_code", userCode));
|
|
2251
|
-
console.log(t("cli.login.waiting"));
|
|
2252
|
-
const deadline = Date.now() + TIMEOUT_MS;
|
|
2253
|
-
while (Date.now() < deadline) {
|
|
2254
|
-
await new Promise((resolve3) => setTimeout(resolve3, POLL_INTERVAL_MS));
|
|
2255
|
-
const pollRes = await fetch(`${API_BASE2}/auth/device/poll`, {
|
|
2256
|
-
method: "POST",
|
|
2257
|
-
headers: { "Content-Type": "application/json" },
|
|
2258
|
-
body: JSON.stringify({ deviceCode })
|
|
2259
|
-
}).catch(() => null);
|
|
2260
|
-
if (!pollRes || !pollRes.ok) continue;
|
|
2261
|
-
const pollJson = await pollRes.json();
|
|
2262
|
-
if (pollJson.success && pollJson.data) {
|
|
2263
|
-
const { accessToken, deviceId, userId, expiresAt, tier } = pollJson.data;
|
|
2264
|
-
saveAuth({
|
|
2265
|
-
authToken: accessToken,
|
|
2266
|
-
authUserId: userId,
|
|
2267
|
-
authDeviceId: deviceId,
|
|
2268
|
-
authTier: tier,
|
|
2269
|
-
authExpiresAt: expiresAt
|
|
2270
|
-
});
|
|
2271
|
-
console.log(t("cli.login.success", tier));
|
|
2272
|
-
if (tier !== "free") {
|
|
2273
|
-
console.log(t("cli.login.migrate_hint"));
|
|
2274
|
-
}
|
|
2275
|
-
return;
|
|
2276
|
-
}
|
|
2277
|
-
}
|
|
2278
|
-
console.log(t("cli.login.timeout"));
|
|
2279
|
-
process.exit(1);
|
|
2280
|
-
}
|
|
2281
|
-
};
|
|
2282
|
-
|
|
2283
|
-
// src/cli/commands/logout.ts
|
|
2284
|
-
var logoutCommand = {
|
|
2285
|
-
command: "logout",
|
|
2286
|
-
describe: "Logout from ReadShell Pro",
|
|
2287
|
-
handler: () => {
|
|
2288
|
-
clearAuth();
|
|
2289
|
-
console.log(t("cli.logout.success"));
|
|
2290
|
-
}
|
|
2291
|
-
};
|
|
2292
|
-
|
|
2293
|
-
// src/cli/commands/sync.ts
|
|
2294
|
-
var syncCommand = {
|
|
2295
|
-
command: "sync",
|
|
2296
|
-
describe: "Sync reading progress and bookmarks to cloud",
|
|
2297
|
-
handler: async () => {
|
|
2298
|
-
if (!isLoggedIn()) {
|
|
2299
|
-
console.log(t("cli.sync.not_logged_in"));
|
|
2300
|
-
process.exit(1);
|
|
2301
|
-
}
|
|
2302
|
-
const tier = getAuthTier();
|
|
2303
|
-
if (!tier || tier === "free") {
|
|
2304
|
-
console.log(t("cli.sync.need_pro"));
|
|
2305
|
-
process.exit(1);
|
|
2306
|
-
}
|
|
2307
|
-
const syncService = createSyncService();
|
|
2308
|
-
if (!syncService) {
|
|
2309
|
-
console.log(t("cli.sync.not_logged_in"));
|
|
2310
|
-
process.exit(1);
|
|
2311
|
-
}
|
|
2312
|
-
console.log(t("cli.sync.running"));
|
|
2313
|
-
try {
|
|
2314
|
-
const result = await syncService.fullSync();
|
|
2315
|
-
console.log(t("cli.sync.success", String(result.pushed), String(result.pulled)));
|
|
2316
|
-
} catch (err) {
|
|
2317
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
2318
|
-
console.log(t("cli.sync.failed", msg));
|
|
2319
|
-
process.exit(1);
|
|
2320
|
-
}
|
|
2321
|
-
}
|
|
2322
|
-
};
|
|
2323
|
-
|
|
2324
|
-
// src/cli/parser.ts
|
|
2325
|
-
function createParser() {
|
|
2326
|
-
const version = true ? "0.3.7" : "dev";
|
|
2327
|
-
return yargs(hideBin(process.argv)).scriptName("novel").usage("$0 <command> [options]").command(importCommand).command(resumeCommand).command(openCommand).command(libraryCommand).command(removeCommand).command(langCommand).command(updateCommand).command(configCommand).command(loginCommand).command(logoutCommand).command(syncCommand).demandCommand(1, "\u8BF7\u6307\u5B9A\u4E00\u4E2A\u547D\u4EE4\u3002\u4F7F\u7528 --help \u67E5\u770B\u53EF\u7528\u547D\u4EE4\u3002").strict().alias("h", "help").alias("v", "version").version(version).epilogue("ReadShell Pro \u2014 \u7EC8\u7AEF\u5185\u4F4E\u6253\u65AD\u8F7B\u9605\u8BFB\u5DE5\u5177 + \u4E91\u540C\u6B65");
|
|
2328
|
-
}
|
|
2329
|
-
|
|
2330
|
-
// src/db/migrate.ts
|
|
2331
|
-
var SCHEMA_VERSION = 2;
|
|
2332
|
-
function initDatabase() {
|
|
2333
|
-
const db2 = getDb();
|
|
2334
|
-
db2.exec(`
|
|
2335
|
-
CREATE TABLE IF NOT EXISTS schema_version (
|
|
2336
|
-
version INTEGER PRIMARY KEY
|
|
2337
|
-
);
|
|
2338
|
-
`);
|
|
2339
|
-
const row = db2.prepare("SELECT version FROM schema_version LIMIT 1").get();
|
|
2340
|
-
const currentVersion = row?.version ?? 0;
|
|
2341
|
-
if (currentVersion < SCHEMA_VERSION) {
|
|
2342
|
-
logger.debug(`\u6570\u636E\u5E93\u8FC1\u79FB: v${currentVersion} \u2192 v${SCHEMA_VERSION}`);
|
|
2343
|
-
migrate(db2, currentVersion);
|
|
2344
|
-
}
|
|
2345
|
-
}
|
|
2346
|
-
function migrate(db2, fromVersion) {
|
|
2347
|
-
const migrations = {
|
|
2348
|
-
1: `
|
|
2349
|
-
-- \u4E66\u7C4D\u5143\u6570\u636E
|
|
2350
|
-
CREATE TABLE IF NOT EXISTS books (
|
|
2351
|
-
id TEXT PRIMARY KEY,
|
|
2352
|
-
title TEXT NOT NULL,
|
|
2353
|
-
author TEXT,
|
|
2354
|
-
file_path TEXT NOT NULL,
|
|
2355
|
-
format TEXT NOT NULL,
|
|
2356
|
-
file_hash TEXT NOT NULL,
|
|
2357
|
-
file_size INTEGER,
|
|
2358
|
-
created_at INTEGER NOT NULL
|
|
2359
|
-
);
|
|
2360
|
-
|
|
2361
|
-
-- \u6838\u5FC3\u72B6\u6001\u8868
|
|
2362
|
-
CREATE TABLE IF NOT EXISTS reading_progress (
|
|
2363
|
-
book_id TEXT PRIMARY KEY REFERENCES books(id),
|
|
2364
|
-
chapter_no INTEGER NOT NULL DEFAULT 0,
|
|
2365
|
-
byte_offset INTEGER NOT NULL DEFAULT 0,
|
|
2366
|
-
percent REAL NOT NULL DEFAULT 0,
|
|
2367
|
-
updated_at INTEGER NOT NULL,
|
|
2368
|
-
opened_at INTEGER NOT NULL
|
|
2369
|
-
);
|
|
2370
|
-
|
|
2371
|
-
-- \u6700\u8FD1\u9605\u8BFB\u6392\u5E8F
|
|
2372
|
-
CREATE TABLE IF NOT EXISTS recent_reads (
|
|
2373
|
-
book_id TEXT PRIMARY KEY REFERENCES books(id),
|
|
2374
|
-
opened_at INTEGER NOT NULL,
|
|
2375
|
-
open_count INTEGER NOT NULL DEFAULT 1
|
|
2376
|
-
);
|
|
2377
|
-
|
|
2378
|
-
-- \u7AE0\u8282\u7D22\u5F15
|
|
2379
|
-
CREATE TABLE IF NOT EXISTS chapter_index (
|
|
2380
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
2381
|
-
book_id TEXT NOT NULL REFERENCES books(id),
|
|
2382
|
-
chapter_no INTEGER NOT NULL,
|
|
2383
|
-
title TEXT,
|
|
2384
|
-
byte_offset INTEGER NOT NULL,
|
|
2385
|
-
UNIQUE(book_id, chapter_no)
|
|
2386
|
-
);
|
|
2387
|
-
|
|
2388
|
-
-- \u7D22\u5F15
|
|
2389
|
-
CREATE INDEX IF NOT EXISTS idx_chapter_book ON chapter_index(book_id);
|
|
2390
|
-
CREATE INDEX IF NOT EXISTS idx_recent_opened ON recent_reads(opened_at DESC);
|
|
2391
|
-
`,
|
|
2392
|
-
2: `
|
|
2393
|
-
-- \u4E66\u7B7E\u7BA1\u7406
|
|
2394
|
-
CREATE TABLE IF NOT EXISTS bookmarks (
|
|
2395
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
2396
|
-
book_id TEXT NOT NULL REFERENCES books(id),
|
|
2397
|
-
title TEXT NOT NULL,
|
|
2398
|
-
byte_offset INTEGER NOT NULL,
|
|
2399
|
-
created_at INTEGER NOT NULL
|
|
2400
|
-
);
|
|
2401
|
-
|
|
2402
|
-
CREATE INDEX IF NOT EXISTS idx_bookmarks_book ON bookmarks(book_id);
|
|
2403
|
-
`
|
|
2404
|
-
};
|
|
2405
|
-
db2.transaction(() => {
|
|
2406
|
-
for (let v = fromVersion + 1; v <= SCHEMA_VERSION; v++) {
|
|
2407
|
-
const sql = migrations[v];
|
|
2408
|
-
if (sql) {
|
|
2409
|
-
db2.exec(sql);
|
|
2410
|
-
logger.debug(`\u5DF2\u6267\u884C\u8FC1\u79FB v${v}`);
|
|
2411
|
-
}
|
|
2412
|
-
}
|
|
2413
|
-
db2.prepare("DELETE FROM schema_version").run();
|
|
2414
|
-
db2.prepare("INSERT INTO schema_version (version) VALUES (?)").run(SCHEMA_VERSION);
|
|
2415
|
-
})();
|
|
2416
|
-
}
|
|
2417
|
-
|
|
2418
|
-
// src/index.ts
|
|
2419
|
-
async function main() {
|
|
2420
|
-
try {
|
|
2421
|
-
initDatabase();
|
|
2422
|
-
initI18n();
|
|
2423
|
-
const parser = createParser();
|
|
2424
|
-
await parser.parse();
|
|
2425
|
-
} catch (error) {
|
|
2426
|
-
logger.error("\u7A0B\u5E8F\u542F\u52A8\u5931\u8D25:", error);
|
|
2427
|
-
process.exit(1);
|
|
2428
|
-
}
|
|
2429
|
-
}
|
|
2430
|
-
main();
|
|
2431
|
-
//# sourceMappingURL=index.js.map
|